parent
7a239b2642
commit
857c316bda
@ -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); |
||||
|
@ -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 |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
Loading…
Reference in new issue