328 lines
8.3 KiB
JavaScript
328 lines
8.3 KiB
JavaScript
/*
|
|
* 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) {
|
|
if (_.isArray(result)) {
|
|
return _.map(result, function(subResult) {
|
|
return parsePropFindResult(subResult, 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 (key in davProperties) {
|
|
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];
|
|
var value = attrs[key];
|
|
if (!changedProp) {
|
|
console.warn('No matching DAV property for property "' + key);
|
|
changedProp = key;
|
|
}
|
|
if (_.isBoolean(value) || _.isNumber(value)) {
|
|
// convert to string
|
|
value = '' + value;
|
|
}
|
|
props[changedProp] = value;
|
|
}
|
|
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 = parsePropFindResult(response.body, propsMapping);
|
|
if (options.depth > 0) {
|
|
// discard root entry
|
|
results.shift();
|
|
}
|
|
|
|
options.success(results);
|
|
return;
|
|
}
|
|
} else if (_.isFunction(options.error)) {
|
|
options.error(response);
|
|
}
|
|
});
|
|
}
|
|
|
|
function callPropPatch(client, options, model, headers) {
|
|
return 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;
|
|
}
|
|
// if multi-status, parse
|
|
if (result.status === 207) {
|
|
var propsMapping = _.invert(options.davProperties);
|
|
options.success(parsePropFindResult(result.body, propsMapping));
|
|
} else {
|
|
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] || 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);
|
|
|