client.js 11 KB

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