jsdav-ext
Version:
jsDAV allows you to easily add WebDAV support to a NodeJS application. jsDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API.
1,289 lines (1,168 loc) • 106 kB
JavaScript
/*
* @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"];
// Need to unset Content-Length, because we'll handle that
// during figuring out the range
delete httpHeaders["content-length"];
}
var range = self.getHTTPRange();
var ifRange = self.httpRequest.headers["if-range"];
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);
});