]> source.dussan.org Git - nextcloud-server.git/commitdiff
Backbone transport for Webdav
authorVincent Petry <pvince81@owncloud.com>
Tue, 1 Dec 2015 20:01:12 +0000 (21:01 +0100)
committerRoeland Jago Douma <rullzer@owncloud.com>
Sat, 16 Jan 2016 10:28:04 +0000 (11:28 +0100)
core/js/core.json
core/js/oc-backbone-webdav.js [new file with mode: 0644]
core/js/oc-backbone.js
core/js/tests/specs/oc-backbone-webdavSpec.js [new file with mode: 0644]
core/vendor/davclient.js/lib/client.js

index c7621a08d627f6c2ad8872d13c3a65461b1e6d5a..43cb1b472f57fb04aecd7c5244e0ec69dac74c00 100644 (file)
@@ -23,6 +23,7 @@
                "oc-dialogs.js",
                "js.js",
                "oc-backbone.js",
+               "oc-backbone-webdav.js",
                "l10n.js",
                "apps.js",
                "share.js",
diff --git a/core/js/oc-backbone-webdav.js b/core/js/oc-backbone-webdav.js
new file mode 100644 (file)
index 0000000..3ca3190
--- /dev/null
@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/**
+ * Webdav transport for Backbone.
+ *
+ * This makes it possible to use Webdav endpoints when
+ * working with Backbone models and collections.
+ *
+ * Requires the davclient.js library.
+ *
+ * Usage example:
+ *
+ *     var PersonModel = OC.Backbone.Model.extend({
+ *         // make it use the DAV transport
+ *         sync: OC.Backbone.davSync,
+ *
+ *         // DAV properties mapping
+ *         davProperties: {
+ *             'id': '{http://example.com/ns}id',
+ *             'firstName': '{http://example.com/ns}first-name',
+ *             'lastName': '{http://example.com/ns}last-name',
+ *             'age': '{http://example.com/ns}age'
+ *         },
+ *
+ *         // additional parsing, if needed
+ *         parse: function(props) {
+ *             // additional parsing (DAV property values are always strings)
+ *             props.age = parseInt(props.age, 10);
+ *             return props;
+ *         }
+ *     });
+ *
+ *     var PersonCollection = OC.Backbone.Collection.extend({
+ *         // make it use the DAV transport
+ *         sync: OC.Backbone.davSync,
+ *
+ *         // use person model
+ *         // note that davProperties will be inherited
+ *         model: PersonModel,
+ *
+ *         // DAV collection URL
+ *         url: function() {
+ *             return OC.linkToRemote('dav') + '/person/';
+ *         },
+ *     });
+ */
+
+/* global dav */
+
+(function(Backbone) {
+       var methodMap = {
+               'create': 'POST',
+               'update': 'PROPPATCH',
+               'patch':  'PROPPATCH',
+               'delete': 'DELETE',
+               'read':   'PROPFIND'
+       };
+
+       // Throw an error when a URL is needed, and none is supplied.
+       function urlError() {
+               throw new Error('A "url" property or function must be specified');
+       }
+
+       /**
+        * Convert a single propfind result to JSON
+        *
+        * @param {Object} result
+        * @param {Object} davProperties properties mapping
+        */
+       function parsePropFindResult(result, davProperties) {
+               var props = {
+                       href: result.href
+               };
+
+               _.each(result.propStat, function(propStat) {
+                       if (propStat.status !== 'HTTP/1.1 200 OK') {
+                               return;
+                       }
+
+                       for (var key in propStat.properties) {
+                               var propKey = key;
+                               if (davProperties[key]) {
+                                       propKey = davProperties[key];
+                               }
+                               props[propKey] = propStat.properties[key];
+                       }
+               });
+
+               if (!props.id) {
+                       // parse id from href
+                       props.id = parseIdFromLocation(props.href);
+               }
+
+               return props;
+       }
+
+       /**
+        * Parse ID from location
+        *
+        * @param {string} url url
+        * @return {string} id
+        */
+       function parseIdFromLocation(url) {
+               var queryPos = url.indexOf('?');
+               if (queryPos > 0) {
+                       url = url.substr(0, queryPos);
+               }
+
+               var parts = url.split('/');
+               return parts[parts.length - 1];
+       }
+
+       function isSuccessStatus(status) {
+               return status >= 200 && status <= 299;
+       }
+
+       function convertModelAttributesToDavProperties(attrs, davProperties) {
+               var props = {};
+               var key;
+               for (key in attrs) {
+                       var changedProp = davProperties[key];
+                       if (!changedProp) {
+                               console.warn('No matching DAV property for property "' + key);
+                               continue;
+                       }
+                       props[changedProp] = attrs[key];
+               }
+               return props;
+       }
+
+       function callPropFind(client, options, model, headers) {
+               return client.propFind(
+                       options.url,
+                       _.values(options.davProperties) || [],
+                       options.depth,
+                       headers
+               ).then(function(response) {
+                       if (isSuccessStatus(response.status)) {
+                               if (_.isFunction(options.success)) {
+                                       var propsMapping = _.invert(options.davProperties);
+                                       var results;
+                                       if (options.depth > 0) {
+                                               results = _.map(response.body, function(data) {
+                                                       return parsePropFindResult(data, propsMapping);
+                                               });
+                                               // discard root entry
+                                               results.shift();
+                                       } else {
+                                               results = parsePropFindResult(response.body, propsMapping);
+                                       }
+
+                                       options.success(results);
+                                       return;
+                               }
+                       } else if (_.isFunction(options.error)) {
+                               options.error(response);
+                       }
+               });
+       }
+
+       function callPropPatch(client, options, model, headers) {
+               client.propPatch(
+                       options.url,
+                       convertModelAttributesToDavProperties(model.changed, options.davProperties),
+                       headers
+               ).then(function(result) {
+                       if (isSuccessStatus(result.status)) {
+                               if (_.isFunction(options.success)) {
+                                       // pass the object's own values because the server
+                                       // does not return the updated model
+                                       options.success(model.toJSON());
+                               }
+                       } else if (_.isFunction(options.error)) {
+                               options.error(result);
+                       }
+               });
+
+       }
+
+       function callMethod(client, options, model, headers) {
+               headers['Content-Type'] = 'application/json';
+               return client.request(
+                       options.type,
+                       options.url,
+                       headers,
+                       options.data
+               ).then(function(result) {
+                       if (!isSuccessStatus(result.status)) {
+                               if (_.isFunction(options.error)) {
+                                       options.error(result);
+                               }
+                               return;
+                       }
+
+                       if (_.isFunction(options.success)) {
+                               if (options.type === 'PUT' || options.type === 'POST') {
+                                       // pass the object's own values because the server
+                                       // does not return anything
+                                       var responseJson = result.body || model.toJSON();
+                                       var locationHeader = result.xhr.getResponseHeader('Content-Location');
+                                       if (options.type === 'POST' && locationHeader) {
+                                               responseJson.id = parseIdFromLocation(locationHeader);
+                                       }
+                                       options.success(responseJson);
+                                       return;
+                               }
+                               options.success(result.body);
+                       }
+               });
+       }
+
+       function davCall(options, model) {
+               var client = new dav.Client({
+                       baseUrl: options.url,
+                       xmlNamespaces: _.extend({
+                               'DAV:': 'd',
+                               'http://owncloud.org/ns': 'oc'
+                       }, options.xmlNamespaces || {})
+               });
+               client.resolveUrl = function() {
+                       return options.url;
+               };
+               var headers = _.extend({
+                       'X-Requested-With': 'XMLHttpRequest'
+               }, options.headers);
+               if (options.type === 'PROPFIND') {
+                       return callPropFind(client, options, model, headers);
+               } else if (options.type === 'PROPPATCH') {
+                       return callPropPatch(client, options, model, headers);
+               } else {
+                       return callMethod(client, options, model, headers);
+               }
+       }
+
+       /**
+        * DAV transport
+        */
+       function davSync(method, model, options) {
+               var params = {type: methodMap[method]};
+               var isCollection = (model instanceof Backbone.Collection);
+
+               if (method === 'update' && (model.usePUT || (model.collection && model.collection.usePUT))) {
+                       // use PUT instead of PROPPATCH
+                       params.type = 'PUT';
+               }
+
+               // Ensure that we have a URL.
+               if (!options.url) {
+                       params.url = _.result(model, 'url') || urlError();
+               }
+
+               // Ensure that we have the appropriate request data.
+               if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
+                       params.data = JSON.stringify(options.attrs || model.toJSON(options));
+               }
+
+               // Don't process data on a non-GET request.
+               if (params.type !== 'PROPFIND') {
+                       params.processData = false;
+               }
+
+               if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') {
+                       var davProperties = model.davProperties;
+                       if (!davProperties && model.model) {
+                               // use dav properties from model in case of collection
+                               davProperties = model.model.prototype.davProperties;
+                       }
+                       if (davProperties) {
+                               if (_.isFunction(davProperties)) {
+                                       params.davProperties = davProperties.call(model);
+                               } else {
+                                       params.davProperties = davProperties;
+                               }
+                       }
+
+                       params.davProperties = _.extend(params.davProperties || {}, options.davProperties);
+
+                       if (_.isUndefined(options.depth)) {
+                               if (isCollection) {
+                                       options.depth = 1;
+                               } else {
+                                       options.depth = 0;
+                               }
+                       }
+               }
+
+               // Pass along `textStatus` and `errorThrown` from jQuery.
+               var error = options.error;
+               options.error = function(xhr, textStatus, errorThrown) {
+                       options.textStatus = textStatus;
+                       options.errorThrown = errorThrown;
+                       if (error) {
+                               error.call(options.context, xhr, textStatus, errorThrown);
+                       }
+               };
+
+               // Make the request, allowing the user to override any Ajax options.
+               var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model);
+               model.trigger('request', model, xhr, options);
+               return xhr;
+       }
+
+       // exports
+       Backbone.davCall = davCall;
+       Backbone.davSync = davSync;
+
+})(OC.Backbone);
+
index 75a409793401bea567c6953d3dc56a4481df342d..5c4eb2d24c84fa959184dc951cc459f00e44c7f0 100644 (file)
@@ -7,6 +7,8 @@
  * See the COPYING-README file.
  *
  */
