1 /*
  2  * Copyright ©2012 SARA bv, The Netherlands
  3  *
  4  * This file is part of js-webdav-client.
  5  *
  6  * js-webdav-client is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU Lesser General Public License as published
  8  * by the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * js-webdav-client is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU Lesser General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU Lesser General Public License
 17  * along with js-webdav-client.  If not, see <http://www.gnu.org/licenses/>.
 18  */
 19 "use strict";
 20 
 21 // If nl.sara.webdav.Client is already defined, we have a namespace clash!
 22 if (nl.sara.webdav.Client !== undefined) {
 23   throw new nl.sara.webdav.Exception('Namespace nl.sara.webdav.Client already taken, could not load JavaScript library for WebDAV connectivity.', nl.sara.webdav.Exception.NAMESPACE_TAKEN);
 24 }
 25 
 26 /**
 27  * @class Connection to a WebDAV server
 28  *
 29  * @param  {Array}  [config]  A configuration object. It can contain the following fields:
 30  *                            - host; String containing the hostname to connect to (default: current host)
 31  *                            - useHTTPS; If set to (boolean) false, a regular HTTP connection will be used, any other value (even 0 or '' which evaluate to false) and HTTPS will be used instead. Is ignored if host is not set. (default: true)
 32  *                            - port; Integer with the port number to use. Is ignored if host is not set. (default: 443 for HTTPS and 80 for HTTP)
 33  *                            - defaultHeaders; An array containing default headers to use for each request. Header names should be the keys. (default: {} i.e. no default headers)
 34  *                            - username; A string with the username to use for authentication. (default: the current username if any)
 35  *                            - password; A string with the password to use for authentication. This is only used if a username is supplied. (default: the current password if any)
 36  *                            For backwards compatibility reasons, it is also
 37  *                            possible to specify the 'host', 'useHTTPS', 'port'
 38  *                            and 'defaultHeaders' as regular parameters (in
 39  *                            that order). Note that useHTTPS works differently
 40  *                            in that case; any other value than boolean True
 41  *                            will result in regular HTTP. If you want to supply
 42  *                            a username and password, you will have to use the
 43  *                            config array, you cannot pass them as extra normal
 44  *                            parameters.
 45  */
 46 nl.sara.webdav.Client = function(config, useHTTPS, port, defaultHeaders) {
 47   // First define private attributes
 48   Object.defineProperty(this, '_baseUrl', {
 49     'value': null,
 50     'enumerable': false,
 51     'configurable': false,
 52     'writable': true
 53   });
 54   Object.defineProperty(this, '_username', {
 55     'value': null,
 56     'enumerable': false,
 57     'configurable': false,
 58     'writable': true
 59   });
 60   Object.defineProperty(this, '_password', {
 61     'value': null,
 62     'enumerable': false,
 63     'configurable': false,
 64     'writable': true
 65   });
 66   Object.defineProperty(this, '_headers', {
 67     'value': {},
 68     'enumerable': false,
 69     'configurable': false,
 70     'writable': true
 71   });
 72 
 73   // Constructor logic
 74   if ( config !== undefined ) {
 75     var host;
 76     if ( typeof( config ) !== 'string' ) {
 77       host = config['host'];
 78       useHTTPS = ( config['useHTTPS'] !== false );
 79       port = config['port'];
 80       defaultHeaders = config['defaultHeaders'];
 81       
 82       if ( config['username'] !== undefined ) {
 83         this._username = config['username'];
 84         if ( config['password'] !== undefined ) {
 85           this._password = config['password'];
 86         }else{
 87           this._password = '';
 88         }
 89       }
 90     }else{
 91       host = config;
 92     }
 93     
 94     if ( host !== undefined ) {
 95       // if the configuration item is a string, then we have to work in compatibility mode; first parameter is the host, second, the protocol, third the port and fourth aditional headers
 96       var protocol = (useHTTPS === true) ? 'https' : 'http';
 97       port = (port !== undefined) ? port : ((protocol === 'https') ? 443 : 80);
 98       this._baseUrl = protocol + '://' + host + ((((protocol === 'http') && (port === 80)) || ((protocol === 'https') && (port === 443))) ? '' : ':' + port);
 99     }
100   }
101   
102   if (defaultHeaders !== undefined) {
103     this._headers = defaultHeaders;
104   }
105 };
106 
107 /**#@+
108  * Class constant
109  */
110 nl.sara.webdav.Client.PROPNAME = 1;
111 nl.sara.webdav.Client.ALLPROP = 2;
112 nl.sara.webdav.Client.INFINITY = 'infinity';
113 nl.sara.webdav.Client.FAIL_ON_OVERWRITE = 3;
114 nl.sara.webdav.Client.TRUNCATE_ON_OVERWRITE = 4;
115 nl.sara.webdav.Client.SILENT_OVERWRITE = 5;
116 /**#@-*/
117 
118 //########################## DEFINE PUBLIC METHODS #############################
119 /**
120  * Converts a path to the full url (i.e. appends the protocol and host part)
121  *
122  * @param   {String}  path  The path on the server
123  * @returns {String}        The full url to the path
124  */
125 nl.sara.webdav.Client.prototype.getUrl = function(path) {
126   if (path.substring(0,1) !== '/') {
127     path = '/' + path;
128   }
129   if (this._baseUrl !== null) {
130     return this._baseUrl + path;
131   }else{
132     return path;
133   }
134 };
135 
136 /**
137  * Perform a WebDAV PROPFIND request
138  *
139  * @param    {String}                         path             The path get a PROPFIND for
140  * @param    {Function(status,body,headers)}  callback         Querying the server is done asynchronously, this callback function is called when the results are in
141  * @param    {String}                         [depth=0]        Optional; Value for the 'depth' header, should be either '0', '1' or the class constant INFINITY. When omitted, '0' is used. See RFC 4918.
142  * @param    {mixed}                          [props=ALLPROP]  Optional; The properties to fetch. Should be either one of the class constants PROPNAME or ALLPROP or an array with Property objects. When omitted, ALLPROP is assumed. See RFC 4918.
143  * @param    {nl.sara.webdav.Property[]}      [include]        Optional; An array with Property objects used for the <include> element. This is only used for ALLPROP requests. When omitted, no <include> element is send. See RFC 4918.
144  * @param    {Array}                          headers          Optional; Additional headers to set
145  * @returns  {nl.sara.webdav.Client}                           The client itself for chaining methods
146  */
147 nl.sara.webdav.Client.prototype.propfind = function(path, callback, depth, props, include, headers) {
148   if ((path === undefined) || (callback === undefined)) {
149     throw new nl.sara.webdav.Exception('PROPFIND requires the parameters path and callback', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
150   }
151   if (!(typeof path === "string")) {
152     throw new nl.sara.webdav.Exception('PROPFIND parameter; path should be a string', nl.sara.webdav.Exception.WRONG_TYPE);
153   }
154 
155   // Get the full URL, based on the specified path
156   var url = this.getUrl(path);
157 
158   // Check the depth header
159   if (depth === undefined) { // We default depth to 0, not to infinity as this is not supported by all servers
160     depth = 0;
161   }
162   var depthHeader = null;
163   switch (depth) {
164     case 0:
165     case 1:
166       depthHeader = depth;
167       break;
168     case nl.sara.webdav.Client.INFINITY:
169       depthHeader = 'infinity';
170       break;
171     default:
172       throw new nl.sara.webdav.Exception("Depth header should be '0', '1' or nl.sara.webdav.Client.INFINITY", nl.sara.webdav.Exception.WRONG_VALUE);
173     break;
174   }
175 
176   // Create the request XML
177   if (props === undefined) {
178     props = nl.sara.webdav.Client.ALLPROP;
179   }
180   var propsBody = document.implementation.createDocument("DAV:", "propfind", null);
181   switch (props) { // Find out what the request is for
182     case nl.sara.webdav.Client.PROPNAME: // User wants all property names
183       propsBody.documentElement.appendChild(propsBody.createElementNS('DAV:', 'propname'));
184       break;
185     case nl.sara.webdav.Client.ALLPROP: // User wants all properties
186       propsBody.documentElement.appendChild(propsBody.createElementNS('DAV:', 'allprop'));
187       if (include !== undefined) { // There is content for the <DAV: include> tags, so parse it
188         if (!(include instanceof Array)) {
189           throw new nl.sara.webdav.Exception('Propfind parameter; include should be an array', nl.sara.webdav.Exception.WRONG_TYPE);
190         }
191         var includeBody = propsBody.createElementNS('DAV:', 'include');
192         for (var i = 0; i < include.length; i++) {
193           var item = include[i];
194           if (!nl.sara.webdav.Ie.isIE && !(item instanceof nl.sara.webdav.Property)) {
195             continue;
196           }
197           includeBody.appendChild(propsBody.createElementNS(item.namespace, item.tagname));
198         }
199         if (includeBody.hasChildNodes()) { // But only really add the <include> tag if there is valid content
200           propsBody.documentElement.appendChild(includeBody);
201         }
202       }
203       break;
204     default: // The default is to expect an array with Property objects; the user wants the values of these properties
205       if (!(props instanceof Array)) {
206         throw new nl.sara.webdav.Exception('Props parameter should be nl.sara.webdav.Client.PROPNAME, nl.sara.webdav.Client.ALLPROP or an array with Property objects', nl.sara.webdav.Exception.WRONG_VALUE);
207       }
208       var propertyBody = propsBody.createElementNS('DAV:', 'prop');
209       for (var i = 0; i < props.length; i++) { // Cycle through the array
210         var prop = props[i];
211         if (!nl.sara.webdav.Ie.isIE && !(prop instanceof nl.sara.webdav.Property)) {
212           continue;
213         }
214         propertyBody.appendChild(propsBody.createElementNS(prop.namespace, prop.tagname));
215       }
216       if (!propertyBody.hasChildNodes()) { // But if no properties are found, then the array didn't have Property objects in it
217         throw new nl.sara.webdav.Exception("Propfind parameter; if props is an array, it should be an array of Properties", nl.sara.webdav.Exception.WRONG_TYPE);
218       }
219       propsBody.documentElement.appendChild(propertyBody);
220     break;
221   }
222   var serializer = new XMLSerializer();
223   var body = '<?xml version="1.0" encoding="utf-8" ?>' + serializer.serializeToString(propsBody);
224 
225   // And then send the request
226   var ajax = this.getAjax("PROPFIND", url, callback, headers);
227   ajax.setRequestHeader('Depth', depthHeader);
228   ajax.setRequestHeader('Content-Type', 'application/xml; charset="utf-8"');
229   ajax.send(body);
230 
231   return this;
232 };
233 
234 /**
235  * Perform a WebDAV PROPPATCH request
236  *
237  * @param    {String}                         path        The path do a PROPPATCH on
238  * @param    {Function(status,body,headers)}  callback    Querying the server is done asynchronously, this callback function is called when the results are in
239  * @param    {nl.sara.webdav.Property[]}      [setProps]  Optional; The properties to set. If not set, delProps should be set. Can be omitted with 'undefined'.
240  * @param    {nl.sara.webdav.Property[]}      [delProps]  Optional; The properties to delete. If not set, setProps should be set.
241  * @param    {Array}                          headers     Optional; Additional headers to set
242  * @returns  {nl.sara.webdav.Client}                      The client itself for chaining methods
243  */
244 nl.sara.webdav.Client.prototype.proppatch = function(path, callback, setProps, delProps, headers) {
245   if ((path === undefined) || (callback === undefined) || ((setProps === undefined) && (delProps === undefined))) {
246     throw new nl.sara.webdav.Exception('PROPPATCH requires the parameters path, callback and at least one of setProps or delProps', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
247   }
248   if (!(typeof path === "string") || ((setProps !== undefined) && !(setProps instanceof Array)) || ((delProps !== undefined) && !(delProps instanceof Array))) {
249     throw new nl.sara.webdav.Exception('PROPPATCH parameter; path should be a string, setProps and delProps should be arrays', nl.sara.webdav.Exception.WRONG_TYPE);
250   }
251 
252   // Get the full URL, based on the specified path
253   var url = this.getUrl(path);
254 
255   // Create the request XML
256   var propsBody = document.implementation.createDocument("DAV:", "propertyupdate", null);
257   propsBody.documentElement.setAttribute("xmlns:D", "DAV:");
258   if (setProps !== undefined) {
259     var props = propsBody.createElementNS('DAV:', 'prop');
260     for (var i = 0; i < setProps.length; i++) { // Cycle through the array
261       var prop = setProps[i];
262       if (!nl.sara.webdav.Ie.isIE && !(prop instanceof nl.sara.webdav.Property)) {
263         continue;
264       }
265       var element = propsBody.createElementNS(prop.namespace, prop.tagname);
266       for (var j = 0; j < prop.xmlvalue.length; j++) {
267         var nodeCopy = propsBody.importNode(prop.xmlvalue.item(j), true);
268         element.appendChild(nodeCopy);
269       }
270       props.appendChild(element);
271     }
272     if (!props.hasChildNodes()) { // But if no properties are found, then the array didn't have Property objects in it
273       throw new nl.sara.webdav.Exception("Proppatch parameter; setProps should be an array of Properties", nl.sara.webdav.Exception.WRONG_TYPE);
274     }
275     var set = propsBody.createElementNS('DAV:', 'set');
276     set.appendChild(props);
277     propsBody.documentElement.appendChild(set);
278   }
279   if (delProps !== undefined) {
280     var props = propsBody.createElementNS('DAV:', 'prop');
281     for (var i = 0; i < delProps.length; i++) { // Cycle through the array
282       var prop = delProps[i];
283       if (!nl.sara.webdav.Ie.isIE && !(prop instanceof nl.sara.webdav.Property)) {
284         continue;
285       }
286       var element = propsBody.createElementNS(prop.namespace, prop.tagname);
287       props.appendChild(element);
288     }
289     if (!props.hasChildNodes()) { // But if no properties are found, then the array didn't have Property objects in it
290       throw new nl.sara.webdav.Exception("Proppatch parameter; delProps should be an array of Properties", nl.sara.webdav.Exception.WRONG_TYPE);
291     }
292     var remove = propsBody.createElementNS('DAV:', 'remove');
293     remove.appendChild(props);
294     propsBody.documentElement.appendChild(remove);
295   }
296   var serializer = new XMLSerializer();
297   var body = '<?xml version="1.0" encoding="utf-8" ?>' + serializer.serializeToString(propsBody);
298 
299   // And then send the request
300   var ajax = this.getAjax("PROPPATCH", url, callback, headers);
301   ajax.setRequestHeader('Content-Type', 'application/xml; charset="utf-8"');
302   ajax.send(body);
303 
304   return this;
305 };
306 
307 /**
308  * Perform a WebDAV MKCOL request
309  *
310  * @param    {String}                         path                                              The path to perform MKCOL on
311  * @param    {Function(status,body,headers)}  callback                                          Querying the server is done asynchronously, this callback function is called when the results are in
312  * @param    {String}                         [body]                                            Optional; a body to include in the MKCOL request.
313  * @param    {String}                         [contenttype='application/xml; charset="utf-8"']  Optional; the content type of the body (i.e. value for the Content-Type header). If omitted, but body is specified, then 'application/xml; charset="utf-8"' is assumed
314  * @param    {Array}                          headers                                           Optional; Additional headers to set
315  * @returns  {nl.sara.webdav.Client}                                                            The client itself for chaining methods
316  */
317 nl.sara.webdav.Client.prototype.mkcol = function(path, callback, body, contenttype, headers) {
318   if ((path === undefined) || (callback === undefined)) {
319     throw new nl.sara.webdav.Exception('MKCOL requires the parameters path and callback', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
320   }
321   if ((typeof path !== "string") || ((contenttype !== undefined) && (typeof contenttype !== 'string'))) {
322     throw new nl.sara.webdav.Exception('MKCOL parameter; path and contenttype should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
323   }
324 
325   // Get the full URL, based on the specified path
326   var url = this.getUrl(path);
327 
328   // And then send the request
329   var ajax = this.getAjax("MKCOL", url, callback, headers);
330   if (body !== undefined) {
331     if (contenttype !== undefined) {
332       ajax.setRequestHeader('Content-Type', contenttype);
333     }
334     ajax.send(body);
335   }else{
336     ajax.send();
337   }
338 
339   return this;
340 };
341 
342 /**
343  * Perform a WebDAV DELETE request
344  *
345  * Because 'delete' is an operator in JavaScript, I had to name this method
346  * 'remove'. However, performs a regular DELETE request as described in
347  * RFC 4918
348  *
349  * @param    {String}                         path      The path to perform DELETE on
350  * @param    {Function(status,body,headers)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
351  * @param    {Array}                          headers   Optional; Additional headers to set
352  * @returns  {nl.sara.webdav.Client}                    The client itself for chaining methods
353  */
354 nl.sara.webdav.Client.prototype.remove = function(path, callback, headers) {
355   if ((path === undefined) || (callback === undefined)) {
356     throw new nl.sara.webdav.Exception('DELETE requires the parameters path and callback', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
357   }
358   if (typeof path !== "string"){
359     throw new nl.sara.webdav.Exception('DELETE parameter; path should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
360   }
361 
362   // Get the full URL, based on the specified path
363   var url = this.getUrl(path);
364 
365   // And then send the request
366   var ajax = this.getAjax("DELETE", url, callback, headers);
367   ajax.send();
368 
369   return this;
370 };
371 
372 /**
373  * Perform a WebDAV GET request
374  *
375  * @param    {String}                    path      The path to GET
376  * @param    {Function(status,content)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
377  * @param    {Array}                     headers   Optional; Additional headers to set
378  * @returns  {nl.sara.webdav.Client}               The client itself for chaining methods
379  */
380 nl.sara.webdav.Client.prototype.get = function(path, callback, headers) {
381   if ((path === undefined) || (callback === undefined)) {
382     throw new nl.sara.webdav.Exception('GET requires the parameters path and callback', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
383   }
384   if (typeof path !== "string"){
385     throw new nl.sara.webdav.Exception('GET parameter; path should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
386   }
387 
388   // Get the full URL, based on the specified path
389   var url = this.getUrl(path);
390 
391   // And then send the request
392   var ajax = null;
393   ajax = this.getAjax("GET", url, callback, headers);
394   ajax.send();
395 
396   return this;
397 };
398 
399 /**
400  * Perform a WebDAV HEAD request
401  *
402  * @param    {String}                         path      The path to perform HEAD on
403  * @param    {Function(status,body,headers)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
404  * @param    {Array}                          headers   Optional; Additional headers to set
405  * @returns  {nl.sara.webdav.Client}                    The client itself for chaining methods
406  */
407 nl.sara.webdav.Client.prototype.head = function(path, callback, headers) {
408   if ((path === undefined) || (callback === undefined)) {
409     throw new nl.sara.webdav.Exception('HEAD requires the parameters path and callback', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
410   }
411   if (typeof path !== "string"){
412     throw new nl.sara.webdav.Exception('HEAD parameter; path should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
413   }
414 
415   // Get the full URL, based on the specified path
416   var url = this.getUrl(path);
417 
418   // And then send the request
419   var ajax = null;
420   ajax = this.getAjax("HEAD", url, callback, headers);
421   ajax.send();
422 
423   return this;
424 };
425 
426 /**
427  * Perform a WebDAV PUT request
428  *
429  * @param    {String}                         path           The path to perform PUT on
430  * @param    {Function(status,body,headers)}  callback       Querying the server is done asynchronously, this callback function is called when the results are in
431  * @param    {String}                         body           The content to include in the PUT request.
432  * @param    {String}                         [contenttype]  Optional; the content type of the body (i.e. value for the Content-Type header).
433  * @param    {Array}                          headers        Optional; Additional headers to set
434  * @returns  {nl.sara.webdav.Client}                         The client itself for chaining methods
435  */
436 nl.sara.webdav.Client.prototype.put = function(path, callback, body, contenttype, headers) {
437   if ((path === undefined) || (callback === undefined) || (body === undefined)) {
438     throw new nl.sara.webdav.Exception('PUT requires the parameters path, callback and body', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
439   }
440   if ((typeof path !== "string") || ((contenttype !== undefined) && (typeof contenttype !== 'string'))) {
441     throw new nl.sara.webdav.Exception('PUT parameter; path and contenttype should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
442   }
443 
444   // Get the full URL, based on the specified path
445   var url = this.getUrl(path);
446 
447   // And then send the request
448   var ajax = null;
449   ajax = this.getAjax("PUT", url, callback, headers);
450   if (contenttype !== undefined) {
451     ajax.setRequestHeader('Content-Type', contenttype);
452   }
453   ajax.send(body);
454 
455   return this;
456 };
457 
458 /**
459  * Perform a WebDAV POST request
460  *
461  * @param    {String}                         path                                               The path to perform POST on
462  * @param    {Function(status,body,headers)}  callback                                           Querying the server is done asynchronously, this callback function is called when the results are in
463  * @param    {String}                         [body]                                             Optional; a body to include in the POST request.
464  * @param    {String}                         [contenttype='application/x-www-form-urlencoded']  Optional; the content type of the body (i.e. value for the Content-Type header). If omitted, but body is specified, then 'application/x-www-form-urlencoded' is assumed
465  * @param    {Array}                          headers                                            Optional; Additional headers to set
466  * @returns  {nl.sara.webdav.Client}                                                             The client itself for chaining methods
467  */
468 nl.sara.webdav.Client.prototype.post = function(path, callback, body, contenttype, headers) {
469   if ((path === undefined) || (callback === undefined)) {
470     throw new nl.sara.webdav.Exception('POST requires the parameters path and callback', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
471   }
472   if ((typeof path !== "string") || ((body !== undefined) && (typeof body !== 'string')) || ((contenttype !== undefined) && (typeof contenttype !== 'string'))) {
473     throw new nl.sara.webdav.Exception('POST parameter; path, body and contenttype should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
474   }
475 
476   // Get the full URL, based on the specified path
477   var url = this.getUrl(path);
478 
479   // And then send the request
480   var ajax = null;
481   ajax = this.getAjax("POST", url, callback, headers);
482   if ( body !== undefined ) {
483     if (contenttype !== undefined) {
484       ajax.setRequestHeader('Content-Type', contenttype);
485     }else{
486       ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
487     }
488     ajax.send(body);
489   }else{
490     ajax.send();
491   }
492 
493   return this;
494 };
495 
496 /**
497  * Perform a WebDAV COPY request
498  *
499  * @param    {String}                         path                              The path to perform COPY on
500  * @param    {Function(status,body,headers)}  callback                          Querying the server is done asynchronously, this callback function is called when the results are in
501  * @param    {String}                         destination                       The destination to copy to. Should be either a full URL or a path from the root on this server (i.e. it should start with a /)
502  * @param    {Integer}                        [overwriteMode=SILENT_OVERWRITE]  Optional; Specify what to do when destination resource already exists. Should be either FAIL_ON_OVERWRITE or SILENT_OVERWRITE. The default is SILENT_OVERWRITE.
503  * @param    {String}                         [depth]                           Optional; Should be '0' or 'infinity'. This is used in case of a collection; 0 means only copy the collection itself, infinity means copy also everything contained in the collection
504  * @param    {Array}                          headers                           Optional; Additional headers to set
505  * @returns  {nl.sara.webdav.Client}                                            The client itself for chaining methods
506  */
507 nl.sara.webdav.Client.prototype.copy = function(path, callback, destination, overwriteMode, depth, headers) {
508   if ((path === undefined) || (callback === undefined) || (destination === undefined)) {
509     throw new nl.sara.webdav.Exception('COPY requires the parameters path, callback and destination', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
510   }
511   if ((typeof path !== "string") || (typeof destination !== "string")){
512     throw new nl.sara.webdav.Exception('COPY parameter; path and destination should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
513   }
514 
515   // Get the full URL, based on the specified path
516   var url = this.getUrl(path);
517 
518   // If the destination starts with a / it is a absolute url on the same host, so prepare a complete URL
519   if (destination.substr(0,1) === '/') {
520     destination = this.getUrl(destination);
521   } // Else I assume it is a complete URL already
522 
523   // And then send the request
524   var ajax = this.getAjax("COPY", url, callback, headers);
525   ajax.setRequestHeader('Destination', destination);
526   if (depth !== undefined) {
527     if ((depth !== 0) && (depth !== 'infinity')) {
528       throw new nl.sara.webdav.Exception("COPY parameter; depth should be '0' or 'infinity'", nl.sara.webdav.Exception.WRONG_VALUE);
529     }
530     ajax.setRequestHeader('Depth', depth);
531   }
532   if (overwriteMode === nl.sara.webdav.Client.FAIL_ON_OVERWRITE) {
533     ajax.setRequestHeader('Overwrite', 'F');
534   }
535   ajax.send();
536 
537   return this;
538 };
539 
540 /**
541  * Perform a WebDAV MOVE request
542  *
543  * @param    {String}                         path                              The path to perform MOVE on
544  * @param    {Function(status,body,headers)}  callback                          Querying the server is done asynchronously, this callback function is called when the results are in
545  * @param    {String}                         destination                       The destination to move to. Should be either a full URL or a path from the root on this server (i.e. it should start with a /)
546  * @param    {Number}                         [overwriteMode=SILENT_OVERWRITE]  Optional; Specify what to do when destination resource already exists. Should be either FAIL_ON_OVERWRITE, TRUNCATE_ON_OVERWRITE or SILENT_OVERWRITE. The default is SILENT_OVERWRITE.
547  * @param    {Array}                          headers                           Optional; Additional headers to set
548  * @returns  {nl.sara.webdav.Client}                                            The client itself for chaining methods
549  */
550 nl.sara.webdav.Client.prototype.move = function(path, callback, destination, overwriteMode, headers) {
551   if ((path === undefined) || (callback === undefined) || (destination === undefined)) {
552     throw new nl.sara.webdav.Exception('MOVE requires the parameters path, callback and destination', nl.sara.webdav.Exception.MISSING_REQUIRED_PARAMETER);
553   }
554   if ((typeof path !== "string") || (typeof destination !== "string")){
555     throw new nl.sara.webdav.Exception('MOVE parameter; path and destination should be strings', nl.sara.webdav.Exception.WRONG_TYPE);
556   }
557 
558   // Get the full URL, based on the specified path
559   var url = this.getUrl(path);
560 
561   // If the destination starts with a / it is a absolute url on the same host, so prepare a complete URL
562   if (destination.substr(0,1) === '/') {
563     destination = this.getUrl(destination);
564   } // Else I assume it is a complete URL already
565 
566   // And then send the request
567   var ajax = this.getAjax("MOVE", url, callback, headers);
568   ajax.setRequestHeader('Destination', destination);
569   if (overwriteMode === nl.sara.webdav.Client.FAIL_ON_OVERWRITE) {
570     ajax.setRequestHeader('Overwrite', 'F');
571   }else if (overwriteMode === nl.sara.webdav.Client.TRUNCATE_ON_OVERWRITE) {
572     ajax.setRequestHeader('Overwrite', 'T');
573   }
574   ajax.send();
575 
576   return this;
577 };
578 
579 /**
580  * Perform a WebDAV LOCK request
581  *
582  * @param    {String}                         path      The path to perform LOCK on
583  * @param    {Function(status,body,headers)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
584  * @param    {Array}                          headers   Optional; Additional headers to set
585  * @returns  {nl.sara.webdav.Client}                    The client itself for chaining methods
586  */
587 nl.sara.webdav.Client.prototype.lock = function(path, callback, headers) {
588   throw new nl.sara.webdav.Exception('LOCK is not implemented yet', nl.sara.webdav.Exception.NOT_IMPLEMENTED);
589   return this;
590 };
591 
592 /**
593  * Perform a WebDAV UNLOCK request
594  *
595  * @param    {String}                         path      The path to perform UNLOCK on
596  * @param    {Function(status,body,headers)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
597  * @param    {Array}                          headers   Optional; Additional headers to set
598  * @returns  {nl.sara.webdav.Client}                    The client itself for chaining methods
599  */
600 nl.sara.webdav.Client.prototype.unlock = function(path, callback, headers) {
601   throw new nl.sara.webdav.Exception('UNLOCK is not implemented yet', nl.sara.webdav.Exception.NOT_IMPLEMENTED);
602   return this;
603 };
604 
605 /**
606  * Prepares a XMLHttpRequest object to be used for an AJAX request
607  *
608  * @static
609  * @param    {String}                         method    The HTTP/webDAV method to use (e.g. GET, POST, PROPFIND)
610  * @param    {String}                         url       The url to connect to
611  * @param    {Function(status,body,headers)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
612  * @param    {Array}                          headers   Additional headers to set
613  * @returns  {XMLHttpRequest}                           A prepared XMLHttpRequest
614  */
615 nl.sara.webdav.Client.prototype.getAjax = function(method, url, callback, headers) {
616   var /** @type XMLHttpRequest */ ajax = new XMLHttpRequest();
617   if ( this._username !== null ) {
618     ajax.open( method, url, true, this._username, this._password );
619   }else{
620     ajax.open( method, url, true );
621   }
622   ajax.onreadystatechange = function() {
623     nl.sara.webdav.Client.ajaxHandler( ajax, callback );
624   };
625   
626   if (headers === undefined) {
627     headers = {};
628   }
629   for (var header in this._headers) {
630     if (headers[header] === undefined) {
631       ajax.setRequestHeader(header, this._headers[header]);
632     }
633   }
634   for (var header in headers) {
635     ajax.setRequestHeader(header, headers[header]);
636   }
637   return ajax;
638 };
639 
640 /**
641  * AJAX request handler. Parses Multistatus (if available) and call a user specified callback function
642  *
643  * @static
644  * @param    {XMLHttpRequest}                 ajax      The XMLHttpRequest object which performed the request
645  * @param    {Function(status,body,headers)}  callback  Querying the server is done asynchronously, this callback function is called when the results are in
646  * @returns  {void}
647  */
648 nl.sara.webdav.Client.ajaxHandler = function(ajax, callback) {
649   if (ajax.readyState === 4){ //if request has completed
650     var body = ajax.responseText;
651     if (ajax.status === 207) {
652       // Parse the response to a Multistatus object
653       for (var counter = 0; counter < ajax.responseXML.childNodes.length; counter++) {
654         if (nl.sara.webdav.Ie.getLocalName(ajax.responseXML.childNodes.item(counter)) === 'multistatus') {
655           body = new nl.sara.webdav.Multistatus(ajax.responseXML.childNodes.item(counter));
656           break;
657         }
658       }
659     }
660     callback(ajax.status, body, ajax.getAllResponseHeaders());
661   }
662 };
663 
664 // End of library
665