oc-backbone-webdav.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. * Webdav transport for Backbone.
  12. *
  13. * This makes it possible to use Webdav endpoints when
  14. * working with Backbone models and collections.
  15. *
  16. * Requires the davclient.js library.
  17. *
  18. * Usage example:
  19. *
  20. * var PersonModel = OC.Backbone.Model.extend({
  21. * // make it use the DAV transport
  22. * sync: OC.Backbone.davSync,
  23. *
  24. * // DAV properties mapping
  25. * davProperties: {
  26. * 'id': '{http://example.com/ns}id',
  27. * 'firstName': '{http://example.com/ns}first-name',
  28. * 'lastName': '{http://example.com/ns}last-name',
  29. * 'age': '{http://example.com/ns}age'
  30. * },
  31. *
  32. * // additional parsing, if needed
  33. * parse: function(props) {
  34. * // additional parsing (DAV property values are always strings)
  35. * props.age = parseInt(props.age, 10);
  36. * return props;
  37. * }
  38. * });
  39. *
  40. * var PersonCollection = OC.Backbone.Collection.extend({
  41. * // make it use the DAV transport
  42. * sync: OC.Backbone.davSync,
  43. *
  44. * // use person model
  45. * // note that davProperties will be inherited
  46. * model: PersonModel,
  47. *
  48. * // DAV collection URL
  49. * url: function() {
  50. * return OC.linkToRemote('dav') + '/person/';
  51. * },
  52. * });
  53. */
  54. /* global dav */
  55. (function(Backbone) {
  56. var methodMap = {
  57. 'create': 'POST',
  58. 'update': 'PROPPATCH',
  59. 'patch': 'PROPPATCH',
  60. 'delete': 'DELETE',
  61. 'read': 'PROPFIND'
  62. };
  63. // Throw an error when a URL is needed, and none is supplied.
  64. function urlError() {
  65. throw new Error('A "url" property or function must be specified');
  66. }
  67. /**
  68. * Convert a single propfind result to JSON
  69. *
  70. * @param {Object} result
  71. * @param {Object} davProperties properties mapping
  72. */
  73. function parsePropFindResult(result, davProperties) {
  74. if (_.isArray(result)) {
  75. return _.map(result, function(subResult) {
  76. return parsePropFindResult(subResult, davProperties);
  77. });
  78. }
  79. var props = {
  80. href: result.href
  81. };
  82. _.each(result.propStat, function(propStat) {
  83. if (propStat.status !== 'HTTP/1.1 200 OK') {
  84. return;
  85. }
  86. for (var key in propStat.properties) {
  87. var propKey = key;
  88. if (key in davProperties) {
  89. propKey = davProperties[key];
  90. }
  91. props[propKey] = propStat.properties[key];
  92. }
  93. });
  94. if (!props.id) {
  95. // parse id from href
  96. props.id = parseIdFromLocation(props.href);
  97. }
  98. return props;
  99. }
  100. /**
  101. * Parse ID from location
  102. *
  103. * @param {string} url url
  104. * @return {string} id
  105. */
  106. function parseIdFromLocation(url) {
  107. var queryPos = url.indexOf('?');
  108. if (queryPos > 0) {
  109. url = url.substr(0, queryPos);
  110. }
  111. var parts = url.split('/');
  112. var result;
  113. do {
  114. result = parts[parts.length - 1];
  115. parts.pop();
  116. // note: first result can be empty when there is a trailing slash,
  117. // so we take the part before that
  118. } while (!result && parts.length > 0);
  119. return result;
  120. }
  121. function isSuccessStatus(status) {
  122. return status >= 200 && status <= 299;
  123. }
  124. function convertModelAttributesToDavProperties(attrs, davProperties) {
  125. var props = {};
  126. var key;
  127. for (key in attrs) {
  128. var changedProp = davProperties[key];
  129. var value = attrs[key];
  130. if (!changedProp) {
  131. console.warn('No matching DAV property for property "' + key);
  132. changedProp = key;
  133. }
  134. if (_.isBoolean(value) || _.isNumber(value)) {
  135. // convert to string
  136. value = '' + value;
  137. }
  138. props[changedProp] = value;
  139. }
  140. return props;
  141. }
  142. function callPropFind(client, options, model, headers) {
  143. return client.propFind(
  144. options.url,
  145. _.values(options.davProperties) || [],
  146. options.depth,
  147. headers
  148. ).then(function(response) {
  149. if (isSuccessStatus(response.status)) {
  150. if (_.isFunction(options.success)) {
  151. var propsMapping = _.invert(options.davProperties);
  152. var results = parsePropFindResult(response.body, propsMapping);
  153. if (options.depth > 0) {
  154. // discard root entry
  155. results.shift();
  156. }
  157. options.success(results);
  158. return;
  159. }
  160. } else if (_.isFunction(options.error)) {
  161. options.error(response);
  162. }
  163. });
  164. }
  165. function callPropPatch(client, options, model, headers) {
  166. return client.propPatch(
  167. options.url,
  168. convertModelAttributesToDavProperties(model.changed, options.davProperties),
  169. headers
  170. ).then(function(result) {
  171. if (isSuccessStatus(result.status)) {
  172. if (_.isFunction(options.success)) {
  173. // pass the object's own values because the server
  174. // does not return the updated model
  175. options.success(model.toJSON());
  176. }
  177. } else if (_.isFunction(options.error)) {
  178. options.error(result);
  179. }
  180. });
  181. }
  182. function callMkCol(client, options, model, headers) {
  183. // call MKCOL without data, followed by PROPPATCH
  184. return client.request(
  185. options.type,
  186. options.url,
  187. headers,
  188. null
  189. ).then(function(result) {
  190. if (!isSuccessStatus(result.status)) {
  191. if (_.isFunction(options.error)) {
  192. options.error(result);
  193. }
  194. return;
  195. }
  196. callPropPatch(client, options, model, headers);
  197. });
  198. }
  199. function callMethod(client, options, model, headers) {
  200. headers['Content-Type'] = 'application/json';
  201. return client.request(
  202. options.type,
  203. options.url,
  204. headers,
  205. options.data
  206. ).then(function(result) {
  207. if (!isSuccessStatus(result.status)) {
  208. if (_.isFunction(options.error)) {
  209. options.error(result);
  210. }
  211. return;
  212. }
  213. if (_.isFunction(options.success)) {
  214. if (options.type === 'PUT' || options.type === 'POST' || options.type === 'MKCOL') {
  215. // pass the object's own values because the server
  216. // does not return anything
  217. var responseJson = result.body || model.toJSON();
  218. var locationHeader = result.xhr.getResponseHeader('Content-Location');
  219. if (options.type === 'POST' && locationHeader) {
  220. responseJson.id = parseIdFromLocation(locationHeader);
  221. }
  222. options.success(responseJson);
  223. return;
  224. }
  225. // if multi-status, parse
  226. if (result.status === 207) {
  227. var propsMapping = _.invert(options.davProperties);
  228. options.success(parsePropFindResult(result.body, propsMapping));
  229. } else {
  230. options.success(result.body);
  231. }
  232. }
  233. });
  234. }
  235. function davCall(options, model) {
  236. var client = new dav.Client({
  237. baseUrl: options.url,
  238. xmlNamespaces: _.extend({
  239. 'DAV:': 'd',
  240. 'http://owncloud.org/ns': 'oc'
  241. }, options.xmlNamespaces || {})
  242. });
  243. client.resolveUrl = function() {
  244. return options.url;
  245. };
  246. var headers = _.extend({
  247. 'X-Requested-With': 'XMLHttpRequest',
  248. 'requesttoken': OC.requestToken
  249. }, options.headers);
  250. if (options.type === 'PROPFIND') {
  251. return callPropFind(client, options, model, headers);
  252. } else if (options.type === 'PROPPATCH') {
  253. return callPropPatch(client, options, model, headers);
  254. } else if (options.type === 'MKCOL') {
  255. return callMkCol(client, options, model, headers);
  256. } else {
  257. return callMethod(client, options, model, headers);
  258. }
  259. }
  260. /**
  261. * DAV transport
  262. */
  263. function davSync(method, model, options) {
  264. var params = {type: methodMap[method] || method};
  265. var isCollection = (model instanceof Backbone.Collection);
  266. if (method === 'update') {
  267. // if a model has an inner collection, it must define an
  268. // attribute "hasInnerCollection" that evaluates to true
  269. if (model.hasInnerCollection) {
  270. // if the model itself is a Webdav collection, use MKCOL
  271. params.type = 'MKCOL';
  272. } else if (model.usePUT || (model.collection && model.collection.usePUT)) {
  273. // use PUT instead of PROPPATCH
  274. params.type = 'PUT';
  275. }
  276. }
  277. // Ensure that we have a URL.
  278. if (!options.url) {
  279. params.url = _.result(model, 'url') || urlError();
  280. }
  281. // Ensure that we have the appropriate request data.
  282. if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
  283. params.data = JSON.stringify(options.attrs || model.toJSON(options));
  284. }
  285. // Don't process data on a non-GET request.
  286. if (params.type !== 'PROPFIND') {
  287. params.processData = false;
  288. }
  289. if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') {
  290. var davProperties = model.davProperties;
  291. if (!davProperties && model.model) {
  292. // use dav properties from model in case of collection
  293. davProperties = model.model.prototype.davProperties;
  294. }
  295. if (davProperties) {
  296. if (_.isFunction(davProperties)) {
  297. params.davProperties = davProperties.call(model);
  298. } else {
  299. params.davProperties = davProperties;
  300. }
  301. }
  302. params.davProperties = _.extend(params.davProperties || {}, options.davProperties);
  303. if (_.isUndefined(options.depth)) {
  304. if (isCollection) {
  305. options.depth = 1;
  306. } else {
  307. options.depth = 0;
  308. }
  309. }
  310. }
  311. // Pass along `textStatus` and `errorThrown` from jQuery.
  312. var error = options.error;
  313. options.error = function(xhr, textStatus, errorThrown) {
  314. options.textStatus = textStatus;
  315. options.errorThrown = errorThrown;
  316. if (error) {
  317. error.call(options.context, xhr, textStatus, errorThrown);
  318. }
  319. };
  320. // Make the request, allowing the user to override any Ajax options.
  321. var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model);
  322. model.trigger('request', model, xhr, options);
  323. return xhr;
  324. }
  325. // exports
  326. Backbone.davCall = davCall;
  327. Backbone.davSync = davSync;
  328. })(OC.Backbone);