PageSourceSearch

https://portal.owncloud.com/core/js/oc-backbone-webdav.js?v=dd2774748f47720d9deea7a9f29fea34

js owncloud.com collected 2026-09-24 08:49:11 UTC 13,716 bytes, 528 lines download raw bytes

1/*
2 * Copyright (c) 2015
3 *
4 * This file is licensed under the Affero General Public License version 3
5 * or later.
6 *
7 * See the COPYING-README file.
8 *
9 */
10
11/**
12 * Webdav transport for Backbone.
13 *
14 * This makes it possible to use Webdav endpoints when
15 * working with Backbone models and collections.
16 *
17 * Requires the davclient.js library.
18 *
19 * Usage example:
20 *
21 *     var PersonModel = OC.Backbone.Model.extend({
22 *         // make it use the DAV transport
23 *         sync: OC.Backbone.davSync,
24 *
25 *         // DAV properties mapping
26 *         davProperties: {
27 *             'id': '{http://example.com/ns}id',
28 *             'firstName': '{http://example.com/ns}first-name',
29 *             'lastName': '{http://example.com/ns}last-name',
30 *             'age': '{http://example.com/ns}age'
31 *         },
32 *
33 *         // additional parsing, if needed
34 *         parse: function(props) {
35 *             // additional parsing (DAV property values are always strings)
36 *             props.age = parseInt(props.age, 10);
37 *             return props;
38 *         }
39 *     });
40 *
41 *     var PersonCollection = OC.Backbone.Collection.extend({
42 *         // make it use the DAV transport
43 *         sync: OC.Backbone.davSync,
44 *
45 *         // use person model
46 *         // note that davProperties will be inherited
47 *         model: PersonModel,
48 *
49 *         // DAV collection URL
50 *         url: function() {
51 *             return OC.linkToRemote('dav') + '/person/';
52 *         },
53 *     });
54 */
55
56/* global dav */
57
58(function(Backbone) {
59	var methodMap = {
60		'create': 'POST',
61		'update': 'PROPPATCH',
62		'patch':  'PROPPATCH',
63		'delete': 'DELETE',
64		'read':   'PROPFIND'
65	};
66
67	// Throw an error when a URL is needed, and none is supplied.
68	function urlError() {
69		throw new Error('A "url" property or function must be specified');
70	}
71
72	/**
73	 * Convert a single propfind result to JSON
74	 *
75	 * @param {Object} result
76	 * @param {Object} davProperties properties mapping
77	 */
78	function parsePropFindResult(result, davProperties) {
79		if (_.isArray(result)) {
80			return _.map(result, function(subResult) {
81				return parsePropFindResult(subResult, davProperties);
82			});
83		}
84		var props = {
85			href: result.href
86		};
87
88		_.each(result.propStat, function(propStat) {
89			if (propStat.status !== 'HTTP/1.1 200 OK') {
90				return;
91			}
92
93			for (var key in propStat.properties) {
94				var propKey = key;
95				if (key in davProperties) {
96					propKey = davProperties[key];
97				}
98				props[propKey] = propStat.properties[key];
99			}
100		});
101
102		if (!props.id) {
103			// parse id from href
104			props.id = parseIdFromLocation(props.href);
105		}
106
107		return props;
108	}
109
110	/**
111	 * Parse ID from location
112	 *
113	 * @param {string} url url
114	 * @return {string} id
115	 */
116	function parseIdFromLocation(url) {
117		var queryPos = url.indexOf('?');
118		if (queryPos > 0) {
119			url = url.substr(0, queryPos);
120		}
121
122		var parts = url.split('/');
123		var result;
124		do {
125			result = parts[parts.length - 1];
126			parts.pop();
127			// note: first result can be empty when there is a trailing slash,
128			// so we take the part before that
129		} while (!result && parts.length > 0);
130
131		return decodeURIComponent(result);
132	}
133
134	function isSuccessStatus(status) {
135		return status >= 200 && status <= 299;
136	}
137
138	function convertModelAttributesToDavProperties(attrs, davProperties) {
139		var props = {};
140		var key;
141		for (key in attrs) {
142			var changedProp = davProperties[key];
143			var value = attrs[key];
144			if (!changedProp) {
145				// no matching DAV property for property, skip
146				continue;
147			}
148			if (_.isBoolean(value) || _.isNumber(value)) {
149				// convert to string
150				value = '' + value;
151			}
152			props[changedProp] = value;
153		}
154		return props;
155	}
156
157	function callPropFind(client, options, model, headers) {
158		return client.propFind(
159			options.url,
160			_.values(options.davProperties) || [],
161			options.depth,
162			headers
163		).then(function(response) {
164			if (isSuccessStatus(response.status)) {
165				if (_.isFunction(options.success)) {
166					var propsMapping = _.invert(options.davProperties);
167					var results = parsePropFindResult(response.body, propsMapping);
168					if (options.depth > 0) {
169						// discard root entry
170						results.shift();
171					}
172
173					options.success(results);
174					return;
175				}
176			} else if (_.isFunction(options.error)) {
177				options.error(response);
178			}
179		});
180	}
181
182	function callPropPatch(client, options, model, headers) {
183		var changes = model.changed;
184		if (options.wait && _.isEmpty(changes)) {
185			// usually with "wait" mode, the changes aren't set yet,
186			changes = options.data;
187
188			// if options.patch is not set, then data contains all the data
189			// instead of just the properties to patch
190			if (!options.patch) {
191				// remove reserved properties
192				delete changes.href;
193				delete changes[_.result(model, 'idAttribute')];
194
195				// note: there is no way to diff with previous values here so
196				// we just send everything
197			}
198		}
199		return client.propPatch(
200			options.url,
201			convertModelAttributesToDavProperties(changes, options.davProperties),
202			headers
203		).then(function(result) {
204			if (result.status === 207 && result.body && result.body.length > 0) {
205				if (_.find(result.body[0].propStat, function(propStat) {
206					var statusCode = parseInt(propStat.status.split(' ')[1], 10);
207					return statusCode >= 400;
208				})) {
209					// in REST, validation errors are usually represented with 422 Unprocessable Entity,
210					result.status = 422;
211				}
212			}
213
214			if (isSuccessStatus(result.status)) {
215				// with wait, we set the changes only after success
216				if (options.wait) {
217					model.set(changes, options);
218				}
219
220				if (_.isFunction(options.success)) {
221					// pass the object's own values because the server
222					// does not return the updated model
223					options.success(model.toJSON());
224				}
225			} else if (_.isFunction(options.error)) {
226				options.error(result);
227			}
228		});
229
230	}
231
232	function callMkCol(client, options, model, headers) {
233		var props = convertModelAttributesToDavProperties(model.attributes, options.davProperties);
234		if (!props['{DAV:}resourcetype']) {
235			props['{DAV:}resourcetype'] = '<d:collection/>';
236		}
237		return client.mkcol(
238			options.url,
239			props,
240			headers
241		).then(function(result) {
242			if (isSuccessStatus(result.status)) {
243				if (_.isFunction(options.success)) {
244					// pass the object's own values because the server
245					// does not return the updated model
246					options.success(model.toJSON());
247				}
248			} else if (_.isFunction(options.error)) {
249				options.error(result);
250			}
251		});
252	}
253
254	function callMethod(client, options, model, headers) {
255		var data = options.data;
256		if (_.isObject(data)) {
257			headers['Content-Type'] = 'application/json';
258			data = JSON.stringify(data);
259		} else if (_.isString(data) && data.substr(0, 6) === '<?xml ') {
260			headers['Content-Type'] = 'application/xml';
261		} else {
262			headers['Content-Type'] = 'text/plain';
263		}
264		return client.request(
265			options.type,
266			options.url,
267			headers,
268			data
269		).then(function(result) {
270			if (!isSuccessStatus(result.status)) {
271				if (_.isFunction(options.error)) {
272					options.error(result);
273				}
274				return;
275			}
276
277			if (_.isFunction(options.success)) {
278				if (options.type === 'PUT' || options.type === 'POST') {
279					// pass the object's own values because the server
280					// does not return anything
281					var responseJson = result.body || model.toJSON();
282					var locationHeader = result.xhr.getResponseHeader('Content-Location');
283					if (options.type === 'POST' && locationHeader) {
284						responseJson.id = parseIdFromLocation(locationHeader);
285					}
286					options.success(responseJson);
287					return;
288				}
289				// if multi-status, parse
290				if (result.status === 207) {
291					var propsMapping = _.invert(options.davProperties);
292					options.success(parsePropFindResult(result.body, propsMapping));
293				} else {
294					options.success(result.body);
295				}
296			}
297		});
298	}
299
300	function davCall(options, model) {
301		var client = new dav.Client({
302			baseUrl: options.url,
303			xmlNamespaces: _.extend({
304				'DAV:': 'd',
305				'http://owncloud.org/ns': 'oc'
306			}, options.xmlNamespaces || {})
307		});
308		client.resolveUrl = function() {
309			return options.url;
310		};
311		var headers = _.extend({
312			'X-Requested-With': 'XMLHttpRequest',
313			'requesttoken': OC.requestToken
314		}, options.headers);
315		if (options.type === 'PROPFIND') {
316			return callPropFind(client, options, model, headers);
317		} else if (options.type === 'PROPPATCH') {
318			return callPropPatch(client, options, model, headers);
319		} else if (options.type === 'MKCOL') {
320			return callMkCol(client, options, model, headers);
321		} else {
322			return callMethod(client, options, model, headers);
323		}
324	}
325
326	/**
327	 * 
328	 */
329	function getTypeForMethod(method, model) {
330		var type = methodMap[method];
331
332		if (!type) {
333			// return method directly
334			return method;
335		}
336
337		// TODO: use special attribute "resourceType" instead
338		var isWebdavCollection = model instanceof WebdavCollectionNode;
339
340		// need to override default behavior and decide what to do
341		if (method === 'create') {
342			if (isWebdavCollection) {
343				if (!_.isUndefined(model.id)) {
344					// create new collection with known id
345					type = 'MKCOL';
346				} else {
347					// unsupported
348					throw 'Cannot create Webdav collection without id';
349				}
350			} else {
351				if (!_.isUndefined(model.id)) {
352					// need to create it first
353					type = 'PUT';
354				} else {
355					// creating without known id, will receive it after creation
356					type = 'POST';
357				}
358			}
359		} else if (method === 'update') {
360			// it exists, only update properties
361			type = 'PROPPATCH';
362			// force PUT usage ?
363			if (model.usePUT || (model.collection && model.collection.usePUT)) {
364				type = 'PUT';
365			}
366		}
367
368		return type;
369	}
370
371	/**
372	 * DAV transport
373	 */
374	function davSync(method, model, options) {
375		var params = {type: getTypeForMethod(method, model)};
376		var isCollection = (model instanceof Backbone.Collection);
377
378		// Ensure that we have a URL.
379		if (!options.url) {
380			params.url = _.result(model, 'url') || urlError();
381		}
382
383		// Ensure that we have the appropriate request data.
384		if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
385			params.data = options.attrs || model.toJSON(options);
386		}
387
388		// Don't process data on a non-GET request.
389		if (params.type !== 'PROPFIND') {
390			params.processData = false;
391		}
392
393		if (params.type === 'PROPFIND' || params.type === 'PROPPATCH' || params.type === 'MKCOL') {
394			var davProperties = model.davProperties;
395			if (!davProperties && model.model) {
396				// use dav properties from model in case of collection
397				davProperties = model.model.prototype.davProperties;
398			}
399			if (davProperties) {
400				if (_.isFunction(davProperties)) {
401					params.davProperties = davProperties.call(model);
402				} else {
403					params.davProperties = davProperties;
404				}
405			}
406
407			params.davProperties = _.extend(params.davProperties || {}, options.davProperties);
408
409			if (_.isUndefined(options.depth)) {
410				if (isCollection) {
411					options.depth = 1;
412				} else {
413					options.depth = 0;
414				}
415			}
416		}
417
418		// Pass along `textStatus` and `errorThrown` from jQuery.
419		var error = options.error;
420		options.error = function(xhr, textStatus, errorThrown) {
421			options.textStatus = textStatus;
422			options.errorThrown = errorThrown;
423			if (error) {
424				error.call(options.context, xhr, textStatus, errorThrown);
425			}
426		};
427
428		// Make the request, allowing the user to override any Ajax options.
429		var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model);
430		model.trigger('request', model, xhr, options);
431		return xhr;
432	}
433
434
435	/**
436	 * Regular Webdav leaf node
437	 */
438	var WebdavNode = Backbone.Model.extend({
439		sync: davSync,
440
441		constructor: function() {
442			this.on('sync', this._onSync, this);
443			this._isNew = true;
444			Backbone.Model.prototype.constructor.apply(this, arguments);
445		},
446
447		_onSync: function() {
448			this._isNew = false;
449		},
450
451		isNew: function() {
452			// we can't rely on the id so use a dummy attribute
453			return !!this._isNew;
454		}
455	});
456
457	/**
458	 * Children collection for a Webdav collection node
459	 */
460	var WebdavChildrenCollection = Backbone.Collection.extend({
461		sync: davSync,
462
463		collectionNode: null,
464		model: WebdavNode,
465
466		constructor: function() {
467			this.on('sync', this._onSync, this);
468			Backbone.Collection.prototype.constructor.apply(this, arguments);
469		},
470
471		initialize: function(models, options) {
472			options = options || {};
473
474			this.collectionNode = options.collectionNode;
475
476			return Backbone.Collection.prototype.initialize.apply(this, arguments);
477		},
478
479		_onSync: function(model) {
480			if (model instanceof Backbone.Model) {
481				// since we saved, mark as non-new
482				if (!_.isUndefined(model._isNew)) {
483					model._isNew = false;
484				}
485			} else {
486				// since we fetched, mark models as non-new
487				model.each(function(model) {
488					if (!_.isUndefined(model._isNew)) {
489						model._isNew = false;
490					}
491				});
492			}
493		},
494
495		url: function() {
496			return this.collectionNode.url();
497		}
498	});
499
500	/**
501	 * Webdav collection which is a special node, represented by a backbone model
502	 * and a sub-collection for its children.
503	 */
504	var WebdavCollectionNode = WebdavNode.extend({
505		sync: davSync,
506
507		childrenCollectionClass: WebdavChildrenCollection,
508
509		_childrenCollection: null,
510
511		getChildrenCollection: function() {
512			if (!this._childrenCollection) {
513				this._childrenCollection = new this.childrenCollectionClass([], {collectionNode: this});
514			}
515			return this._childrenCollection;
516		}
517	});
518
519	// exports
520	Backbone.davCall = davCall;
521	Backbone.davSync = davSync;
522
523	Backbone.WebdavNode = WebdavNode;
524	Backbone.WebdavChildrenCollection = WebdavChildrenCollection;
525	Backbone.WebdavCollectionNode = WebdavCollectionNode;
526
527})(OC.Backbone);
528

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.