"oc-dialogs.js",
"js.js",
"oc-backbone.js",
+ "oc-backbone-webdav.js",
"l10n.js",
"apps.js",
"share.js",
--- /dev/null
+/*
+ * 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);
+
* See the COPYING-README file.
*
*/
+
+/* global Backbone */
if(!_.isUndefined(Backbone)) {
OC.Backbone = Backbone.noConflict();
}
--- /dev/null
+/**
+* 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
+ });
+ });
+ });
+ });
+});
+
if (typeof dav == 'undefined') { dav = {}; };
+dav._XML_CHAR_MAP = {
+ '<': '<',
+ '>': '>',
+ '&': '&',
+ '"': '"',
+ "'": '''
+};
+
+dav._escapeXml = function(s) {
+ return s.replace(/[<>&"']/g, function (ch) {
+ return dav._XML_CHAR_MAP[ch];
+ });
+};
+
dav.Client = function(options) {
var i;
for(i in options) {
},
+ /**
+ * Generates a propPatch request.
+ *
+ * @param {string} url Url to do the proppatch request on
+ * @param {Array} properties List of properties to store.
+ * @return {Promise}
+ */
+ propPatch : function(url, properties, headers) {
+ headers = headers || {};
+
+ headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+ var body =
+ '<?xml version="1.0"?>\n' +
+ '<d:propertyupdate ';
+ var namespace;
+ for (namespace in this.xmlNamespaces) {
+ body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"';
+ }
+ body += '>\n' +
+ ' <d:set>\n' +
+ ' <d:prop>\n';
+
+ for(var ii in properties) {
+
+ var property = this.parseClarkNotation(ii);
+ var propName;
+ var propValue = properties[ii];
+ if (this.xmlNamespaces[property.namespace]) {
+ propName = this.xmlNamespaces[property.namespace] + ':' + property.name;
+ } else {
+ propName = 'x:' + property.name + ' xmlns:x="' + property.namespace + '"';
+ }
+ body += ' <' + propName + '>' + dav._escapeXml(propValue) + '</' + propName + '>\n';
+ }
+ body+=' </d:prop>\n';
+ body+=' </d:set>\n';
+ body+='</d:propertyupdate>';
+
+ return this.request('PROPPATCH', url, headers, body).then(
+ function(result) {
+ return {
+ status: result.status,
+ body: result.body,
+ xhr: result.xhr
+ };
+ }.bind(this)
+ );
+
+ },
+
/**
* Performs a HTTP request, and returns a Promise
*