From ee322bae1fcf09af062fcf0ea5c54a145f5c9942 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 8 Feb 2017 18:21:49 +0100 Subject: [PATCH 1/2] Add Webdav specific models and collections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit f17e39142a72fcfb786a495c0cb9b021cd7b9281) Signed-off-by: Daniel Calviño Sánchez --- core/js/oc-backbone-webdav.js | 160 +++++++- core/js/tests/specs/oc-backbone-webdavSpec.js | 355 ++++++++++++++++++ 2 files changed, 496 insertions(+), 19 deletions(-) diff --git a/core/js/oc-backbone-webdav.js b/core/js/oc-backbone-webdav.js index 4e3f11ce09111..d95f783d3d9ee 100644 --- a/core/js/oc-backbone-webdav.js +++ b/core/js/oc-backbone-webdav.js @@ -142,8 +142,8 @@ var changedProp = davProperties[key]; var value = attrs[key]; if (!changedProp) { - console.warn('No matching DAV property for property "' + key); - changedProp = key; + // no matching DAV property for property, skip + continue; } if (_.isBoolean(value) || _.isNumber(value)) { // convert to string @@ -179,10 +179,10 @@ }); } - function callPropPatch(client, options, model, headers) { + function callPropPatch(client, options, model, headers, changed) { return client.propPatch( options.url, - convertModelAttributesToDavProperties(model.changed, options.davProperties), + convertModelAttributesToDavProperties(changed || model.changed, options.davProperties), headers ).then(function(result) { if (isSuccessStatus(result.status)) { @@ -213,7 +213,7 @@ return; } - callPropPatch(client, options, model, headers); + callPropPatch(client, options, model, headers, model.attributes); }); } @@ -282,24 +282,57 @@ } /** - * DAV transport + * */ - function davSync(method, model, options) { - var params = {type: methodMap[method] || method}; - var isCollection = (model instanceof Backbone.Collection); + function getTypeForMethod(method, model) { + var type = methodMap[method]; + + if (!type) { + // return method directly + return method; + } + + // TODO: use special attribute "resourceType" instead + var isWebdavCollection = model instanceof WebdavCollectionNode; - if (method === 'update') { - // if a model has an inner collection, it must define an - // attribute "hasInnerCollection" that evaluates to true - if (model.hasInnerCollection) { - // if the model itself is a Webdav collection, use MKCOL - params.type = 'MKCOL'; - } else if (model.usePUT || (model.collection && model.collection.usePUT)) { - // use PUT instead of PROPPATCH - params.type = 'PUT'; + // need to override default behavior and decide what to do + if (method === 'create') { + if (isWebdavCollection) { + if (!_.isUndefined(model.id)) { + // create new collection with known id + type = 'MKCOL'; + } else { + // unsupported + throw 'Cannot create Webdav collection without id'; + } + } else { + if (!_.isUndefined(model.id)) { + // need to create it first + type = 'PUT'; + } else { + // creating without known id, will receive it after creation + type = 'POST'; + } + } + } else if (method === 'update') { + // it exists, only update properties + type = 'PROPPATCH'; + // force PUT usage ? + if (model.usePUT || (model.collection && model.collection.usePUT)) { + type = 'PUT'; } } + return type; + } + + /** + * DAV transport + */ + function davSync(method, model, options) { + var params = {type: getTypeForMethod(method, model)}; + var isCollection = (model instanceof Backbone.Collection); + // Ensure that we have a URL. if (!options.url) { params.url = _.result(model, 'url') || urlError(); @@ -315,7 +348,7 @@ params.processData = false; } - if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') { + if (params.type === 'PROPFIND' || params.type === 'PROPPATCH' || params.type === 'MKCOL') { var davProperties = model.davProperties; if (!davProperties && model.model) { // use dav properties from model in case of collection @@ -356,9 +389,98 @@ return xhr; } + + /** + * Regular Webdav leaf node + */ + var WebdavNode = Backbone.Model.extend({ + sync: davSync, + + constructor: function() { + this.on('sync', this._onSync, this); + this._isNew = true; + Backbone.Model.prototype.constructor.apply(this, arguments); + }, + + _onSync: function() { + this._isNew = false; + }, + + isNew: function() { + // we can't rely on the id so use a dummy attribute + return !!this._isNew; + } + }); + + /** + * Children collection for a Webdav collection node + */ + var WebdavChildrenCollection = Backbone.Collection.extend({ + sync: davSync, + + collectionNode: null, + model: WebdavNode, + + constructor: function() { + this.on('sync', this._onSync, this); + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, + + initialize: function(models, options) { + options = options || {}; + + this.collectionNode = options.collectionNode; + + return Backbone.Collection.prototype.initialize.apply(this, arguments); + }, + + _onSync: function(model) { + if (model instanceof Backbone.Model) { + // since we saved, mark as non-new + if (!_.isUndefined(model._isNew)) { + model._isNew = false; + } + } else { + // since we fetched, mark models as non-new + model.each(function(model) { + if (!_.isUndefined(model._isNew)) { + model._isNew = false; + } + }); + } + }, + + url: function() { + return this.collectionNode.url(); + } + }); + + /** + * Webdav collection which is a special node, represented by a backbone model + * and a sub-collection for its children. + */ + var WebdavCollectionNode = WebdavNode.extend({ + sync: davSync, + + childrenCollectionClass: WebdavChildrenCollection, + + _childrenCollection: null, + + getChildrenCollection: function() { + if (!this._childrenCollection) { + this._childrenCollection = new this.childrenCollectionClass([], {collectionNode: this}); + } + return this._childrenCollection; + } + }); + // exports Backbone.davCall = davCall; Backbone.davSync = davSync; + Backbone.WebdavNode = WebdavNode; + Backbone.WebdavChildrenCollection = WebdavChildrenCollection; + Backbone.WebdavCollectionNode = WebdavCollectionNode; + })(OC.Backbone); diff --git a/core/js/tests/specs/oc-backbone-webdavSpec.js b/core/js/tests/specs/oc-backbone-webdavSpec.js index 97281e982ce10..ed9b17d4cd3ae 100644 --- a/core/js/tests/specs/oc-backbone-webdavSpec.js +++ b/core/js/tests/specs/oc-backbone-webdavSpec.js @@ -386,5 +386,360 @@ describe('Backbone Webdav extension', function() { }); }); }); + + + describe('WebdavNode', function() { + var NodeModel; + + beforeEach(function() { + NodeModel = OC.Backbone.WebdavNode.extend({ + url: function() { + return 'http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/' + this.id; + }, + davProperties: { + 'firstName': '{http://owncloud.org/ns}first-name', + 'lastName': '{http://owncloud.org/ns}last-name' + } + }); + }); + it('isNew at creation time even with an id set', function() { + var model = new NodeModel({ + id: 'someuri' + }); + expect(model.isNew()).toEqual(true); + }); + it('is not new as soon as fetched', function() { + var model = new NodeModel({ + id: 'someuri' + }); + model.fetch(); + + expect(davClientPropFindStub.calledOnce).toEqual(true); + expect(davClientPropFindStub.getCall(0).args[0]) + .toEqual('http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/someuri'); + 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/dav/endpoint/nodemodel/someuri', + propStat: [{ + status: 'HTTP/1.1 200 OK', + properties: { + '{http://owncloud.org/ns}first-name': 'Hello', + '{http://owncloud.org/ns}last-name': 'World' + } + }] + } + }); + expect(model.isNew()).toEqual(false); + }); + it('saves new model with PUT', function() { + var model = new NodeModel({ + id: 'someuri' + }); + model.save({ + firstName: 'Hello', + lastName: 'World', + }); + + // PUT + expect(davClientRequestStub.calledOnce).toEqual(true); + expect(davClientRequestStub.getCall(0).args[0]) + .toEqual('PUT'); + expect(davClientRequestStub.getCall(0).args[1]) + .toEqual('http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/someuri'); + expect(davClientRequestStub.getCall(0).args[2]['X-Requested-With']) + .toEqual('XMLHttpRequest'); + expect(davClientRequestStub.getCall(0).args[3]) + .toEqual(JSON.stringify({ + id: 'someuri', + firstName: 'Hello', + lastName: 'World' + })); + + deferredRequest.resolve({ + status: 201, + body: '', + xhr: { + getResponseHeader: _.noop + } + }); + + expect(model.id).toEqual('someuri'); + expect(model.isNew()).toEqual(false); + }); + it('updates existing model with PROPPATCH', function() { + var model = new NodeModel({ + id: 'someuri' + }); + + model.fetch(); + + // from here, the model will exist + expect(davClientPropFindStub.calledOnce).toEqual(true); + expect(davClientPropFindStub.getCall(0).args[0]) + .toEqual('http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/someuri'); + 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.com/owncloud/remote.php/dav/endpoint/nodemodel/someuri', + propStat: [{ + status: 'HTTP/1.1 200 OK', + properties: { + '{http://owncloud.org/ns}first-name': 'Hello', + '{http://owncloud.org/ns}last-name': 'World' + } + }] + } + }); + + expect(model.isNew()).toEqual(false); + + model.save({ + firstName: 'Hey', + }); + + expect(davClientPropPatchStub.calledOnce).toEqual(true); + expect(davClientPropPatchStub.getCall(0).args[0]) + .toEqual('http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/someuri'); + 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: '', + xhr: { + getResponseHeader: _.noop + } + }); + + expect(model.isNew()).toEqual(false); + }); + }); + + describe('WebdavCollectionNode and WebdavChildrenCollection', function() { + var NodeModel; + var ChildrenCollection; + + beforeEach(function() { + ChildModel = OC.Backbone.WebdavNode.extend({ + url: function() { + return 'http://example.com/owncloud/remote.php/dav/davcol/' + this.id; + }, + davProperties: { + 'firstName': '{http://owncloud.org/ns}first-name', + 'lastName': '{http://owncloud.org/ns}last-name' + } + }); + ChildrenCollection = OC.Backbone.WebdavChildrenCollection.extend({ + model: ChildModel + }); + + NodeModel = OC.Backbone.WebdavCollectionNode.extend({ + childrenCollectionClass: ChildrenCollection, + url: function() { + return 'http://example.com/owncloud/remote.php/dav/' + this.id; + }, + davProperties: { + 'firstName': '{http://owncloud.org/ns}first-name', + 'lastName': '{http://owncloud.org/ns}last-name' + } + }); + }); + + it('returns the children collection pointing to the same url', function() { + var model = new NodeModel({ + id: 'davcol' + }); + + var collection = model.getChildrenCollection(); + expect(collection instanceof ChildrenCollection).toEqual(true); + + collection.instanceCheck = true; + + // returns the same instance + var collection2 = model.getChildrenCollection(); + expect(collection2.instanceCheck).toEqual(true); + + expect(collection.url()).toEqual(model.url()); + }); + + it('resets isNew to false for every model after fetching', function() { + var model = new NodeModel({ + id: 'davcol' + }); + + var collection = model.getChildrenCollection(); + collection.fetch(); + + expect(davClientPropFindStub.calledOnce).toEqual(true); + expect(davClientPropFindStub.getCall(0).args[0]) + .toEqual('http://example.com/owncloud/remote.php/dav/davcol'); + 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.com/owncloud/remote.php/dav/davcol/', + propStat: [] + }, + // first model + { + href: 'http://example.com/owncloud/remote.php/dav/davcol/hello', + 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.com/owncloud/remote.php/dav/davcol/test', + 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); + + expect(collection.at(0).url()).toEqual('http://example.com/owncloud/remote.php/dav/davcol/hello'); + expect(collection.at(0).isNew()).toEqual(false); + expect(collection.at(1).url()).toEqual('http://example.com/owncloud/remote.php/dav/davcol/test'); + expect(collection.at(1).isNew()).toEqual(false); + }); + + it('creates the Webdav collection with MKCOL', function() { + var model = new NodeModel({ + id: 'davcol' + }); + model.save({ + firstName: 'Hello', + lastName: 'World', + }); + + expect(davClientRequestStub.calledOnce).toEqual(true); + expect(davClientRequestStub.getCall(0).args[0]) + .toEqual('MKCOL'); + expect(davClientRequestStub.getCall(0).args[1]) + .toEqual('http://example.com/owncloud/remote.php/dav/davcol'); + expect(davClientRequestStub.getCall(0).args[2]['X-Requested-With']) + .toEqual('XMLHttpRequest'); + expect(davClientRequestStub.getCall(0).args[3]) + .toEqual(null); + + deferredRequest.resolve({ + status: 201, + body: '', + xhr: { + getResponseHeader: _.noop + } + }); + + expect(model.id).toEqual('davcol'); + expect(model.isNew()).toEqual(false); + }); + it('updates Webdav collection properties with PROPPATCH', function() { + var model = new NodeModel({ + id: 'davcol' + }); + + model.fetch(); + + // from here, the model will exist + expect(davClientPropFindStub.calledOnce).toEqual(true); + expect(davClientPropFindStub.getCall(0).args[0]) + .toEqual('http://example.com/owncloud/remote.php/dav/davcol'); + 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.com/owncloud/remote.php/dav/davcol', + propStat: [{ + status: 'HTTP/1.1 200 OK', + properties: { + '{http://owncloud.org/ns}first-name': 'Hello', + '{http://owncloud.org/ns}last-name': 'World' + } + }] + } + }); + + expect(model.isNew()).toEqual(false); + + model.save({ + firstName: 'Hey', + }); + + expect(davClientPropPatchStub.calledOnce).toEqual(true); + expect(davClientPropPatchStub.getCall(0).args[0]) + .toEqual('http://example.com/owncloud/remote.php/dav/davcol'); + 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: '', + xhr: { + getResponseHeader: _.noop + } + }); + + expect(model.isNew()).toEqual(false); + }); + }); }); From 57ef3037327e721cd342009c6234ba4a24beb2a2 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 27 Mar 2017 15:20:59 +0200 Subject: [PATCH 2/2] Properly decode id from URI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When parsing an href to get the id of a WebdavNode, make sure to also decode it. (cherry picked from commit d2e45321e8f7586f3086d7e521aeb887aa2d06b6) Signed-off-by: Daniel Calviño Sánchez --- core/js/oc-backbone-webdav.js | 2 +- core/js/tests/specs/oc-backbone-webdavSpec.js | 44 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/core/js/oc-backbone-webdav.js b/core/js/oc-backbone-webdav.js index d95f783d3d9ee..667bf1c955368 100644 --- a/core/js/oc-backbone-webdav.js +++ b/core/js/oc-backbone-webdav.js @@ -128,7 +128,7 @@ // so we take the part before that } while (!result && parts.length > 0); - return result; + return decodeURIComponent(result); } function isSuccessStatus(status) { diff --git a/core/js/tests/specs/oc-backbone-webdavSpec.js b/core/js/tests/specs/oc-backbone-webdavSpec.js index ed9b17d4cd3ae..f5e441c52394d 100644 --- a/core/js/tests/specs/oc-backbone-webdavSpec.js +++ b/core/js/tests/specs/oc-backbone-webdavSpec.js @@ -229,7 +229,7 @@ describe('Backbone Webdav extension', function() { 'married': '{http://owncloud.org/ns}married', // bool }, url: function() { - return 'http://example.com/owncloud/remote.php/test/' + this.id; + return 'http://example.com/owncloud/remote.php/test/' + encodeURIComponent(this.id); }, parse: function(data) { return { @@ -394,7 +394,7 @@ describe('Backbone Webdav extension', function() { beforeEach(function() { NodeModel = OC.Backbone.WebdavNode.extend({ url: function() { - return 'http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/' + this.id; + return 'http://example.com/owncloud/remote.php/dav/endpoint/nodemodel/' + encodeURIComponent(this.id); }, davProperties: { 'firstName': '{http://owncloud.org/ns}first-name', @@ -547,7 +547,7 @@ describe('Backbone Webdav extension', function() { beforeEach(function() { ChildModel = OC.Backbone.WebdavNode.extend({ url: function() { - return 'http://example.com/owncloud/remote.php/dav/davcol/' + this.id; + return 'http://example.com/owncloud/remote.php/dav/davcol/' + encodeURIComponent(this.id); }, davProperties: { 'firstName': '{http://owncloud.org/ns}first-name', @@ -561,7 +561,7 @@ describe('Backbone Webdav extension', function() { NodeModel = OC.Backbone.WebdavCollectionNode.extend({ childrenCollectionClass: ChildrenCollection, url: function() { - return 'http://example.com/owncloud/remote.php/dav/' + this.id; + return 'http://example.com/owncloud/remote.php/dav/' + encodeURIComponent(this.id); }, davProperties: { 'firstName': '{http://owncloud.org/ns}first-name', @@ -649,6 +649,42 @@ describe('Backbone Webdav extension', function() { expect(collection.at(1).isNew()).toEqual(false); }); + it('parses id from href if no id was queried', function() { + var model = new NodeModel({ + id: 'davcol' + }); + + var collection = model.getChildrenCollection(); + collection.fetch(); + + deferredRequest.resolve({ + status: 207, + body: [ + // root element + { + href: 'http://example.com/owncloud/remote.php/dav/davcol/', + propStat: [] + }, + // first model + { + href: 'http://example.com/owncloud/remote.php/dav/davcol/sub%40thing', + propStat: [{ + status: 'HTTP/1.1 200 OK', + properties: { + '{http://owncloud.org/ns}first-name': 'Hello', + '{http://owncloud.org/ns}last-name': 'World' + } + }] + } + ] + }); + + expect(collection.length).toEqual(1); + + expect(collection.at(0).id).toEqual('sub@thing'); + expect(collection.at(0).url()).toEqual('http://example.com/owncloud/remote.php/dav/davcol/sub%40thing'); + }); + it('creates the Webdav collection with MKCOL', function() { var model = new NodeModel({ id: 'davcol'