nextcloud/core/js/oc-backbone-webdav.js
Vincent Petry 6488ed3cff
Backbone Webdav Adapter MKCOL support
Usually Backbone collections cannot be created and just simply exists.
But in the Webdav world they need to be creatable.

This enhancement makes it possible to use a Backbone Model to represent
such collections and when creating it, it will use MKCOL instead of PUT.

Signed-off-by: Morris Jobke <hey@morrisjobke.de>
2017-03-17 00:08:48 -06:00

365 lines
9.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('/');
var result;
do {
result = parts[parts.length - 1];
parts.pop();
// note: first result can be empty when there is a trailing slash,
// so we take the part before that
} while (!result && parts.length > 0);
return result;
}
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 callMkCol(client, options, model, headers) {
// call MKCOL without data, followed by PROPPATCH
return client.request(
options.type,
options.url,
headers,
null
).then(function(result) {
if (!isSuccessStatus(result.status)) {
if (_.isFunction(options.error)) {
options.error(result);
}
return;
}
callPropPatch(client, options, model, headers);
});
}
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' || options.type === 'MKCOL') {
// 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',
'requesttoken': OC.requestToken
}, options.headers);
if (options.type === 'PROPFIND') {
return callPropFind(client, options, model, headers);
} else if (options.type === 'PROPPATCH') {
return callPropPatch(client, options, model, headers);
} else if (options.type === 'MKCOL') {
return callMkCol(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') {
// 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';
}
}
// 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);