<commit_before>/*
 * @package jsDAV
 * @subpackage DAV
 * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org>
 * @author <NAME> <info AT mikedeboer DOT nl>
 * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
 */

var Url          = require("url"),
    Fs           = require("fs"),
    Path         = require("path"),
    Exc          = require("./exceptions"),
    Util         = require("./util"),
    Async        = require("./../../support/async.js"),

    // DAV classes used directly by the Handler object
    jsDAV                             = require("./../jsdav"),
    jsDAV_Server                      = require("./server"),
    jsDAV_Property_Response           = require("./property/response").jsDAV_Property_Response,
    jsDAV_Property_GetLastModified    = require("./property/getLastModified").jsDAV_Property_GetLastModified,
    jsDAV_Property_ResourceType       = require("./property/resourceType").jsDAV_Property_ResourceType,
    jsDAV_Property_SupportedReportSet = require("./property/supportedReportSet").jsDAV_Property_SupportedReportSet;

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}
 */
function jsDAV_Handler(server, req, resp) {
    this.server       = server;
    this.httpRequest  = Util.streamBuffer(req);
    this.httpResponse = resp;
    this.plugins      = {};
    
    for (var plugin in server.plugins)
        this.plugins[plugin] = new server.plugins[plugin](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
 */
exports.DEPTH_INFINITY = -1;

/**
 * Nodes that are files, should have this as the type property
 */
exports.NODE_FILE      = 1;

/**
 * Nodes that are directories, should use this value as the type property
 */
exports.NODE_DIRECTORY = 2;

exports.PROP_SET       = 1;
exports.PROP_REMOVE    = 2;

exports.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
};

/**
 * XML namespace for all jsDAV related elements
 */
exports.NS_AJAXORG = "http://ajax.org/2005/aml";

(function() {
    /**
     * httpResponse
     *
     * @var HTTP_Response
     */
    this.httpResponse =

    /**
     * httpRequest
     *
     * @var HTTP_Request
     */
    this.httpRequest = null;

    /**
     * This is a default list of namespaces.
     *
     * If you are defining your own custom namespace, add it here to reduce
     * bandwidth and improve legibility of xml bodies.
     *
     * @var array
     */
    this.xmlNamespaces = {
        "DAV:": "d",
        "http://ajax.org/2005/aml": "a"
    };

    /**
     * The propertymap can be used to map properties from
     * requests to property classes.
     *
     * @var array
     */
    this.propertyMap = {};

    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",
    ];

    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;
            console.log("REQUEST START: ", this.httpRequest.url, this.id);
            console.log("INFO: invoking method '" + method + "'");
            var wh = this.httpResponse.writeHead,
                we = this.httpResponse.end;
            this.httpResponse.writeHead = function(code, headers) {
                console.log("INFO: sending header: " + code, headers);
                this.writeHead = wh
                this.writeHead(code, headers);
            }
            this.httpResponse.end = function(content) {
                console.log("request end: ", _self.id);
                console.log("INFO: writing body: '" + (content || "")  + "'");
                this.end = we;
                this.end(content);
            };
        }

        this.dispatchEvent("beforeMethod", method, function(stop) {
            if (stop === true)
                return;
            // 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, function(stop) {
                    if (stop === true)
                        return;
                    // Unsupported method
                    _self.handleError(new Exc.jsDAV_Exception_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"?>'
                + '<d:error xmlns:d="DAV:" xmlns:a="' + exports.NS_AJAXORG + '">'
                + '    <a:exception>' + (e.type || e.toString()) + '</a:exception>'
                + '    <a:message>'   + e.message + '</a:message>'
        if (this.server.debugExceptions) {
            xml += '<a:file>' + (e.filename || "") + '</a:file>'
                +  '<a:line>' + (e.line || "") + '</a:line>';
        }
        xml += '<a:jsdav-version>' + jsDAV_Server.VERSION + '</a:jsdav-version>';

        var code    = 500,
            _self   = this;
        if (e.type && e.type.indexOf("jsDAV_Exception") === 0) {
            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) {
                console.log("ERROR: ", e);
                //throw e; // DEBUGGING!
            }
        }
    };

    /**
     * HTTP OPTIONS
     *
     * @return {void}
     * @throws {Error}
     */
    this.httpOptions = function() {
        var uri   = this.getRequestUri(),
            _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" : exports.VERSION,
                    "Content-Length"  : 0
                },
                features = ["1", "3", "extended-mkcol"];

            for (var plugin in _self.plugins) {
                if (!_self.plugins[plugin].getFeatures)
                    console.log("ERR: method getFeatures() NOT implemented for plugin " + plugin);
                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,
            uri   = this.getRequestUri(),
            _self = this;

        this.checkPreconditions(true, function(err, redirected) {
            if (!Util.empty(err))
                return _self.handleError(err);
            if (redirected)
                return;
            _self.server.tree.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.jsDAV_Exception_NotImplemented(
                    "GET is only implemented on File objects"));
            }
            node.get(function(err, body) {
                if (!Util.empty(err))
                    return _self.handleError(err);

                _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"];
                        // Need to unset Content-Length, because we'll handle that
                        // during figuring out the range
                        delete httpHeaders["Content-Length"];
                    }

                    //this.httpResponse.setHeaders(httpHeaders);

                    var range             = _self.getHTTPRange(),
                        ifRange           = _self.httpRequest.headers["if-range"],
                        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]) {
                            start = range[0];
                            end   = range[1] ? range[1] : nodeSize - 1;
                            if (start > nodeSize) {
                                return _self.handleError(new Exc.jsDAV_Exception_RequestedRangeNotSatisfiable(
                                    "The start offset (" + range[0] + ") exceeded the size of the entity ("
                                    + nodeSize + ")")
                                );
                            }

                            if (end < start) {
                                return _self.handleError(new Exc.jsDAV_Exception_RequestedRangeNotSatisfiable(
                                    "The end offset (" + range[1] + ") is lower than the start offset ("
                                    + range[0] + ")")
                                );
                            }
                            if (end > nodeSize)
                                end = nodeSize - 1;

                        }
                        else {
                            start = nodeSize - range[1];
                            end   = nodeSize - 1;
                            if (start < 0)
                                start = 0;
                        }

                        // New read/write stream
                        var offlen    = end - start + 1,
                            newStream = new Buffer(offlen);
                        body.copy(newStream, 0, start, offlen);

                        httpHeaders["Content-Length"] = offlen;
                        httpHeaders["Content-Range"]  = "bytes " + start + "-" + end + "/" + nodeSize;
                        _self.httpResponse.writeHead(206, httpHeaders);
                        _self.httpResponse.end(newStream);
                    }
                    else {
                        var since        = _self.httpRequest.headers["if-modified-since"],
                            oldEtag      = _self.httpRequest.headers["if-none-match"],
                            lastModified = httpHeaders["Last-Modified"],
                            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;
                            _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.server.tree.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, headers) {
                    if (!Util.empty(err))
                        return _self.handleError(err);
                    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(),
            _self = this;

        this.server.tree.getNodeForPath(uri, function(err, node) {
            if (!Util.empty(err))
                return _self.handleError(err);

            _self.dispatchEvent("beforeUnbind", uri, function(stop) {
                if (stop === true)
                    return;
                node["delete"](function(err) {
                    if (!Util.empty(err))
                        return _self.handleError(err);
                    _self.httpResponse.writeHead(204, {"Content-Length": "0"});
                    _self.httpResponse.end();
                });
            });
        });
    };

    /**
     * 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", function(err, data) {
            if (jsDAV.debugMode)
                console.log("INFO: 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
                try {
                    var path = _self.getRequestUri();
                }
                catch (ex) {
                    return _self.handleError(ex);
                }
                if (jsDAV.debugMode)
                    console.log("DEBUG: httpPropfind BEFORE getPropertiesForPath '" + path + "';", requestedProperties);
                _self.getPropertiesForPath(path, requestedProperties, depth, function(err, newProperties) {
                    if (!Util.empty(err))
                        return _self.handleError(err);
                    // This is a multi-status response
                    _self.httpResponse.writeHead(207, {"Content-Type": "application/xml; charset=utf-8"});
                    _self.httpResponse.end(_self.generateMultiStatus(newProperties));
                });
            });
        });
    };

    /**
     * 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", function(err, data) {
            if (jsDAV.debugMode)
                console.log("INFO: 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);
                    _self.httpResponse.writeHead(207, {"Content-Type": "application/xml; charset=utf-8"});
                    _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,
            uri   = this.getRequestUri();
            
        this.getRequestBody("binary", function(err, body, cleanup) {
            if (!Util.empty(err))
                return _self.handleError(err);

            // First we'll do a check to see if the resource already exists
            _self.server.tree.getNodeForPath(uri, function(err, node) {
                if (!Util.empty(err)) {
                    if (err instanceof Exc.jsDAV_Exception_FileNotFound) {
                        // If we got here, the resource didn't exist yet.
                        _self.createFile(uri, body, "binary", function() {
                            cleanup();
                            _self.httpResponse.writeHead(201, {"Content-Length": "0"});
                            _self.httpResponse.end();
                        });
                    }
                    else {
                        cleanup();
                        return _self.handleError(err);
                    }
                }
                else {
                    // Checking If-None-Match and related headers.
                    _self.checkPreconditions(false, function(err, redirected) {
                        if (!Util.empty(err)) {
                            cleanup();
                            return _self.handleError(err);
                        }
                        if (redirected) {
                            cleanup();
                            return false;
                        }
                        // If the node is a collection, we'll deny it
                        if (!node.hasFeature(jsDAV.__IFILE__)) {
                            cleanup();
                            return _self.handleError(new Exc.jsDAV_Exception_Conflict("PUT is not allowed on non-files."));
                        }

                        _self.dispatchEvent("beforeWriteContent", uri, function(stop) {
                            if (stop === true)
                                return cleanup();
                            node.put(body, "binary", function(err) {
                                cleanup();
                                if (!Util.empty(err))
                                    return _self.handleError(err);
                                _self.httpResponse.writeHead(200, {"Content-Length": "0"});
                                _self.httpResponse.end();
                            });
                        });
                    });
                }
            });
        });
    };

    /**
     * WebDAV MKCOL
     *
     * The MKCOL method is used to create a new collection (directory) on the server
     *
     * @return {void}
     */
    this.httpMkcol = function() {
        var resourceType,
            properties  = {},
            _self       = this,
            req         = this.httpRequest;

        this.getRequestBody("utf8", function(err, requestBody) {
            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.jsDAV_Exception_UnsupportedMediaType(
                        "The request body for the MKCOL request must have an xml Content-Type"));
                }

                Util.loadDOMDocument(requestBody, function(err, dom) {
                    var firstChild = dom.firstChild;
                    if (Util.toClarkNotation(firstChild) !== "{DAV:}mkcol") {
                        // We must throw 415 for unsupport mkcol bodies
                        return _self.handleError(new Exc.jsDAV_Exception_UnsupportedMediaType(
                            "The request body for the MKCOL request must be a {DAV:}mkcol request construct."));
                    }

                    var childNode,
                        i = 0,
                        c = firstChild.childNodes,
                        l = c.length;
                    for (; i < l; ++i) {
                        childNode = c[i];
                        if (Util.toClarkNotation(childNode) !== "{DAV:}set") continue;
                        properties = Util.extend(properties, Util.parseProperties(childNode, _self.propertyMap));
                    }
                    if (!properties["{DAV:}resourcetype"]) {
                        return _self.handleError(new Exc.jsDAV_Exception_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(Util.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 === true)
                        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 === true)
                        return false;
                    _self.dispatchEvent("beforeBind", moveInfo["destination"], function(stop) {
                        if (stop === true)
                            return false;
                        _self.server.tree.move(moveInfo["source"], moveInfo["destination"], function(err) {
                            if (!Util.empty(err))
                                return _self.handleError(err);

                            _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();
                        });
                    });
                });
            }
        });
    };

    /**
     * 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 === true)
                        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 === true)
                        return false;
                    _self.server.tree.copy(copyInfo["source"], copyInfo["destination"], function(err) {
                        if (!Util.empty(err))
                            return _self.handleError(err);
                        _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();
                    });
                });
            }
        });
    };

    /**
     * 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", function(err, data) {
            Util.loadDOMDocument(data, function(err, dom) {
                var reportName = Util.toClarkNotation(dom);
                _self.dispatchEvent("report", reportName, dom, function(stop) {
                    if (stop !== true) {
                        // If broadcastEvent returned true, it means the report was not supported
                        return _self.handleError(new Exc.jsDAV_Exception_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,
            methods = [
                "OPTIONS",
                "GET",
                "HEAD",
                "DELETE",
                "PROPFIND",
                "PUT",
                "PROPPATCH",
                "COPY",
                "MOVE",
                "REPORT"
            ];

        // The MKCOL is only allowed on an unmapped uri
        this.server.tree.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)
                    console.log("ERR: method getHTTPMethods() NOT implemented for plugin " + plugin);
                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.
     * 
     * @param  {Function} callback
     * @return {void}
     */
    this.getRequestBody = function(enc, cbreqbody) {
        var ctype,
            req      = this.httpRequest,
            data     = "",
            isStream = (!(ctype = req.headers["content-type"]) || !ctype.match(/(urlencoded|multipart)/i)),
            cleanup  = function() {},
            _self    = this;

        // 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(null, enc == "binary"
                ? req.$data 
                : req.$data.toString(enc), cleanup);
        }

        if (isStream) {
            var stream = [];
            
            req.streambuffer.ondata(function(data) {
                stream.push(data);
            });

            req.streambuffer.onend(function() {
                _self.$reading = false;
                req.$data = Util.concatBuffers(stream);
                readDone(null, enc == "binary"
                    ? req.$data 
                    : req.$data.toString(enc), cleanup);
            });
        }
        else {
            var formidable = require("./../../support/formidable/lib/formidable"),
                form       = new formidable.IncomingForm();
            form.uploadDir = this.server.tmpDir;

            form.addListener("file", function(field, file) {
                Fs.readFile(file.path, function(err, data) {
                    _self.$reading = false;
                    req.$data = data;
                    readDone(err, enc == "binary" ? data : data.toString(enc), function() {
                        Fs.unlink(file.path);
                    });
                });
            });

            form.addListener("error", callback);

            form.parse(req);
        }
        
        function readDone(err, data, cleanup) {
            cbreqbody(err, data, cleanup);
        }
    };

    /**
     * Calculates the uri for a request, making sure that the base uri is stripped out
     *
     * @param  {String} uri
     * @throws {jsDAV_Exception_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) != "/" && uri.indexOf("://") > -1)
            uri = Url.parse(uri).pathname;

        uri = uri.replace("//", "/");

        if (uri.indexOf(this.server.baseUri) === 0) {
            return Util.trim(decodeURI(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 "";
        }<commit_msg>[BACKLOG-817] can't preview an html file with a querystring (#218)<commit_after>/*
 * @package jsDAV
 * @subpackage DAV
 * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org>
 * @author <NAME> <info AT mikedeboer DOT nl>
 * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
 */