+
+/* global Backbone */
 if(!_.isUndefined(Backbone)) {
        OC.Backbone = Backbone.noConflict();
 }
diff --git a/core/js/tests/specs/oc-backbone-webdavSpec.js b/core/js/tests/specs/oc-backbone-webdavSpec.js
new file mode 100644 (file)
index 0000000..8fe0b9e
--- /dev/null
@@ -0,0 +1,352 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+/* global dav */
+
+describe('Backbone Webdav extension', function() {
+       var davClientRequestStub;
+       var davClientPropPatchStub;
+       var davClientPropFindStub;
+       var deferredRequest;
+
+       beforeEach(function() {
+               deferredRequest = $.Deferred();
+               davClientRequestStub = sinon.stub(dav.Client.prototype, 'request');
+               davClientPropPatchStub = sinon.stub(dav.Client.prototype, 'propPatch');
+               davClientPropFindStub = sinon.stub(dav.Client.prototype, 'propFind');
+               davClientRequestStub.returns(deferredRequest.promise());
+               davClientPropPatchStub.returns(deferredRequest.promise());
+               davClientPropFindStub.returns(deferredRequest.promise());
+       });
+       afterEach(function() {
+               davClientRequestStub.restore();
+               davClientPropPatchStub.restore();
+               davClientPropFindStub.restore();
+       });
+
+       describe('collections', function() {
+               var TestModel;
+               var TestCollection;
+               beforeEach(function() {
+                       TestModel = OC.Backbone.Model.extend({
+                               sync: OC.Backbone.davSync,
+                               davProperties: {
+                                       'firstName': '{http://owncloud.org/ns}first-name',
+                                       'lastName': '{http://owncloud.org/ns}last-name',
+                               }
+                       });
+                       TestCollection = OC.Backbone.Collection.extend({
+                               sync: OC.Backbone.davSync,
+                               model: TestModel,
+                               url: 'http://example.com/owncloud/remote.php/test/'
+                       });
+               });
+
+               it('makes a POST request to create model into collection', function() {
+                       var collection = new TestCollection();
+                       var model = collection.create({
+                               firstName: 'Hello',
+                               lastName: 'World'
+                       });
+
+                       expect(davClientRequestStub.calledOnce).toEqual(true);
+                       expect(davClientRequestStub.getCall(0).args[0])
+                               .toEqual('POST');
+                       expect(davClientRequestStub.getCall(0).args[1])
+                               .toEqual('http://example.com/owncloud/remote.php/test/');
+                       expect(davClientRequestStub.getCall(0).args[2]['Content-Type'])
+                               .toEqual('application/json');
+                       expect(davClientRequestStub.getCall(0).args[2]['X-Requested-With'])
+                               .toEqual('XMLHttpRequest');
+                       expect(davClientRequestStub.getCall(0).args[3])
+                               .toEqual(JSON.stringify({
+                                       'firstName': 'Hello',
+                                       'lastName': 'World'
+                               }));
+
+                       var responseHeaderStub = sinon.stub()
+                               .withArgs('Content-Location')
+                               .returns('http://example.com/owncloud/remote.php/test/123');
+                       deferredRequest.resolve({
+                               status: 201,
+                               body: '',
+                               xhr: {
+                                       getResponseHeader: responseHeaderStub
+                               }
+                       });
+
+                       expect(model.id).toEqual('123');
+               });
+
+               it('uses PROPFIND to retrieve collection', function() {
+                       var successStub = sinon.stub();
+                       var errorStub = sinon.stub();
+                       var collection = new TestCollection();
+                       collection.fetch({
+                               success: successStub,
+                               error: errorStub
+                       });
+
+                       expect(davClientPropFindStub.calledOnce).toEqual(true);
+                       expect(davClientPropFindStub.getCall(0).args[0])
+                               .toEqual('http://example.com/owncloud/remote.php/test/');
+                       expect(davClientPropFindStub.getCall(0).args[1])
+                               .toEqual([
+                                       '{http://owncloud.org/ns}first-name',
+                                       '{http://owncloud.org/ns}last-name'
+                               ]);
+                       expect(davClientPropFindStub.getCall(0).args[2])
+                               .toEqual(1);
+                       expect(davClientPropFindStub.getCall(0).args[3]['X-Requested-With'])
+                               .toEqual('XMLHttpRequest');
+
+                       deferredRequest.resolve({
+                               status: 207,
+                               body: [
+                                       // root element
+                                       {
+                                               href: 'http://example.org/owncloud/remote.php/test/',
+                                               propStat: []
+                                       },
+                                       // first model
+                                       {
+                                               href: 'http://example.org/owncloud/remote.php/test/123',
+                                               propStat: [{
+                                                       status: 'HTTP/1.1 200 OK',
+                                                       properties: {
+                                                               '{http://owncloud.org/ns}first-name': 'Hello',
+                                                               '{http://owncloud.org/ns}last-name': 'World'
+                                                       }
+                                               }]
+                                       },
+                                       // second model
+                                       {
+                                               href: 'http://example.org/owncloud/remote.php/test/456',
+                                               propStat: [{
+                                                       status: 'HTTP/1.1 200 OK',
+                                                       properties: {
+                                                               '{http://owncloud.org/ns}first-name': 'Test',
+                                                               '{http://owncloud.org/ns}last-name': 'Person'
+                                                       }
+                                               }]
+                                       }
+                               ]
+                       });
+
+                       expect(collection.length).toEqual(2);
+
+                       var model = collection.get('123');
+                       expect(model.id).toEqual('123');
+                       expect(model.get('firstName')).toEqual('Hello');
+                       expect(model.get('lastName')).toEqual('World');
+
+                       model = collection.get('456');
+                       expect(model.id).toEqual('456');
+                       expect(model.get('firstName')).toEqual('Test');
+                       expect(model.get('lastName')).toEqual('Person');
+
+                       expect(successStub.calledOnce).toEqual(true);
+                       expect(errorStub.notCalled).toEqual(true);
+               });
+
+               function testMethodError(doCall) {
+                       var successStub = sinon.stub();
+                       var errorStub = sinon.stub();
+
+                       doCall(successStub, errorStub);
+
+                       deferredRequest.resolve({
+                               status: 404,
+                               body: ''
+                       });
+
+                       expect(successStub.notCalled).toEqual(true);
+                       expect(errorStub.calledOnce).toEqual(true);
+               }
+
+               it('calls error handler if error status in PROPFIND response', function() {
+                       testMethodError(function(success, error) {
+                               var collection = new TestCollection();
+                               collection.fetch({
+                                       success: success,
+                                       error: error
+                               });
+                       });
+               });
+               it('calls error handler if error status in POST response', function() {
+                       testMethodError(function(success, error) {
+                               var collection = new TestCollection();
+                               collection.create({
+                                       firstName: 'Hello',
+                                       lastName: 'World'
+                               }, {
+                                       success: success,
+                                       error: error
+                               });
+                       });
+               });
+       });
+       describe('models', function() {
+               var TestModel;
+               beforeEach(function() {
+                       TestModel = OC.Backbone.Model.extend({
+                               sync: OC.Backbone.davSync,
+                               davProperties: {
+                                       'firstName': '{http://owncloud.org/ns}first-name',
+                                       'lastName': '{http://owncloud.org/ns}last-name',
+                               },
+                               url: function() {
+                                       return 'http://example.com/owncloud/remote.php/test/' + this.id;
+                               }
+                       });
+               });
+
+               it('makes a PROPPATCH request to update model', function() {
+                       var model = new TestModel({
+                               id: '123',
+                               firstName: 'Hello',
+                               lastName: 'World'
+                       });
+
+                       model.save({
+                               firstName: 'Hey'
+                       });
+
+                       expect(davClientPropPatchStub.calledOnce).toEqual(true);
+                       expect(davClientPropPatchStub.getCall(0).args[0])
+                               .toEqual('http://example.com/owncloud/remote.php/test/123');
+                       expect(davClientPropPatchStub.getCall(0).args[1])
+                               .toEqual({
+                                       '{http://owncloud.org/ns}first-name': 'Hey'
+                               });
+                       expect(davClientPropPatchStub.getCall(0).args[2]['X-Requested-With'])
+                               .toEqual('XMLHttpRequest');
+
+                       deferredRequest.resolve({
+                               status: 201,
+                               body: ''
+                       });
+
+                       expect(model.id).toEqual('123');
+                       expect(model.get('firstName')).toEqual('Hey');
+               });
+
+               it('uses PROPFIND to fetch single model', function() {
+                       var model = new TestModel({
+                               id: '123'
+                       });
+
+                       model.fetch();
+
+                       expect(davClientPropFindStub.calledOnce).toEqual(true);
+                       expect(davClientPropFindStub.getCall(0).args[0])
+                               .toEqual('http://example.com/owncloud/remote.php/test/123');
+                       expect(davClientPropFindStub.getCall(0).args[1])
+                               .toEqual([
+                                       '{http://owncloud.org/ns}first-name',
+                                       '{http://owncloud.org/ns}last-name'
+                               ]);
+                       expect(davClientPropFindStub.getCall(0).args[2])
+                               .toEqual(0);
+                       expect(davClientPropFindStub.getCall(0).args[3]['X-Requested-With'])
+                               .toEqual('XMLHttpRequest');
+
+                       deferredRequest.resolve({
+                               status: 207,
+                               body: {
+                                       href: 'http://example.org/owncloud/remote.php/test/123',
+                                       propStat: [{
+                                               status: 'HTTP/1.1 200 OK',
+                                               properties: {
+                                                       '{http://owncloud.org/ns}first-name': 'Hello',
+                                                       '{http://owncloud.org/ns}last-name': 'World'
+                                               }
+                                       }]
+                               }
+                       });
+
+                       expect(model.id).toEqual('123');
+                       expect(model.get('firstName')).toEqual('Hello');
+                       expect(model.get('lastName')).toEqual('World');
+               });
+               it('makes a DELETE request to destroy model', function() {
+                       var model = new TestModel({
+                               id: '123',
+                               firstName: 'Hello',
+                               lastName: 'World'
+                       });
+
+                       model.destroy();
+
+                       expect(davClientRequestStub.calledOnce).toEqual(true);
+                       expect(davClientRequestStub.getCall(0).args[0])
+                               .toEqual('DELETE');
+                       expect(davClientRequestStub.getCall(0).args[1])
+                               .toEqual('http://example.com/owncloud/remote.php/test/123');
+                       expect(davClientRequestStub.getCall(0).args[2]['X-Requested-With'])
+                               .toEqual('XMLHttpRequest');
+                       expect(davClientRequestStub.getCall(0).args[3])
+                               .toBeFalsy();
+
+                       deferredRequest.resolve({
+                               status: 200,
+                               body: ''
+                       });
+               });
+
+               function testMethodError(doCall) {
+                       var successStub = sinon.stub();
+                       var errorStub = sinon.stub();
+
+                       doCall(successStub, errorStub);
+
+                       deferredRequest.resolve({
+                               status: 404,
+                               body: ''
+                       });
+
+                       expect(successStub.notCalled).toEqual(true);
+                       expect(errorStub.calledOnce).toEqual(true);
+               }
+
+               it('calls error handler if error status in PROPFIND response', function() {
+                       testMethodError(function(success, error) {
+                               var model = new TestModel();
+                               model.fetch({
+                                       success: success,
+                                       error: error
+                               });
+                       });
+               });
+               it('calls error handler if error status in PROPPATCH response', function() {
+                       testMethodError(function(success, error) {
+                               var model = new TestModel();
+                               model.save({
+                                       firstName: 'Hey'
+                               }, {
+                                       success: success,
+                                       error: error
+                               });
+                       });
+               });
+       });
+});
+
index 18bbf13f3cd8e474e04dc68e0d75743b098ec92c..1a73c7db020b7c5c04e957f50624e9c1d3e7453c 100644 (file)
@@ -1,5 +1,19 @@
 if (typeof dav == 'undefined') { dav = {}; };
 
