client.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /*
  2. * vim: expandtab shiftwidth=4 softtabstop=4
  3. */
  4. /* global dav */
  5. if (typeof dav == 'undefined') { dav = {}; };
  6. dav._XML_CHAR_MAP = {
  7. '<': '&lt;',
  8. '>': '&gt;',
  9. '&': '&amp;',
  10. '"': '&quot;',
  11. "'": '&apos;'
  12. };
  13. dav._escapeXml = function(s) {
  14. return s.replace(/[<>&"']/g, function (ch) {
  15. return dav._XML_CHAR_MAP[ch];
  16. });
  17. };
  18. dav.Client = function(options) {
  19. var i;
  20. for(i in options) {
  21. this[i] = options[i];
  22. }
  23. };
  24. dav.Client.prototype = {
  25. baseUrl : null,
  26. userName : null,
  27. password : null,
  28. xmlNamespaces : {
  29. 'DAV:' : 'd'
  30. },
  31. /**
  32. * Generates a propFind request.
  33. *
  34. * @param {string} url Url to do the propfind request on
  35. * @param {Array} properties List of properties to retrieve.
  36. * @param {Object} [headers] headers
  37. * @return {Promise}
  38. */
  39. propFind : function(url, properties, depth, headers) {
  40. if(typeof depth == "undefined") {
  41. depth = 0;
  42. }
  43. headers = headers || {};
  44. headers['Depth'] = depth;
  45. headers['Content-Type'] = 'application/xml; charset=utf-8';
  46. var body =
  47. '<?xml version="1.0"?>\n' +
  48. '<d:propfind ';
  49. var namespace;
  50. for (namespace in this.xmlNamespaces) {
  51. body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"';
  52. }
  53. body += '>\n' +
  54. ' <d:prop>\n';
  55. for(var ii in properties) {
  56. if (!properties.hasOwnProperty(ii)) {
  57. continue;
  58. }
  59. var property = this.parseClarkNotation(properties[ii]);
  60. if (this.xmlNamespaces[property.namespace]) {
  61. body+=' <' + this.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n';
  62. } else {
  63. body+=' <x:' + property.name + ' xmlns:x="' + property.namespace + '" />\n';
  64. }
  65. }
  66. body+=' </d:prop>\n';
  67. body+='</d:propfind>';
  68. return this.request('PROPFIND', url, headers, body).then(
  69. function(result) {
  70. if (depth===0) {
  71. return {
  72. status: result.status,
  73. body: result.body[0],
  74. xhr: result.xhr
  75. };
  76. } else {
  77. return {
  78. status: result.status,
  79. body: result.body,
  80. xhr: result.xhr
  81. };
  82. }
  83. }.bind(this)
  84. );
  85. },
  86. /**
  87. * Renders a "d:set" block for the given properties.
  88. *
  89. * @param {Object.<String,String>} properties
  90. * @return {String} XML "<d:set>" block
  91. */
  92. _renderPropSet: function(properties) {
  93. var body = ' <d:set>\n' +
  94. ' <d:prop>\n';
  95. for(var ii in properties) {
  96. if (!properties.hasOwnProperty(ii)) {
  97. continue;
  98. }
  99. var property = this.parseClarkNotation(ii);
  100. var propName;
  101. var propValue = properties[ii];
  102. if (this.xmlNamespaces[property.namespace]) {
  103. propName = this.xmlNamespaces[property.namespace] + ':' + property.name;
  104. } else {
  105. propName = 'x:' + property.name + ' xmlns:x="' + property.namespace + '"';
  106. }
  107. // FIXME: hard-coded for now until we allow properties to
  108. // specify whether to be escaped or not
  109. if (propName !== 'd:resourcetype') {
  110. propValue = dav._escapeXml(propValue);
  111. }
  112. body += ' <' + propName + '>' + propValue + '</' + propName + '>\n';
  113. }
  114. body +=' </d:prop>\n';
  115. body +=' </d:set>\n';
  116. return body;
  117. },
  118. /**
  119. * Generates a propPatch request.
  120. *
  121. * @param {string} url Url to do the proppatch request on
  122. * @param {Object.<String,String>} properties List of properties to store.
  123. * @param {Object} [headers] headers
  124. * @return {Promise}
  125. */
  126. propPatch : function(url, properties, headers) {
  127. headers = headers || {};
  128. headers['Content-Type'] = 'application/xml; charset=utf-8';
  129. var body =
  130. '<?xml version="1.0"?>\n' +
  131. '<d:propertyupdate ';
  132. var namespace;
  133. for (namespace in this.xmlNamespaces) {
  134. body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"';
  135. }
  136. body += '>\n' + this._renderPropSet(properties);
  137. body += '</d:propertyupdate>';
  138. return this.request('PROPPATCH', url, headers, body).then(
  139. function(result) {
  140. return {
  141. status: result.status,
  142. body: result.body,
  143. xhr: result.xhr
  144. };
  145. }.bind(this)
  146. );
  147. },
  148. /**
  149. * Generates a MKCOL request.
  150. * If attributes are given, it will use an extended MKCOL request.
  151. *
  152. * @param {string} url Url to do the proppatch request on
  153. * @param {Object.<String,String>} [properties] list of properties to store.
  154. * @param {Object} [headers] headers
  155. * @return {Promise}
  156. */
  157. mkcol : function(url, properties, headers) {
  158. var body = '';
  159. headers = headers || {};
  160. headers['Content-Type'] = 'application/xml; charset=utf-8';
  161. if (properties) {
  162. body =
  163. '<?xml version="1.0"?>\n' +
  164. '<d:mkcol';
  165. var namespace;
  166. for (namespace in this.xmlNamespaces) {
  167. body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"';
  168. }
  169. body += '>\n' + this._renderPropSet(properties);
  170. body +='</d:mkcol>';
  171. }
  172. return this.request('MKCOL', url, headers, body).then(
  173. function(result) {
  174. return {
  175. status: result.status,
  176. body: result.body,
  177. xhr: result.xhr
  178. };
  179. }.bind(this)
  180. );
  181. },
  182. /**
  183. * Performs a HTTP request, and returns a Promise
  184. *
  185. * @param {string} method HTTP method
  186. * @param {string} url Relative or absolute url
  187. * @param {Object} headers HTTP headers as an object.
  188. * @param {string} body HTTP request body.
  189. * @return {Promise}
  190. */
  191. request : function(method, url, headers, body) {
  192. var self = this;
  193. var xhr = this.xhrProvider();
  194. headers = headers || {};
  195. if (this.userName) {
  196. headers['Authorization'] = 'Basic ' + btoa(this.userName + ':' + this.password);
  197. // xhr.open(method, this.resolveUrl(url), true, this.userName, this.password);
  198. }
  199. xhr.open(method, this.resolveUrl(url), true);
  200. var ii;
  201. for(ii in headers) {
  202. xhr.setRequestHeader(ii, headers[ii]);
  203. }
  204. // Work around for edge
  205. if (body === undefined) {
  206. xhr.send();
  207. } else {
  208. xhr.send(body);
  209. }
  210. return new Promise(function(fulfill, reject) {
  211. xhr.onreadystatechange = function() {
  212. if (xhr.readyState !== 4) {
  213. return;
  214. }
  215. var resultBody = xhr.response;
  216. if (xhr.status === 207) {
  217. resultBody = self.parseMultiStatus(xhr.response);
  218. }
  219. fulfill({
  220. body: resultBody,
  221. status: xhr.status,
  222. xhr: xhr
  223. });
  224. };
  225. xhr.ontimeout = function() {
  226. reject(new Error('Timeout exceeded'));
  227. };
  228. });
  229. },
  230. /**
  231. * Returns an XMLHttpRequest object.
  232. *
  233. * This is in its own method, so it can be easily overridden.
  234. *
  235. * @return {XMLHttpRequest}
  236. */
  237. xhrProvider : function() {
  238. return new XMLHttpRequest();
  239. },
  240. /**
  241. * Parses a property node.
  242. *
  243. * Either returns a string if the node only contains text, or returns an
  244. * array of non-text subnodes.
  245. *
  246. * @param {Object} propNode node to parse
  247. * @return {string|Array} text content as string or array of subnodes, excluding text nodes
  248. */
  249. _parsePropNode: function(propNode) {
  250. var content = null;
  251. if (propNode.childNodes && propNode.childNodes.length > 0) {
  252. var subNodes = [];
  253. // filter out text nodes
  254. for (var j = 0; j < propNode.childNodes.length; j++) {
  255. var node = propNode.childNodes[j];
  256. if (node.nodeType === 1) {
  257. subNodes.push(node);
  258. }
  259. }
  260. if (subNodes.length) {
  261. content = subNodes;
  262. }
  263. }
  264. return content || propNode.textContent || propNode.text || '';
  265. },
  266. /**
  267. * Parses a multi-status response body.
  268. *
  269. * @param {string} xmlBody
  270. * @param {Array}
  271. */
  272. parseMultiStatus : function(xmlBody) {
  273. var parser = new DOMParser();
  274. var doc = parser.parseFromString(xmlBody, "application/xml");
  275. var resolver = function(foo) {
  276. var ii;
  277. for(ii in this.xmlNamespaces) {
  278. if (this.xmlNamespaces[ii] === foo) {
  279. return ii;
  280. }
  281. }
  282. }.bind(this);
  283. var responseIterator = doc.evaluate('/d:multistatus/d:response', doc, resolver, XPathResult.ANY_TYPE, null);
  284. var result = [];
  285. var responseNode = responseIterator.iterateNext();
  286. while(responseNode) {
  287. var response = {
  288. href : null,
  289. propStat : []
  290. };
  291. response.href = doc.evaluate('string(d:href)', responseNode, resolver, XPathResult.ANY_TYPE, null).stringValue;
  292. var propStatIterator = doc.evaluate('d:propstat', responseNode, resolver, XPathResult.ANY_TYPE, null);
  293. var propStatNode = propStatIterator.iterateNext();
  294. while(propStatNode) {
  295. var propStat = {
  296. status : doc.evaluate('string(d:status)', propStatNode, resolver, XPathResult.ANY_TYPE, null).stringValue,
  297. properties : {},
  298. };
  299. var propIterator = doc.evaluate('d:prop/*', propStatNode, resolver, XPathResult.ANY_TYPE, null);
  300. var propNode = propIterator.iterateNext();
  301. while(propNode) {
  302. var content = this._parsePropNode(propNode);
  303. propStat.properties['{' + propNode.namespaceURI + '}' + propNode.localName] = content;
  304. propNode = propIterator.iterateNext();
  305. }
  306. response.propStat.push(propStat);
  307. propStatNode = propStatIterator.iterateNext();
  308. }
  309. result.push(response);
  310. responseNode = responseIterator.iterateNext();
  311. }
  312. return result;
  313. },
  314. /**
  315. * Takes a relative url, and maps it to an absolute url, using the baseUrl
  316. *
  317. * @param {string} url
  318. * @return {string}
  319. */
  320. resolveUrl : function(url) {
  321. // Note: this is rudamentary.. not sure yet if it handles every case.
  322. if (/^https?:\/\//i.test(url)) {
  323. // absolute
  324. return url;
  325. }
  326. var baseParts = this.parseUrl(this.baseUrl);
  327. if (url.charAt('/')) {
  328. // Url starts with a slash
  329. return baseParts.root + url;
  330. }
  331. // Url does not start with a slash, we need grab the base url right up until the last slash.
  332. var newUrl = baseParts.root + '/';
  333. if (baseParts.path.lastIndexOf('/')!==-1) {
  334. newUrl = newUrl = baseParts.path.subString(0, baseParts.path.lastIndexOf('/')) + '/';
  335. }
  336. newUrl+=url;
  337. return url;
  338. },
  339. /**
  340. * Parses a url and returns its individual components.
  341. *
  342. * @param {String} url
  343. * @return {Object}
  344. */
  345. parseUrl : function(url) {
  346. var parts = url.match(/^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/);
  347. var result = {
  348. url : parts[0],
  349. scheme : parts[1],
  350. host : parts[3],
  351. port : parts[4],
  352. path : parts[5],
  353. query : parts[6],
  354. fragment : parts[7],
  355. };
  356. result.root =
  357. result.scheme + '://' +
  358. result.host +
  359. (result.port ? ':' + result.port : '');
  360. return result;
  361. },
  362. parseClarkNotation : function(propertyName) {
  363. var result = propertyName.match(/^{([^}]+)}(.*)$/);
  364. if (!result) {
  365. return;
  366. }
  367. return {
  368. name : result[2],
  369. namespace : result[1]
  370. };
  371. }
  372. };