oc-backbone-webdav.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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. return parts[parts.length - 1];
  113. }
  114. function isSuccessStatus(status) {
  115. return status >= 200 && status <= 299;
  116. }
  117. function convertModelAttributesToDavProperties(attrs, davProperties) {
  118. var props = {};
  119. var key;
  120. for (key in attrs) {
  121. var changedProp = davProperties[key];
  122. var value = attrs[key];
  123. if (!changedProp) {
  124. console.warn('No matching DAV property for property "' + key);
  125. changedProp = key;
  126. }
  127. if (_.isBoolean(value) || _.isNumber(value)) {
  128. // convert to string
  129. value = '' + value;
  130. }
  131. props[changedProp] = value;
  132. }
  133. return props;
  134. }
  135. function callPropFind(client, options, model, headers) {
  136. return client.propFind(
  137. options.url,
  138. _.values(options.davProperties) || [],
  139. options.depth,
  140. headers
  141. ).then(function(response) {
  142. if (isSuccessStatus(response.status)) {
  143. if (_.isFunction(options.success)) {
  144. var propsMapping = _.invert(options.davProperties);
  145. var results = parsePropFindResult(response.body, propsMapping);
  146. if (options.depth > 0) {
  147. // discard root entry
  148. results.shift();
  149. }
  150. options.success(results);
  151. return;
  152. }
  153. } else if (_.isFunction(options.error)) {
  154. options.error(response);
  155. }
  156. });
  157. }
  158. function callPropPatch(client, options, model, headers) {
  159. return client.propPatch(
  160. options.url,
  161. convertModelAttributesToDavProperties(model.changed, options.davProperties),
  162. headers
  163. ).then(function(result) {
  164. if (isSuccessStatus(result.status)) {
  165. if (_.isFunction(options.success)) {
  166. // pass the object's own values because the server
  167. // does not return the updated model
  168. options.success(model.toJSON());
  169. }
  170. } else if (_.isFunction(options.error)) {
  171. options.error(result);
  172. }
  173. });
  174. }
  175. function callMethod(client, options, model, headers) {
  176. headers['Content-Type'] = 'application/json';
  177. return client.request(
  178. options.type,
  179. options.url,
  180. headers,
  181. options.data
  182. ).then(function(result) {
  183. if (!isSuccessStatus(result.status)) {
  184. if (_.isFunction(options.error)) {
  185. options.error(result);
  186. }
  187. return;
  188. }
  189. if (_.isFunction(options.success)) {
  190. if (options.type === 'PUT' || options.type === 'POST') {
  191. // pass the object's own values because the server
  192. // does not return anything
  193. var responseJson = result.body || model.toJSON();
  194. var locationHeader = result.xhr.getResponseHeader('Content-Location');
  195. if (options.type === 'POST' && locationHeader) {
  196. responseJson.id = parseIdFromLocation(locationHeader);
  197. }
  198. options.success(responseJson);
  199. return;
  200. }
  201. // if multi-status, parse
  202. if (result.status === 207) {
  203. var propsMapping = _.invert(options.davProperties);
  204. options.success(parsePropFindResult(result.body, propsMapping));
  205. } else {
  206. options.success(result.body);
  207. }
  208. }
  209. });
  210. }
  211. function davCall(options, model) {
  212. var client = new dav.Client({
  213. baseUrl: options.url,
  214. xmlNamespaces: _.extend({
  215. 'DAV:': 'd',
  216. 'http://owncloud.org/ns': 'oc'
  217. }, options.xmlNamespaces || {})
  218. });
  219. client.resolveUrl = function() {
  220. return options.url;
  221. };
  222. var headers = _.extend({
  223. 'X-Requested-With': 'XMLHttpRequest',
  224. 'requesttoken': OC.requestToken
  225. }, options.headers);
  226. if (options.type === 'PROPFIND') {
  227. return callPropFind(client, options, model, headers);
  228. } else if (options.type === 'PROPPATCH') {
  229. return callPropPatch(client, options, model, headers);
  230. } else {
  231. return callMethod(client, options, model, headers);
  232. }
  233. }
  234. /**
  235. * DAV transport
  236. */
  237. function davSync(method, model, options) {
  238. var params = {type: methodMap[method] || method};
  239. var isCollection = (model instanceof Backbone.Collection);
  240. if (method === 'update' && (model.usePUT || (model.collection && model.collection.usePUT))) {
  241. // use PUT instead of PROPPATCH
  242. params.type = 'PUT';
  243. }
  244. // Ensure that we have a URL.
  245. if (!options.url) {
  246. params.url = _.result(model, 'url') || urlError();
  247. }
  248. // Ensure that we have the appropriate request data.
  249. if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
  250. params.data = JSON.stringify(options.attrs || model.toJSON(options));
  251. }
  252. // Don't process data on a non-GET request.
  253. if (params.type !== 'PROPFIND') {
  254. params.processData = false;
  255. }
  256. if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') {
  257. var davProperties = model.davProperties;
  258. if (!davProperties && model.model) {
  259. // use dav properties from model in case of collection
  260. davProperties = model.model.prototype.davProperties;
  261. }
  262. if (davProperties) {
  263. if (_.isFunction(davProperties)) {
  264. params.davProperties = davProperties.call(model);
  265. } else {
  266. params.davProperties = davProperties;
  267. }
  268. }
  269. params.davProperties = _.extend(params.davProperties || {}, options.davProperties);
  270. if (_.isUndefined(options.depth)) {
  271. if (isCollection) {
  272. options.depth = 1;
  273. } else {
  274. options.depth = 0;
  275. }
  276. }
  277. }
  278. // Pass along `textStatus` and `errorThrown` from jQuery.
  279. var error = options.error;
  280. options.error = function(xhr, textStatus, errorThrown) {
  281. options.textStatus = textStatus;
  282. options.errorThrown = errorThrown;
  283. if (error) {
  284. error.call(options.context, xhr, textStatus, errorThrown);
  285. }
  286. };
  287. // Make the request, allowing the user to override any Ajax options.
  288. var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model);
  289. model.trigger('request', model, xhr, options);
  290. return xhr;
  291. }
  292. // exports
  293. Backbone.davCall = davCall;
  294. Backbone.davSync = davSync;
  295. })(OC.Backbone);