+dav._XML_CHAR_MAP = {
+       '<': '&lt;',
+       '>': '&gt;',
+       '&': '&amp;',
+       '"': '&quot;',
+       "'": '&apos;'
+};
+
+dav._escapeXml = function(s) {
+       return s.replace(/[<>&"']/g, function (ch) {
+               return dav._XML_CHAR_MAP[ch];
+       });
+};
+
 dav.Client = function(options) {
     var i;
     for(i in options) {
@@ -85,6 +99,57 @@ dav.Client.prototype = {
 
     },
 
+    /**
+     * Generates a propPatch request.
+     *
+     * @param {string} url Url to do the proppatch request on
+     * @param {Array} properties List of properties to store.
+     * @return {Promise}
+     */
+    propPatch : function(url, properties, headers) {
+        headers = headers || {};
+
+        headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+        var body =
+            '<?xml version="1.0"?>\n' +
+            '<d:propertyupdate ';
+        var namespace;
+        for (namespace in this.xmlNamespaces) {
+            body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"';
+        }
+        body += '>\n' +
+            '  <d:set>\n' +
+            '   <d:prop>\n';
+
+        for(var ii in properties) {
+
+            var property = this.parseClarkNotation(ii);
+            var propName;
+            var propValue = properties[ii];
+            if (this.xmlNamespaces[property.namespace]) {
+                propName = this.xmlNamespaces[property.namespace] + ':' + property.name;
+            } else {
+                propName = 'x:' + property.name + ' xmlns:x="' + property.namespace + '"';
+            }
+            body += '      <' + propName + '>' + dav._escapeXml(propValue) + '</' + propName + '>\n';
+        }
+        body+='    </d:prop>\n';
+        body+='  </d:set>\n';
+        body+='</d:propertyupdate>';
+
+        return this.request('PROPPATCH', url, headers, body).then(
+            function(result) {
+                return {
+                    status: result.status,
+                    body: result.body,
+                    xhr: result.xhr
+                };
+            }.bind(this)
+        );
+
+    },
+
     /**
      * Performs a HTTP request, and returns a Promise
      *