<NME> handler.js <BEF> /* * @package jsDAV * @subpackage DAV * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org> * @author Mike de Boer <info AT mikedeboer DOT nl> * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License */ "use strict"; // DAV classes used directly by the Handler object var jsDAV = require("./../jsdav"); var jsDAV_Server = require("./server"); var jsDAV_Property_Response = require("./property/response"); var jsDAV_Property_GetLastModified = require("./property/getLastModified"); var jsDAV_Property_ResourceType = require("./property/resourceType"); var jsDAV_Property_SupportedReportSet = require("./property/supportedReportSet"); // interfaces to check for: var jsDAV_iFile = require("./interfaces/iFile"); var jsDAV_iCollection = require("./interfaces/iCollection"); var jsDAV_iExtendedCollection = require("./interfaces/iExtendedCollection") var jsDAV_iQuota = require("./interfaces/iQuota"); var jsDAV_iProperties = require("./interfaces/iProperties"); var Url = require("url"); var Fs = require("fs"); var Path = require("path"); var AsyncEventEmitter = require("./../shared/asyncEvents").EventEmitter; var Exc = require("./../shared/exceptions"); var Util = require("./../shared/util"); var Xml = require("./../shared/xml"); var Async = require("asyncjs"); var Formidable = require("formidable"); var requestCounter = 0; /** * Called when an http request comes in, pass it on to invoke and handle any * exceptions that might be thrown * * @param {jsDav_Server} server * @param {ServerRequest} req * @param {ServerResponse} resp * @return {jsDAV_Handler} */ var jsDAV_Handler = module.exports = function(server, req, resp) { this.server = server; this.httpRequest = Util.streamBuffer(req); this.httpResponse = resp; this.plugins = {}; this.nodeCache = {}; for (var plugin in server.plugins) { if (typeof server.plugins[plugin] != "object") continue; this.plugins[plugin] = server.plugins[plugin].new(this); } try { this.invoke(); } catch (ex) { this.handleError(ex); } }; /** * Inifinity is used for some request supporting the HTTP Depth header and indicates * that the operation should traverse the entire tree */ jsDAV_Handler.DEPTH_INFINITY = -1; /** * Nodes that are files, should have this as the type property */ jsDAV_Handler.NODE_FILE = 1; /** * Nodes that are directories, should use this value as the type property */ jsDAV_Handler.NODE_DIRECTORY = 2; jsDAV_Handler.PROP_SET = 1; jsDAV_Handler.PROP_REMOVE = 2; jsDAV_Handler.STATUS_MAP = { "100": "Continue", "101": "Switching Protocols", "200": "OK", "201": "Created", "202": "Accepted", "203": "Non-Authorative Information", "204": "No Content", "205": "Reset Content", "206": "Partial Content", "207": "Multi-Status", // RFC 4918 "208": "Already Reported", // RFC 5842 "300": "Multiple Choices", "301": "Moved Permanently", "302": "Found", "303": "See Other", "304": "Not Modified", "305": "Use Proxy", "307": "Temporary Redirect", "400": "Bad request", "401": "Unauthorized", "402": "Payment Required", "403": "Forbidden", "404": "Not Found", "405": "Method Not Allowed", "406": "Not Acceptable", "407": "Proxy Authentication Required", "408": "Request Timeout", "409": "Conflict", "410": "Gone", "411": "Length Required", "412": "Precondition failed", "413": "Request Entity Too Large", "414": "Request-URI Too Long", "415": "Unsupported Media Type", "416": "Requested Range Not Satisfiable", "417": "Expectation Failed", "418": "I'm a teapot", // RFC 2324 "422": "Unprocessable Entity", // RFC 4918 "423": "Locked", // RFC 4918 "424": "Failed Dependency", // RFC 4918 "500": "Internal Server Error", "501": "Not Implemented", "502": "Bad Gateway", "503": "Service Unavailable", "504": "Gateway Timeout", "505": "HTTP Version not supported", "507": "Unsufficient Storage", // RFC 4918 "508": "Loop Detected" // RFC 5842 }; (function() { /** * httpResponse * * @var HTTP_Response */ this.httpResponse = /** * httpRequest * * @var HTTP_Request */ this.httpRequest = null; /** * The propertymap can be used to map properties from * requests to property classes. * * @var array */ this.propertyMap = { "{DAV:}resourcetype": jsDAV_Property_ResourceType }; this.protectedProperties = [ // RFC4918 "{DAV:}getcontentlength", "{DAV:}getetag", "{DAV:}getlastmodified", "{DAV:}lockdiscovery", "{DAV:}resourcetype", "{DAV:}supportedlock", // RFC4331 "{DAV:}quota-available-bytes", "{DAV:}quota-used-bytes", // RFC3744 "{DAV:}alternate-URI-set", "{DAV:}principal-URL", "{DAV:}group-membership", "{DAV:}supported-privilege-set", "{DAV:}current-user-privilege-set", "{DAV:}acl", "{DAV:}acl-restrictions", "{DAV:}inherited-acl-set", "{DAV:}principal-collection-set", // RFC5397 "{DAV:}current-user-principal" ]; /** * This property allows you to automatically add the 'resourcetype' value * based on a node's classname or interface. * * The preset ensures that {DAV:}collection is automaticlly added for nodes * implementing jsDAV_iCollection. * * @var object */ this.resourceTypeMapping = { "{DAV:}collection": jsDAV_iCollection }; var internalMethods = { "OPTIONS":1, "GET":1, "HEAD":1, "DELETE":1, "PROPFIND":1, "MKCOL":1, "PUT":1, "PROPPATCH":1, "COPY":1, "MOVE":1, "REPORT":1 }; /** * Handles a http request, and execute a method based on its name * * @return void */ this.invoke = function() { var method = this.httpRequest.method.toUpperCase(), self = this; if (jsDAV.debugMode) { this.id = ++requestCounter; Util.log("{" + this.id + "}", method, this.httpRequest.url); Util.log("{" + this.id + "}", this.httpRequest.headers); var wh = this.httpResponse.writeHead, we = this.httpResponse.end; this.httpResponse.writeHead = function(code, headers) { Util.log("{" + self.id + "}", code, headers); this.writeHead = wh; this.writeHead(code, headers); }; this.httpResponse.end = function(content) { Util.log("{" + self.id + "}", "'" + (content || "") + "'"); this.end = we; this.end(content); }; } var uri = this.getRequestUri(); this.dispatchEvent("beforeMethod", method, uri, function(stop) { if (stop === true) return; if (stop) return self.handleError(stop); // Make sure this is a HTTP method we support if (internalMethods[method]) { self["http" + method.charAt(0) + method.toLowerCase().substr(1)](); } else { self.dispatchEvent("unknownMethod", method, uri, function(stop) { if (stop === true) return; // Unsupported method self.handleError(stop || new Exc.NotImplemented()); }); } }); }; /** * Centralized error and exception handler, which constructs a proper WebDAV * 500 server error, or different depending on the error object implementation * and/ or extensions. * * @param {Error} e Error string or Exception object * @return {void} */ this.handleError = function(e) { //if (jsDAV.debugMode) // console.trace(); if (e === true) return; // plugins should return TRUE to prevent error reporting. if (typeof e == "string") e = new Exc.jsDAV_Exception(e); var xml = '<?xml version="1.0" encoding="utf-8"?>\n' + '<d:error xmlns:d="DAV:" xmlns:a="' + Xml.NS_AJAXORG + '">\n' + ' <a:exception>' + (e.type || e.toString()) + '</a:exception>\n' + ' <a:message>' + e.message + '</a:message>\n'; if (this.server.debugExceptions) { xml += '<a:file>' + (e.filename || "") + '</a:file>\n' + '<a:line>' + (e.line || "") + '</a:line>\n'; } xml += '<a:jsdav-version>' + jsDAV_Server.VERSION + '</a:jsdav-version>\n'; var code = 500; var self = this; if (e instanceof Exc.jsDAV_Exception) { code = e.code; xml = e.serialize(this, xml); e.getHTTPHeaders(this, function(err, h) { afterHeaders(h); }); } else { afterHeaders({}); } function afterHeaders(headers) { headers["Content-Type"] = "application/xml; charset=utf-8"; self.httpResponse.writeHead(code, headers); self.httpResponse.end(xml + '</d:error>', "utf-8"); if (jsDAV.debugMode) { Util.log(e, "error"); console.log(e.stack); //throw e; // DEBUGGING! } } }; /** * Caching version of jsDAV_Server#tree#getNodeForPath(), to make node lookups * during the same request (the scope of a handler instance) more cheap. * * @param {String} path * @param {Function} cbgetnodefp * @returns {void} */ this.getNodeForPath = function(path, cbgetnodefp) { if (this.nodeCache[path]) return cbgetnodefp(null, this.nodeCache[path]); var self = this; this.server.tree.getNodeForPath(path, function(err, node) { if (err) return cbgetnodefp(err); self.nodeCache[path] = node; cbgetnodefp(null, node); }); }; /** * This method is called with every tree update * * Examples of tree updates are: * * node deletions * * node creations * * copy * * move * * renaming nodes * * If Tree classes implement a form of caching, this will allow * them to make sure caches will be expired. * * If a path is passed, it is assumed that the entire subtree is dirty * * @param {String} path * @return void */ this.markDirty = function(path) { // We don't care enough about sub-paths // flushing the entire cache path = Util.trim(path, "/"); for (var nodePath in this.nodeCache) { if (nodePath.indexOf(path) === 0) delete this.nodeCache[nodePath]; } }; /** * HTTP OPTIONS * * @return {void} * @throws {Error} */ this.httpOptions = function() { var uri = this.getRequestUri(); var self = this; this.getAllowedMethods(uri, function(err, methods) { if (!Util.empty(err)) return self.handleError(err); var headers = { "Allow": methods.join(",").toUpperCase(), "MS-Author-Via" : "DAV", "Accept-Ranges" : "bytes", "X-jsDAV-Version" : jsDAV_Server.VERSION, "Content-Length" : 0 }; var features = ["1", "3", "extended-mkcol"]; for (var plugin in self.plugins) { if (!self.plugins[plugin].getFeatures) Util.log("method getFeatures() NOT implemented for plugin " + plugin, "error"); else features = features.concat(self.plugins[plugin].getFeatures()); } headers["DAV"] = features.join(","); self.httpResponse.writeHead(200, headers); self.httpResponse.end(); }); }; /** * HTTP GET * * This method simply fetches the contents of a uri, like normal * * @return {void} * @throws {Error} */ this.httpGet = function() { var node; var uri = this.getRequestUri(); var self = this; this.checkPreconditions(true, function(err, redirected) { if (!Util.empty(err)) return self.handleError(err); if (redirected) return; self.getNodeForPath(uri, function(err, n) { if (!Util.empty(err)) return self.handleError(err); node = n; afterCheck(); }); }); function afterCheck() { if (!node.hasFeature(jsDAV_iFile)) { return self.handleError(new Exc.NotImplemented( "GET is only implemented on File objects")); } var hasStream = !!node.getStream; self.getHTTPHeaders(uri, function(err, httpHeaders) { if (!Util.empty(err)) return self.handleError(err); var nodeSize = null; // ContentType needs to get a default, because many webservers // will otherwise default to text/html, and we don't want this // for security reasons. if (!httpHeaders["content-type"]) httpHeaders["content-type"] = "application/octet-stream"; if (httpHeaders["content-length"]) { nodeSize = httpHeaders["content-length"]; // New read/write stream var offlen = end - start + 1, newStream = new Buffer(offlen); body.copy(newStream, offlen, start); httpHeaders["Content-Length"] = offlen; httpHeaders["Content-Range"] = "bytes " + start + "-" + end + "/" + nodeSize; var ignoreRangeHeader = false; // If ifRange is set, and range is specified, we first need // to check the precondition. if (nodeSize && range && ifRange) { // if IfRange is parsable as a date we'll treat it as a // DateTime otherwise, we must treat it as an etag. try { var ifRangeDate = new Date(ifRange); // It's a date. We must check if the entity is modified // since the specified date. if (!httpHeaders["last-modified"]) { ignoreRangeHeader = true; } else { var modified = new Date(httpHeaders["last-modified"]); if (modified > ifRangeDate) ignoreRangeHeader = true; } } catch (ex) { // It's an entity. We can do a simple comparison. if (!httpHeaders["etag"]) ignoreRangeHeader = true; else if (httpHeaders["etag"] !== ifRange) ignoreRangeHeader = true; } } // We're only going to support HTTP ranges if the backend // provided a filesize if (!ignoreRangeHeader && nodeSize && range) { // Determining the exact byte offsets var start, end; if (range[0] !== null) { start = range[0]; // the browser/ client sends 'end' offsets as factor of nodeSize - 1, // so we need to correct it, because NodeJS streams byte offsets // are inclusive end = range[1] !== null ? range[1] + 1 : nodeSize; if (start > nodeSize) { return self.handleError(new Exc.RequestedRangeNotSatisfiable( "The start offset (" + range[0] + ") exceeded the size of the entity (" + nodeSize + ")") ); } if (end < start) { return self.handleError(new Exc.RequestedRangeNotSatisfiable( "The end offset (" + range[1] + ") is lower than the start offset (" + range[0] + ")") ); } if (end > nodeSize) end = nodeSize; } else { start = nodeSize - range[1]; end = nodeSize; if (start < 0) start = 0; } var offlen = end - start; // Prevent buffer error // https://github.com/joyent/node/blob/v0.4.5/lib/buffer.js#L337 if (end < start) { var swapTmp = end; end = start; start = swapTmp; } // report a different end offset, corrected by 1: var clientEnd = end > 0 ? end - 1 : end; httpHeaders["content-length"] = offlen; httpHeaders["content-range"] = "bytes " + start + "-" + clientEnd + "/" + nodeSize; if (hasStream) { var writeStreamingHeader = function () { self.httpResponse.writeHead(206, httpHeaders); }; node.getStream(start, end, function(err, data) { if (err) { if (!writeStreamingHeader) { self.httpResponse.end(); console.error("jsDAV GET error", err); } else { self.handleError(err); } return; } // write header on first incoming buffer if (writeStreamingHeader) { writeStreamingHeader(); writeStreamingHeader = null; } if (!data) return self.httpResponse.end(); self.httpResponse.write(data); }); } else { node.get(function(err, body) { if (!Util.empty(err)) return self.handleError(err); // New read/write stream var newStream = new Buffer(offlen); body.copy(newStream, 0, start, offlen); self.httpResponse.writeHead(206, httpHeaders); self.httpResponse.end(newStream); }); } } else { var since = self.httpRequest.headers["if-modified-since"]; var oldEtag = self.httpRequest.headers["if-none-match"]; var lastModified = httpHeaders["last-modified"]; var etag = httpHeaders["etag"]; since = since && Date.parse(since).valueOf(); lastModified = lastModified && Date.parse(lastModified).valueOf(); // If there is no match, then move on. if (!((since && lastModified === since) || (etag && oldEtag === etag))) { if (nodeSize) httpHeaders["content-length"] = nodeSize; if (hasStream) { var writeStreamingHeader = function () { self.httpResponse.writeHead(200, httpHeaders); }; // no start or end means: get all file contents. node.getStream(null, null, function(err, data) { if (err) { if (!writeStreamingHeader) { self.httpResponse.end(); console.error("jsDAV GET error", err); } else { self.handleError(err); } return; } // write header on first incoming buffer if (writeStreamingHeader) { writeStreamingHeader(); writeStreamingHeader = null; } if (!data) return self.httpResponse.end(); self.httpResponse.write(data); }); } else { node.get(function(err, body) { if (!Util.empty(err)) return self.handleError(err); self.httpResponse.writeHead(200, httpHeaders); self.httpResponse.end(body); }); } } else { // Filter out any Content based headers since there // is no content. var newHeaders = {}; Object.keys(httpHeaders).forEach(function(key) { if (key.indexOf("content") < 0) newHeaders[key] = httpHeaders[key]; }); self.httpResponse.writeHead(304, newHeaders); self.httpResponse.end(); } } }); } }; /** * HTTP HEAD * * This method is normally used to take a peak at a url, and only get the * HTTP response headers, without the body. * This is used by clients to determine if a remote file was changed, so * they can use a local cached version, instead of downloading it again * * @return {void} * @throws {Error} */ this.httpHead = function() { var uri = this.getRequestUri(), self = this; this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) return self.handleError(err); /* This information is only collection for File objects. * Ideally we want to throw 405 Method Not Allowed for every * non-file, but MS Office does not like this */ var headers = {}; if (node.hasFeature(jsDAV_iFile)) { self.getHTTPHeaders(uri, function(err, headersFetched) { if (!Util.empty(err)) return self.handleError(err); headers = headersFetched; if (!headers["content-type"]) headers["content-type"] = "application/octet-stream"; afterHeaders(); }); } else { afterHeaders(); } function afterHeaders() { self.httpResponse.writeHead(200, headers); self.httpResponse.end(); } }); }; /** * HTTP Delete * * The HTTP delete method, deletes a given uri * * @return {void} * @throws {Error} */ this.httpDelete = function() { var uri = this.getRequestUri(); var self = this; this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) return self.handleError(err); self.dispatchEvent("beforeUnbind", uri, function(stop) { if (stop) return; node["delete"](function(err) { if (!Util.empty(err)) return self.handleError(err); self.markDirty(uri); self.httpResponse.writeHead(204, {"content-length": "0"}); self.httpResponse.end(); self.dispatchEvent("afterDelete", uri); }); }); }); }; /** * WebDAV PROPFIND * * This WebDAV method requests information about an uri resource, or a list * of resources * If a client wants to receive the properties for a single resource it will * add an HTTP Depth: header with a 0 value. * If the value is 1, it means that it also expects a list of sub-resources * (e.g.: files in a directory) * * The request body contains an XML data structure that has a list of * properties the client understands. * The response body is also an xml document, containing information about * every uri resource and the requested properties * * It has to return a HTTP 207 Multi-status status code * * @throws {Error} */ this.httpPropfind = function() { var self = this; this.getRequestBody("utf8", null, false, function(err, data) { if (!Util.empty(err)) return self.handleError(err); //if (jsDAV.debugMode) // Util.log("{" + self.id + "}", "data received " + data); self.parsePropfindRequest(data, function(err, requestedProperties) { if (!Util.empty(err)) return self.handleError(err); var depth = self.getHTTPDepth(1); // The only two options for the depth of a propfind is 0 or 1 if (depth !== 0) depth = 1; // The requested path var path; try { path = self.getRequestUri(); } catch (ex) { return self.handleError(ex); } //if (jsDAV.debugMode) // Util.log("httpPropfind BEFORE getPropertiesForPath '" + path + "';", requestedProperties); self.getPropertiesForPath(path, requestedProperties, depth, function(err, newProperties) { if (!Util.empty(err)) return self.handleError(err); // Normally this header is only needed for OPTIONS responses, however.. // iCal seems to also depend on these being set for PROPFIND. Since // this is not harmful, we'll add it. var features = ["1", "3", "extended-mkcol"]; for (var plugin in self.plugins) { if (!self.plugins[plugin].getFeatures) Util.log("method getFeatures() NOT implemented for plugin " + plugin, "error"); else features = features.concat(self.plugins[plugin].getFeatures()); } // This is a multi-status response. self.httpResponse.writeHead(207, { "content-type": "application/xml; charset=utf-8", "vary": "Brief,Prefer", "DAV": features.join(",") }); var prefer = self.getHTTPPrefer(); self.httpResponse.end(self.generateMultiStatus(newProperties, prefer["return-minimal"])); }); }); }); }; /** * WebDAV PROPPATCH * * This method is called to update properties on a Node. The request is an * XML body with all the mutations. * In this XML body it is specified which properties should be set/updated * and/or deleted * * @return {void} */ this.httpProppatch = function() { var self = this; this.getRequestBody("utf8", null, false, function(err, data) { if (!Util.empty(err)) return self.handleError(err); //if (jsDAV.debugMode) // Util.log("{" + self.id + "}", "data received " + data); self.parseProppatchRequest(data, function(err, newProperties) { if (!Util.empty(err)) return self.handleError(err); self.updateProperties(self.getRequestUri(), newProperties, function(err, result) { if (!Util.empty(err)) return self.handleError(err); var prefer = self.getHTTPPrefer(); if (prefer["return-minimal"]) { // If return-minimal is specified, we only have to check if the // request was succesful, and don't need to return the // multi-status. var prop; var ok = true; for (var code in result) { prop = result[code]; if (parseInt(code, 10) > 299) { ok = false; break; } } if (ok) { self.httpResponse.writeHead(204, { "vary": "Brief, Prefer" }); self.httpResponse.end(); return; } } self.httpResponse.writeHead(207, { "content-type": "application/xml; charset=utf-8", "vary": "Brief, Prefer" }); self.httpResponse.end(self.generateMultiStatus(result)); }); }); }); }; /** * HTTP PUT method * * This HTTP method updates a file, or creates a new one. * If a new resource was created, a 201 Created status code should be returned. * If an existing resource is updated, it's a 200 Ok * * @return {void} */ this.httpPut = function() { var self = this; var uri = this.getRequestUri(); // Intercepting Content-Range if (this.httpRequest.headers["content-range"]) { /* Content-Range is dangerous for PUT requests: PUT per definition stores a full resource. draft-ietf-httpbis-p2-semantics-15 says in section 7.6: An origin server SHOULD reject any PUT request that contains a Content-Range header field, since it might be misinterpreted as partial content (or might be partial content that is being mistakenly PUT as a full representation). Partial content updates are possible by targeting a separately identified resource with state that overlaps a portion of the larger resource, or by using a different method that has been specifically defined for partial updates (for example, the PATCH method defined in [RFC5789]). This clarifies RFC2616 section 9.6: The recipient of the entity MUST NOT ignore any Content-* (e.g. Content-Range) headers that it does not understand or implement and MUST return a 501 (Not Implemented) response in such cases. OTOH is a PUT request with a Content-Range currently the only way to continue an aborted upload request and is supported by curl, mod_dav, Tomcat and others. Since some clients do use this feature which results in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject all PUT requests with a Content-Range for now. */ return this.handleError(new Exc.NotImplemented("PUT with Content-Range is not allowed.")); } // First we'll do a check to see if the resource already exists this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) { if (err instanceof Exc.FileNotFound) { // If we got here, the resource didn't exist yet. // `data` is set to `null` to use streamed write. self.createFile(uri, null, "binary", function(err, etag) { if (!Util.empty(err)) return self.handleError(err); var headers = {"content-length": "0"}; if (etag) headers.etag = etag; self.httpResponse.writeHead(201, headers); self.httpResponse.end(); self.dispatchEvent("afterWriteContent", uri); }); } else { return self.handleError(err); } } else { var hasStream = !!node.putStream; // Checking If-None-Match and related headers. self.checkPreconditions(false, function(err, redirected) { if (!Util.empty(err)) return self.handleError(err); if (redirected) return false; // If the node is a collection, we'll deny it if (!node.hasFeature(jsDAV_iFile)) return self.handleError(new Exc.Conflict("PUT is not allowed on non-files.")); self.dispatchEvent("beforeWriteContent", uri, node, function(stop) { if (stop) return; if (hasStream) { node.putStream(self, "binary", afterPut); } else { self.getRequestBody("binary", null, false, function(err, body) { if (!Util.empty(err)) return self.handleError(err); node.put(body, "binary", afterPut); }); } function afterPut(err, etag) { if (!Util.empty(err)) return self.handleError(err); var headers = {"content-length": "0"}; if (etag) headers.etag = etag; self.httpResponse.writeHead(200, headers); self.httpResponse.end(); self.dispatchEvent("afterWriteContent", uri); } }); }); } }); }; /** * WebDAV MKCOL * * The MKCOL method is used to create a new collection (directory) on the server * * @return {void} */ this.httpMkcol = function() { var resourceType; var properties = {}; var self = this; var req = this.httpRequest; this.getRequestBody("utf8", null, false, function(err, requestBody) { if (!Util.empty(err)) return self.handleError(err); if (requestBody) { var contentType = req.headers["content-type"]; if (contentType.indexOf("application/xml") !== 0 && contentType.indexOf("text/xml") !== 0) { // We must throw 415 for unsupported mkcol bodies return self.handleError(new Exc.UnsupportedMediaType( "The request body for the MKCOL request must have an xml Content-Type")); } Xml.loadDOMDocument(requestBody, self.server.options.parser, function(err, dom) { var firstChild = dom.firstChild; if (Xml.toClarkNotation(firstChild) !== "{DAV:}mkcol") { // We must throw 415 for unsupport mkcol bodies return self.handleError(new Exc.UnsupportedMediaType( "The request body for the MKCOL request must be a {DAV:}mkcol request construct.")); } var childNode; var i = 0; var c = firstChild.childNodes; var l = c.length; for (; i < l; ++i) { childNode = c[i]; if (Xml.toClarkNotation(childNode) !== "{DAV:}set") continue; properties = Util.extend(properties, Xml.parseProperties(childNode, self.propertyMap)); } if (!properties["{DAV:}resourcetype"]) { return self.handleError(new Exc.BadRequest( "The mkcol request must include a {DAV:}resourcetype property") ); } delete properties["{DAV:}resourcetype"]; resourceType = []; // Need to parse out all the resourcetypes var rtNode = firstChild.getElementsByTagNameNS("urn:DAV", "resourcetype")[0]; for (i = 0, c = rtNode.childNodes, l = c.length; i < l; ++i) resourceType.push(Xml.toClarkNotation(c[i])); afterParse(); }); } else { resourceType = ["{DAV:}collection"]; afterParse(); } function afterParse() { try { var uri = self.getRequestUri() } catch (ex) { return self.handleError(ex); } self.createCollection(uri, resourceType, properties, function(err, result) { if (!Util.empty(err)) return self.handleError(err); if (result && result.length) { self.httpResponse.writeHead(207, {"content-type": "application/xml; charset=utf-8"}); self.httpResponse.end(self.generateMultiStatus(result)); } else { self.httpResponse.writeHead(201, {"content-length": "0"}); self.httpResponse.end(); } }); } }); }; /** * WebDAV HTTP MOVE method * * This method moves one uri to a different uri. A lot of the actual request * processing is done in getCopyMoveInfo * * @return {void} */ this.httpMove = function() { var self = this; this.getCopyAndMoveInfo(function(err, moveInfo) { if (!Util.empty(err)) return self.handleError(err); if (moveInfo.destinationExists) { self.dispatchEvent("beforeUnbind", moveInfo.destination, function(stop) { if (stop) return false; moveInfo.destinationNode["delete"](function(err) { if (!Util.empty(err)) return self.handleError(err); afterDelete(); }); }); } else { afterDelete(); } function afterDelete() { self.dispatchEvent("beforeUnbind", moveInfo.source, function(stop) { if (stop) return false; self.dispatchEvent("beforeBind", moveInfo.destination, function(stop) { if (stop) return false; self.server.tree.move(moveInfo.source, moveInfo.destination, function(err, sourceDir, destinationDir) { if (!Util.empty(err)) return self.handleError(err); self.markDirty(moveInfo.source); self.markDirty(moveInfo.destination); self.dispatchEvent("afterBind", moveInfo.destination, Path.join(self.server.tree.basePath, moveInfo.destination)); // If a resource was overwritten we should send a 204, otherwise a 201 self.httpResponse.writeHead(moveInfo.destinationExists ? 204 : 201, {"content-length": "0"}); self.httpResponse.end(); self.dispatchEvent("afterMove", moveInfo.destination); }); }); }); } }); }; /** * WebDAV HTTP COPY method * * This method copies one uri to a different uri, and works much like the MOVE request * A lot of the actual request processing is done in getCopyMoveInfo * * @return {void} */ this.httpCopy = function() { var self = this; this.getCopyAndMoveInfo(function(err, copyInfo) { if (!Util.empty(err)) return self.handleError(err); if (copyInfo.destinationExists) { self.dispatchEvent("beforeUnbind", copyInfo.destination, function(stop) { if (stop) return false; copyInfo.destinationNode["delete"](function(err) { if (!Util.empty(err)) return self.handleError(err); afterDelete(); }); }); } else { afterDelete(); } function afterDelete() { self.dispatchEvent("beforeBind", copyInfo.destination, function(stop) { if (stop) return false; self.server.tree.copy(copyInfo.source, copyInfo.destination, function(err) { if (!Util.empty(err)) return self.handleError(err); self.markDirty(copyInfo.destination); self.dispatchEvent("afterBind", copyInfo.destination, Path.join(self.server.tree.basePath, copyInfo.destination)); // If a resource was overwritten we should send a 204, otherwise a 201 self.httpResponse.writeHead(copyInfo.destinationExists ? 204 : 201, {"Content-Length": "0"}); self.httpResponse.end(); self.dispatchEvent("afterCopy", copyInfo.destination); }); }); } }); }; /** * HTTP REPORT method implementation * * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) * It's used in a lot of extensions, so it made sense to implement it into the core. * * @return {void} */ this.httpReport = function() { var self = this; this.getRequestBody("utf8", null, false, function(err, data) { Xml.loadDOMDocument(data, self.server.options.parser, function(err, dom) { var reportName = Xml.toClarkNotation(dom); self.dispatchEvent("report", reportName, dom, function(stop) { if (stop !== true) { // if dispatchEvent didn't return true, it means the report was not supported return self.handleError(new Exc.ReportNotImplemented()); } }); }); }); }; /** * Returns an array with all the supported HTTP methods for a specific uri. * * @param {String} uri * @param {Function} cbmethods Callback that is the return body of this function * @return {Array} */ this.getAllowedMethods = function(uri, cbmethods) { var self = this; var methods = [ "OPTIONS", "GET", "HEAD", "DELETE", "PROPFIND", "PUT", "PROPPATCH", "COPY", "MOVE", "REPORT" ]; // The MKCOL is only allowed on an unmapped uri this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) methods.push("MKCOL"); // We're also checking if any of the plugins register any new methods for (var plugin in self.plugins) { if (!self.plugins[plugin].getHTTPMethods) Util.log("method getHTTPMethods() NOT implemented for plugin " + plugin, "error"); else methods = methods.concat(self.plugins[plugin].getHTTPMethods(uri, node)); } cbmethods(null, Util.makeUnique(methods)); }); }; /** * Gets the uri for the request, keeping the base uri into consideration * * @return {String} * @throws {Error} */ this.getRequestUri = function() { return this.calculateUri(this.httpRequest.url); }; /** * Fetch the binary data for the HTTP request and return it to a callback OR * write it to a WritableStream instance. * * @param {String} enc * @param {WritableStream} [stream] * @param {Boolean} [forceStream] * @param {Function} callback * @return {void} */ this.getRequestBody = function(enc, stream, forceStream, cbreqbody) { if (!cbreqbody) { cbreqbody = stream; stream = null; forceStream = false; } var ctype; var self = this; var req = this.httpRequest; var isStream = forceStream === true ? true : (!(ctype = req.headers["content-type"]) || !ctype.match(/(urlencoded|multipart)/i)); // HACK: MacOSX Finder and NodeJS don't play nice together with files // that start with '._' //if (/\/\.[D_]{1}[^\/]+$/.test(req.url)) // return cbreqbody(null, "", cleanup); enc = (enc || "utf8").replace("-", ""); if (enc == "raw") enc = "binary"; if (req.$data) { return cbreqbody(req.$data.err || null, enc == "binary" ? req.$data : req.$data.toString(enc)); } if (isStream) { var buff = []; var contentLength = req.headers["content-length"]; var lengthCount = 0; var err; if (stream) stream.on("error", function(ex) { err = ex; }); req.streambuffer.ondata(function(data) { lengthCount += data.length; if (stream && stream.writable) stream.write(data); else buff.push(data); }); req.streambuffer.onend(function() { // TODO: content-length check and rollback... if (stream) { if (err) return cbreqbody(err); stream.on("close", function() { cbreqbody(err); }); stream.end(); } else { buff = Util.concatBuffers(buff); if (contentLength && parseInt(contentLength, 10) != buff.length) { readDone(new Exc.BadRequest("Content-Length mismatch: Request Header claimed " + contentLength + " bytes, but received " + buff.length + " bytes"), buff); } else readDone(null, buff); } }); } else { var form = new Formidable.IncomingForm(); form.uploadDir = this.server.tmpDir; form.on("file", function(field, file) { // in the case of a streamed write, we can move the file to its // correct position right away: if (stream) { stream.on("close", function() { Fs.rename(file.path, stream.path, cbreqbody); }); stream.end(); } else { Fs.readFile(file.path, function(err, data) { readDone(err, data); Fs.unlink(file.path); }); } }); form.on("error", cbreqbody); form.parse(req); } function readDone(err, data) { req.$data = data; if (err) req.$data.err = err; if (jsDAV.debugMode && enc != "binary") Util.log("{" + self.id + "}", req.$data.toString("utf8")); cbreqbody(err, enc == "binary" ? req.$data : req.$data.toString(enc)); } }; /** * Calculates the uri for a request, making sure that the base uri is stripped out * * @param {String} uri * @throws {Exc.Forbidden} A permission denied exception is thrown * whenever there was an attempt to supply a uri outside of the base uri * @return {String} */ this.calculateUri = function(uri) { if (uri.charAt(0) != "/") { if (uri.indexOf("://") > -1) uri = Url.parse(uri).pathname; else uri = "/" + uri; } else if (uri.indexOf("?") > -1) uri = Url.parse(uri).pathname; uri = uri.replace("//", "/"); if (uri.indexOf(this.server.baseUri) === 0) { return Util.trim(decodeURIComponent(uri.substr(this.server.baseUri.length)), "/"); } // A special case, if the baseUri was accessed without a trailing // slash, we'll accept it as well. else if (uri + "/" === this.server.baseUri) { return ""; } else { throw new Exc.Forbidden("Requested uri (" + uri + ") is out of base uri (" + this.server.baseUri + ")"); } }; /** * This method checks the main HTTP preconditions. * * Currently these are: * * If-Match * * If-None-Match * * If-Modified-Since * * If-Unmodified-Since * * The method will return true if all preconditions are met * The method will return false, or throw an exception if preconditions * failed. If false is returned the operation should be aborted, and * the appropriate HTTP response headers are already set. * * Normally this method will throw 412 Precondition Failed for failures * related to If-None-Match, If-Match and If-Unmodified Since. It will * set the status to 304 Not Modified for If-Modified_since. * * If the handleAsGET argument is set to true, it will also return 304 * Not Modified for failure of the If-None-Match precondition. This is the * desired behaviour for HTTP GET and HTTP HEAD requests. * * @param {Boolean} handleAsGET * @param {Function} cbprecond Callback that is the return body of this function * @return {void} */ this.checkPreconditions = function(handleAsGET, cbprecond) { handleAsGET = handleAsGET || false; var uri, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince; var node = null; var lastMod = null; var etag = null; var self = this; try { uri = this.getRequestUri(); } catch (ex) { return cbprecond(ex); } if (ifMatch = this.httpRequest.headers["if-match"]) { // If-Match contains an entity tag. Only if the entity-tag // matches we are allowed to make the request succeed. // If the entity-tag is '*' we are only allowed to make the // request succeed if a resource exists at that url. this.getNodeForPath(uri, function(err, n) { if (!Util.empty(err)) { return cbprecond(new Exc.PreconditionFailed( "An If-Match header was specified and the resource did not exist", "If-Match")); } node = n; // Only need to check entity tags if they are not * if (ifMatch !== "*") { node.getETag(function(err, etag) { if (err) return cbprecond(err); var haveMatch = false; // There can be multiple etags ifMatch = ifMatch.split(","); var ifMatchItem; for (var i = 0, l = ifMatch.length; i < l; ++i) { // Stripping any extra spaces ifMatchItem = Util.trim(ifMatch[i], " "); if (etag === ifMatchItem) { haveMatch = true; break; } else { // Evolution has a bug where it sometimes prepends the " // with a \. This is our workaround. if (ifMatchItem.replace('\\"','"') === etag) { haveMatch = true; break; } } } if (!haveMatch) { return cbprecond(new Exc.PreconditionFailed( "An If-Match header was specified, but none of the specified the ETags matched", "If-Match") ); } afterIfMatch(); }); return; } afterIfMatch(); }); } else { afterIfMatch(); } function afterIfMatch() { if (ifNoneMatch = self.httpRequest.headers["if-none-match"]) { // The If-None-Match header contains an etag. // Only if the ETag does not match the current ETag, the request will succeed // The header can also contain *, in which case the request // will only succeed if the entity does not exist at all. var nodeExists = true; if (!node) { self.getNodeForPath(uri, function(err, n) { if (!Util.empty(err)) nodeExists = false; else node = n; if (nodeExists) { // The Etag is surrounded by double-quotes, so those must be // stripped. ifNoneMatch = Util.trim(ifNoneMatch, '"'); node.getETag(function(err, etag) { if (err) return cbprecond(err); if (ifNoneMatch === "*" || etag === ifNoneMatch) { if (handleAsGET) { self.httpResponse.writeHead(304); self.httpResponse.end(); cbprecond(null, true); // @todo call cbprecond() differently here? } else { cbprecond(new Exc.PreconditionFailed( "An If-None-Match header was specified, but the ETag " + "matched (or * was specified).", "If-None-Match") ); } } else { afterIfNoneMatch(); } }); return; } else { afterIfNoneMatch(); } }); } else { afterIfNoneMatch(); } } else { afterIfNoneMatch(); } function afterIfNoneMatch() { if (!ifNoneMatch && (ifModifiedSince = self.httpRequest.headers["if-modified-since"])) { // The If-Modified-Since header contains a date. We // will only return the entity if it has been changed since // that date. If it hasn't been changed, we return a 304 // header // Note that this header only has to be checked if there was no // If-None-Match header as per the HTTP spec. var date = new Date(ifModifiedSince); if (!node) self.getNodeForPath(uri, function(err, n) { if (!Util.empty(err)) return cbprecond(err); node = n; node.getLastModified(function(err, lastMod) { if (lastMod) { lastMod = new Date("@" + lastMod); if (lastMod <= date) { self.httpResponse.writeHead(304); self.httpResponse.end(); cbprecond(null, true); // @todo call cbprecond() differently here? return; } } afterIfModifiedSince(); }); }); } else { afterIfModifiedSince(); } function afterIfModifiedSince() { var date; if (ifUnmodifiedSince = self.httpRequest.headers["if-unmodified-since"]) { // The If-Unmodified-Since will allow the request if the // entity has not changed since the specified date. date = new Date(ifUnmodifiedSince); if (!node) { self.getNodeForPath(uri, function(err, n) { if (!Util.empty(err)) return cbprecond(err); node = n; finale(); }); } else { finale(); } } else { cbprecond(null, false); } function finale() { node.getLastModified(function(err, lastMod) { if (lastMod) { lastMod = new Date("@" + lastMod); if (lastMod > date) { return cbprecond(new Exc.PreconditionFailed( "An If-Unmodified-Since header was specified, but the " + "entity has been changed since the specified date.", "If-Unmodified-Since") ); } } cbprecond(null, false); }); } } } } }; /** * Generates a WebDAV propfind response body based on a list of nodes * * @param {Array} fileProperties The list with nodes * @return {String} */ this.generateMultiStatus = function(fileProperties, strip404s) { var namespace, prefix, entry, href, response; var xml = '<?xml version="1.0" encoding="utf-8"?><d:multistatus'; // Adding in default namespaces for (namespace in Xml.xmlNamespaces) { prefix = Xml.xmlNamespaces[namespace]; xml += ' xmlns:' + prefix + '="' + namespace + '"'; } xml += ">"; for (var i in fileProperties) { entry = fileProperties[i]; href = entry["href"]; //delete entry["href"]; if (strip404s && entry["404"]) delete entry["404"]; response = jsDAV_Property_Response.new(href, entry); xml = response.serialize(this, xml); } return xml + "</d:multistatus>"; }; /** * Returns a list of HTTP headers for a particular resource * * The generated http headers are based on properties provided by the * resource. The method basically provides a simple mapping between * DAV property and HTTP header. * * The headers are intended to be used for HEAD and GET requests. * * @param {String} path */ this.getHTTPHeaders = function(path, cbheaders) { var header; var propertyMap = { "{DAV:}getcontenttype" : "content-type", "{DAV:}getcontentlength" : "content-length", "{DAV:}getlastmodified" : "last-modified", "{DAV:}getetag" : "etag" }; var headers = { "pragma" : "no-cache", "cache-control" : "no-cache, no-transform" }; this.getProperties(path, ["{DAV:}getcontenttype", "{DAV:}getcontentlength", "{DAV:}getlastmodified", "{DAV:}getetag"], function(err, properties) { if (!Util.empty(err)) return cbheaders(err, headers); for (var prop in propertyMap) { header = propertyMap[prop]; if (properties[prop]) { // GetLastModified gets special cased if (properties[prop].hasFeature && properties[prop].hasFeature(jsDAV_Property_GetLastModified)) headers[header] = Util.dateFormat(properties[prop].getTime(), Util.DATE_RFC1123); else if (typeof properties[prop] != "object") headers[header] = properties[prop]; } } cbheaders(null, headers); }); }; /** * Returns a list of properties for a path * * This is a simplified version getPropertiesForPath. * if you aren't interested in status codes, but you just * want to have a flat list of properties. Use this method. * * @param {String} path * @param {Array} propertyNames */ this.getProperties = function(path, propertyNames, cbgetprops) { this.getPropertiesForPath(path, propertyNames, 0, function(err, result) { if (!Util.empty(err)) return cbgetprops(err); return cbgetprops(null, result[path]["200"]); }); }; /** * Returns a list of properties for a given path * * The path that should be supplied should have the baseUrl stripped out * The list of properties should be supplied in Clark notation. If the list * is empty 'allprops' is assumed. * * If a depth of 1 is requested child elements will also be returned. * * @param {String} path * @param {Array} propertyNames * @param {Number} depth * @return {Array} */ this.getPropertiesForPath = function(path, propertyNames, depth, cbgetpropspath) { propertyNames = propertyNames || []; depth = depth || 0; if (depth !== 0) depth = 1; path = Util.rtrim(path, "/"); var returnPropertyList = {}; var self = this; var reportPlugins = []; Object.keys(this.plugins).forEach(function(pluginName) { if (self.plugins[pluginName].getSupportedReportSet) reportPlugins.push(self.plugins[pluginName]); }); this.getNodeForPath(path, function(err, parentNode) { if (!Util.empty(err)) return cbgetpropspath(err); var nodes = {}; var nodesPath = []; nodes[path] = parentNode; nodesPath.push(path); //if (jsDAV.debugMode) // Util.log("getPropertiesForPath", depth, parentNode,parentNode.hasFeature(jsDAV_iCollection)); if (depth === 1 && parentNode.hasFeature(jsDAV_iCollection)) { parentNode.getChildren(function(err, cNodes) { if (!Util.empty(err)) return cbgetpropspath(err); for (var i = 0, l = cNodes.length; i < l; ++i) { nodes[path + "/" + cNodes[i].getName()] = cNodes[i]; nodesPath.push(path + "/" + cNodes[i].getName()); } afterGetChildren(nodes, nodesPath); }); } else { afterGetChildren(nodes, nodesPath); } function afterGetChildren(nodes, nodesPath) { // If the propertyNames array is empty, it means all properties are requested. // We shouldn't actually return everything we know though, and only return a // sensible list. var allProperties = (propertyNames.length === 0); if (allProperties) { // Default list of propertyNames, when all properties were requested. propertyNames = [ "{DAV:}getlastmodified", "{DAV:}getcontentlength", "{DAV:}resourcetype", "{DAV:}quota-used-bytes", "{DAV:}quota-available-bytes", "{DAV:}getetag", "{DAV:}getcontenttype" ]; } // If the resourceType was not part of the list, we manually add it // and mark it for removal. We need to know the resourcetype in order // to make certain decisions about the entry. // WebDAV dictates we should add a / and the end of href's for collections var removeRT = false; if (propertyNames.indexOf("{DAV:}resourcetype") === -1) { propertyNames.push("{DAV:}resourcetype"); removeRT = true; } function afterGetProperty(rprop, rpath, newProps, cbnext) { // If we were unable to find the property, we will list it as 404. if (!allProperties && !newProps["200"][rprop]) newProps["404"][rprop] = null; var node = nodes[rpath]; rpath = Util.trim(rpath, "/"); self.dispatchEvent("afterGetProperties", rpath, newProps, node, function() { if (parentNode.hasFeature(jsDAV_iCollection)) { // correct href when mountpoint is different than the // absolute location of the path var s = Util.trim(self.server.tree.basePath, "/"); if (s.charAt(0) != ".") { rpath = s.indexOf(self.server.baseUri) !== 0 ? rpath.replace(new RegExp("^" + Util.escapeRegExp(s)), "").replace(/^[\/]+/, "") : rpath; } } newProps["href"] = rpath; // Its is a WebDAV recommendation to add a trailing slash to collectionnames. // Apple's iCal also requires a trailing slash for principals (rfc 3744). // Therefore we add a trailing / for any non-file. This might need adjustments // if we find there are other edge cases. var rt = newProps["200"]["{DAV:}resourcetype"]; if (rpath !== "" && rt && (rt.is("{DAV:}collection") || rt.is("{DAV:}principal"))) { newProps["href"] += "/"; } // If the resourcetype property was manually added to the requested property list, // we will remove it again. if (removeRT) delete newProps["200"]["{DAV:}resourcetype"]; returnPropertyList[rpath] = newProps; cbnext(); }); } Async.list(nodesPath) .delay(0, 10) .each(function(myPath, cbnextpfp) { var node = nodes[myPath]; var newProperties = { "200" : {}, "403" : {}, "404" : {} }; var myProperties = [].concat(propertyNames); var propertyMap = Util.arrayToMap(myProperties); self.dispatchEvent("beforeGetProperties", myPath, node, propertyMap, newProperties, function(stop) { if (stop) return cbnextpfp(stop === true ? null : stop); myProperties = Object.keys(propertyMap); if (node.hasFeature(jsDAV_iProperties)) { node.getProperties(myProperties, function(err, props) { // The getProperties method may give us too much, // properties, in case the implementor was lazy. // // So as we loop through this list, we will only take the // properties that were actually requested and discard the // rest. for (var i = myProperties.length - 1; i >= 0; --i) { if (props[myProperties[i]]) { newProperties["200"][myProperties[i]] = props[myProperties[i]]; myProperties.splice(i, 1); } } buildProperties(); }); } else buildProperties(); function buildProperties() { // next loop! Async.list(myProperties) .delay(0, 10) .each(function(prop, cbnextprops) { if (typeof newProperties["200"][prop] != "undefined") return cbnextprops(); if (prop == "{DAV:}getlastmodified") { node.getLastModified(function(err, dt) { if (!err && dt) newProperties["200"][prop] = jsDAV_Property_GetLastModified.new(dt); afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}getcontentlength" && node.hasFeature(jsDAV_iFile)) { node.getSize(function(err, size) { if (!err && typeof size != "undefined") newProperties["200"][prop] = parseInt(size, 10); afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}quota-used-bytes" && node.hasFeature(jsDAV_iQuota)) { node.getQuotaInfo(function(err, quotaInfoUsed) { if (!err && quotaInfoUsed.length) newProperties["200"][prop] = quotaInfoUsed[0]; afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}quota-available-bytes" && node.hasFeature(jsDAV_iQuota)) { node.getQuotaInfo(function(err, quotaInfoAvail) { if (!err && quotaInfoAvail.length) newProperties["200"][prop] = quotaInfoAvail[1]; afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}getetag" && node.hasFeature(jsDAV_iFile)) { node.getETag(function(err, etag) { if (!err && etag) newProperties["200"][prop] = etag; afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}getcontenttype" && node.hasFeature(jsDAV_iFile)) { node.getContentType(function(err, ct) { if (!err && ct) newProperties["200"][prop] = ct; afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}supported-report-set") { var reports = []; Async.list(reportPlugins) .each(function(plugin, cbnextplugin) { plugin.getSupportedReportSet(myPath, function(err, rset) { if (!err && rset.length) reports = reports.concat(rset); cbnextplugin(); }); }) .end(function() { newProperties["200"][prop] = jsDAV_Property_SupportedReportSet.new(reports); afterGetProperty(prop, myPath, newProperties, cbnextprops); }); } else if (prop == "{DAV:}resourcetype") { newProperties["200"][prop] = jsDAV_Property_ResourceType.new(); for (var resourceType in self.resourceTypeMapping) { if (node.hasFeature(self.resourceTypeMapping[resourceType])) newProperties["200"][prop].add(resourceType); } afterGetProperty(prop, myPath, newProperties, cbnextprops); } else { afterGetProperty(prop, myPath, newProperties, cbnextprops); } }) .end(function(err) { cbnextpfp(err); }); } }); }) .end(function(err) { if (!Util.empty(err)) return cbgetpropspath(err); cbgetpropspath(null, returnPropertyList); }); } }); }; /** * Returns the HTTP range header * * This method returns null if there is no well-formed HTTP range request * header or array(start, end). * * The first number is the offset of the first byte in the range. * The second number is the offset of the last byte in the range. * * If the second offset is null, it should be treated as the offset of the * last byte of the entity. * If the first offset is null, the second offset should be used to retrieve * the last x bytes of the entity. * * return mixed */ this.getHTTPRange = function() { var range = this.httpRequest.headers["range"]; if (!range) return null; // Matching "Range: bytes=1234-5678: both numbers are optional var matches = range.match(/^bytes=([0-9]*)-([0-9]*)$/i); if (!matches || !matches.length) return null; if (matches[1] === "" && matches[2] === "") return null; return matches.slice(1).map(function(rangePart) { rangePart = parseFloat(rangePart); return isNaN(rangePart) ? null : rangePart; }); }; /** * Returns the HTTP depth header * * This method returns the contents of the HTTP depth request header. If the * depth header was 'infinity' it will return the jsDAV_Handler.DEPTH_INFINITY object * It is possible to supply a default depth value, which is used when the depth * header has invalid content, or is completely non-existant * * @param {mixed} default * @return {Number} */ this.getHTTPDepth = function(def) { def = def || jsDAV_Handler.DEPTH_INFINITY; // If its not set, we'll grab the default var depth = this.httpRequest.headers["depth"]; if (!depth) return def; if (depth == "infinity") return jsDAV_Handler.DEPTH_INFINITY; depth = parseInt(depth, 10); // If its an unknown value. we'll grab the default if (isNaN(depth)) return def; return depth; }; /** * Returns the HTTP Prefer header information. * * The prefer header is defined in: * http://tools.ietf.org/html/draft-snell-http-prefer-14 * * This method will return an array with options. * * Currently, the following options may be returned: * { * "return-asynch" : true, * "return-minimal" : true, * "return-representation" : true, * "wait" : 30, * "strict" : true, * "lenient" : true, * } * * This method also supports the Brief header, and will also return * 'return-minimal' if the brief header was set to 't'. * * For the boolean options, false will be returned if the headers are not * specified. For the integer options it will be 'null'. * * @return array */ this.getHTTPPrefer = function() { var result = { "return-asynch" : false, "return-minimal" : false, "return-representation" : false, "wait" : null, "strict" : false, "lenient" : false }; var prefer = this.httpRequest.headers.prefer; if (prefer) { prefer.split(",").map(function(item) { return Util.trim(item); }).forEach(function(parameter) { // Right now our regex only supports the tokens actually // specified in the draft. We may need to expand this if new // tokens get registered. var matches = parameter.match(/^([a-z0-9\-]+)(?:=([0-9a-z]+))?$/); if (matches !== null && matches.length > 0) { var token = matches[1]; // Early RFC drafts declares return-minimal, return-asynch, etc... // Now http://tools.ietf.org/html/rfc7240 declares return=minimal, etc... if (token == "return" && matches[2]) { // Convert return=??? to return-??? for backward compatibility token = token + "-" + matches[2]; } switch(token) { case "return-asynch" : case "return-minimal" : case "return-representation" : case "strict" : case "lenient" : result[token] = true; break; case "wait" : result[token] = matches[2]; break; } } }); } var brief = this.httpRequest.headers.brief; if (brief && brief == "t") result["return-minimal"] = true; return result; }; /** * This method parses the PROPFIND request and returns its information * * This will either be a list of properties, or an empty array; in which case * an {DAV:}allprop was requested. * * @param {String} body * @return {Array} */ this.parsePropfindRequest = function(body, cbpropfindreq) { // If the propfind body was empty, it means IE is requesting 'all' properties if (!body) return cbpropfindreq(null, []); Xml.loadDOMDocument(body, this.server.options.parser, function(err, oXml) { //Util.log("XML ", oXml); if (!Util.empty(err)) return cbpropfindreq(err); cbpropfindreq(null, Object.keys(Xml.parseProperties(oXml.propfind || oXml))); }); }; /** * This method parses a Proppatch request * * Proppatch changes the properties for a resource. This method * returns a list of properties. * * The keys in the returned array contain the property name (e.g.: {DAV:}displayname, * and the value contains the property value. If a property is to be removed * the value will be null. * * @param {String} body Xml body * @param {Function} cbproppatchreq Callback that is the return body of this function * @return {Object} list of properties in need of updating or deletion */ this.parseProppatchRequest = function(body, cbproppatchreq) { //We'll need to change the DAV namespace declaration to something else //in order to make it parsable var operation, innerProperties, propertyValue; var self = this; Xml.loadDOMDocument(body, this.server.options.parser, function(err, dom) { if (!Util.empty(err)) return cbproppatchreq(err); var child, propertyName; var newProperties = {}; var i = 0; var c = dom.childNodes; var l = c.length; for (; i < l; ++i) { child = c[i]; operation = Xml.toClarkNotation(child); if (!operation || operation !== "{DAV:}set" && operation !== "{DAV:}remove") continue; innerProperties = Xml.parseProperties(child, self.propertyMap); for (propertyName in innerProperties) { propertyValue = innerProperties[propertyName]; if (operation === "{DAV:}remove") propertyValue = null; newProperties[propertyName] = propertyValue; } } cbproppatchreq(null, newProperties); }); }; /** * This method updates a resource's properties * * The properties array must be a list of properties. Array-keys are * property names in clarknotation, array-values are it's values. * If a property must be deleted, the value should be null. * * Note that this request should either completely succeed, or * completely fail. * * The response is an array with statuscodes for keys, which in turn * contain arrays with propertynames. This response can be used * to generate a multistatus body. * * @param {String} uri * @param {Object} properties * @return {Object} */ this.updateProperties = function(uri, properties, cbupdateprops) { // we'll start by grabbing the node, this will throw the appropriate // exceptions if it doesn't. var props; var self = this; var remainingProperties = Util.extend({}, properties); var hasError = false; var result = {}; result[uri] = { "200" : [], "403" : [], "424" : [] }; this.getNodeForPath(uri, function(err, node) { if (!Util.empty(err)) return cbupdateprops(err); // If the node is not an instance of jsDAV_iProperties, every // property is 403 Forbidden // simply return a 405. var propertyName; if (!node.hasFeature(jsDAV_iProperties)) { hasError = true; for (propertyName in properties) Util.arrayRemove(result[uri]["403"], propertyName); remainingProperties = {}; } // Running through all properties to make sure none of them are protected if (!hasError) { for (propertyName in properties) { if (self.protectedProperties.indexOf(propertyName) > -1) { Util.arrayRemove(result[uri]["403"], propertyName); delete remainingProperties[propertyName]; hasError = true; } } } // Only if there were no errors we may attempt to update the resource if (!hasError) { node.updateProperties(properties, function(err, updateResult) { if (err) return cbupdateprops(err); remainingProperties = {}; if (updateResult === true) { // success for (propertyName in properties) Util.arrayRemove(result[uri]["200"], propertyName); } else if (updateResult === false) { // The node failed to update the properties for an // unknown reason for (propertyName in properties) Util.arrayRemove(result[uri]["403"], propertyName); } else if (typeof updateResult == "object") { // The node has detailed update information result[uri] = updateResult; } else { return cbupdateprops(new Exc.jsDAV_Exception("Invalid result from updateProperties")); } afterUpdateProperties(); }); } else afterUpdateProperties(); function afterUpdateProperties() { for (propertyName in remainingProperties) { // if there are remaining properties, it must mean // there's a dependency failure Util.arrayRemove(result[uri]["424"], propertyName); } // Removing empty array values for (var status in result[uri]) { if (!result[uri][status].length) delete result[uri][status]; } result[uri]["href"] = uri; cbupdateprops(null, result); } }); }; /** * This method is invoked by sub-systems creating a new file. * * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). * It was important to get this done through a centralized function, * allowing plugins to intercept this using the beforeCreateFile event. * * @param {String} uri * @param {Buffer} dataOrStream. Set to `null` to use a stream * @param {String} [enc] * @return {void} */ this.createFile = function(uri, dataOrStream, enc, cbcreatefile) { var parts = Util.splitPath(uri); var dir = parts[0]; var name = parts[1]; var self = this; this.dispatchEvent("beforeBind", uri, function(stop, etag) { if (stop) return cbcreatefile(stop === true ? null : stop, etag); self.getNodeForPath(dir, function(err, parent) { if (!Util.empty(err)) return cbcreatefile(err); self.dispatchEvent("beforeCreateFile", uri, dataOrStream, enc, parent, function(stop, etag) { if (stop) return cbcreatefile(stop === true ? null : stop, etag); if (dataOrStream === null) { if (!parent.createFileStream) { self.getRequestBody(enc, null, false, function(err, body) { if (err) return cbcreatefile(err); parent.createFile(name, body, enc || "utf8", afterBind); }); } else parent.createFileStream(self, name, enc || "utf8", afterBind); } else { if (parent.createFileStreamRaw && dataOrStream instanceof Fs.ReadStream) parent.createFileStreamRaw(name, dataOrStream, enc || "utf8", afterBind); else parent.createFile(name, dataOrStream, enc || "utf8", afterBind); } function afterBind(err, etag) { if (!Util.empty(err)) return cbcreatefile(err); var path = Path.join(dir, name); self.markDirty(path); self.dispatchEvent("afterBind", path); cbcreatefile(null, etag); } }); }); }); }; /** * This method is invoked by sub-systems creating a new directory. * * @param {String} uri * @param {Function} cbcreatedir * @return {void} */ this.createDirectory = function(uri, cbcreatedir) { this.createCollection(uri, ["{DAV:}collection"], {}, cbcreatedir); }; /** * Use this method to create a new collection * * The {DAV:}resourcetype is specified using the resourceType array. * At the very least it must contain {DAV:}collection. * * The properties array can contain a list of additional properties. * * @param {string} uri The new uri * @param {Array} resourceType The resourceType(s) * @param {Object} properties A list of properties * @return {void} */ this.createCollection = function(uri, resourceType, properties, cbcreatecoll) { var self = this; var path = Util.splitPath(uri); var parentUri = path[0]; var newName = path[1]; // Making sure {DAV:}collection was specified as resourceType if (resourceType.indexOf("{DAV:}collection") == -1) { return cbcreatecoll(new Exc.InvalidResourceType( "The resourceType for this collection must at least include {DAV:}collection") ); } // Making sure the parent exists this.getNodeForPath(parentUri, function(err, parent) { if (!Util.empty(err)) return cbcreatecoll(new Exc.Conflict("Parent node does not exist")); // Making sure the parent is a collection if (!parent.hasFeature(jsDAV_iCollection)) return cbcreatecoll(new Exc.Conflict("Parent node is not a collection")); // Making sure the child does not already exist parent.getChild(newName, function(err, ch) { // If we got here.. it means there's already a node on that url, // and we need to throw a 405 if (typeof ch != "undefined") { return cbcreatecoll(new Exc.MethodNotAllowed( "The resource you
{'commit': 'diff --git a/lib/DAV/handler.js b/lib/DAV/handler.js\nindex 28c28ae..2bf9eb2 100644\n--- a/lib/DAV/handler.js\n+++ b/lib/DAV/handler.js\n@@ -446,7 +446,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml";\n                         // New read/write stream\n                         var offlen    = end - start + 1,\n                             newStream = new Buffer(offlen);\n-                        body.copy(newStream, offlen, start);\n+                        body.copy(newStream, start, offlen);\n \n                         httpHeaders["Content-Length"] = offlen;\n                         httpHeaders["Content-Range"]  = "bytes " + start + "-" + end + "/" + nodeSize;\n', 'commit_subject': 'fixed chunked GET requests with Range headers set (reported and fix supplied by drahgon55)', 'repo_name': 'mikedeboer/jsDAV', 'committer': {'name': 'mikedeboer', 'email': '59bd0a3ff43b32849b319e645d4798d8a5d1e889@mikedeboer.nl', 'time_sec': '1300272520', 'tz_offset': '60', 'date': {'seconds': '1300272520'}}, 'commit_tree': '37b5681e3706a59aabe859568724c903b4532cfd', 'commit_message': 'fixed chunked GET requests with Range headers set (reported and fix supplied by drahgon55)', 'language_name': 'Shell', 'license': 'mit', 'hunks': '@@ -446,7 +446,7 @@ exports.NS_AJAXORG = "http://ajax.org/2005/aml";\n                         // New read/write stream\n                         var offlen    = end - start + 1,\n                             newStream = new Buffer(offlen);\n-                        body.copy(newStream, offlen, start);\n+                        body.copy(newStream, start, offlen);\n \n                         httpHeaders["Content-Length"] = offlen;\n                         httpHeaders["Content-Range"]  = "bytes " + start + "-" + end + "/" + nodeSize;\n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/lib/DAV/handler.js', 'tgt_file': 'b/lib/DAV/handler.js', 'file_extension': '.js', 'before_file': '/*\n * @package jsDAV\n * @subpackage DAV\n * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org>\n * @author Mike de Boer <info AT mikedeboer DOT nl>\n * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License\n */\n"use strict";\n\n// DAV classes used directly by the Handler object\nvar jsDAV = require("./../jsdav");\nvar jsDAV_Server = require("./server");\nvar jsDAV_Property_Response = require("./property/response");\nvar jsDAV_Property_GetLastModified = require("./property/getLastModified");\nvar jsDAV_Property_ResourceType = require("./property/resourceType");\nvar jsDAV_Property_SupportedReportSet = require("./property/supportedReportSet");\n// interfaces to check for:\nvar jsDAV_iFile = require("./interfaces/iFile");\nvar jsDAV_iCollection = require("./interfaces/iCollection");\nvar jsDAV_iExtendedCollection = require("./interfaces/iExtendedCollection")\nvar jsDAV_iQuota = require("./interfaces/iQuota");\nvar jsDAV_iProperties = require("./interfaces/iProperties");\n\nvar Url = require("url");\nvar Fs = require("fs");\nvar Path = require("path");\nvar AsyncEventEmitter = require("./../shared/asyncEvents").EventEmitter;\nvar Exc = require("./../shared/exceptions");\nvar Util = require("./../shared/util");\nvar Xml = require("./../shared/xml");\nvar Async = require("asyncjs");\nvar Formidable = require("formidable");\n\nvar requestCounter = 0;\n\n/**\n * Called when an http request comes in, pass it on to invoke and handle any\n * exceptions that might be thrown\n *\n * @param {jsDav_Server}   server\n * @param {ServerRequest}  req\n * @param {ServerResponse} resp\n * @return {jsDAV_Handler}\n */\nvar jsDAV_Handler = module.exports = function(server, req, resp) {\n    this.server       = server;\n    this.httpRequest  = Util.streamBuffer(req);\n    this.httpResponse = resp;\n    this.plugins      = {};\n    this.nodeCache    = {};\n\n    for (var plugin in server.plugins) {\n        if (typeof server.plugins[plugin] != "object")\n            continue;\n        this.plugins[plugin] = server.plugins[plugin].new(this);\n    }\n\n    try {\n        this.invoke();\n    }\n    catch (ex) {\n        this.handleError(ex);\n    }\n};\n\n/**\n * Inifinity is used for some request supporting the HTTP Depth header and indicates\n * that the operation should traverse the entire tree\n */\njsDAV_Handler.DEPTH_INFINITY = -1;\n\n/**\n * Nodes that are files, should have this as the type property\n */\njsDAV_Handler.NODE_FILE      = 1;\n\n/**\n * Nodes that are directories, should use this value as the type property\n */\njsDAV_Handler.NODE_DIRECTORY = 2;\n\njsDAV_Handler.PROP_SET       = 1;\njsDAV_Handler.PROP_REMOVE    = 2;\n\njsDAV_Handler.STATUS_MAP     = {\n    "100": "Continue",\n    "101": "Switching Protocols",\n    "200": "OK",\n    "201": "Created",\n    "202": "Accepted",\n    "203": "Non-Authorative Information",\n    "204": "No Content",\n    "205": "Reset Content",\n    "206": "Partial Content",\n    "207": "Multi-Status", // RFC 4918\n    "208": "Already Reported", // RFC 5842\n    "300": "Multiple Choices",\n    "301": "Moved Permanently",\n    "302": "Found",\n    "303": "See Other",\n    "304": "Not Modified",\n    "305": "Use Proxy",\n    "307": "Temporary Redirect",\n    "400": "Bad request",\n    "401": "Unauthorized",\n    "402": "Payment Required",\n    "403": "Forbidden",\n    "404": "Not Found",\n    "405": "Method Not Allowed",\n    "406": "Not Acceptable",\n    "407": "Proxy Authentication Required",\n    "408": "Request Timeout",\n    "409": "Conflict",\n    "410": "Gone",\n    "411": "Length Required",\n    "412": "Precondition failed",\n    "413": "Request Entity Too Large",\n    "414": "Request-URI Too Long",\n    "415": "Unsupported Media Type",\n    "416": "Requested Range Not Satisfiable",\n    "417": "Expectation Failed",\n    "418": "I\'m a teapot", // RFC 2324\n    "422": "Unprocessable Entity", // RFC 4918\n    "423": "Locked", // RFC 4918\n    "424": "Failed Dependency", // RFC 4918\n    "500": "Internal Server Error",\n    "501": "Not Implemented",\n    "502": "Bad Gateway",\n    "503": "Service Unavailable",\n    "504": "Gateway Timeout",\n    "505": "HTTP Version not supported",\n    "507": "Unsufficient Storage", // RFC 4918\n    "508": "Loop Detected" // RFC 5842\n};\n\n(function() {\n    /**\n     * httpResponse\n     *\n     * @var HTTP_Response\n     */\n    this.httpResponse =\n\n    /**\n     * httpRequest\n     *\n     * @var HTTP_Request\n     */\n    this.httpRequest = null;\n\n    /**\n     * The propertymap can be used to map properties from\n     * requests to property classes.\n     *\n     * @var array\n     */\n    this.propertyMap = {\n        "{DAV:}resourcetype": jsDAV_Property_ResourceType\n    };\n\n    this.protectedProperties = [\n        // RFC4918\n        "{DAV:}getcontentlength",\n        "{DAV:}getetag",\n        "{DAV:}getlastmodified",\n        "{DAV:}lockdiscovery",\n        "{DAV:}resourcetype",\n        "{DAV:}supportedlock",\n\n        // RFC4331\n        "{DAV:}quota-available-bytes",\n        "{DAV:}quota-used-bytes",\n\n        // RFC3744\n        "{DAV:}alternate-URI-set",\n        "{DAV:}principal-URL",\n        "{DAV:}group-membership",\n        "{DAV:}supported-privilege-set",\n        "{DAV:}current-user-privilege-set",\n        "{DAV:}acl",\n        "{DAV:}acl-restrictions",\n        "{DAV:}inherited-acl-set",\n        "{DAV:}principal-collection-set",\n\n        // RFC5397\n        "{DAV:}current-user-principal"\n    ];\n    \n    /**\n     * This property allows you to automatically add the \'resourcetype\' value\n     * based on a node\'s classname or interface.\n     *\n     * The preset ensures that {DAV:}collection is automaticlly added for nodes\n     * implementing jsDAV_iCollection.\n     *\n     * @var object\n     */\n    this.resourceTypeMapping = {\n        "{DAV:}collection": jsDAV_iCollection\n    };\n\n    var internalMethods = {\n        "OPTIONS":1,\n        "GET":1,\n        "HEAD":1,\n        "DELETE":1,\n        "PROPFIND":1,\n        "MKCOL":1,\n        "PUT":1,\n        "PROPPATCH":1,\n        "COPY":1,\n        "MOVE":1,\n        "REPORT":1\n    };\n\n    /**\n     * Handles a http request, and execute a method based on its name\n     *\n     * @return void\n     */\n    this.invoke = function() {\n        var method = this.httpRequest.method.toUpperCase(),\n            self  = this;\n        if (jsDAV.debugMode) {\n            this.id = ++requestCounter;\n            Util.log("{" + this.id + "}", method, this.httpRequest.url);\n            Util.log("{" + this.id + "}", this.httpRequest.headers);\n            var wh = this.httpResponse.writeHead,\n                we = this.httpResponse.end;\n            this.httpResponse.writeHead = function(code, headers) {\n                Util.log("{" + self.id + "}", code, headers);\n                this.writeHead = wh;\n                this.writeHead(code, headers);\n            };\n            this.httpResponse.end = function(content) {\n                Util.log("{" + self.id + "}", "\'" + (content || "") + "\'");\n                this.end = we;\n                this.end(content);\n            };\n        }\n\n        var uri = this.getRequestUri();\n        this.dispatchEvent("beforeMethod", method, uri, function(stop) {\n            if (stop === true)\n                return;\n            if (stop)\n                return self.handleError(stop);\n            // Make sure this is a HTTP method we support\n            if (internalMethods[method]) {\n                self["http" + method.charAt(0) + method.toLowerCase().substr(1)]();\n            }\n            else {\n                self.dispatchEvent("unknownMethod", method, uri, function(stop) {\n                    if (stop === true)\n                        return;\n                    // Unsupported method\n                    self.handleError(stop || new Exc.NotImplemented());\n                });\n            }\n        });\n    };\n\n    /**\n     * Centralized error and exception handler, which constructs a proper WebDAV\n     * 500 server error, or different depending on the error object implementation\n     * and/ or extensions.\n     *\n     * @param  {Error} e Error string or Exception object\n     * @return {void}\n     */\n    this.handleError = function(e) {\n        //if (jsDAV.debugMode)\n        //    console.trace();\n        if (e === true)\n            return; // plugins should return TRUE to prevent error reporting.\n        if (typeof e == "string")\n            e = new Exc.jsDAV_Exception(e);\n        var xml = \'<?xml version="1.0" encoding="utf-8"?>\\n\'\n                + \'<d:error xmlns:d="DAV:" xmlns:a="\' + Xml.NS_AJAXORG + \'">\\n\'\n                + \'    <a:exception>\' + (e.type || e.toString()) + \'</a:exception>\\n\'\n                + \'    <a:message>\'   + e.message + \'</a:message>\\n\';\n        if (this.server.debugExceptions) {\n            xml += \'<a:file>\' + (e.filename || "") + \'</a:file>\\n\'\n                +  \'<a:line>\' + (e.line || "") + \'</a:line>\\n\';\n        }\n        xml += \'<a:jsdav-version>\' + jsDAV_Server.VERSION + \'</a:jsdav-version>\\n\';\n\n        var code = 500;\n        var self = this;\n        if (e instanceof Exc.jsDAV_Exception) {\n            code = e.code;\n            xml  = e.serialize(this, xml);\n            e.getHTTPHeaders(this, function(err, h) {\n                afterHeaders(h);\n            });\n        }\n        else {\n            afterHeaders({});\n        }\n\n        function afterHeaders(headers) {\n            headers["Content-Type"] = "application/xml; charset=utf-8";\n\n            self.httpResponse.writeHead(code, headers);\n            self.httpResponse.end(xml + \'</d:error>\', "utf-8");\n\n            if (jsDAV.debugMode) {\n                Util.log(e, "error");\n                console.log(e.stack);\n                //throw e; // DEBUGGING!\n            }\n        }\n    };\n\n    /**\n     * Caching version of jsDAV_Server#tree#getNodeForPath(), to make node lookups\n     * during the same request (the scope of a handler instance) more cheap.\n     *\n     * @param {String} path\n     * @param {Function} cbgetnodefp\n     * @returns {void}\n     */\n    this.getNodeForPath = function(path, cbgetnodefp) {\n        if (this.nodeCache[path])\n            return cbgetnodefp(null, this.nodeCache[path]);\n\n        var self = this;\n        this.server.tree.getNodeForPath(path, function(err, node) {\n            if (err)\n                return cbgetnodefp(err);\n            self.nodeCache[path] = node;\n            cbgetnodefp(null, node);\n        });\n    };\n    \n    /**\n     * This method is called with every tree update\n     *\n     * Examples of tree updates are:\n     *   * node deletions\n     *   * node creations\n     *   * copy\n     *   * move\n     *   * renaming nodes\n     *\n     * If Tree classes implement a form of caching, this will allow\n     * them to make sure caches will be expired.\n     *\n     * If a path is passed, it is assumed that the entire subtree is dirty\n     *\n     * @param {String} path\n     * @return void\n     */\n    this.markDirty = function(path) {\n        // We don\'t care enough about sub-paths\n        // flushing the entire cache\n        path = Util.trim(path, "/");\n        for (var nodePath in this.nodeCache) {\n            if (nodePath.indexOf(path) === 0)\n                delete this.nodeCache[nodePath];\n        }\n    };\n\n    /**\n     * HTTP OPTIONS\n     *\n     * @return {void}\n     * @throws {Error}\n     */\n    this.httpOptions = function() {\n        var uri  = this.getRequestUri();\n        var self = this;\n\n        this.getAllowedMethods(uri, function(err, methods) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            var headers = {\n                "Allow": methods.join(",").toUpperCase(),\n                "MS-Author-Via"   : "DAV",\n                "Accept-Ranges"   : "bytes",\n                "X-jsDAV-Version" : jsDAV_Server.VERSION,\n                "Content-Length"  : 0\n            };\n            var features = ["1", "3", "extended-mkcol"];\n\n            for (var plugin in self.plugins) {\n                if (!self.plugins[plugin].getFeatures)\n                    Util.log("method getFeatures() NOT implemented for plugin " + plugin, "error");\n                else\n                    features = features.concat(self.plugins[plugin].getFeatures());\n            }\n\n            headers["DAV"] = features.join(",");\n\n            self.httpResponse.writeHead(200, headers);\n            self.httpResponse.end();\n        });\n    };\n\n    /**\n     * HTTP GET\n     *\n     * This method simply fetches the contents of a uri, like normal\n     *\n     * @return {void}\n     * @throws {Error}\n     */\n    this.httpGet = function() {\n        var node;\n        var uri  = this.getRequestUri();\n        var self = this;\n\n        this.checkPreconditions(true, function(err, redirected) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            if (redirected)\n                return;\n            self.getNodeForPath(uri, function(err, n) {\n                if (!Util.empty(err))\n                    return self.handleError(err);\n                node = n;\n                afterCheck();\n            });\n        });\n\n        function afterCheck() {\n            if (!node.hasFeature(jsDAV_iFile)) {\n                return self.handleError(new Exc.NotImplemented(\n                    "GET is only implemented on File objects"));\n            }\n\n            var hasStream = !!node.getStream;\n\n            self.getHTTPHeaders(uri, function(err, httpHeaders) {\n                if (!Util.empty(err))\n                    return self.handleError(err);\n                var nodeSize = null;\n                // ContentType needs to get a default, because many webservers\n                // will otherwise default to text/html, and we don\'t want this\n                // for security reasons.\n                if (!httpHeaders["content-type"])\n                    httpHeaders["content-type"] = "application/octet-stream";\n\n                if (httpHeaders["content-length"]) {\n                    nodeSize = httpHeaders["content-length"];\n                        // New read/write stream\n                        var offlen    = end - start + 1,\n                            newStream = new Buffer(offlen);\n                        body.copy(newStream, offlen, start);\n\n                        httpHeaders["Content-Length"] = offlen;\n                        httpHeaders["Content-Range"]  = "bytes " + start + "-" + end + "/" + nodeSize;\n                var ignoreRangeHeader = false;\n\n                // If ifRange is set, and range is specified, we first need\n                // to check the precondition.\n                if (nodeSize && range && ifRange) {\n                    // if IfRange is parsable as a date we\'ll treat it as a\n                    // DateTime otherwise, we must treat it as an etag.\n                    try {\n                        var ifRangeDate = new Date(ifRange);\n\n                        // It\'s a date. We must check if the entity is modified\n                        // since the specified date.\n                        if (!httpHeaders["last-modified"]) {\n                            ignoreRangeHeader = true;\n                        }\n                        else {\n                            var modified = new Date(httpHeaders["last-modified"]);\n                            if (modified > ifRangeDate)\n                                ignoreRangeHeader = true;\n                        }\n                    }\n                    catch (ex) {\n                        // It\'s an entity. We can do a simple comparison.\n                        if (!httpHeaders["etag"])\n                            ignoreRangeHeader = true;\n                        else if (httpHeaders["etag"] !== ifRange)\n                            ignoreRangeHeader = true;\n                    }\n                }\n\n                // We\'re only going to support HTTP ranges if the backend\n                // provided a filesize\n                if (!ignoreRangeHeader && nodeSize && range) {\n                    // Determining the exact byte offsets\n                    var start, end;\n                    if (range[0] !== null) {\n                        start = range[0];\n                        // the browser/ client sends \'end\' offsets as factor of nodeSize - 1,\n                        // so we need to correct it, because NodeJS streams byte offsets\n                        // are inclusive\n                        end   = range[1] !== null ? range[1] + 1 : nodeSize;\n                        if (start > nodeSize) {\n                            return self.handleError(new Exc.RequestedRangeNotSatisfiable(\n                                "The start offset (" + range[0] + ") exceeded the size of the entity ("\n                                + nodeSize + ")")\n                            );\n                        }\n\n                        if (end < start) {\n                            return self.handleError(new Exc.RequestedRangeNotSatisfiable(\n                                "The end offset (" + range[1] + ") is lower than the start offset ("\n                                + range[0] + ")")\n                            );\n                        }\n                        if (end > nodeSize)\n                            end = nodeSize;\n\n                    }\n                    else {\n                        start = nodeSize - range[1];\n                        end   = nodeSize;\n                        if (start < 0)\n                            start = 0;\n                    }\n\n                    var offlen = end - start;\n                    // Prevent buffer error\n                    // https://github.com/joyent/node/blob/v0.4.5/lib/buffer.js#L337\n                    if (end < start) {\n                        var swapTmp = end;\n                        end = start;\n                        start = swapTmp;\n                    }\n\n                    // report a different end offset, corrected by 1:\n                    var clientEnd = end > 0 ? end - 1 : end;\n                    httpHeaders["content-length"] = offlen;\n                    httpHeaders["content-range"]  = "bytes " + start + "-" + clientEnd + "/" + nodeSize;\n\n                    if (hasStream) {\n                        var writeStreamingHeader = function () {\n                            self.httpResponse.writeHead(206, httpHeaders);\n                        };\n\n                        node.getStream(start, end, function(err, data) {\n                            if (err) {\n                                if (!writeStreamingHeader) {\n                                    self.httpResponse.end();\n                                    console.error("jsDAV GET error", err);\n                                }\n                                else {\n                                    self.handleError(err);\n                                }\n                                return;\n                            }\n\n                            // write header on first incoming buffer\n                            if (writeStreamingHeader) {\n                                writeStreamingHeader();\n                                writeStreamingHeader = null;\n                            }\n\n                            if (!data)\n                                return self.httpResponse.end();\n\n                            self.httpResponse.write(data);\n                        });\n                    }\n                    else {\n                        node.get(function(err, body) {\n                            if (!Util.empty(err))\n                                return self.handleError(err);\n\n                            // New read/write stream\n                            var newStream = new Buffer(offlen);\n                            body.copy(newStream, 0, start, offlen);\n\n                            self.httpResponse.writeHead(206, httpHeaders);\n                            self.httpResponse.end(newStream);\n                        });\n                    }\n                }\n                else {\n                    var since        = self.httpRequest.headers["if-modified-since"];\n                    var oldEtag      = self.httpRequest.headers["if-none-match"];\n                    var lastModified = httpHeaders["last-modified"];\n                    var etag         = httpHeaders["etag"];\n                    since = since && Date.parse(since).valueOf();\n                    lastModified = lastModified && Date.parse(lastModified).valueOf();\n                    // If there is no match, then move on.\n                    if (!((since && lastModified === since) || (etag && oldEtag === etag))) {\n                        if (nodeSize)\n                            httpHeaders["content-length"] = nodeSize;\n                        if (hasStream) {\n                            var writeStreamingHeader = function () {\n                                self.httpResponse.writeHead(200, httpHeaders);\n                            };\n\n                            // no start or end means: get all file contents.\n                            node.getStream(null, null, function(err, data) {\n                                if (err) {\n                                    if (!writeStreamingHeader) {\n                                        self.httpResponse.end();\n                                        console.error("jsDAV GET error", err);\n                                    }\n                                    else {\n                                        self.handleError(err);\n                                    }\n                                    return;\n                                }\n\n                                // write header on first incoming buffer\n                                if (writeStreamingHeader) {\n                                    writeStreamingHeader();\n                                    writeStreamingHeader = null;\n                                }\n\n                                if (!data)\n                                    return self.httpResponse.end();\n\n                                self.httpResponse.write(data);\n                            });\n                        }\n                        else {\n                            node.get(function(err, body) {\n                                if (!Util.empty(err))\n                                    return self.handleError(err);\n\n                                self.httpResponse.writeHead(200, httpHeaders);\n                                self.httpResponse.end(body);\n                            });\n                        }\n                    }\n                    else {\n                        // Filter out any Content based headers since there\n                        // is no content.\n                        var newHeaders = {};\n                        Object.keys(httpHeaders).forEach(function(key) {\n                            if (key.indexOf("content") < 0)\n                                newHeaders[key] = httpHeaders[key];\n                        });\n                        self.httpResponse.writeHead(304, newHeaders);\n                        self.httpResponse.end();\n                    }\n                }\n            });\n        }\n    };\n\n    /**\n     * HTTP HEAD\n     *\n     * This method is normally used to take a peak at a url, and only get the\n     * HTTP response headers, without the body.\n     * This is used by clients to determine if a remote file was changed, so\n     * they can use a local cached version, instead of downloading it again\n     *\n     * @return {void}\n     * @throws {Error}\n     */\n    this.httpHead = function() {\n        var uri   = this.getRequestUri(),\n            self = this;\n\n        this.getNodeForPath(uri, function(err, node) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            /* This information is only collection for File objects.\n             * Ideally we want to throw 405 Method Not Allowed for every\n             * non-file, but MS Office does not like this\n             */\n            var headers = {};\n            if (node.hasFeature(jsDAV_iFile)) {\n                self.getHTTPHeaders(uri, function(err, headersFetched) {\n                    if (!Util.empty(err))\n                        return self.handleError(err);\n                    headers = headersFetched;\n                    if (!headers["content-type"])\n                        headers["content-type"] = "application/octet-stream";\n                    afterHeaders();\n                });\n            }\n            else {\n                afterHeaders();\n            }\n\n            function afterHeaders() {\n                self.httpResponse.writeHead(200, headers);\n                self.httpResponse.end();\n            }\n        });\n    };\n\n    /**\n     * HTTP Delete\n     *\n     * The HTTP delete method, deletes a given uri\n     *\n     * @return {void}\n     * @throws {Error}\n     */\n    this.httpDelete = function() {\n        var uri  = this.getRequestUri();\n        var self = this;\n\n        this.getNodeForPath(uri, function(err, node) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n\n            self.dispatchEvent("beforeUnbind", uri, function(stop) {\n                if (stop)\n                    return;\n                node["delete"](function(err) {\n                    if (!Util.empty(err))\n                        return self.handleError(err);\n\n                    self.markDirty(uri);\n                    self.httpResponse.writeHead(204, {"content-length": "0"});\n                    self.httpResponse.end();\n                    self.dispatchEvent("afterDelete", uri);\n                });\n            });\n        });\n    };\n\n    /**\n     * WebDAV PROPFIND\n     *\n     * This WebDAV method requests information about an uri resource, or a list\n     * of resources\n     * If a client wants to receive the properties for a single resource it will\n     * add an HTTP Depth: header with a 0 value.\n     * If the value is 1, it means that it also expects a list of sub-resources\n     * (e.g.: files in a directory)\n     *\n     * The request body contains an XML data structure that has a list of\n     * properties the client understands.\n     * The response body is also an xml document, containing information about\n     * every uri resource and the requested properties\n     *\n     * It has to return a HTTP 207 Multi-status status code\n     *\n     * @throws {Error}\n     */\n    this.httpPropfind = function() {\n        var self = this;\n        this.getRequestBody("utf8", null, false, function(err, data) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            //if (jsDAV.debugMode)\n            //    Util.log("{" + self.id + "}", "data received " + data);\n            self.parsePropfindRequest(data, function(err, requestedProperties) {\n                if (!Util.empty(err))\n                    return self.handleError(err);\n                var depth = self.getHTTPDepth(1);\n                // The only two options for the depth of a propfind is 0 or 1\n                if (depth !== 0)\n                    depth = 1;\n\n                // The requested path\n                var path;\n                try {\n                    path = self.getRequestUri();\n                }\n                catch (ex) {\n                    return self.handleError(ex);\n                }\n                //if (jsDAV.debugMode)\n                //    Util.log("httpPropfind BEFORE getPropertiesForPath \'" + path + "\';", requestedProperties);\n                self.getPropertiesForPath(path, requestedProperties, depth, function(err, newProperties) {\n                    if (!Util.empty(err))\n                        return self.handleError(err);\n\n                    // Normally this header is only needed for OPTIONS responses, however..\n                    // iCal seems to also depend on these being set for PROPFIND. Since\n                    // this is not harmful, we\'ll add it.\n                    var features = ["1", "3", "extended-mkcol"];\n                    for (var plugin in self.plugins) {\n                        if (!self.plugins[plugin].getFeatures)\n                            Util.log("method getFeatures() NOT implemented for plugin " + plugin, "error");\n                        else\n                            features = features.concat(self.plugins[plugin].getFeatures());\n                    }\n\n                    // This is a multi-status response.\n                    self.httpResponse.writeHead(207, {\n                        "content-type": "application/xml; charset=utf-8",\n                        "vary": "Brief,Prefer",\n                        "DAV": features.join(",")\n                    });\n                    var prefer = self.getHTTPPrefer();\n                    self.httpResponse.end(self.generateMultiStatus(newProperties, prefer["return-minimal"]));\n                });\n            });\n        });\n    };\n\n    /**\n     * WebDAV PROPPATCH\n     *\n     * This method is called to update properties on a Node. The request is an\n     * XML body with all the mutations.\n     * In this XML body it is specified which properties should be set/updated\n     * and/or deleted\n     *\n     * @return {void}\n     */\n    this.httpProppatch = function() {\n        var self = this;\n        this.getRequestBody("utf8", null, false, function(err, data) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            //if (jsDAV.debugMode)\n            //    Util.log("{" + self.id + "}", "data received " + data);\n            self.parseProppatchRequest(data, function(err, newProperties) {\n                if (!Util.empty(err))\n                    return self.handleError(err);\n                self.updateProperties(self.getRequestUri(), newProperties, function(err, result) {\n                    if (!Util.empty(err))\n                        return self.handleError(err);\n\n                    var prefer = self.getHTTPPrefer();\n                    if (prefer["return-minimal"]) {\n                        // If return-minimal is specified, we only have to check if the\n                        // request was succesful, and don\'t need to return the\n                        // multi-status.\n                        var prop;\n                        var ok = true;\n                        for (var code in result) {\n                            prop = result[code];\n                            if (parseInt(code, 10) > 299) {\n                                ok = false;\n                                break;\n                            }\n                        }\n                        if (ok) {\n                            self.httpResponse.writeHead(204, {\n                                "vary": "Brief, Prefer"\n                            });\n                            self.httpResponse.end();\n                            return;\n                        }\n                    }\n\n                    self.httpResponse.writeHead(207, {\n                        "content-type": "application/xml; charset=utf-8",\n                        "vary": "Brief, Prefer"\n                    });\n                    self.httpResponse.end(self.generateMultiStatus(result));\n                });\n            });\n        });\n    };\n\n    /**\n     * HTTP PUT method\n     *\n     * This HTTP method updates a file, or creates a new one.\n     * If a new resource was created, a 201 Created status code should be returned.\n     * If an existing resource is updated, it\'s a 200 Ok\n     *\n     * @return {void}\n     */\n    this.httpPut = function() {\n        var self = this;\n        var uri  = this.getRequestUri();\n        \n        // Intercepting Content-Range\n        if (this.httpRequest.headers["content-range"]) {\n            /*\n            Content-Range is dangerous for PUT requests:  PUT per definition\n            stores a full resource.  draft-ietf-httpbis-p2-semantics-15 says\n            in section 7.6:\n              An origin server SHOULD reject any PUT request that contains a\n              Content-Range header field, since it might be misinterpreted as\n              partial content (or might be partial content that is being mistakenly\n              PUT as a full representation).  Partial content updates are possible\n              by targeting a separately identified resource with state that\n              overlaps a portion of the larger resource, or by using a different\n              method that has been specifically defined for partial updates (for\n              example, the PATCH method defined in [RFC5789]).\n            This clarifies RFC2616 section 9.6:\n              The recipient of the entity MUST NOT ignore any Content-*\n              (e.g. Content-Range) headers that it does not understand or implement\n              and MUST return a 501 (Not Implemented) response in such cases.\n            OTOH is a PUT request with a Content-Range currently the only way to\n            continue an aborted upload request and is supported by curl, mod_dav,\n            Tomcat and others.  Since some clients do use this feature which results\n            in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject\n            all PUT requests with a Content-Range for now.\n            */\n\n            return this.handleError(new Exc.NotImplemented("PUT with Content-Range is not allowed."));\n        }\n\n        // First we\'ll do a check to see if the resource already exists\n        this.getNodeForPath(uri, function(err, node) {\n            if (!Util.empty(err)) {\n                if (err instanceof Exc.FileNotFound) {\n                    // If we got here, the resource didn\'t exist yet.\n                    // `data` is set to `null` to use streamed write.\n                    self.createFile(uri, null, "binary", function(err, etag) {\n                        if (!Util.empty(err))\n                            return self.handleError(err);\n\n                        var headers = {"content-length": "0"};\n                        if (etag)\n                            headers.etag = etag;\n                        self.httpResponse.writeHead(201, headers);\n                        self.httpResponse.end();\n                        self.dispatchEvent("afterWriteContent", uri);\n                    });\n                }\n                else {\n                    return self.handleError(err);\n                }\n            }\n            else {\n                var hasStream = !!node.putStream;\n                // Checking If-None-Match and related headers.\n                self.checkPreconditions(false, function(err, redirected) {\n                    if (!Util.empty(err))\n                        return self.handleError(err);\n                    if (redirected)\n                        return false;\n                    // If the node is a collection, we\'ll deny it\n                    if (!node.hasFeature(jsDAV_iFile))\n                        return self.handleError(new Exc.Conflict("PUT is not allowed on non-files."));\n\n                    self.dispatchEvent("beforeWriteContent", uri, node, function(stop) {\n                        if (stop)\n                            return;\n\n                        if (hasStream) {\n                            node.putStream(self, "binary", afterPut);\n                        }\n                        else {\n                            self.getRequestBody("binary", null, false, function(err, body) {\n                                if (!Util.empty(err))\n                                    return self.handleError(err);\n\n                                node.put(body, "binary", afterPut);\n                            });\n                        }\n\n                        function afterPut(err, etag) {\n                            if (!Util.empty(err))\n                                return self.handleError(err);\n\n                            var headers = {"content-length": "0"};\n                            if (etag)\n                                headers.etag = etag;\n                            self.httpResponse.writeHead(200, headers);\n                            self.httpResponse.end();\n                            self.dispatchEvent("afterWriteContent", uri);\n                        }\n                    });\n                });\n            }\n        });\n    };\n\n    /**\n     * WebDAV MKCOL\n     *\n     * The MKCOL method is used to create a new collection (directory) on the server\n     *\n     * @return {void}\n     */\n    this.httpMkcol = function() {\n        var resourceType;\n        var properties = {};\n        var self       = this;\n        var req        = this.httpRequest;\n\n        this.getRequestBody("utf8", null, false, function(err, requestBody) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n\n            if (requestBody) {\n                var contentType = req.headers["content-type"];\n                if (contentType.indexOf("application/xml") !== 0 && contentType.indexOf("text/xml") !== 0) {\n                    // We must throw 415 for unsupported mkcol bodies\n                    return self.handleError(new Exc.UnsupportedMediaType(\n                        "The request body for the MKCOL request must have an xml Content-Type"));\n                }\n\n                Xml.loadDOMDocument(requestBody, self.server.options.parser, function(err, dom) {\n                    var firstChild = dom.firstChild;\n                    if (Xml.toClarkNotation(firstChild) !== "{DAV:}mkcol") {\n                        // We must throw 415 for unsupport mkcol bodies\n                        return self.handleError(new Exc.UnsupportedMediaType(\n                            "The request body for the MKCOL request must be a {DAV:}mkcol request construct."));\n                    }\n\n                    var childNode;\n                    var i = 0;\n                    var c = firstChild.childNodes;\n                    var l = c.length;\n                    for (; i < l; ++i) {\n                        childNode = c[i];\n                        if (Xml.toClarkNotation(childNode) !== "{DAV:}set")\n                            continue;\n                        properties = Util.extend(properties, Xml.parseProperties(childNode, self.propertyMap));\n                    }\n                    if (!properties["{DAV:}resourcetype"]) {\n                        return self.handleError(new Exc.BadRequest(\n                            "The mkcol request must include a {DAV:}resourcetype property")\n                        );\n                    }\n\n                    delete properties["{DAV:}resourcetype"];\n\n                    resourceType = [];\n                    // Need to parse out all the resourcetypes\n                    var rtNode = firstChild.getElementsByTagNameNS("urn:DAV", "resourcetype")[0];\n                    for (i = 0, c = rtNode.childNodes, l = c.length; i < l; ++i)\n                        resourceType.push(Xml.toClarkNotation(c[i]));\n\n                    afterParse();\n                });\n            }\n            else {\n                resourceType = ["{DAV:}collection"];\n                afterParse();\n            }\n\n            function afterParse() {\n                try {\n                    var uri = self.getRequestUri()\n                }\n                catch (ex) {\n                    return self.handleError(ex);\n                }\n                self.createCollection(uri, resourceType, properties, function(err, result) {\n                    if (!Util.empty(err))\n                        return self.handleError(err);\n                    if (result && result.length) {\n                        self.httpResponse.writeHead(207, {"content-type": "application/xml; charset=utf-8"});\n                        self.httpResponse.end(self.generateMultiStatus(result));\n                    }\n                    else {\n                        self.httpResponse.writeHead(201, {"content-length": "0"});\n                        self.httpResponse.end();\n                    }\n                });\n            }\n        });\n    };\n\n    /**\n     * WebDAV HTTP MOVE method\n     *\n     * This method moves one uri to a different uri. A lot of the actual request\n     * processing is done in getCopyMoveInfo\n     *\n     * @return {void}\n     */\n    this.httpMove = function() {\n        var self = this;\n\n        this.getCopyAndMoveInfo(function(err, moveInfo) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            if (moveInfo.destinationExists) {\n                self.dispatchEvent("beforeUnbind", moveInfo.destination, function(stop) {\n                    if (stop)\n                        return false;\n                    moveInfo.destinationNode["delete"](function(err) {\n                        if (!Util.empty(err))\n                            return self.handleError(err);\n                        afterDelete();\n                    });\n                });\n            }\n            else {\n                afterDelete();\n            }\n\n            function afterDelete() {\n                self.dispatchEvent("beforeUnbind", moveInfo.source, function(stop) {\n                    if (stop)\n                        return false;\n                    self.dispatchEvent("beforeBind", moveInfo.destination, function(stop) {\n                        if (stop)\n                            return false;\n                        self.server.tree.move(moveInfo.source, moveInfo.destination, function(err, sourceDir, destinationDir) {\n                            if (!Util.empty(err))\n                                return self.handleError(err);\n\n                            self.markDirty(moveInfo.source);\n                            self.markDirty(moveInfo.destination);\n\n                            self.dispatchEvent("afterBind", moveInfo.destination,\n                                Path.join(self.server.tree.basePath, moveInfo.destination));\n                            // If a resource was overwritten we should send a 204, otherwise a 201\n                            self.httpResponse.writeHead(moveInfo.destinationExists ? 204 : 201,\n                                {"content-length": "0"});\n                            self.httpResponse.end();\n                            self.dispatchEvent("afterMove", moveInfo.destination);\n                        });\n                    });\n                });\n            }\n        });\n    };\n\n    /**\n     * WebDAV HTTP COPY method\n     *\n     * This method copies one uri to a different uri, and works much like the MOVE request\n     * A lot of the actual request processing is done in getCopyMoveInfo\n     *\n     * @return {void}\n     */\n    this.httpCopy = function() {\n        var self = this;\n\n        this.getCopyAndMoveInfo(function(err, copyInfo) {\n            if (!Util.empty(err))\n                return self.handleError(err);\n            if (copyInfo.destinationExists) {\n                self.dispatchEvent("beforeUnbind", copyInfo.destination, function(stop) {\n                    if (stop)\n                        return false;\n                    copyInfo.destinationNode["delete"](function(err) {\n                        if (!Util.empty(err))\n                            return self.handleError(err);\n                        afterDelete();\n                    });\n                });\n            }\n            else {\n                afterDelete();\n            }\n\n            function afterDelete() {\n                self.dispatchEvent("beforeBind", copyInfo.destination, function(stop) {\n                    if (stop)\n                        return false;\n                    self.server.tree.copy(copyInfo.source, copyInfo.destination, function(err) {\n                        if (!Util.empty(err))\n                            return self.handleError(err);\n\n                        self.markDirty(copyInfo.destination);\n\n                        self.dispatchEvent("afterBind", copyInfo.destination,\n                            Path.join(self.server.tree.basePath, copyInfo.destination));\n\n                        // If a resource was overwritten we should send a 204, otherwise a 201\n                        self.httpResponse.writeHead(copyInfo.destinationExists ? 204 : 201,\n                            {"Content-Length": "0"});\n                        self.httpResponse.end();\n                        self.dispatchEvent("afterCopy", copyInfo.destination);\n                    });\n                });\n            }\n        });\n    };\n\n    /**\n     * HTTP REPORT method implementation\n     *\n     * Although the REPORT method is not part of the standard WebDAV spec (it\'s from rfc3253)\n     * It\'s used in a lot of extensions, so it made sense to implement it into the core.\n     *\n     * @return {void}\n     */\n    this.httpReport = function() {\n        var self = this;\n        this.getRequestBody("utf8", null, false, function(err, data) {\n            Xml.loadDOMDocument(data, self.server.options.parser, function(err, dom) {\n                var reportName = Xml.toClarkNotation(dom);\n                self.dispatchEvent("report", reportName, dom, function(stop) {\n                    if (stop !== true) {\n                        // if dispatchEvent didn\'t return true, it means the report was not supported\n                        return self.handleError(new Exc.ReportNotImplemented());\n                    }\n                });\n            });\n        });\n    };\n\n    /**\n     * Returns an array with all the supported HTTP methods for a specific uri.\n     *\n     * @param  {String}   uri\n     * @param  {Function} cbmethods Callback that is the return body of this function\n     * @return {Array}\n     */\n    this.getAllowedMethods = function(uri, cbmethods) {\n        var self   = this;\n        var methods = [\n            "OPTIONS",\n            "GET",\n            "HEAD",\n            "DELETE",\n            "PROPFIND",\n            "PUT",\n            "PROPPATCH",\n            "COPY",\n            "MOVE",\n            "REPORT"\n        ];\n\n        // The MKCOL is only allowed on an unmapped uri\n        this.getNodeForPath(uri, function(err, node) {\n            if (!Util.empty(err))\n                methods.push("MKCOL");\n\n            // We\'re also checking if any of the plugins register any new methods\n            for (var plugin in self.plugins) {\n                if (!self.plugins[plugin].getHTTPMethods)\n                    Util.log("method getHTTPMethods() NOT implemented for plugin " + plugin, "error");\n                else\n                    methods = methods.concat(self.plugins[plugin].getHTTPMethods(uri, node));\n            }\n\n            cbmethods(null, Util.makeUnique(methods));\n        });\n    };\n\n    /**\n     * Gets the uri for the request, keeping the base uri into consideration\n     *\n     * @return {String}\n     * @throws {Error}\n     */\n    this.getRequestUri = function() {\n        return this.calculateUri(this.httpRequest.url);\n    };\n\n    /**\n     * Fetch the binary data for the HTTP request and return it to a callback OR\n     * write it to a WritableStream instance.\n     *\n     * @param  {String} enc\n     * @param  {WritableStream} [stream]\n     * @param  {Boolean} [forceStream]\n     * @param  {Function} callback\n     * @return {void}\n     */\n    this.getRequestBody = function(enc, stream, forceStream, cbreqbody) {\n        if (!cbreqbody) {\n            cbreqbody = stream;\n            stream = null;\n            forceStream = false;\n        }\n\n        var ctype;\n        var self = this;\n        var req = this.httpRequest;\n        var isStream = forceStream === true \n            ? true\n            : (!(ctype = req.headers["content-type"]) || !ctype.match(/(urlencoded|multipart)/i));\n\n        // HACK: MacOSX Finder and NodeJS don\'t play nice together with files\n        // that start with \'._\'\n        //if (/\\/\\.[D_]{1}[^\\/]+$/.test(req.url))\n        //    return cbreqbody(null, "", cleanup);\n\n        enc = (enc || "utf8").replace("-", "");\n        if (enc == "raw")\n            enc = "binary";\n        if (req.$data) {\n            return cbreqbody(req.$data.err || null, enc == "binary"\n                ? req.$data\n                : req.$data.toString(enc));\n        }\n\n        if (isStream) {\n            var buff = [];\n            var contentLength = req.headers["content-length"];\n            var lengthCount = 0;\n            var err;\n\n            if (stream)\n                stream.on("error", function(ex) { err = ex; });\n\n            req.streambuffer.ondata(function(data) {\n                lengthCount += data.length;\n                if (stream && stream.writable)\n                    stream.write(data);\n                else\n                    buff.push(data);\n            });\n\n            req.streambuffer.onend(function() {\n                // TODO: content-length check and rollback...\n                if (stream) {\n                    if (err)\n                        return cbreqbody(err);\n\n                    stream.on("close", function() {\n                        cbreqbody(err);\n                    });\n                    stream.end();\n                }\n                else {\n                    buff = Util.concatBuffers(buff);\n                    if (contentLength && parseInt(contentLength, 10) != buff.length) {\n                        readDone(new Exc.BadRequest("Content-Length mismatch: Request Header claimed "\n                            + contentLength + " bytes, but received " + buff.length + " bytes"), buff);\n                    }\n                    else\n                        readDone(null, buff);\n                }\n            });\n        }\n        else {\n            var form = new Formidable.IncomingForm();\n            form.uploadDir = this.server.tmpDir;\n\n            form.on("file", function(field, file) {\n                // in the case of a streamed write, we can move the file to its\n                // correct position right away:\n                if (stream) {\n                    stream.on("close", function() {\n                        Fs.rename(file.path, stream.path, cbreqbody);\n                    });\n                    stream.end();\n                }\n                else {\n                    Fs.readFile(file.path, function(err, data) {\n                        readDone(err, data);\n                        Fs.unlink(file.path);\n                    });\n                }\n            });\n\n            form.on("error", cbreqbody);\n\n            form.parse(req);\n        }\n\n        function readDone(err, data) {\n            req.$data = data;\n            if (err)\n                req.$data.err = err;\n            if (jsDAV.debugMode && enc != "binary")\n                Util.log("{" + self.id + "}", req.$data.toString("utf8"));\n            cbreqbody(err, enc == "binary"\n                ? req.$data\n                : req.$data.toString(enc));\n        }\n    };\n\n    /**\n     * Calculates the uri for a request, making sure that the base uri is stripped out\n     *\n     * @param  {String} uri\n     * @throws {Exc.Forbidden} A permission denied exception is thrown\n     *         whenever there was an attempt to supply a uri outside of the base uri\n     * @return {String}\n     */\n    this.calculateUri = function(uri) {\n        if (uri.charAt(0) != "/") {\n            if (uri.indexOf("://") > -1)\n                uri = Url.parse(uri).pathname;\n            else\n                uri = "/" + uri;\n        }\n        else if (uri.indexOf("?") > -1)\n            uri = Url.parse(uri).pathname;\n\n        uri = uri.replace("//", "/");\n\n        if (uri.indexOf(this.server.baseUri) === 0) {\n            return Util.trim(decodeURIComponent(uri.substr(this.server.baseUri.length)), "/");\n        }\n        // A special case, if the baseUri was accessed without a trailing\n        // slash, we\'ll accept it as well.\n        else if (uri + "/" === this.server.baseUri) {\n            return "";\n        }\n        else {\n            throw new Exc.Forbidden("Requested uri (" + uri\n                + ") is out of base uri (" + this.server.baseUri + ")");\n        }\n    };\n\n    /**\n     * This method checks the main HTTP preconditions.\n     *\n     * Currently these are:\n     *   * If-Match\n     *   * If-None-Match\n     *   * If-Modified-Since\n     *   * If-Unmodified-Since\n     *\n     * The method will return true if all preconditions are met\n     * The method will return false, or throw an exception if preconditions\n     * failed. If false is returned the operation should be aborted, and\n     * the appropriate HTTP response headers are already set.\n     *\n     * Normally this method will throw 412 Precondition Failed for failures\n     * related to If-None-Match, If-Match and If-Unmodified Since. It will\n     * set the status to 304 Not Modified for If-Modified_since.\n     *\n     * If the handleAsGET argument is set to true, it will also return 304\n     * Not Modified for failure of the If-None-Match precondition. This is the\n     * desired behaviour for HTTP GET and HTTP HEAD requests.\n     *\n     * @param  {Boolean}  handleAsGET\n     * @param  {Function} cbprecond   Callback that is the return body of this function\n     * @return {void}\n     */\n    this.checkPreconditions = function(handleAsGET, cbprecond) {\n        handleAsGET = handleAsGET || false;\n        var uri, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince;\n        var node    = null;\n        var lastMod = null;\n        var etag    = null;\n        var self    = this;\n\n        try {\n            uri = this.getRequestUri();\n        }\n        catch (ex) {\n            return cbprecond(ex);\n        }\n\n        if (ifMatch = this.httpRequest.headers["if-match"]) {\n            // If-Match contains an entity tag. Only if the entity-tag\n            // matches we are allowed to make the request succeed.\n            // If the entity-tag is \'*\' we are only allowed to make the\n            // request succeed if a resource exists at that url.\n            this.getNodeForPath(uri, function(err, n) {\n                if (!Util.empty(err)) {\n                    return cbprecond(new Exc.PreconditionFailed(\n                        "An If-Match header was specified and the resource did not exist",\n                        "If-Match"));\n                }\n                node = n;\n                // Only need to check entity tags if they are not *\n                if (ifMatch !== "*") {\n                    node.getETag(function(err, etag) {\n                        if (err)\n                            return cbprecond(err);\n\n                        var haveMatch = false;\n                        // There can be multiple etags\n                        ifMatch = ifMatch.split(",");\n                        var ifMatchItem;\n                        for (var i = 0, l = ifMatch.length; i < l; ++i) {\n                            // Stripping any extra spaces\n                            ifMatchItem = Util.trim(ifMatch[i], " ");\n        \n                            if (etag === ifMatchItem) {\n                                haveMatch = true;\n                                break;\n                            }\n                            else {\n                                // Evolution has a bug where it sometimes prepends the "\n                                // with a \\. This is our workaround.\n                                if (ifMatchItem.replace(\'\\\\"\',\'"\') === etag) {\n                                    haveMatch = true;\n                                    break;\n                                }\n                            }\n                        }\n                        if (!haveMatch) {\n                             return cbprecond(new Exc.PreconditionFailed(\n                                "An If-Match header was specified, but none of the specified the ETags matched",\n                                "If-Match")\n                            );\n                        }\n                        afterIfMatch();\n                    });\n                    return;\n                }\n                afterIfMatch();\n            });\n        }\n        else {\n            afterIfMatch();\n        }\n\n        function afterIfMatch() {\n            if (ifNoneMatch = self.httpRequest.headers["if-none-match"]) {\n                // The If-None-Match header contains an etag.\n                // Only if the ETag does not match the current ETag, the request will succeed\n                // The header can also contain *, in which case the request\n                // will only succeed if the entity does not exist at all.\n                var nodeExists = true;\n                if (!node) {\n                    self.getNodeForPath(uri, function(err, n) {\n                        if (!Util.empty(err))\n                            nodeExists = false;\n                        else\n                            node = n;\n                        if (nodeExists) {\n                            // The Etag is surrounded by double-quotes, so those must be\n                            // stripped.\n                            ifNoneMatch = Util.trim(ifNoneMatch, \'"\');\n                            node.getETag(function(err, etag) {\n                                if (err)\n                                    return cbprecond(err);\n\n                                if (ifNoneMatch === "*" || etag === ifNoneMatch) {\n                                    if (handleAsGET) {\n                                        self.httpResponse.writeHead(304);\n                                        self.httpResponse.end();\n                                        cbprecond(null, true);\n                                        // @todo call cbprecond() differently here?\n                                    }\n                                    else {\n                                        cbprecond(new Exc.PreconditionFailed(\n                                            "An If-None-Match header was specified, but the ETag "\n                                          + "matched (or * was specified).", "If-None-Match")\n                                        );\n                                    }\n                                }\n                                else {\n                                    afterIfNoneMatch();\n                                }\n                            });\n                            return;\n                        }\n                        else {\n                            afterIfNoneMatch();\n                        }\n                    });\n                }\n                else {\n                    afterIfNoneMatch();\n                }\n            }\n            else {\n                afterIfNoneMatch();\n            }\n\n            function afterIfNoneMatch() {\n                if (!ifNoneMatch && (ifModifiedSince = self.httpRequest.headers["if-modified-since"])) {\n                    // The If-Modified-Since header contains a date. We\n                    // will only return the entity if it has been changed since\n                    // that date. If it hasn\'t been changed, we return a 304\n                    // header\n                    // Note that this header only has to be checked if there was no\n                    // If-None-Match header as per the HTTP spec.\n                    var date = new Date(ifModifiedSince);\n\n                    if (!node)\n                        self.getNodeForPath(uri, function(err, n) {\n                            if (!Util.empty(err))\n                                return cbprecond(err);\n                            node = n;\n                            node.getLastModified(function(err, lastMod) {\n                                if (lastMod) {\n                                    lastMod = new Date("@" + lastMod);\n                                    if (lastMod <= date) {\n                                        self.httpResponse.writeHead(304);\n                                        self.httpResponse.end();\n                                        cbprecond(null, true);\n                                        // @todo call cbprecond() differently here?\n                                        return;\n                                    }\n                                }\n                                afterIfModifiedSince();\n                            });\n                        });\n                }\n                else {\n                    afterIfModifiedSince();\n                }\n\n                function afterIfModifiedSince() {\n                    var date;\n                    if (ifUnmodifiedSince = self.httpRequest.headers["if-unmodified-since"]) {\n                        // The If-Unmodified-Since will allow the request if the\n                        // entity has not changed since the specified date.\n                        date = new Date(ifUnmodifiedSince);\n                        if (!node) {\n                            self.getNodeForPath(uri, function(err, n) {\n                                if (!Util.empty(err))\n                                    return cbprecond(err);\n                                node = n;\n                                finale();\n                            });\n                        }\n                        else {\n                            finale();\n                        }\n                    }\n                    else {\n                        cbprecond(null, false);\n                    }\n\n                    function finale() {\n                        node.getLastModified(function(err, lastMod) {\n                            if (lastMod) {\n                                lastMod = new Date("@" + lastMod);\n                                if (lastMod > date) {\n                                    return cbprecond(new Exc.PreconditionFailed(\n                                        "An If-Unmodified-Since header was specified, but the "\n                                            + "entity has been changed since the specified date.",\n                                        "If-Unmodified-Since")\n                                                    );\n                                }\n                            }\n                            cbprecond(null, false);\n                        });\n                    }\n                }\n            }\n        }\n    };\n\n    /**\n     * Generates a WebDAV propfind response body based on a list of nodes\n     *\n     * @param  {Array} fileProperties The list with nodes\n     * @return {String}\n     */\n    this.generateMultiStatus = function(fileProperties, strip404s) {\n        var namespace, prefix, entry, href, response;\n        var xml = \'<?xml version="1.0" encoding="utf-8"?><d:multistatus\';\n\n        // Adding in default namespaces\n        for (namespace in Xml.xmlNamespaces) {\n            prefix = Xml.xmlNamespaces[namespace];\n            xml += \' xmlns:\' + prefix + \'="\' + namespace + \'"\';\n        }\n\n        xml += ">";\n\n        for (var i in fileProperties) {\n            entry = fileProperties[i];\n            href = entry["href"];\n            //delete entry["href"];\n\n            if (strip404s && entry["404"])\n                delete entry["404"];\n\n            response = jsDAV_Property_Response.new(href, entry);\n            xml = response.serialize(this, xml);\n        }\n\n        return xml + "</d:multistatus>";\n    };\n\n    /**\n     * Returns a list of HTTP headers for a particular resource\n     *\n     * The generated http headers are based on properties provided by the\n     * resource. The method basically provides a simple mapping between\n     * DAV property and HTTP header.\n     *\n     * The headers are intended to be used for HEAD and GET requests.\n     *\n     * @param {String} path\n     */\n    this.getHTTPHeaders = function(path, cbheaders) {\n        var header;\n        var propertyMap = {\n            "{DAV:}getcontenttype"   : "content-type",\n            "{DAV:}getcontentlength" : "content-length",\n            "{DAV:}getlastmodified"  : "last-modified",\n            "{DAV:}getetag"          : "etag"\n        };\n        var headers    = {\n            "pragma"        : "no-cache",\n            "cache-control" : "no-cache, no-transform"\n        };\n        this.getProperties(path, ["{DAV:}getcontenttype", "{DAV:}getcontentlength",\n            "{DAV:}getlastmodified", "{DAV:}getetag"],\n            function(err, properties) {\n                if (!Util.empty(err))\n                    return cbheaders(err, headers);\n                for (var prop in propertyMap) {\n                    header = propertyMap[prop];\n                    if (properties[prop]) {\n                        // GetLastModified gets special cased\n                        if (properties[prop].hasFeature && properties[prop].hasFeature(jsDAV_Property_GetLastModified))\n                            headers[header] = Util.dateFormat(properties[prop].getTime(), Util.DATE_RFC1123);\n                        else if (typeof properties[prop] != "object")\n                            headers[header] = properties[prop];\n                    }\n                }\n                cbheaders(null, headers);\n            });\n    };\n\n    /**\n     * Returns a list of properties for a path\n     *\n     * This is a simplified version getPropertiesForPath.\n     * if you aren\'t interested in status codes, but you just\n     * want to have a flat list of properties. Use this method.\n     *\n     * @param {String} path\n     * @param {Array}  propertyNames\n     */\n    this.getProperties = function(path, propertyNames, cbgetprops) {\n        this.getPropertiesForPath(path, propertyNames, 0, function(err, result) {\n            if (!Util.empty(err))\n                return cbgetprops(err);\n            return cbgetprops(null, result[path]["200"]);\n        });\n    };\n\n    /**\n     * Returns a list of properties for a given path\n     *\n     * The path that should be supplied should have the baseUrl stripped out\n     * The list of properties should be supplied in Clark notation. If the list\n     * is empty \'allprops\' is assumed.\n     *\n     * If a depth of 1 is requested child elements will also be returned.\n     *\n     * @param {String} path\n     * @param {Array}  propertyNames\n     * @param {Number} depth\n     * @return {Array}\n     */\n    this.getPropertiesForPath = function(path, propertyNames, depth, cbgetpropspath) {\n        propertyNames = propertyNames || [];\n        depth = depth || 0;\n        if (depth !== 0)\n            depth = 1;\n        path = Util.rtrim(path, "/");\n\n        var returnPropertyList = {};\n        var self = this;\n        var reportPlugins = [];\n        Object.keys(this.plugins).forEach(function(pluginName) {\n            if (self.plugins[pluginName].getSupportedReportSet)\n                reportPlugins.push(self.plugins[pluginName]);\n        });\n        this.getNodeForPath(path, function(err, parentNode) {\n            if (!Util.empty(err))\n                return cbgetpropspath(err);\n\n            var nodes = {};\n            var nodesPath = [];\n            nodes[path] = parentNode;\n            nodesPath.push(path);\n\n            //if (jsDAV.debugMode)\n            //    Util.log("getPropertiesForPath", depth, parentNode,parentNode.hasFeature(jsDAV_iCollection));\n            if (depth === 1 && parentNode.hasFeature(jsDAV_iCollection)) {\n                parentNode.getChildren(function(err, cNodes) {\n                    if (!Util.empty(err))\n                        return cbgetpropspath(err);\n                    for (var i = 0, l = cNodes.length; i < l; ++i) {\n                        nodes[path + "/" + cNodes[i].getName()] = cNodes[i];\n                        nodesPath.push(path + "/" + cNodes[i].getName());\n                    }\n                    afterGetChildren(nodes, nodesPath);\n                });\n            }\n            else {\n                afterGetChildren(nodes, nodesPath);\n            }\n\n            function afterGetChildren(nodes, nodesPath) {\n                // If the propertyNames array is empty, it means all properties are requested.\n                // We shouldn\'t actually return everything we know though, and only return a\n                // sensible list.\n                var allProperties = (propertyNames.length === 0);\n                if (allProperties) {\n                    // Default list of propertyNames, when all properties were requested.\n                    propertyNames = [\n                        "{DAV:}getlastmodified",\n                        "{DAV:}getcontentlength",\n                        "{DAV:}resourcetype",\n                        "{DAV:}quota-used-bytes",\n                        "{DAV:}quota-available-bytes",\n                        "{DAV:}getetag",\n                        "{DAV:}getcontenttype"\n                    ];\n                }\n                \n                \n                // If the resourceType was not part of the list, we manually add it\n                // and mark it for removal. We need to know the resourcetype in order\n                // to make certain decisions about the entry.\n                // WebDAV dictates we should add a / and the end of href\'s for collections\n                var removeRT = false;\n                if (propertyNames.indexOf("{DAV:}resourcetype") === -1) {\n                    propertyNames.push("{DAV:}resourcetype");\n                    removeRT = true;\n                }\n\n                function afterGetProperty(rprop, rpath, newProps, cbnext) {\n                    // If we were unable to find the property, we will list it as 404.\n                    if (!allProperties && !newProps["200"][rprop])\n                        newProps["404"][rprop] = null;\n\n                    var node = nodes[rpath];\n                    rpath = Util.trim(rpath, "/");\n                    self.dispatchEvent("afterGetProperties", rpath, newProps, node, function() {\n                        if (parentNode.hasFeature(jsDAV_iCollection)) {\n                            // correct href when mountpoint is different than the\n                            // absolute location of the path\n                            var s = Util.trim(self.server.tree.basePath, "/");\n                            if (s.charAt(0) != ".") {\n                                rpath = s.indexOf(self.server.baseUri) !== 0\n                                    ? rpath.replace(new RegExp("^" + Util.escapeRegExp(s)), "").replace(/^[\\/]+/, "")\n                                    : rpath;\n                            }\n                        }\n                        newProps["href"] = rpath;\n\n                        // Its is a WebDAV recommendation to add a trailing slash to collectionnames.\n                        // Apple\'s iCal also requires a trailing slash for principals (rfc 3744).\n                        // Therefore we add a trailing / for any non-file. This might need adjustments\n                        // if we find there are other edge cases.\n                        var rt = newProps["200"]["{DAV:}resourcetype"];\n                        if (rpath !== "" && rt && (rt.is("{DAV:}collection") || rt.is("{DAV:}principal"))) {\n                            newProps["href"] += "/";\n                        }\n\n                        // If the resourcetype property was manually added to the requested property list,\n                        // we will remove it again.\n                        if (removeRT)\n                            delete newProps["200"]["{DAV:}resourcetype"];\n\n                        returnPropertyList[rpath] = newProps;\n                        cbnext();\n                    });\n                }\n\n                Async.list(nodesPath)\n                    .delay(0, 10)\n                    .each(function(myPath, cbnextpfp) {\n                        var node = nodes[myPath];\n                        var newProperties = {\n                            "200" : {},\n                            "403" : {},\n                            "404" : {}\n                        };\n                         \n                        var myProperties = [].concat(propertyNames);\n\n                        var propertyMap = Util.arrayToMap(myProperties);\n                        self.dispatchEvent("beforeGetProperties", myPath, node, propertyMap, newProperties, function(stop) {\n                            if (stop)\n                                return cbnextpfp(stop === true ? null : stop);\n\n                            myProperties = Object.keys(propertyMap);\n                            if (node.hasFeature(jsDAV_iProperties)) {\n                                node.getProperties(myProperties, function(err, props) {\n                                    // The getProperties method may give us too much,\n                                    // properties, in case the implementor was lazy.\n                                    //\n                                    // So as we loop through this list, we will only take the\n                                    // properties that were actually requested and discard the\n                                    // rest.\n                                    for (var i = myProperties.length - 1; i >= 0; --i) {\n                                        if (props[myProperties[i]]) {\n                                            newProperties["200"][myProperties[i]] = props[myProperties[i]];\n                                            myProperties.splice(i, 1);\n                                        }\n                                    }\n                                    buildProperties();\n                                });\n                            }\n                            else\n                                buildProperties();\n\n                            function buildProperties() {\n                                // next loop!\n                                Async.list(myProperties)\n                                    .delay(0, 10)\n                                    .each(function(prop, cbnextprops) {\n                                        if (typeof newProperties["200"][prop] != "undefined")\n                                            return cbnextprops();\n\n                                        if (prop == "{DAV:}getlastmodified") {\n                                             node.getLastModified(function(err, dt) {\n                                                if (!err && dt)\n                                                    newProperties["200"][prop] = jsDAV_Property_GetLastModified.new(dt);\n                                                afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                            });\n                                        }\n                                        else if (prop == "{DAV:}getcontentlength" && node.hasFeature(jsDAV_iFile)) {\n                                            node.getSize(function(err, size) {\n                                                if (!err && typeof size != "undefined")\n                                                    newProperties["200"][prop] = parseInt(size, 10);\n                                                afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                            });\n                                        }\n                                        else if (prop == "{DAV:}quota-used-bytes" && node.hasFeature(jsDAV_iQuota)) {\n                                            node.getQuotaInfo(function(err, quotaInfoUsed) {\n                                                if (!err && quotaInfoUsed.length)\n                                                    newProperties["200"][prop] = quotaInfoUsed[0];\n                                                afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                            });\n                                        }\n                                        else if (prop == "{DAV:}quota-available-bytes" && node.hasFeature(jsDAV_iQuota)) {\n                                            node.getQuotaInfo(function(err, quotaInfoAvail) {\n                                                if (!err && quotaInfoAvail.length)\n                                                    newProperties["200"][prop] = quotaInfoAvail[1];\n                                                afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                            });\n                                        }\n                                        else if (prop == "{DAV:}getetag" && node.hasFeature(jsDAV_iFile)) {\n                                            node.getETag(function(err, etag) {\n                                                if (!err && etag)\n                                                    newProperties["200"][prop] = etag;\n                                                afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                            });\n                                        }\n                                        else if (prop == "{DAV:}getcontenttype" && node.hasFeature(jsDAV_iFile)) {\n                                            node.getContentType(function(err, ct) {\n                                                if (!err && ct)\n                                                    newProperties["200"][prop] = ct;\n                                                afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                            });\n                                        }\n                                        else if (prop == "{DAV:}supported-report-set") {\n                                            var reports = [];\n                                            Async.list(reportPlugins)\n                                                .each(function(plugin, cbnextplugin) {\n                                                    plugin.getSupportedReportSet(myPath, function(err, rset) {\n                                                        if (!err && rset.length)\n                                                            reports = reports.concat(rset);\n                                                        cbnextplugin();\n                                                    });\n                                                })\n                                                .end(function() {\n                                                    newProperties["200"][prop] = jsDAV_Property_SupportedReportSet.new(reports);\n                                                    afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                                });\n                                        }\n                                        else if (prop == "{DAV:}resourcetype") {\n                                            newProperties["200"][prop] = jsDAV_Property_ResourceType.new();\n                                            for (var resourceType in self.resourceTypeMapping) {\n                                                if (node.hasFeature(self.resourceTypeMapping[resourceType]))\n                                                    newProperties["200"][prop].add(resourceType);\n                                            }\n                                            afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                        }\n                                        else {\n                                            afterGetProperty(prop, myPath, newProperties, cbnextprops);\n                                        }\n                                    })\n                                    .end(function(err) {\n                                        cbnextpfp(err);\n                                    });\n                            }\n                        });\n                    })\n                    .end(function(err) {\n                        if (!Util.empty(err))\n                            return cbgetpropspath(err);\n\n                        cbgetpropspath(null, returnPropertyList);\n                    });\n            }\n        });\n    };\n\n    /**\n     * Returns the HTTP range header\n     *\n     * This method returns null if there is no well-formed HTTP range request\n     * header or array(start, end).\n     *\n     * The first number is the offset of the first byte in the range.\n     * The second number is the offset of the last byte in the range.\n     *\n     * If the second offset is null, it should be treated as the offset of the\n     * last byte of the entity.\n     * If the first offset is null, the second offset should be used to retrieve\n     * the last x bytes of the entity.\n     *\n     * return mixed\n     */\n    this.getHTTPRange = function() {\n        var range = this.httpRequest.headers["range"];\n        if (!range)\n            return null;\n\n        // Matching "Range: bytes=1234-5678: both numbers are optional\n        var matches = range.match(/^bytes=([0-9]*)-([0-9]*)$/i);\n        if (!matches || !matches.length)\n            return null;\n\n        if (matches[1] === "" && matches[2] === "")\n            return null;\n\n        return matches.slice(1).map(function(rangePart) {\n            rangePart = parseFloat(rangePart);\n            return isNaN(rangePart) ? null : rangePart;\n        });\n    };\n\n    /**\n     * Returns the HTTP depth header\n     *\n     * This method returns the contents of the HTTP depth request header. If the\n     * depth header was \'infinity\' it will return the jsDAV_Handler.DEPTH_INFINITY object\n     * It is possible to supply a default depth value, which is used when the depth\n     * header has invalid content, or is completely non-existant\n     *\n     * @param  {mixed}   default\n     * @return {Number}\n     */\n    this.getHTTPDepth = function(def) {\n        def = def || jsDAV_Handler.DEPTH_INFINITY;\n        // If its not set, we\'ll grab the default\n        var depth = this.httpRequest.headers["depth"];\n        if (!depth)\n            return def;\n\n        if (depth == "infinity")\n            return jsDAV_Handler.DEPTH_INFINITY;\n\n        depth = parseInt(depth, 10);\n        // If its an unknown value. we\'ll grab the default\n        if (isNaN(depth))\n            return def;\n\n        return depth;\n    };\n\n    /**\n     * Returns the HTTP Prefer header information.\n     *\n     * The prefer header is defined in:\n     * http://tools.ietf.org/html/draft-snell-http-prefer-14\n     *\n     * This method will return an array with options.\n     *\n     * Currently, the following options may be returned:\n     *   {\n     *      "return-asynch"         : true,\n     *      "return-minimal"        : true,\n     *      "return-representation" : true,\n     *      "wait"                  : 30,\n     *      "strict"                : true,\n     *      "lenient"               : true,\n     *   }\n     *\n     * This method also supports the Brief header, and will also return\n     * \'return-minimal\' if the brief header was set to \'t\'.\n     *\n     * For the boolean options, false will be returned if the headers are not\n     * specified. For the integer options it will be \'null\'.\n     *\n     * @return array\n     */\n    this.getHTTPPrefer = function() {\n        var result = {\n            "return-asynch"         : false,\n            "return-minimal"        : false,\n            "return-representation" : false,\n            "wait"                  : null,\n            "strict"                : false,\n            "lenient"               : false\n        };\n\n        var prefer = this.httpRequest.headers.prefer;\n        if (prefer) {\n            prefer.split(",").map(function(item) {\n                return Util.trim(item);\n            }).forEach(function(parameter) {\n                // Right now our regex only supports the tokens actually\n                // specified in the draft. We may need to expand this if new\n                // tokens get registered.\n                var matches = parameter.match(/^([a-z0-9\\-]+)(?:=([0-9a-z]+))?$/);\n\n                if (matches !== null && matches.length > 0) {\n                    var token = matches[1];\n\n                    // Early RFC drafts declares return-minimal, return-asynch, etc...\n                    // Now http://tools.ietf.org/html/rfc7240 declares return=minimal, etc...\n                    if (token == "return" && matches[2]) {\n                        // Convert return=??? to return-??? for backward compatibility\n                        token = token + "-" + matches[2];\n                    }\n\n                    switch(token) {\n                        case "return-asynch" :\n                        case "return-minimal" :\n                        case "return-representation" :\n                        case "strict" :\n                        case "lenient" :\n                            result[token] = true;\n                            break;\n                        case "wait" :\n                            result[token] = matches[2];\n                            break;\n                    }\n                }\n            });\n        }\n\n        var brief = this.httpRequest.headers.brief;\n        if (brief && brief == "t")\n            result["return-minimal"] = true;\n\n        return result;\n    };\n\n    /**\n     * This method parses the PROPFIND request and returns its information\n     *\n     * This will either be a list of properties, or an empty array; in which case\n     * an {DAV:}allprop was requested.\n     *\n     * @param  {String} body\n     * @return {Array}\n     */\n    this.parsePropfindRequest = function(body, cbpropfindreq) {\n        // If the propfind body was empty, it means IE is requesting \'all\' properties\n        if (!body)\n            return cbpropfindreq(null, []);\n\n        Xml.loadDOMDocument(body, this.server.options.parser, function(err, oXml) {\n            //Util.log("XML ", oXml);\n            if (!Util.empty(err))\n                return cbpropfindreq(err);\n            cbpropfindreq(null, Object.keys(Xml.parseProperties(oXml.propfind || oXml)));\n        });\n    };\n\n    /**\n     * This method parses a Proppatch request\n     *\n     * Proppatch changes the properties for a resource. This method\n     * returns a list of properties.\n     *\n     * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,\n     * and the value contains the property value. If a property is to be removed\n     * the value will be null.\n     *\n     * @param  {String}   body           Xml body\n     * @param  {Function} cbproppatchreq Callback that is the return body of this function\n     * @return {Object}   list of properties in need of updating or deletion\n     */\n    this.parseProppatchRequest = function(body, cbproppatchreq) {\n        //We\'ll need to change the DAV namespace declaration to something else\n        //in order to make it parsable\n        var operation, innerProperties, propertyValue;\n        var self = this;\n        Xml.loadDOMDocument(body, this.server.options.parser, function(err, dom) {\n            if (!Util.empty(err))\n                return cbproppatchreq(err);\n            var child, propertyName;\n            var newProperties = {};\n            var i             = 0;\n            var c             = dom.childNodes;\n            var l             = c.length;\n            for (; i < l; ++i) {\n                child     = c[i];\n                operation = Xml.toClarkNotation(child);\n                if (!operation || operation !== "{DAV:}set" && operation !== "{DAV:}remove")\n                    continue;\n\n                innerProperties = Xml.parseProperties(child, self.propertyMap);\n                for (propertyName in innerProperties) {\n                    propertyValue = innerProperties[propertyName];\n                    if (operation === "{DAV:}remove")\n                        propertyValue = null;\n                    newProperties[propertyName] = propertyValue;\n                }\n            }\n            cbproppatchreq(null, newProperties);\n        });\n    };\n\n    /**\n     * This method updates a resource\'s properties\n     *\n     * The properties array must be a list of properties. Array-keys are\n     * property names in clarknotation, array-values are it\'s values.\n     * If a property must be deleted, the value should be null.\n     *\n     * Note that this request should either completely succeed, or\n     * completely fail.\n     *\n     * The response is an array with statuscodes for keys, which in turn\n     * contain arrays with propertynames. This response can be used\n     * to generate a multistatus body.\n     *\n     * @param  {String}  uri\n     * @param  {Object}  properties\n     * @return {Object}\n     */\n    this.updateProperties = function(uri, properties, cbupdateprops) {\n        // we\'ll start by grabbing the node, this will throw the appropriate\n        // exceptions if it doesn\'t.\n        var props;\n        var self   = this;\n        var remainingProperties = Util.extend({}, properties);\n        var hasError = false;\n        var result = {};\n        result[uri] = {\n            "200" : [],\n            "403" : [],\n            "424" : []\n        };\n\n        this.getNodeForPath(uri, function(err, node) {\n            if (!Util.empty(err))\n                return cbupdateprops(err);\n            // If the node is not an instance of jsDAV_iProperties, every\n            // property is 403 Forbidden\n            // simply return a 405.\n            var propertyName;\n            if (!node.hasFeature(jsDAV_iProperties)) {\n                hasError = true;\n                for (propertyName in properties)\n                    Util.arrayRemove(result[uri]["403"], propertyName);\n                remainingProperties = {};\n            }\n\n            // Running through all properties to make sure none of them are protected\n            if (!hasError) {\n                for (propertyName in properties) {\n                    if (self.protectedProperties.indexOf(propertyName) > -1) {\n                        Util.arrayRemove(result[uri]["403"], propertyName);\n                        delete remainingProperties[propertyName];\n                        hasError = true;\n                    }\n                }\n            }\n\n            // Only if there were no errors we may attempt to update the resource\n            if (!hasError) {\n                node.updateProperties(properties, function(err, updateResult) {\n                    if (err)\n                        return cbupdateprops(err);\n\n                    remainingProperties = {};\n\n                    if (updateResult === true) {\n                        // success\n                        for (propertyName in properties)\n                            Util.arrayRemove(result[uri]["200"], propertyName);\n                    }\n                    else if (updateResult === false) {\n                        // The node failed to update the properties for an\n                        // unknown reason\n                        for (propertyName in properties)\n                            Util.arrayRemove(result[uri]["403"], propertyName);\n                    }\n                    else if (typeof updateResult == "object") {\n                        // The node has detailed update information\n                        result[uri] = updateResult;\n                    }\n                    else {\n                        return cbupdateprops(new Exc.jsDAV_Exception("Invalid result from updateProperties"));\n                    }\n\n                    afterUpdateProperties();\n                });\n            }\n            else\n                afterUpdateProperties();\n\n            function afterUpdateProperties() {\n                for (propertyName in remainingProperties) {\n                    // if there are remaining properties, it must mean\n                    // there\'s a dependency failure\n                    Util.arrayRemove(result[uri]["424"], propertyName);\n                }\n\n                // Removing empty array values\n                for (var status in result[uri]) {\n                    if (!result[uri][status].length)\n                        delete result[uri][status];\n                }\n                result[uri]["href"] = uri;\n                cbupdateprops(null, result);\n            }\n        });\n    };\n\n    /**\n     * This method is invoked by sub-systems creating a new file.\n     *\n     * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).\n     * It was important to get this done through a centralized function,\n     * allowing plugins to intercept this using the beforeCreateFile event.\n     *\n     * @param {String} uri\n     * @param {Buffer} dataOrStream. Set to `null` to use a stream\n     * @param {String} [enc]\n     * @return {void}\n     */\n    this.createFile = function(uri, dataOrStream, enc, cbcreatefile) {\n        var parts = Util.splitPath(uri);\n        var dir   = parts[0];\n        var name  = parts[1];\n        var self  = this;\n\n        this.dispatchEvent("beforeBind", uri, function(stop, etag) {\n            if (stop)\n                return cbcreatefile(stop === true ? null : stop, etag);\n\n            self.getNodeForPath(dir, function(err, parent) {\n                if (!Util.empty(err))\n                    return cbcreatefile(err);\n\n                self.dispatchEvent("beforeCreateFile", uri, dataOrStream, enc, parent, function(stop, etag) {\n                    if (stop)\n                        return cbcreatefile(stop === true ? null : stop, etag);\n\n                    if (dataOrStream === null) {\n                        if (!parent.createFileStream) {\n                            self.getRequestBody(enc, null, false, function(err, body) {\n                                if (err)\n                                    return cbcreatefile(err);\n\n                                parent.createFile(name, body, enc || "utf8", afterBind);\n                            });\n                        }\n                        else\n                            parent.createFileStream(self, name, enc || "utf8", afterBind);\n                    }\n                    else {\n                        if (parent.createFileStreamRaw && dataOrStream instanceof Fs.ReadStream)\n                            parent.createFileStreamRaw(name, dataOrStream, enc || "utf8", afterBind);\n                        else\n                            parent.createFile(name, dataOrStream, enc || "utf8", afterBind);\n                    }\n\n                    function afterBind(err, etag) {\n                        if (!Util.empty(err))\n                            return cbcreatefile(err);\n\n                        var path = Path.join(dir, name);\n                        self.markDirty(path);\n                        self.dispatchEvent("afterBind", path);\n                        cbcreatefile(null, etag);\n                    }\n                });\n            });\n        });\n    };\n\n    /**\n     * This method is invoked by sub-systems creating a new directory.\n     *\n     * @param  {String}   uri\n     * @param  {Function} cbcreatedir\n     * @return {void}\n     */\n    this.createDirectory = function(uri, cbcreatedir) {\n        this.createCollection(uri, ["{DAV:}collection"], {}, cbcreatedir);\n    };\n\n    /**\n     * Use this method to create a new collection\n     *\n     * The {DAV:}resourcetype is specified using the resourceType array.\n     * At the very least it must contain {DAV:}collection.\n     *\n     * The properties array can contain a list of additional properties.\n     *\n     * @param  {string} uri          The new uri\n     * @param  {Array}  resourceType The resourceType(s)\n     * @param  {Object} properties   A list of properties\n     * @return {void}\n     */\n    this.createCollection = function(uri, resourceType, properties, cbcreatecoll) {\n        var self      = this;\n        var path      = Util.splitPath(uri);\n        var parentUri = path[0];\n        var newName   = path[1];\n\n        // Making sure {DAV:}collection was specified as resourceType\n        if (resourceType.indexOf("{DAV:}collection") == -1) {\n            return cbcreatecoll(new Exc.InvalidResourceType(\n                "The resourceType for this collection must at least include {DAV:}collection")\n            );\n        }\n\n        // Making sure the parent exists\n        this.getNodeForPath(parentUri, function(err, parent) {\n            if (!Util.empty(err))\n                return cbcreatecoll(new Exc.Conflict("Parent node does not exist"));\n\n            // Making sure the parent is a collection\n            if (!parent.hasFeature(jsDAV_iCollection))\n                return cbcreatecoll(new Exc.Conflict("Parent node is not a collection"));\n\n            // Making sure the child does not already exist\n            parent.getChild(newName, function(err, ch) {\n                // If we got here.. it means there\'s already a node on that url,\n                // and we need to throw a 405\n                if (typeof ch != "undefined") {\n                    return cbcreatecoll(new Exc.MethodNotAllowed(\n                        "The resource you tried to create already exists")\n                    );\n                }\n                if (err && err.type != "FileNotFound")\n                    return cbcreatecoll(err);\n\n                self.dispatchEvent("beforeBind", uri, function(stop) {\n                    if (stop)\n                        return cbcreatecoll(stop === true ? null : stop);\n\n                    // There are 2 modes of operation. The standard collection\n                    // creates the directory, and then updates properties\n                    // the extended collection can create it directly.\n                    if (parent.hasFeature(jsDAV_iExtendedCollection)) {\n                        parent.createExtendedCollection(newName, resourceType, properties, cbcreatecoll);\n                    }\n                    else {\n                        // No special resourcetypes are supported\n                        if (resourceType.length > 1) {\n                            return cbcreatecoll(new Exc.InvalidResourceType(\n                                "The {DAV:}resourcetype you specified is not supported here.")\n                            );\n                        }\n                        parent.createDirectory(newName, function(err, res) {\n                            if (!Util.empty(err))\n                                return cbcreatecoll(err);\n\n                            if (properties.length > 0) {\n                                self.updateProperties(uri, properties, function(err, errorResult) {\n                                    if (err || !errorResult["200"].length)\n                                        return rollback(err, errorResult);\n                                    onDone();\n                                });\n                            }\n                            else\n                                onDone();\n\n                            function rollback(exc, res) {\n                                self.server.tree.getNodeForPath(uri, function(err, node) {\n                                    if (err)\n                                        return cbcreatecoll(err);\n                                    self.dispatchEvent("beforeUnbind", uri, function(stop) {\n                                        if (stop)\n                                            return cbcreatecoll(stop === true ? null : stop);\n                                        node["delete"]();\n                                        // Re-throwing exception\n                                        cbcreatecoll(exc, err);\n                                    });\n                                });\n                            }\n                            \n                            function onDone() {\n                                self.markDirty(parentUri);\n                                self.dispatchEvent("afterBind", uri, Path.join(parent.path, newName));\n                                cbcreatecoll();\n                            }\n                        });\n                    }\n                });\n            });\n        });\n    };\n\n    /**\n     * Returns information about Copy and Move requests\n     *\n     * This function is created to help getting information about the source and\n     * the destination for the WebDAV MOVE and COPY HTTP request. It also\n     * validates a lot of information and throws proper exceptions\n     *\n     * The returned value is an array with the following keys:\n     *   * source - Source path\n     *   * destination - Destination path\n     *   * destinationExists - Wether or not the destination is an existing url\n     *     (and should therefore be overwritten)\n     *\n     * @return {Object}\n     */\n    this.getCopyAndMoveInfo = function(cbcopymove) {\n        var source, destination;\n        try {\n            source = this.getRequestUri();\n        }\n        catch (ex) {\n            return cbcopymove(ex);\n        }\n\n        // Collecting the relevant HTTP headers\n        if (!this.httpRequest.headers["destination"])\n            return cbcopymove(new Exc.BadRequest("The destination header was not supplied"));\n\n        try {\n            destination = this.calculateUri(this.httpRequest.headers["destination"]);\n        }\n        catch (ex) {\n            return cbcopymove(ex);\n        }\n        var overwrite = this.httpRequest.headers["overwrite"];\n        if (!overwrite)\n            overwrite = "T";\n        if (overwrite.toUpperCase() == "T") {\n            overwrite = true;\n        }\n        else if (overwrite.toUpperCase() == "F") {\n            overwrite = false;\n        }\n        else {\n            // We need to throw a bad request exception, if the header was invalid\n            return cbcopymove(new Exc.BadRequest(\n                "The HTTP Overwrite header should be either T or F")\n            );\n        }\n\n        var destinationDir = Util.splitPath(destination)[0];\n        var self           = this;\n\n        // Collection information on relevant existing nodes\n        this.getNodeForPath(destinationDir, function(err, destinationParent) {\n            if (!Util.empty(err)) {\n                // If the destination parent node is not found, we throw a 409\n                return cbcopymove(err.type == "FileNotFound"\n                    ? new Exc.Conflict("The destination node is not found")\n                    : err);\n            }\n            if (!destinationParent.hasFeature(jsDAV_iCollection)) {\n                return cbcopymove(new Exc.UnsupportedMediaType(\n                    "The destination node is not a collection")\n                );\n            }\n\n            self.getNodeForPath(destination, function(err, destinationNode) {\n                // Destination didn\'t exist, we\'re all good\n                if (!Util.empty(err)) {\n                     if (err.type == "FileNotFound")\n                        destinationNode = false;\n                     else\n                         return cbcopymove(err);\n                }\n                // If this succeeded, it means the destination already exists\n                // we"ll need to throw precondition failed in case overwrite is false\n                if (destinationNode && !overwrite) {\n                    return cbcopymove(new Exc.PreconditionFailed(\n                        "The destination node already exists, and the overwrite header is set to false",\n                        "Overwrite"));\n                }\n\n                // These are the three relevant properties we need to return\n                cbcopymove(null, {\n                    "source"            : source,\n                    "destination"       : destination,\n                    "destinationExists" : !Util.empty(destinationNode),\n                    "destinationNode"   : destinationNode\n                });\n            });\n        });\n    };\n\n    /**\n     * Returns a full HTTP status message for an HTTP status code\n     *\n     * @param {Number} code\n     * @return {string}\n     */\n    this.getStatusMessage = function(code) {\n        code = String(code);\n        return "HTTP/1.1 " + code + " " + jsDAV_Handler.STATUS_MAP[code];\n    };\n}).call(jsDAV_Handler.prototype = new AsyncEventEmitter());\n', 'message': 'fixed chunked GET requests with Range headers set (reported and fix supplied by drahgon55)', 'author_name': 'mikedeboer', 'lang': 'js'}