diff options
Diffstat (limited to 'core/js/oc-backbone-webdav.js')
-rw-r--r-- | core/js/oc-backbone-webdav.js | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/core/js/oc-backbone-webdav.js b/core/js/oc-backbone-webdav.js new file mode 100644 index 00000000000..3ca31902c93 --- /dev/null +++ b/core/js/oc-backbone-webdav.js @@ -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); + |