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.