var Url          = require("url"),
    Fs           = require("fs"),
    Path         = require("path"),
    Exc          = require("./exceptions"),
    Util         = require("./util"),
    Async        = require("./../../support/async.js"),

    // DAV classes used directly by the Handler object
    jsDAV                             = require("./../jsdav"),
    jsDAV_Server                      = require("./server"),
    jsDAV_Property_Response           = require("./property/response").jsDAV_Property_Response,
    jsDAV_Property_GetLastModified    = require("./property/getLastModified").jsDAV_Property_GetLastModified,
    jsDAV_Property_ResourceType       = require("./property/resourceType").jsDAV_Property_ResourceType,
    jsDAV_Property_SupportedReportSet = require("./property/supportedReportSet").jsDAV_Property_SupportedReportSet;

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}
 */
function jsDAV_Handler(server, req, resp) {
    this.server       = server;
    this.httpRequest  = Util.streamBuffer(req);
    this.httpResponse = resp;
    this.plugins      = {};
    
    for (var plugin in server.plugins)
        this.plugins[plugin] = new server.plugins[plugin](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
 */
exports.DEPTH_INFINITY = -1;

/**
 * Nodes that are files, should have this as the type property
 */
exports.NODE_FILE      = 1;

/**
 * Nodes that are directories, should use this value as the type property
 */
exports.NODE_DIRECTORY = 2;

exports.PROP_SET       = 1;
exports.PROP_REMOVE    = 2;

exports.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
};

/**
 * XML namespace for all jsDAV related elements
 */
exports.NS_AJAXORG = "http://ajax.org/2005/aml";

(function() {
    /**
     * httpResponse
     *
     * @var HTTP_Response
     */
    this.httpResponse =

    /**
     * httpRequest
     *
     * @var HTTP_Request
     */
    this.httpRequest = null;

    /**
     * This is a default list of namespaces.
     *
     * If you are defining your own custom namespace, add it here to reduce
     * bandwidth and improve legibility of xml bodies.
     *
     * @var array
     */
    this.xmlNamespaces = {
        "DAV:": "d",
        "http://ajax.org/2005/aml": "a"
    };

    /**
     * The propertymap can be used to map properties from
     * requests to property classes.
     *
     * @var array
     */
    this.propertyMap = {};

    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",
    ];

    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;
            console.log("REQUEST START: ", this.httpRequest.url, this.id);
            console.log("INFO: invoking method '" + method + "'");
            var wh = this.httpResponse.writeHead,
                we = this.httpResponse.end;
            this.httpResponse.writeHead = function(code, headers) {
                console.log("INFO: sending header: " + code, headers);
                this.writeHead = wh
                this.writeHead(code, headers);
            }
            this.httpResponse.end = function(content) {
                console.log("request end: ", _self.id);
                console.log("INFO: writing body: '" + (content || "")  + "'");
                this.end = we;
                this.end(content);
            };
        }

        this.dispatchEvent("beforeMethod", method, function(stop) {
            if (stop === true)
                return;
            // 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, function(stop) {
                    if (stop === true)
                        return;
                    // Unsupported method
                    _self.handleError(new Exc.jsDAV_Exception_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"?>'
                + '<d:error xmlns:d="DAV:" xmlns:a="' + exports.NS_AJAXORG + '">'
                + '    <a:exception>' + (e.type || e.toString()) + '</a:exception>'
                + '    <a:message>'   + e.message + '</a:message>'
        if (this.server.debugExceptions) {
            xml += '<a:file>' + (e.filename || "") + '</a:file>'
                +  '<a:line>' + (e.line || "") + '</a:line>';
        }
        xml += '<a:jsdav-version>' + jsDAV_Server.VERSION + '</a:jsdav-version>';

        var code    = 500,
            _self   = this;
        if (e.type && e.type.indexOf("jsDAV_Exception") === 0) {
            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) {
                console.log("ERROR: ", e);
                //throw e; // DEBUGGING!
            }
        }
    };

    /**
     * HTTP OPTIONS
     *
     * @return {void}
     * @throws {Error}
     */
    this.httpOptions = function() {
        var uri   = this.getRequestUri(),
            _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" : exports.VERSION,
                    "Content-Length"  : 0
                },
                features = ["1", "3", "extended-mkcol"];

            for (var plugin in _self.plugins) {
                if (!_self.plugins[plugin].getFeatures)
                    console.log("ERR: method getFeatures() NOT implemented for plugin " + plugin);
                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,
            uri   = this.getRequestUri(),
            _self = this;

        this.checkPreconditions(true, function(err, redirected) {
            if (!Util.empty(err))
                return _self.handleError(err);
            if (redirected)
                return;
            _self.server.tree.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.jsDAV_Exception_NotImplemented(
                    "GET is only implemented on File objects"));
            }
            node.get(function(err, body) {
                if (!Util.empty(err))
                    return _self.handleError(err);

                _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"];
                        // Need to unset Content-Length, because we'll handle that
                        // during figuring out the range
                        delete httpHeaders["Content-Length"];
                    }

                    //this.httpResponse.setHeaders(httpHeaders);

                    var range             = _self.getHTTPRange(),
                        ifRange           = _self.httpRequest.headers["if-range"],
                        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]) {
                            start = range[0];
                            end   = range[1] ? range[1] : nodeSize - 1;
                            if (start > nodeSize) {
                                return _self.handleError(new Exc.jsDAV_Exception_RequestedRangeNotSatisfiable(
                                    "The start offset (" + range[0] + ") exceeded the size of the entity ("
                                    + nodeSize + ")")
                                );
                            }

                            if (end < start) {
                                return _self.handleError(new Exc.jsDAV_Exception_RequestedRangeNotSatisfiable(
                                    "The end offset (" + range[1] + ") is lower than the start offset ("
                                    + range[0] + ")")
                                );
                            }
                            if (end > nodeSize)
                                end = nodeSize - 1;

                        }
                        else {
                            start = nodeSize - range[1];
                            end   = nodeSize - 1;
                            if (start < 0)
                                start = 0;
                        }

                        // New read/write stream
                        var offlen    = end - start + 1,
                            newStream = new Buffer(offlen);
                        body.copy(newStream, 0, start, offlen);

                        httpHeaders["Content-Length"] = offlen;
                        httpHeaders["Content-Range"]  = "bytes " + start + "-" + end + "/" + nodeSize;
                        _self.httpResponse.writeHead(206, httpHeaders);
                        _self.httpResponse.end(newStream);
                    }
                    else {
                        var since        = _self.httpRequest.headers["if-modified-since"],
                            oldEtag      = _self.httpRequest.headers["if-none-match"],
                            lastModified = httpHeaders["Last-Modified"],
                            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;
                            _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.server.tree.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, headers) {
                    if (!Util.empty(err))
                        return _self.handleError(err);
                    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(),
            _self = this;

        this.server.tree.getNodeForPath(uri, function(err, node) {
            if (!Util.empty(err))
                return _self.handleError(err);

            _self.dispatchEvent("beforeUnbind", uri, function(stop) {
                if (stop === true)
                    return;
                node["delete"](function(err) {
                    if (!Util.empty(err))
                        return _self.handleError(err);
                    _self.httpResponse.writeHead(204, {"Content-Length": "0"});
                    _self.httpResponse.end();
                });
            });
        });
    };

    /**
     * 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", function(err, data) {
            if (jsDAV.debugMode)
                console.log("INFO: 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
                try {
                    var path = _self.getRequestUri();
                }
                catch (ex) {
                    return _self.handleError(ex);
                }
                if (jsDAV.debugMode)
                    console.log("DEBUG: httpPropfind BEFORE getPropertiesForPath '" + path + "';", requestedProperties);
                _self.getPropertiesForPath(path, requestedProperties, depth, function(err, newProperties) {
                    if (!Util.empty(err))
                        return _self.handleError(err);
                    // This is a multi-status response
                    _self.httpResponse.writeHead(207, {"Content-Type": "application/xml; charset=utf-8"});
                    _self.httpResponse.end(_self.generateMultiStatus(newProperties));
                });
            });
        });
    };

    /**
     * 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", function(err, data) {
            if (jsDAV.debugMode)
                console.log("INFO: 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);
                    _self.httpResponse.writeHead(207, {"Content-Type": "application/xml; charset=utf-8"});
                    _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,
            uri   = this.getRequestUri();
            
        this.getRequestBody("binary", function(err, body, cleanup) {
            if (!Util.empty(err))
                return _self.handleError(err);

            // First we'll do a check to see if the resource already exists
            _self.server.tree.getNodeForPath(uri, function(err, node) {
                if (!Util.empty(err)) {
                    if (err instanceof Exc.jsDAV_Exception_FileNotFound) {
                        // If we got here, the resource didn't exist yet.
                        _self.createFile(uri, body, "binary", function() {
                            cleanup();
                            _self.httpResponse.writeHead(201, {"Content-Length": "0"});
                            _self.httpResponse.end();
                        });
                    }
                    else {
                        cleanup();
                        return _self.handleError(err);
                    }
                }
                else {
                    // Checking If-None-Match and related headers.
                    _self.checkPreconditions(false, function(err, redirected) {
                        if (!Util.empty(err)) {
                            cleanup();
                            return _self.handleError(err);
                        }
                        if (redirected) {
                            cleanup();
                            return false;
                        }
                        // If the node is a collection, we'll deny it
                        if (!node.hasFeature(jsDAV.__IFILE__)) {
                            cleanup();
                            return _self.handleError(new Exc.jsDAV_Exception_Conflict("PUT is not allowed on non-files."));
                        }

                        _self.dispatchEvent("beforeWriteContent", uri, function(stop) {
                            if (stop === true)
                                return cleanup();
                            node.put(body, "binary", function(err) {
                                cleanup();
                                if (!Util.empty(err))
                                    return _self.handleError(err);
                                _self.httpResponse.writeHead(200, {"Content-Length": "0"});
                                _self.httpResponse.end();
                            });
                        });
                    });
                }
            });
        });
    };

    /**
     * WebDAV MKCOL
     *
     * The MKCOL method is used to create a new collection (directory) on the server
     *
     * @return {void}
     */
    this.httpMkcol = function() {
        var resourceType,
            properties  = {},
            _self       = this,
            req         = this.httpRequest;

        this.getRequestBody("utf8", function(err, requestBody) {
            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.jsDAV_Exception_UnsupportedMediaType(
                        "The request body for the MKCOL request must have an xml Content-Type"));
                }

                Util.loadDOMDocument(requestBody, function(err, dom) {
                    var firstChild = dom.firstChild;
                    if (Util.toClarkNotation(firstChild) !== "{DAV:}mkcol") {
                        // We must throw 415 for unsupport mkcol bodies
                        return _self.handleError(new Exc.jsDAV_Exception_UnsupportedMediaType(
                            "The request body for the MKCOL request must be a {DAV:}mkcol request construct."));
                    }

                    var childNode,
                        i = 0,
                        c = firstChild.childNodes,
                        l = c.length;
                    for (; i < l; ++i) {
                        childNode = c[i];
                        if (Util.toClarkNotation(childNode) !== "{DAV:}set") continue;
                        properties = Util.extend(properties, Util.parseProperties(childNode, _self.propertyMap));
                    }
                    if (!properties["{DAV:}resourcetype"]) {
                        return _self.handleError(new Exc.jsDAV_Exception_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(Util.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 === true)
                        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 === true)
                        return false;
                    _self.dispatchEvent("beforeBind", moveInfo["destination"], function(stop) {
                        if (stop === true)
                            return false;
                        _self.server.tree.move(moveInfo["source"], moveInfo["destination"], function(err) {
                            if (!Util.empty(err))
                                return _self.handleError(err);

                            _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();
                        });
                    });
                });
            }
        });
    };

    /**
     * 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 === true)
                        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 === true)
                        return false;
                    _self.server.tree.copy(copyInfo["source"], copyInfo["destination"], function(err) {
                        if (!Util.empty(err))
                            return _self.handleError(err);
                        _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();
                    });
                });
            }
        });
    };

    /**
     * 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", function(err, data) {
            Util.loadDOMDocument(data, function(err, dom) {
                var reportName = Util.toClarkNotation(dom);
                _self.dispatchEvent("report", reportName, dom, function(stop) {
                    if (stop !== true) {
                        // If broadcastEvent returned true, it means the report was not supported
                        return _self.handleError(new Exc.jsDAV_Exception_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,
            methods = [
                "OPTIONS",
                "GET",
                "HEAD",
                "DELETE",
                "PROPFIND",
                "PUT",
                "PROPPATCH",
                "COPY",
                "MOVE",
                "REPORT"
            ];

        // The MKCOL is only allowed on an unmapped uri
        this.server.tree.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)
                    console.log("ERR: method getHTTPMethods() NOT implemented for plugin " + plugin);
                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.
     * 
     * @param  {Function} callback
     * @return {void}
     */
    this.getRequestBody = function(enc, cbreqbody) {
        var ctype,
            req      = this.httpRequest,
            data     = "",
            isStream = (!(ctype = req.headers["content-type"]) || !ctype.match(/(urlencoded|multipart)/i)),
            cleanup  = function() {},
            _self    = this;

        // 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(null, enc == "binary"
                ? req.$data 
                : req.$data.toString(enc), cleanup);
        }

        if (isStream) {
            var stream = [];
            
            req.streambuffer.ondata(function(data) {
                stream.push(data);
            });

            req.streambuffer.onend(function() {
                _self.$reading = false;
                req.$data = Util.concatBuffers(stream);
                readDone(null, enc == "binary"
                    ? req.$data 
                    : req.$data.toString(enc), cleanup);
            });
        }
        else {
            var formidable = require("./../../support/formidable/lib/formidable"),
                form       = new formidable.IncomingForm();
            form.uploadDir = this.server.tmpDir;

            form.addListener("file", function(field, file) {
                Fs.readFile(file.path, function(err, data) {
                    _self.$reading = false;
                    req.$data = data;
                    readDone(err, enc == "binary" ? data : data.toString(enc), function() {
                        Fs.unlink(file.path);
                    });
                });
            });

            form.addListener("error", callback);

            form.parse(req);
        }
        
        function readDone(err, data, cleanup) {
            cbreqbody(err, data, cleanup);
        }
    };

    /**
     * Calculates the uri for a request, making sure that the base uri is stripped out
     *
     * @param  {String} uri
     * @throws {jsDAV_Exception_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) != "/" && uri.indexOf("://") > -1)
            uri = Url.parse(uri).pathname;
        else if (uri.indexOf("?") > -1)
            uri = Url.parse(uri).pathname;
            
        uri = uri.replace("//", "/");
        
        if (uri.indexOf(this.server.baseUri) === 0) {
            return Util.trim(decodeURI(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 "";
        }
{'fraction_non_alphanumeric': 0.11350477479509738, 'fraction_numerical': 0.00926134797102539, 'mean_word_length': 1.326045765923335, 'pattern_counts': {'":': 144, '<': 45, '<?xml version=': 2, '>': 44, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 38}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 36, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4182025', 'n_tokens_mistral': 21319, 'n_tokens_neox': 19922, 'n_words': 4971}