summaryrefslogtreecommitdiffstats
path: root/core/js/oc-backbone-webdav.js
blob: 1c1b5c71d81a02fca6c254f870eb5b3b158ecdfb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
id='n183' href='#n183'>183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
/*
 * 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',
			'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 {
			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);