Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

carddav almost ready...integration phase

  • Loading branch information...
commit 5902f973f99bf11879d9579d8d273e28c70ad2fa 1 parent 48de4c4
@mikedeboer authored
Showing with 1,747 additions and 843 deletions.
  1. +76 −0 examples/addressbookserver.js
  2. +16 −0 examples/fileserver.js
  3. +3 −3 lib/CardDAV/addressBook.js
  4. +1 −1  lib/CardDAV/addressBookRoot.js
  5. +0 −2  lib/CardDAV/backends/redis.js
  6. +3 −3 lib/CardDAV/card.js
  7. +19 −15 lib/CardDAV/plugin.js
  8. +3 −4 lib/CardDAV/userAddressBooks.js
  9. +7 −2 lib/DAV/handler.js
  10. +8 −1 lib/DAV/plugins/auth.js
  11. +5 −4 lib/DAV/plugins/auth/redis.js
  12. +7 −0 lib/DAV/plugins/browser.js
  13. +7 −0 lib/DAV/plugins/codesearch.js
  14. +7 −0 lib/DAV/plugins/filelist.js
  15. +7 −0 lib/DAV/plugins/filesearch.js
  16. +7 −0 lib/DAV/plugins/locks.js
  17. +7 −0 lib/DAV/plugins/mount.js
  18. +7 −0 lib/DAV/plugins/temporaryfilefilter.js
  19. +1 −1  lib/DAV/property/hrefList.js
  20. +32 −18 lib/DAV/server.js
  21. +5 −4 lib/DAV/simpleCollection.js
  22. +8 −14 lib/DAVACL/abstractPrincipalCollection.js
  23. +158 −196 lib/DAVACL/backends/redis.js
  24. +1,294 −539 lib/DAVACL/plugin.js
  25. +2 −2 lib/DAVACL/principal.js
  26. +1 −1  lib/DAVACL/principalCollection.js
  27. +14 −16 lib/DAVACL/property/acl.js
  28. +4 −4 lib/DAVACL/property/aclRestrictions.js
  29. +3 −3 lib/DAVACL/property/currentUserPrivilegeSet.js
  30. +4 −4 lib/DAVACL/property/principal.js
  31. +3 −3 lib/DAVACL/property/supportedPrivilegeSet.js
  32. +6 −3 lib/shared/asyncEvents.js
  33. +15 −0 lib/shared/db.js
  34. +7 −0 lib/shared/util.js
View
76 examples/addressbookserver.js
@@ -0,0 +1,76 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax.org>
+ * @author Mike de Boer <info AT mikedeboer DOT nl>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+"use strict";
+
+/*
+
+Addressbook/CardDAV server example
+
+This server features CardDAV support
+
+*/
+
+var jsDAV = require("./../lib/jsdav");
+jsDAV.debugMode = true;
+var jsDAV_Auth_Backend_Redis = require("./../lib/DAV/plugins/auth/redis");
+var jsDAVACL_PrincipalBackend_Redis = require("./../lib/DAVACL/backends/redis");
+var jsCardDAV_Backend_Redis = require("./../lib/CardDAV/backends/redis");
+// node classes:
+var jsDAVACL_PrincipalCollection = require("./../lib/DAVACL/principalCollection");
+var jsCardDAV_AddressBookRoot = require("./../lib/CardDAV/addressBookRoot");
+// plugins:
+var jsDAV_Auth_Plugin = require("./../lib/DAV/plugins/auth");
+var jsDAV_Browser_Plugin = require("./../lib/DAV/plugins/browser");
+var jsCardDAV_Plugin = require("./../lib/CardDAV/plugin");
+var jsDAVACL_Plugin = require("./../lib/DAVACL/plugin");
+
+var Db = require("./../lib/shared/db");
+
+// Make sure this setting is turned on and reflect the root url for your WebDAV server.
+// This can be for example the root / or a complete path to your server script
+var baseUri = "/";
+
+/* Database */
+var redis = Db.redisConnection();
+// set it up for demo use:
+redis.multi([
+ ["FLUSHDB"],
+ // create user admin. NOTE: if you change the real to something other than 'jsDAV',
+ // you need to change the hash below here to: md5("<username>:<realm>:<password>").
+ ["SET", "users/admin", "6838d8a7454372f68a6abffbdb58911c"],
+ // create the initial ACL rules for user 'admin'
+ ["HMSET", "principals/principals/admin", "email", "admin@example.org", "displayname", "Administrator"],
+ ["HMSET", "principals/principals/admin/calendar-proxy-read", "email", "", "displayname", ""],
+ ["HMSET", "principals/principals/admin/calendar-proxy-write", "email", "", "displayname", ""],
+ // create the first addressbook
+ ["HMSET", "addressbooks/principals/admin", "displayname", "default calendar", "uri", "default", "description", "", "ctag", "1"]
+]).exec(function(err) {
+ if (err)
+ throw(err);
+
+ // Backends
+ var authBackend = jsDAV_Auth_Backend_Redis.new(redis);
+ var principalBackend = jsDAVACL_PrincipalBackend_Redis.new(redis);
+ var carddavBackend = jsCardDAV_Backend_Redis.new(redis);
+ //var caldavBackend = jsCalDAV_Backend_Redis(redis);
+
+ // Setting up the directory tree //
+ var nodes = [
+ jsDAVACL_PrincipalCollection.new(principalBackend),
+ //jsCalDAV_CalendarRootNode.new(authBackend, caldavBackend),
+ jsCardDAV_AddressBookRoot.new(principalBackend, carddavBackend),
+ ];
+
+ jsDAV.createServer({
+ node: nodes,
+ baseUri: baseUri,
+ authBackend: authBackend,
+ realm: "jsDAV",
+ plugins: [jsDAV_Auth_Plugin, jsDAV_Browser_Plugin, jsCardDAV_Plugin, jsDAVACL_Plugin]
+ }, 8000);
+});
View
16 examples/fileserver.js
@@ -0,0 +1,16 @@
+/*
+ * @package jsDAV
+ * @subpackage DAV
+ * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax.org>
+ * @author Mike de Boer <info AT mikedeboer DOT nl>
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
+ */
+"use strict";
+
+var jsDAV = require("./../lib/jsdav");
+var jsDAV_Locks_Backend_FS = require("./../lib/DAV/plugins/locks/fs");
+
+jsDAV.createServer({
+ node: __dirname + "/../test/assets",
+ locksBackend: jsDAV_Locks_Backend_FS.new(__dirname + "/../test/assets")
+}, 8000);
View
6 lib/CardDAV/addressBook.js
@@ -239,8 +239,8 @@ var jsCardDAV_AddressBook = module.exports = jsDAV_Collection.extend(jsCardDAV_i
*
* @return array
*/
- getACL: function(callback) {
- return callback(null, [
+ getACL: function() {
+ return [
{
"privilege" : "{DAV:}read",
"principal" : this.addressBookInfo.principaluri,
@@ -251,7 +251,7 @@ var jsCardDAV_AddressBook = module.exports = jsDAV_Collection.extend(jsCardDAV_i
"principal" : this.addressBookInfo.principaluri,
"protected" : true
}
- ]);
+ ];
},
/**
View
2  lib/CardDAV/addressBookRoot.js
@@ -70,6 +70,6 @@ var jsCardDAV_AddressBookRoot = module.exports = jsDAVACL_AbstractPrincipalColle
* @return jsDAV_iNode
*/
getChildForPrincipal: function(principal) {
- return jsCardDAV_UserAddressBooks(this.carddavBackend, principal.uri);
+ return jsCardDAV_UserAddressBooks.new(this.carddavBackend, principal.uri);
}
});
View
2  lib/CardDAV/backends/redis.js
@@ -15,8 +15,6 @@ var Db = require("./../../shared/db");
var Exc = require("./../../shared/exceptions");
var Util = require("./../../shared/util");
-var Redis = require("redis");
-
/**
* Redis CardDAV backend
*
View
6 lib/CardDAV/card.js
@@ -193,8 +193,8 @@ var jsCardDAV_Card = module.exports = jsDAV_File.extend(jsCardDAV_iCard, jsDAVAC
*
* @return array
*/
- getACL: function(callback) {
- return callback(null, [
+ getACL: function() {
+ return [
{
"privilege" : "{DAV:}read",
"principal" : this.addressBookInfo.principaluri,
@@ -205,7 +205,7 @@ var jsCardDAV_Card = module.exports = jsDAV_File.extend(jsCardDAV_iCard, jsDAVAC
"principal" : this.addressBookInfo.principaluri,
"protected" : true
}
- ]);
+ ];
},
/**
View
34 lib/CardDAV/plugin.js
@@ -19,6 +19,7 @@ var jsCardDAV_AddressBookQueryParser = require("./addressBookQueryParser");
var jsDAVACL_iPrincipal = require("./../DAVACL/interfaces/iPrincipal");
var jsVObject_Reader = require("./../VObject/reader");
+var AsyncEventEmitter = require("./../shared/asyncEvents").EventEmitter;
var Exc = require("./../shared/exceptions");
var Util = require("./../shared/util");
var Xml = require("./../shared/xml");
@@ -32,6 +33,13 @@ var Async = require("asyncjs");
*/
var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
/**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "carddav",
+
+ /**
* Url to the addressbooks
*/
ADDRESSBOOK_ROOT: "addressbooks",
@@ -70,8 +78,8 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
handler.addEventListener("afterGetProperties", this.afterGetProperties.bind(this));
handler.addEventListener("updateProperties", this.updateProperties.bind(this));
handler.addEventListener("report", this.report.bind(this));
- handler.addEventListener("onHTMLActionsPanel", this.htmlActionsPanel.bind(this));
- handler.addEventListener("onBrowserPostAction", this.browserPostAction.bind(this));
+ handler.addEventListener("onHTMLActionsPanel", this.htmlActionsPanel.bind(this), AsyncEventEmitter.PRIO_HIGH);
+ handler.addEventListener("onBrowserPostAction", this.browserPostAction.bind(this), AsyncEventEmitter.PRIO_HIGH);
handler.addEventListener("beforeWriteContent", this.beforeWriteContent.bind(this));
handler.addEventListener("beforeCreateFile", this.beforeCreateFile.bind(this));
@@ -146,18 +154,16 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
if (node.hasFeature(jsDAVACL_iPrincipal)) {
// calendar-home-set property
var addHome = "{" + this.NS_CARDDAV + "}addressbook-home-set";
- var addHomeIdx = requestedProperties.indexOf(addHome);
- if (addHomeIdx > -1) {
+ if (requestedProperties[addHome]) {
var principalId = node.getName();
var addressbookHomePath = this.ADDRESSBOOK_ROOT + "/" + principalId + "/";
- requestedProperties.splice(addHomeIdx, 1);
- returnedProperties["200"][addHome] = new jsDAV_Property_Href.new(addressbookHomePath);
+ delete requestedProperties[addHome];
+ returnedProperties["200"][addHome] = jsDAV_Property_Href.new(addressbookHomePath);
}
var directories = "{" + this.NS_CARDDAV + "}directory-gateway";
- var dirIdx = requestedProperties.indexOf(directories);
- if (this.directories && dirIdx > -1) {
- requestedProperties.splice(dirIdx, 1);
+ if (this.directories && requestedProperties[directories]) {
+ delete requestedProperties[directories];
returnedProperties["200"][directories] = jsDAV_Property_HrefList.new(this.directories);
}
@@ -169,8 +175,8 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
// Therefore we simply expose it as a property.
var addressDataProp = "{" + this.NS_CARDDAV + "}address-data";
var addressIdx = requestedProperties.indexOf(addressDataProp)
- if (addressIdx > -1) {
- requestedProperties.splice(addressIdx, 1);
+ if (requestedProperties[addressDataProp]) {
+ delete requestedProperties[addressDataProp];
node.get(function(err, val) {
if (err)
return e.next(err);
@@ -187,8 +193,7 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
function afterICard() {
if (node.hasFeature(jsCardDAV_UserAddressBooks)) {
var meCardProp = "{http://calendarserver.org/ns/}me-card";
- var propIdx = requestedProperties.indexOf(meCardProp);
- if (propIdx > -1) {
+ if (requestedProperties[meCardProp]) {
self.handler.getProperties(node.getOwner(), ["{http://sabredav.org/ns}vcard-url"], function(err, props) {
if (err)
return e.next(err);
@@ -197,7 +202,7 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
returnedProperties["200"][meCardProp] = jsDAV_Property_Href.new(
props["{http://sabredav.org/ns}vcard-url"]
);
- requestedProperties.splice(propIdx, 1);
+ delete requestedProperties[meCardProp];
}
e.next();
});
@@ -416,7 +421,6 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
query.parse();
}
catch(ex) {
- console.log("QUERY PARSE ERROR:",ex);
return e.next(ex);
}
View
7 lib/CardDAV/userAddressBooks.js
@@ -196,9 +196,8 @@ var jsCardDAV_UserAddressBooks = module.exports = jsDAV_Collection.extend(jsDAV_
*
* @return array
*/
- getACL: function(callback) {
-
- callback(null, [
+ getACL: function() {
+ return [
{
privilege: "{DAV:}read",
principal: this.principalUri,
@@ -209,7 +208,7 @@ var jsCardDAV_UserAddressBooks = module.exports = jsDAV_Collection.extend(jsDAV_
principal: this.principalUri,
"protected": true
}
- ]);
+ ];
},
/**
View
9 lib/DAV/handler.js
@@ -306,6 +306,7 @@ jsDAV_Handler.STATUS_MAP = {
if (jsDAV.debugMode) {
Util.log(e, "error");
+ console.log(e.stack);
//throw e; // DEBUGGING!
}
}
@@ -1639,6 +1640,8 @@ jsDAV_Handler.STATUS_MAP = {
if (!Util.empty(err))
return cbgetpropspath(err);
for (var i = 0, l = cNodes.length; i < l; ++i) {
+ if (!cNodes[i].path)
+ continue;
nodes[cNodes[i].path] = cNodes[i];
nodesPath.push(cNodes[i].path);
}
@@ -1733,10 +1736,12 @@ jsDAV_Handler.STATUS_MAP = {
removeRT = true;
}
- self.dispatchEvent("beforeGetProperties", myPath, node, propertyNames, newProperties, function(stop) {
+ var propertyMap = Util.arrayToMap(propertyNames);
+ self.dispatchEvent("beforeGetProperties", myPath, node, propertyMap, newProperties, function(stop) {
if (stop === true)
- return cbnextpfp();
+ return cbnextpfp();
+ propertyNames = Object.keys(propertyMap);
if (node.hasFeature(jsDAV_iProperties)) {
node.getProperties(propertyNames, function(err, props) {
if (!err && props)
View
9 lib/DAV/plugins/auth.js
@@ -22,6 +22,13 @@ var AsyncEventEmitter = require("./../../shared/asyncEvents").EventEmitter;
*/
var jsDAV_Auth_Plugin = module.exports = jsDAV_ServerPlugin.extend({
/**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "auth",
+
+ /**
* Authentication backend
*
* @var jsDAV_Auth_Backend_Abstract
@@ -38,7 +45,7 @@ var jsDAV_Auth_Plugin = module.exports = jsDAV_ServerPlugin.extend({
initialize: function(handler) {
this.handler = handler;
this.authBackend = handler.server.options.authBackend || null;
- this.realm = handler.server.options.realm || "jsdav";
+ this.realm = handler.server.options.realm || "jsDAV";
handler.addEventListener("beforeMethod", this.beforeMethod.bind(this), AsyncEventEmitter.PRIO_HIGH);
},
View
9 lib/DAV/plugins/auth/redis.js
@@ -13,9 +13,10 @@ var jsDAV_Auth_Backend_AbstractDigest = require("./abstractDigest");
* This is an authentication backend that uses a redis database to manage passwords.
*/
var jsDAV_Auth_Backend_Redis = module.exports = jsDAV_Auth_Backend_AbstractDigest.extend({
- initialize: function(redisClient) {
+ initialize: function(authBackend, tableName) {
jsDAV_Auth_Backend_AbstractDigest.initialize.call(this);
- this.redisClient = redisClient;
+ this.authBackend = authBackend;
+ this.tableName = tableName || "users"
},
/**
@@ -26,10 +27,10 @@ var jsDAV_Auth_Backend_Redis = module.exports = jsDAV_Auth_Backend_AbstractDiges
* @return {string}
*/
getDigestHash: function(realm, username, cbdigest) {
- this.redisClient.get("u/" + realm + "/" + username, function(err, A1) {
+ this.authBackend.get(this.tableName + "/" + username, function(err, A1) {
if (err)
return cbdigest(err);
- cbdigest(null, A1.toString());
+ cbdigest(null, A1 && A1.toString());
});
}
});
View
7 lib/DAV/plugins/browser.js
@@ -38,6 +38,13 @@ var Formidable = require("formidable");
*/
var jsDAV_Browser_Plugin = module.exports = jsDAV_ServerPlugin.extend({
/**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "browser",
+
+ /**
* List of default icons for nodes.
*
* This is an array with class / interface names as keys, and asset names
View
7 lib/DAV/plugins/codesearch.js
@@ -117,6 +117,13 @@ var GREP_CMD = GnuTools.GREP_CMD;
var PERL_CMD = "perl";
var jsDAV_Codesearch_Plugin = module.exports = jsDAV_ServerPlugin.extend({
+ /**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "codesearch",
+
IGNORE_DIRS: IGNORE_DIRS,
MAPPINGS: MAPPINGS,
PATTERN_EXT: PATTERN_EXT,
View
7 lib/DAV/plugins/filelist.js
@@ -16,6 +16,13 @@ var GnuTools = require("gnu-tools");
var platform = require("os").platform();
var jsDAV_Filelist_Plugin = module.exports = jsDAV_ServerPlugin.extend({
+ /**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "filelist",
+
FIND_CMD: GnuTools.FIND_CMD,
initialize: function(handler) {
View
7 lib/DAV/plugins/filesearch.js
@@ -17,6 +17,13 @@ var Xml = require("./../../shared/xml");
var GnuTools = require("gnu-tools");
var jsDAV_Filesearch_Plugin = module.exports = jsDAV_ServerPlugin.extend({
+ /**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "filesearch",
+
FIND_CMD: GnuTools.FIND_CMD,
initialize: function(handler) {
View
7 lib/DAV/plugins/locks.js
@@ -19,6 +19,13 @@ var Util = require("./../../shared/util");
var Xml = require("./../../shared/xml");
var jsDAV_Locks_Plugin = module.exports = jsDAV_ServerPlugin.extend({
+ /**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "locks",
+
initialize: function(handler) {
this.handler = handler;
//this.locksBackend = locksBackend;
View
7 lib/DAV/plugins/mount.js
@@ -17,6 +17,13 @@ var Xml = require("./../../shared/xml");
* Simply append ?mount to any collection to generate the davmount response.
*/
var jsDAV_Mount_Plugin = module.exports = jsDAV_ServerPlugin.extend({
+ /**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "mount",
+
initialize: function(handler) {
this.handler = handler;
this.handler.addEventListener("beforeMethod", this.beforeMethod.bind(this));
View
7 lib/DAV/plugins/temporaryfilefilter.js
@@ -39,6 +39,13 @@ var Xml = require("./../../shared/xml");
*/
var jsDAV_TemporaryFileFilter_Plugin = module.exports = jsDAV_ServerPlugin.extend({
/**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "temporaryfilefilter",
+
+ /**
* This is the list of patterns we intercept.
* If new patterns are added, they must be valid patterns for preg_match.
*
View
2  lib/DAV/property/hrefList.js
@@ -7,7 +7,7 @@
*/
"use strict";
-var jsDAV_Property = require("./../interfaces/property");
+var jsDAV_Property = require("./../property");
var Xml = require("./../../shared/xml");
View
50 lib/DAV/server.js
@@ -44,8 +44,9 @@ exports.DEFAULT_TMPDIR = (function() {
exports.DEFAULT_PLUGINS = {};
Fs.readdirSync(__dirname + "/plugins").forEach(function(filename){
if (/\.js$/.test(filename)) {
- var name = filename.substr(0, filename.lastIndexOf('.'));
- exports.DEFAULT_PLUGINS[name] = require("./plugins/" + name);
+ var name = filename.substr(0, filename.lastIndexOf("."));
+ var pluginCls = require("./plugins/" + name);
+ exports.DEFAULT_PLUGINS[pluginCls.name || name] = pluginCls;
}
});
@@ -71,35 +72,41 @@ function Server(options) {
if (typeof options.standalone == "undefined")
options.standalone = true;
- this.plugins = Util.extend({}, exports.DEFAULT_PLUGINS);
-
if (options.plugins) {
- if (!Array.isArray(options.plugins))
- options.plugins = Object.keys(options.plugins);
- var allPlugins = Object.keys(this.plugins);
- for (var i = 0, l = allPlugins.length; i < l; ++i) {
- // if the plugin is not in the list options.plugins, remove it from
- // the available plugins altogether so that the handler won't know
- // they exist.
- if (options.plugins.indexOf(allPlugins[i]) == -1)
- delete this.plugins[allPlugins[i]];
+ var i, l;
+ var plugins = {};
+ if (Array.isArray(options.plugins)) {
+ for (i = 0, l = options.plugins.length; i < l; ++i) {
+ if (!options.plugins[i] || !options.plugins[i].name)
+ continue;
+ plugins[options.plugins[i].name] = options.plugins[i];
+ }
}
+ else
+ Util.extend(plugins, options.plugins);
+
+ this.plugins = plugins;
}
+ else
+ this.plugins = Util.extend({}, exports.DEFAULT_PLUGINS);
var root;
// setup the filesystem tree for this server instance.
if (typeof options.type == "string") {
var TreeClass = require("./backends/" + options.type + "/tree");
this.tree = TreeClass.new(options);
+ if (options.baseUri)
+ this.setBaseUri(options.baseUri);
}
else if (typeof options.node == "string" && options.node.indexOf("/") > -1) {
this.tree = jsDAV_Tree_Filesystem.new(options.node, options);
+ if (options.baseUri)
+ this.setBaseUri(options.baseUri);
}
else if (options.tree && options.tree.hasFeature(jsDAV_Tree)) {
this.tree = options.tree;
- }
- else if (options.node && options.node.hasFeature(jsDAV_iNode)) {
- this.tree = jsDAV_ObjectTree.new(options.node, options);
+ if (options.baseUri)
+ this.setBaseUri(options.baseUri);
}
else if (options.node && Array.isArray(options.node)) {
// If it's an array, a list of nodes was passed, and we need to
@@ -109,8 +116,15 @@ function Server(options) {
throw new Error("Invalid argument passed to constructor. If you're passing an array, all the values must implement jsDAV_iNode");
});
- root = new jsDAV_SimpleCollection("root", options.node);
- this.tree = new jsDAV_ObjectTree(root);
+ root = jsDAV_SimpleCollection.new("root", options.node);
+ this.tree = jsDAV_ObjectTree.new(root);
+ if (options.baseUri)
+ this.setBaseUri(options.baseUri);
+ }
+ else if (options.node && options.node.hasFeature(jsDAV_iNode)) {
+ this.tree = jsDAV_ObjectTree.new(options.node, options);
+ if (options.baseUri)
+ this.setBaseUri(options.baseUri);
}
else {
if (exports.debugMode) {
View
9 lib/DAV/simpleCollection.js
@@ -28,7 +28,7 @@ var Exc = require("./../shared/exceptions");
*/
var jsDAV_SimpleCollection = module.exports = jsDAV_Collection.extend({
initialize: function(name, children) {
- children = children || [];
+ children = children || {};
this.name = name;
for (var child, i = 0, l = children.length; i < l; ++i) {
child = children[i];
@@ -94,9 +94,10 @@ var jsDAV_SimpleCollection = module.exports = jsDAV_Collection.extend({
* @return array
*/
getChildren: function(callback) {
- var childlist = [];
- for (var i in this.children)
- childlist.push(this.children[i]);
+ var self = this;
+ var childlist = Object.keys(this.children).map(function(name) {
+ return self.children[name];
+ });
callback(null, childlist);
}
View
22 lib/DAVACL/abstractPrincipalCollection.js
@@ -8,7 +8,7 @@
"use strict";
var jsDAV_Collection = require("./../DAV/collection");
-var jsDAVACL_iPrincipalCollection = require("./interfaces/iPricipalCollection");
+var jsDAVACL_iPrincipalCollection = require("./interfaces/iPrincipalCollection");
var Util = require("./../shared/util");
var Exc = require("./../shared/exceptions");
@@ -74,7 +74,7 @@ var jsDAVACL_AbstractPrincipalCollection = module.exports = jsDAV_Collection.ext
* @param Function callback
* @return jsDAV_iPrincipal
*/
- getChildForPrincipal: function(principalInfo, callback) {},
+ getChildForPrincipal: function(principalInfo) {},
/**
* Returns the name of this collection.
@@ -101,17 +101,11 @@ var jsDAVACL_AbstractPrincipalCollection = module.exports = jsDAV_Collection.ext
if (err)
return callback(err);
- Async.list(principals)
- .each(function(principalInfo, next) {
- self.getChildForPrincipal(principalInfo, function(err, child) {
- if (err)
- return next(err);
- children.push(child);
- });
- })
- .end(function(err) {
- callback(err, children);
- });
+ console.log("PRINCIPALS:",JSON.stringify(principals));
+ var children = principals.map(function(principalInfo) {
+ return self.getChildForPrincipal(principalInfo);
+ });
+ callback(null, children);
});
},
@@ -128,7 +122,7 @@ var jsDAVACL_AbstractPrincipalCollection = module.exports = jsDAV_Collection.ext
this.principalBackend.getPrincipalByPath(this.principalPrefix + "/" + name, function(err, principalInfo) {
if (!principalInfo)
return callback(new Exc.NotFound("Principal with name " + name + " not found"));
- self.getChildForPrincipal(principalInfo, callback);
+ callback(null, self.getChildForPrincipal(principalInfo));
});
},
View
354 lib/DAVACL/backends/redis.js
@@ -13,8 +13,6 @@ var Db = require("./../../shared/db");
var Exc = require("./../../shared/exceptions");
var Util = require("./../../shared/util");
-var Redis = require("redis");
-
/**
* Redis principal backend
*
@@ -53,28 +51,22 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
/**
* This property can be used to display the users' real name.
*/
- "{DAV:}displayname": {
- "dbField": "displayname"
- },
+ "{DAV:}displayname": "displayname",
/**
* This property is actually used by the CardDAV plugin, where it gets
- * mapped to {http://calendarserver.orgi/ns/}me-card.
+ * mapped to {http://calendarserver.org/ns/}me-card.
*
* The reason we don't straight-up use that property, is because
* me-card is defined as a property on the users' addressbook
* collection.
*/
- "{http://sabredav.org/ns}vcard-url": {
- "dbField": "vcardurl"
- },
+ "{http://ajax.org/2005/aml}vcard-url": "vcardurl",
/**
* This is the users' primary email-address.
*/
- "{http://ajax.org/2005/aml}email-address": {
- "dbField": "email"
- },
+ "{http://ajax.org/2005/aml}email-address": "email",
},
/**
@@ -86,8 +78,12 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
*/
initialize: function(redis, tableName, groupMembersTableName) {
this.redis = redis;
- this.tableName = tableName || "pricipals";
+ this.tableName = tableName || "principals";
this.groupMembersTableName = groupMembersTableName || "groupmembers";
+
+ this.fieldMapReverse = {};
+ for (var prop in this.fieldMap)
+ this.fieldMapReverse[this.fieldMap[prop]] = prop;
},
/**
@@ -106,39 +102,41 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param string prefixPath
* @return array
*/
- getPrincipalsByPrefix: function(prefixPath) {
- var fields = ["uri"];
-
- for (var prop in this.fieldMap)
- fields.push(this.fieldMap[prop].dbField);
-
- command = [this.tableName]
+ getPrincipalsByPrefix: function(prefixPath, callback) {
+ var fields = Object.keys(this.fieldMapReverse);
+ var self = this;
- this.redis.hmget.apply(this.redis, command);
- result = this.pdo.query("SELECT ".implode(",", fields)." FROM ". this.tableName);
-
- principals = array();
-
- while(row = result.fetch(\PDO::FETCH_ASSOC)) {
-
- // Checking if the principal is in the prefix
- list(rowPrefix) = DAV\URLUtil::splitPath(row["uri"]);
- if (rowPrefix !== prefixPath) continue;
-
- principal = array(
- "uri": row["uri"],
- );
- foreach(this.fieldMap as key=>value) {
- if (row[value["dbField"]]) {
- principal[key] = row[value["dbField"]];
- }
- }
- principals[] = principal;
-
- }
-
- return principals;
-
+ this.redis.keys(this.tableName + "/" + prefixPath + "*", function(err, res) {
+ if (err)
+ return callback(err);
+
+ var keys = Db.fromMultiBulk(res);
+ var commands = keys.map(function(key) {
+ return ["HMGET", key].concat(fields);
+ });
+ if (!commands.length)
+ return callback();
+
+ self.redis.multi(commands).exec(function(err, res) {
+ if (err)
+ return callback(err);
+
+ var strip = (self.tableName + "/").length
+ var principals = Db.fromMultiBulk(res).map(function(row, idx) {
+ var obj = {
+ uri: keys[idx].substr(strip)
+ };
+ for (var i = 1, l = fields.length; i < l; ++i) {
+ if (!row[i])
+ continue;
+ // put it back like 'obj["{DAV:}displayname"] = val'
+ obj[self.fieldMapReverse[fields[i]]] = row[i];
+ }
+ return obj;
+ });
+ callback(null, principals);
+ });
+ });
},
/**
@@ -149,33 +147,31 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param string path
* @return array
*/
- getPrincipalByPath: function(path) {
-
- fields = array(
- "id",
- "uri",
- );
-
- foreach(this.fieldMap as key=>value) {
- fields[] = value["dbField"];
- }
- stmt = this.pdo.prepare("SELECT ".implode(",", fields)." FROM ". this.tableName . " WHERE uri = ?");
- stmt.execute(array(path));
-
- row = stmt.fetch(\PDO::FETCH_ASSOC);
- if (!row) return;
-
- principal = array(
- "id" : row["id"],
- "uri": row["uri"],
- );
- foreach(this.fieldMap as key=>value) {
- if (row[value["dbField"]]) {
- principal[key] = row[value["dbField"]];
+ getPrincipalByPath: function(path, callback) {
+ var fields = ["uri"].concat(Object.keys(this.fieldMapReverse));
+ var self = this;
+
+ var command = [this.tableName + "/" + path].concat(fields);
+ command.push(function(err, res) {
+ if (err)
+ return callback(err);
+
+ res = Db.fromMultiBulk(res);
+ if (!res)
+ return callback();
+
+ var principal = {
+ uri: path
+ };
+ for (var i = 1, l = fields.length; i < l; ++i) {
+ if (!res[i])
+ continue;
+ // put it back like 'principal["{DAV:}displayname"] = val'
+ principal[self.fieldMapReverse[fields[i]]] = res[i];
}
- }
- return principal;
-
+ callback(null, principal);
+ });
+ this.redis.hmget.apply(this.redis, command);
},
/**
@@ -226,50 +222,41 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param array mutations
* @return array|bool
*/
- updatePrincipal: function(path, mutations) {
- updateAble = array();
- foreach(mutations as key=>value) {
-
+ updatePrincipal: function(path, mutations, callback) {
+ var updateAble = {};
+ var key, value, forbidden, failedDep, subKey;
+ for (key in mutations) {
+ value = mutations[key];
// We are not aware of this field, we must fail.
- if (!isset(this.fieldMap[key])) {
-
- response = array(
- 403: array(
- key: null,
- ),
- 424: array(),
- );
-
+ if (!this.fieldMap[key]) {
+ forbidden = {};
+ forbidden[key] = null;
+
+ failedDep = {};
// Adding the rest to the response as a 424
- foreach(mutations as subKey=>subValue) {
- if (subKey !== key) {
- response[424][subKey] = null;
- }
+ for (subKey in mutations) {
+ if (subKey !== key)
+ failedDep[subKey] = null;
}
- return response;
+ return callback({
+ "403": forbidden,
+ "424": failedDep
+ }, false);
}
- updateAble[this.fieldMap[key]["dbField"]] = value;
-
- }
-
- // No fields to update
- query = "UPDATE " . this.tableName . " SET ";
-
- first = true;
- foreach(updateAble as key: value) {
- if (!first) {
- query.= ", ";
- }
- first = false;
- query.= "key = :key ";
+ updateAble[this.fieldMap[key]] = value;
}
- query.="WHERE uri = :uri";
- stmt = this.pdo.prepare(query);
- updateAble["uri"] = path;
- stmt.execute(updateAble);
- return true;
+ var command = [this.tableName + "/" + path];
+ for (key in updateAble)
+ command.push(key, updateAble[key]);
+ command.push(function(err) {
+ if (err)
+ return callback(err, false);
+ callback(null, true);
+ });
+
+ this.redis.hmset.apply(this.redis, command);
},
/**
@@ -300,43 +287,9 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param array searchProperties
* @return array
*/
- searchPrincipals: function(prefixPath, array searchProperties) {
- query = "SELECT uri FROM " . this.tableName . " WHERE 1=1 ";
- values = array();
- foreach(searchProperties as property: value) {
-
- switch(property) {
-
- case "{DAV:}displayname" :
- query.=" AND displayname LIKE ?";
- values[] = "%" . value . "%";
- break;
- case "{http://ajax.org/ns}email-address" :
- query.=" AND email LIKE ?";
- values[] = "%" . value . "%";
- break;
- default :
- // Unsupported property
- return array();
-
- }
-
- }
- stmt = this.pdo.prepare(query);
- stmt.execute(values);
-
- principals = array();
- while(row = stmt.fetch(\PDO::FETCH_ASSOC)) {
-
- // Checking if the principal is in the prefix
- list(rowPrefix) = DAV\URLUtil::splitPath(row["uri"]);
- if (rowPrefix !== prefixPath) continue;
-
- principals[] = row["uri"];
-
- }
-
- return principals;
+ searchPrincipals: function(prefixPath, searchProperties, callback) {
+ // TODO: support search LATER
+ callback(null, []);
},
/**
@@ -345,18 +298,21 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param string principal
* @return array
*/
- getGroupMemberSet: function(principal) {
- principal = this.getPrincipalByPath(principal);
- if (!principal) throw new DAV\Exception("Principal not found");
-
- stmt = this.pdo.prepare("SELECT principals.uri as uri FROM ".this.groupMembersTableName." AS groupmembers LEFT JOIN ".this.tableName." AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?");
- stmt.execute(array(principal["id"]));
-
- result = array();
- while (row = stmt.fetch(\PDO::FETCH_ASSOC)) {
- result[] = row["uri"];
- }
- return result;
+ getGroupMemberSet: function(principal, callback) {
+ var self = this;
+ this.getPrincipalByPath(principal, function(err, principal) {
+ if (err)
+ return callback(err);
+ if (!principal)
+ return callback(new Exc.jsDAV_Exception("Principal not found"));
+
+ self.redis.zrange(self.groupMembersTableName + "/" + principal, 0, -1, function(err, res) {
+ if (err)
+ return err;
+
+ callback(null, Db.formMultiBulk(res));
+ });
+ });
},
/**
@@ -365,18 +321,21 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param string principal
* @return array
*/
- getGroupMembership: function(principal) {
- principal = this.getPrincipalByPath(principal);
- if (!principal) throw new DAV\Exception("Principal not found");
-
- stmt = this.pdo.prepare("SELECT principals.uri as uri FROM ".this.groupMembersTableName." AS groupmembers LEFT JOIN ".this.tableName." AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?");
- stmt.execute(array(principal["id"]));
-
- result = array();
- while (row = stmt.fetch(\PDO::FETCH_ASSOC)) {
- result[] = row["uri"];
- }
- return result;
+ getGroupMemberShip: function(principal, callback) {
+ var self = this;
+ this.getPrincipalByPath(principal, function(err, principal) {
+ if (err)
+ return callback(err);
+ if (!principal)
+ return callback(new Exc.jsDAV_Exception("Principal not found"));
+
+ self.redis.zrange(self.tableName + "/" + principal + "/" + self.groupMembersTableName, 0, -1, function(err, res) {
+ if (err)
+ return callback(err);
+
+ callback(null, Db.fromMultiBulk(res));
+ });
+ });
},
/**
@@ -388,32 +347,35 @@ var jsDAVACL_Backend_Redis = module.exports = jsDAVACL_iBackend.extend({
* @param array members
* @return void
*/
- setGroupMemberSet: function(principal, array members) {
- // Grabbing the list of principal id's.
- stmt = this.pdo.prepare("SELECT id, uri FROM ".this.tableName." WHERE uri IN (? " . str_repeat(", ? ", count(members)) . ");");
- stmt.execute(array_merge(array(principal), members));
-
- memberIds = array();
- principalId = null;
-
- while(row = stmt.fetch(\PDO::FETCH_ASSOC)) {
- if (row["uri"] == principal) {
- principalId = row["id"];
- } else {
- memberIds[] = row["id"];
- }
- }
- if (!principalId) throw new DAV\Exception("Principal not found");
-
- // Wiping out old members
- stmt = this.pdo.prepare("DELETE FROM ".this.groupMembersTableName." WHERE principal_id = ?;");
- stmt.execute(array(principalId));
-
- foreach(memberIds as memberId) {
-
- stmt = this.pdo.prepare("INSERT INTO ".this.groupMembersTableName." (principal_id, member_id) VALUES (?, ?);");
- stmt.execute(array(principalId, memberId));
-
- }
+ setGroupMemberSet: function(principal, members, callback) {
+ var self = this;
+ // Fetch all current members from the group principal, so we may wipe 'em
+ this.redis.zrange(this.groupMembersTableName + "/" + principal, 0, -1, function(err, res) {
+ if (err)
+ return callback(err);
+
+ res = Db.fromMultiBulk(res);
+ var commands = [
+ ["DEL", self.groupMembersTableName + "/" + principal]
+ ];
+ res.forEach(function(member) {
+ commands.push(["ZREM", self.tableName + "/" + member + "/" + self.groupMembersTableName, principal]);
+ });
+ self.redis.multi(commands).exec(function(err) {
+ if (err)
+ return callback(err);
+
+ // now add each member from the new set to the group principal
+ commands = [];
+ members.forEach(function(member) {
+ var now = Date.now();
+ commands.push(
+ ["ZADD", self.tableName + "/" + member + "/" + self.groupMembersTableName, now, principal],
+ ["ZADD", self.groupMembersTableName + "/" + principal, now, member]
+ );
+ });
+ self.redis.multi(commands).exec(callback);
+ });
+ });
}
});
View
1,833 lib/DAVACL/plugin.js
@@ -1,108 +1,217 @@
/*
* @package jsDAV
- * @subpackage CardDAV
+ * @subpackage DAVACL
* @copyright Copyright(c) 2013 Mike de Boer. <info AT mikedeboer DOT nl>
* @author Mike de Boer <info AT mikedeboer DOT nl>
* @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
*/
"use strict";
-var jsDAV_Plugin = require("./../DAV/plugin");
-var jsCardDAV_iAddressBook = require("./interfaces/iAddressBook");
-var jsCardDAV_iDirectory = require("./interfaces/iDirectory");
-var jsCardDAV_iCard = require("./interfaces/iCard");
-var jsCardDAV_UserAddressBooks = require("./userAddressBooks");
-var jsCardDAV_AddressBookQueryParser = require("./addressBookQueryParser");
-var jsDAVACL_iPrincipal = require("./../DAVACL/interfaces/iPrincipal");
-var jsDAV_Property_iHref = require("./../DAV/interfaces/iProperty");
+var jsDAV_ServerPlugin = require("./../DAV/plugin");
var jsDAV_Property_Href = require("./../DAV/property/href");
var jsDAV_Property_HrefList = require("./../DAV/property/hrefList");
-var jsVObject_Reader = require("./../jsVObject/reader");
-
+var jsDAV_Property_Response = require("./../DAV/property/response");
+var jsDAV_Property_ResponseList = require("./../DAV/property/responseList");
+var jsDAV_iHref = require("./../DAV/interfaces/iHref");
+var jsDAVACL_iPrincipal = require("./interfaces/iPrincipal");
+var jsDAVACL_iACL = require("./interfaces/iAcl");
+var jsDAVACL_iPrincipalCollection = require("./interfaces/iPrincipalCollection");
+var jsDAVACL_Property_Principal = require("./property/principal");
+var jsDAVACL_Property_SupportedPrivilegeSet = require("./property/supportedPrivilegeSet");
+var jsDAVACL_Property_CurrentUserPrivilegeSet = require("./property/currentUserPrivilegeSet");
+var jsDAVACL_Property_Acl = require("./property/acl");
+var jsDAVACL_Property_AclRestrictions = require("./property/aclRestrictions");
+
+var AsyncEventEmitter = require("./../shared/asyncEvents").EventEmitter;
var Exc = require("./../shared/exceptions");
var Util = require("./../shared/util");
var Xml = require("./../shared/xml");
-var Path = require("path");
var Async = require("asyncjs");
/**
- * CardDAV plugin
+ * jsDAV ACL Plugin
+ *
+ * This plugin provides functionality to enforce ACL permissions.
+ * ACL is defined in RFC3744.
*
- * The CardDAV plugin adds CardDAV functionality to the WebDAV server
+ * In addition it also provides support for the {DAV:}current-user-principal
+ * property, defined in RFC5397 and the {DAV:}expand-property report, as
+ * defined in RFC3253.
*/
-var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
+var jsDAVACL_Plugin = module.exports = jsDAV_ServerPlugin.extend({
+ /**
+ * Plugin name
+ *
+ * @var String
+ */
+ name: "acl",
+
/**
- * Url to the addressbooks
+ * Recursion constants
+ *
+ * This only checks the base node
*/
- ADDRESSBOOK_ROOT: "addressbooks",
+ R_PARENT: 1,
/**
- * xml namespace for CardDAV elements
+ * Recursion constants
+ *
+ * This checks every node in the tree
*/
- NS_CARDDAV: "urn:ietf:params:xml:ns:carddav",
+ R_RECURSIVE: 2,
/**
- * Add urls to this property to have them automatically exposed as
- * 'directories' to the user.
+ * Recursion constants
*
- * @var array
+ * This checks every parentnode in the tree, but not leaf-nodes.
*/
- directories: [],
+ R_RECURSIVEPARENTS: 3,
/**
- * Handler class
+ * Reference to server object.
*
* @var jsDAV_Handler
*/
handler: null,
/**
- * Initializes the plugin
+ * List of urls containing principal collections.
+ * Modify this if your principals are located elsewhere.
+ *
+ * @var array
+ */
+ principalCollectionSet: [
+ "principals"
+ ],
+
+ /**
+ * By default ACL is only enforced for nodes that have ACL support (the
+ * ones that implement IACL). For any other node, access is
+ * always granted.
+ *
+ * To override this behaviour you can turn this setting off. This is useful
+ * if you plan to fully support ACL in the entire tree.
+ *
+ * @var bool
+ */
+ allowAccessToNodesWithoutACL: true,
+
+ /**
+ * By default nodes that are inaccessible by the user, can still be seen
+ * in directory listings (PROPFIND on parent with Depth: 1)
+ *
+ * In certain cases it's desirable to hide inaccessible nodes. Setting this
+ * to true will cause these nodes to be hidden from directory listings.
+ *
+ * @var bool
+ */
+ hideNodesFromListings: false,
+
+ /**
+ * This string is prepended to the username of the currently logged in
+ * user. This allows the plugin to determine the principal path based on
+ * the username.
+ *
+ * @var string
+ */
+ defaultUsernamePath: "principals",
+
+ /**
+ * This list of properties are the properties a client can search on using
+ * the {DAV:}principal-property-search report.
+ *
+ * The keys are the property names, values are descriptions.
+ *
+ * @var Object
+ */
+ principalSearchPropertySet: {
+ "{DAV:}displayname": "Display name",
+ "{http://ajax.org/2005/aml}email-address": "Email address"
+ },
+
+ /**
+ * Any principal uri's added here, will automatically be added to the list
+ * of ACL's. They will effectively receive {DAV:}all privileges, as a
+ * protected privilege.
+ *
+ * @var array
+ */
+ adminPrincipals: [],
+
+ /**
+ * Sets up the plugin
+ *
+ * This method is automatically called by the server class.
*
* @param jsDAV_Handler handler
* @return void
*/
initialize: function(handler) {
- // Events
+ this.handler = handler;
+
+ var options = this.handler.server.options;
+ if (options.allowAccessToNodesWithoutACL)
+ this.allowAccessToNodesWithoutACL = options.allowAccessToNodesWithoutACL;
+ if (options.hideNodesFromListings)
+ this.hideNodesFromListings = options.hideNodesFromListings;
+ if (options.defaultUsernamePath)
+ this.defaultUsernamePath = options.defaultUsernamePath;
+ if (options.adminPrincipals)
+ this.adminPrincipals = Util.makeUnique(this.adminPrincipals.concat(options.adminPrincipals));
+
handler.addEventListener("beforeGetProperties", this.beforeGetProperties.bind(this));
- handler.addEventListener("afterGetProperties", this.afterGetProperties.bind(this));
- handler.addEventListener("updateProperties", this.updateProperties.bind(this));
- handler.addEventListener("report", this.report.bind(this));
- handler.addEventListener("onHTMLActionsPanel", this.htmlActionsPanel.bind(this));
- handler.addEventListener("onBrowserPostAction", this.browserPostAction.bind(this));
- handler.addEventListener("beforeWriteContent", this.beforeWriteContent.bind(this));
- handler.addEventListener("beforeCreateFile", this.beforeCreateFile.bind(this));
-
- // Namespaces
- Xml.xmlNamespaces[this.NS_CARDDAV] = "card";
-
- // Mapping Interfaces to {DAV:}resourcetype values
- handler.resourceTypeMapping["{" + this.NS_CARDDAV + "}addressbook"] = jsCardDAV_iAddressBook;
- handler.resourceTypeMapping["{" + this.NS_CARDDAV + "}directory"] = jsCardDAV_iDirectory;
-
- // Adding properties that may never be changed
+ handler.addEventListener("beforeMethod", this.beforeMethod.bind(this), AsyncEventEmitter.PRIO_HIGH);
+ handler.addEventListener("beforeBind", this.beforeBind.bind(this), AsyncEventEmitter.PRIO_HIGH);
+ handler.addEventListener("beforeUnbind", this.beforeUnbind.bind(this), AsyncEventEmitter.PRIO_HIGH);
+ handler.addEventListener("updateProperties",this.updateProperties.bind(this));
+ handler.addEventListener("beforeUnlock", this.beforeUnlock.bind(this), AsyncEventEmitter.PRIO_HIGH);
+ handler.addEventListener("report",this.report.bind(this));
+ handler.addEventListener("unknownMethod", this.unknownMethod.bind(this));
+
handler.protectedProperties.push(
- "{" + this.NS_CARDDAV + "}supported-address-data",
- "{" + this.NS_CARDDAV + "}max-resource-size",
- "{" + this.NS_CARDDAV + "}addressbook-home-set",
- "{" + this.NS_CARDDAV + "}supported-collation-set"
+ "{DAV:}alternate-URI-set",
+ "{DAV:}principal-URL",
+ "{DAV:}group-membership",
+ "{DAV:}principal-collection-set",
+ "{DAV:}current-user-principal",
+ "{DAV:}supported-privilege-set",
+ "{DAV:}current-user-privilege-set",
+ "{DAV:}acl",
+ "{DAV:}acl-restrictions",
+ "{DAV:}inherited-acl-set",
+ "{DAV:}owner",
+ "{DAV:}group"
);
-
- handler.propertyMap["{http://calendarserver.org/ns/}me-card"] = jsDAV_Property_Href;
- this.handler = handler;
+ // Automatically mapping nodes implementing IPrincipal to the
+ // {DAV:}principal resourcetype.
+ handler.resourceTypeMapping["jsDAVACL_iPrincipal"] = "{DAV:}principal";
+
+ // Mapping the group-member-set property to the HrefList property
+ // class.
+ handler.propertyMap["{DAV:}group-member-set"] = "jsDAV_Property_HrefList";
},
/**
- * Returns a list of supported features.
+ * Returns a list of features added by this plugin.
*
- * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
+ * This list is used in the response of a HTTP OPTIONS request.
*
* @return array
*/
getFeatures: function() {
- return ["addressbook"];
+ return ["access-control", "calendarserver-principal-property-search"];
+ },
+
+ /**
+ * Returns a list of available methods for a given url
+ *
+ * @param string uri
+ * @return array
+ */
+ getHTTPMethods: function(uri) {
+ return ["ACL"];
},
/**
@@ -115,618 +224,1264 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
* @param string uri
* @return array
*/
- getSupportedReportSet: function(uri, callback) {
- var self = this;
- this.handler.getNodeForPath(uri, function(err, node) {
- if (err)
- return callback(err);
-
- if (node.hasFeature(jsCardDAV_iAddressBook) || node.hasFeature(jsCardDAV_iCard)) {
- return callback(null, [
- "{" + self.NS_CARDDAV + "}addressbook-multiget",
- "{" + self.NS_CARDDAV + "}addressbook-query"
- ]);
- }
- callback(null, []);
- });
+ getSupportedReportSet: function(uri) {
+ return [
+ "{DAV:}expand-property",
+ "{DAV:}principal-property-search",
+ "{DAV:}principal-search-property-set",
+ ];
},
/**
- * Adds all CardDAV-specific properties
+ * Checks if the current user has the specified privilege(s).
*
- * @param string path
- * @param DAV\INode node
- * @param array requestedProperties
- * @param array returnedProperties
- * @return void
+ * You can specify a single privilege, or a list of privileges.
+ * This method will throw an exception if the privilege is not available
+ * and return true otherwise.
+ *
+ * @param string uri
+ * @param array|string privileges
+ * @param number recursion
+ * @throws jsDAV_Exception_NeedPrivileges
+ * @return bool
*/
- beforeGetProperties: function(e, path, node, requestedProperties, returnedProperties) {
+ checkPrivileges: function(uri, privileges, recursion, callback) {
+ if (!Array.isArray(privileges))
+ privileges = [privileges];
+
+ recursion = recursion || this.R_PARENT;
var self = this;
- if (node.hasFeature(jsDAVACL_iPrincipal)) {
- // calendar-home-set property
- var addHome = "{" + this.NS_CARDDAV + "}addressbook-home-set";
- var addHomeIdx = requestedProperties.indexOf(addHome);
- if (addHomeIdx > -1) {
- var principalId = node.getName();
- var addressbookHomePath = this.ADDRESSBOOK_ROOT + "/" + principalId + "/";
- requestedProperties.splice(addHomeIdx, 1);
- returnedProperties["200"][addHome] = jsDAV_Property_Href.new(addressbookHomePath);
- }
- var directories = "{" + this.NS_CARDDAV + "}directory-gateway";
- var dirIdx = requestedProperties.indexOf(directories);
- if (this.directories && dirIdx > -1) {
- requestedProperties.splice(dirIdx, 1);
- returnedProperties["200"][directories] = jsDAV_Property_HrefList.new(this.directories);
- }
- }
+ this.getCurrentUserPrivilegeSet(uri, function(err, acl) {
+ if (err)
+ return callback(err);
- if (node.hasFeature(jsCardDAV_iCard)) {
- // The address-data property is not supposed to be a 'real'
- // property, but in large chunks of the spec it does act as such.
- // Therefore we simply expose it as a property.
- var addressDataProp = "{" + this.NS_CARDDAV + "}address-data";
- var addressIdx = requestedProperties.indexOf(addressDataProp);
- if (addressIdx > -1) {
- requestedProperties.splice(addressIdx, 1);
- node.get(function(err, val) {
- if (err)
- return e.next(err);
-
- returnedProperties["200"][addressDataProp] = val;
- afterICard();
- });
- }
- else
- afterICard();
- }
- else
- afterICard();
-
- function afterICard() {
- if (node.hasFeature(jsCardDAV_UserAddressBooks)) {
- var meCardProp = "{http://calendarserver.org/ns/}me-card";
- var meCardIdx = requestedProperties.indexOf(meCardProp);
- if (meCardIdx > -1) {
- self.handler.getProperties(node.getOwner(), ["{http://sabredav.org/ns}vcard-url"], function(err, props) {
- if (err)
- return e.next(err);
-
- if (props["{http://sabredav.org/ns}vcard-url"]) {
- returnedProperties["200"][meCardProp] = jsDAV_Property_Href.new(
- props["{http://sabredav.org/ns}vcard-url"]
- );
- requestedProperties.splice(meCardIdx, 1);
- }
- e.next();
- });
- }
+ if (!acl) {
+ if (self.allowAccessToNodesWithoutACL)
+ return callback(null, true);
else
- e.next();
+ return callback(new Exc.NeedPrivileges(uri, privileges), false);
}
- else
- e.next();
- }
+
+ var failed = privileges.filter(function(priv) {
+ return acl.indexOf(priv) === -1;
+ });
+
+ if (failed.length)
+ return callback(new Exc.NeedPrivileges(uri, failed), false);
+
+ callback(null, true);
+ });
},
/**
- * This event is triggered when a PROPPATCH method is executed
+ * Returns the standard users' principal.
*
- * @param array mutations
- * @param array result
- * @param DAV\INode node
- * @return bool
+ * This is one authorative principal url for the current user.
+ * This method will return null if the user wasn't logged in.
+ *
+ * @return string|null
*/
- updateProperties: function(e, mutations, result, node) {
- if (!node.hasFeature(jsCardDAV_UserAddressBooks))
- return e.next();
-
- var meCard = "{http://calendarserver.org/ns/}me-card";
-
- // The only property we care about
- if (!mutations[meCard])
- return e.next();
-
- var value = mutations[meCard];
- delete mutations[meCard];
+ getCurrentUserPrincipal: function(callback) {
+ var authPlugin = this.handler.plugins.auth;
- if (value.hasFeature(jsDAV_Property_iHref)) {
- value = this.handler.calculateUri(value.getHref());
- }
- else if (value) {
- result["400"][meCard] = null;
- return e.stop();
- }
+ if (!authPlugin)
+ return callback();
+ /** @var authPlugin jsDAV_Auth_Plugin */
- this.server.updateProperties(node.getOwner(), {"{http://sabredav.org/ns}vcard-url": value}, function(err, innerResult) {
+ var self = this;
+ authPlugin.getCurrentUser(function(err, userName) {
if (err)
- return e.next(err);
-
- var closureResult = false;
- var props;
- for (var status in innerResult) {
- status = parseInt(status, 10);
- props = innerResult[status];
- if (typeof props == "object" && props["{http://sabredav.org/ns}vcard-url"]) {
- result[status][meCard] = null;
- closureResult = (status >= 200 && status < 300);
- }
- }
-
- //return result;
- e.next();
+ return callback(err);
+ if (!userName)
+ return callback();
+ callback(null, self.defaultUsernamePath + "/" + userName);
});
},
+
/**
- * This functions handles REPORT requests specific to CardDAV
+ * Returns a list of principals that's associated to the current
+ * user, either directly or through group membership.
*
- * @param string reportName
- * @param DOMNode dom
- * @return bool
+ * @return array
*/
- report: function(e, reportName, dom) {
- switch (reportName) {
- case "{" + this.NS_CARDDAV + "}addressbook-multiget" :
- this.addressbookMultiGetReport(e, dom);
- break;
- case "{" + this.NS_CARDDAV + "}addressbook-query" :
- this.addressBookQueryReport(e, dom);
- break;
- default :
- return e.next();
- }
+ getCurrentUserPrincipals: function(callback) {
+ var self = this;
+ this.getCurrentUserPrincipal(function(err, currentUser) {
+ if (err)
+ return callback(err);
+ if (!currentUser)
+ return callback(null, []);
+
+ self.getPrincipalMembership(currentUser, function(err, membership) {
+ if (err)
+ return callback(err);
+ callback(null, [currentUser].concat(membership));
+ });
+ });
},
/**
- * This function handles the addressbook-multiget REPORT.
+ * This object holds a cache for all the principals that are associated with
+ * a single principal.
*
- * This report is used by the client to fetch the content of a series
- * of urls. Effectively avoiding a lot of redundant requests.
+ * @var object
+ */
+ principalMembershipCache: {},
+
+ /**
+ * Returns all the principal groups the specified principal is a member of.
*
- * @param DOMNode dom
- * @return void
+ * @param string principal
+ * @return array
*/
- addressbookMultiGetReport: function(e, dom) {
- var properties = Object.keys(Xml.parseProperties(dom.firstChild));
+ getPrincipalMembership: function(mainPrincipal, callback) {
+ // First check our cache
+ if (this.principalMembershipCache[mainPrincipal])
+ return callback(null, this.principalMembershipCache[mainPrincipal]);
- var hrefElems = dom.getElementsByTagNameNS("urn:DAV","href");
- var propertyList = {};
+ var check = [mainPrincipal];
+ var principals = [];
var self = this;
- Async.list(hrefElems)
- .each(function(elem, next) {
- var uri = self.handler.calculateUri(elem.nodeValue);
- self.handler.getPropertiesForPath(uri, properties, function(err, props) {
- if (err)
- return next(err);
-
- Util.extend(propertyList, props);
- next();
- });
- })
- .end(function(err) {
+ function checkNext() {
+ var principal = check.shift();
+ if (!principal)
+ return checkedAll();
+
+ self.handler.getNodeForPath(principal, function(err, node) {
if (err)
- return e.next(err);
-
- e.stop();
-
- var prefer = self.handler.getHTTPPRefer();
- self.handler.httpResponse.writeHead(207,{
- "content-type": "application/xml; charset=utf-8",
- "vary": "Brief,Prefer"
- });
- self.handler.httpResponse.end(self.handler.generateMultiStatus(propertyList, prefer["return-minimal"]));
+ return checkedAll(err);
+
+ if (node.hasFeature(jsDAVACL_iPrincipal)) {
+ node.getGroupMembership(function(err, memberships) {
+ if (err)
+ return checkedAll(err);
+
+ memberships.forEach(function(groupMember) {
+ if (principals.indexOf(groupMember) === -1) {
+ check.push(groupMember);
+ principals.push(groupMember);
+ }
+ });
+ checkNext();
+ });
+ }
+ else
+ checkNext();
});
+ }
+
+ function checkedAll(err) {
+ if (err)
+ return callback(err, []);
+
+ // Store the result in the cache
+ self.principalMembershipCache[mainPrincipal] = principals;
+
+ callback(err, principals);
+ }
+
+ checkNext();
},
/**
- * This method is triggered before a file gets updated with new content.
+ * Returns the supported privilege structure for this ACL plugin.
*
- * This plugin uses this method to ensure that Card nodes receive valid
- * vcard data.
+ * See RFC3744 for more details. Currently we default on a simple,
+ * standard structure.
*
- * @param string path
- * @param DAV\IFile node
- * @param resource data
- * @return void
+ * You can either get the list of privileges by a uri (path) or by
+ * specifying a Node.
+ *
+ * @param string|DAV\INode node
+ * @return array
*/
- beforeWriteContent: function(e, path, node, data) {
- if (!node.hasFeature(jsCardDAV_iCard))
- return e.next();
-
- try {
- this.validateVCard(data);
+ getSupportedPrivilegeSet: function(node, callback) {
+ var self = this;
+
+ if (!node.hasFeature) {
+ this.handler.getNodeForPath(node, function(err, n) {
+ if (err)
+ return callback(err);
+ node = n;
+ gotNodePrivSet();
+ });
}
- catch(ex) {
- return e.next(ex);
+ else
+ gotNodePrivSet();
+
+ function gotNodePrivSet() {
+ if (node.hasFeature(jsDAVACL_iACL))
+ return callback(null, node.getSupportedPrivilegeSet() || self.getDefaultSupportedPrivilegeSet());
+ callback(null, self.getDefaultSupportedPrivilegeSet());
}
- e.next();
},
/**
- * This method is triggered before a new file is created.
+ * Returns a fairly standard set of privileges, which may be useful for
+ * other systems to use as a basis.
*
- * This plugin uses this method to ensure that Card nodes receive valid
- * vcard data.
- *
- * @param string path
- * @param resource data
- * @param DAV\ICollection parentNode
- * @return void
+ * @return array
*/
- beforeCreateFile: function(e, path, data, parentNode) {
- if (!parentNode.hasFeature(jsCardDAV_iAddressBook))
- return e.next();
-
- try {
- this.validateVCard(data);
- }
- catch(ex) {
- return e.next(ex);
- }
- e.next();
+ getDefaultSupportedPrivilegeSet: function() {
+ return {
+ "privilege" : "{DAV:}all",
+ "abstract" : true,
+ "aggregates" : [
+ {
+ "privilege" : "{DAV:}read",
+ "aggregates" : [
+ {
+ "privilege" : "{DAV:}read-acl",
+ "abstract" : true
+ },
+ {
+ "privilege" : "{DAV:}read-current-user-privilege-set",
+ "abstract" : true
+ }
+ ]
+ }, // {DAV:}read
+ {
+ "privilege" : "{DAV:}write",
+ "aggregates" : [
+ {
+ "privilege" : "{DAV:}write-acl",
+ "abstract" : true
+ },
+ {
+ "privilege" : "{DAV:}write-properties",
+ "abstract" : true
+ },
+ {
+ "privilege" : "{DAV:}write-content",
+ "abstract" : true
+ },
+ {
+ "privilege" : "{DAV:}bind",
+ "abstract" : true
+ },
+ {
+ "privilege" : "{DAV:}unbind",
+ "abstract" : true
+ },
+ {
+ "privilege" : "{DAV:}unlock",
+ "abstract" : true
+ }
+ ]
+ } // {DAV:}write
+ ]
+ }; // {DAV:}all
},
/**
- * Checks if the submitted iCalendar data is in fact, valid.
+ * Returns the supported privilege set as a flat list
*
- * An exception is thrown if it's not.
+ * This is much easier to parse.
*
- * @param resource|string data
- * @return void
+ * The returned list will be index by privilege name.
+ * The value is a struct containing the following properties:
+ * - aggregates
+ * - abstract
+ * - concrete
+ *
+ * @param string|DAV\INode node
+ * @return array
*/
- validateVCard: function(data) {
- // If it's a stream, we convert it to a string first.
- if (typeof data != "string")
- data = data.toString("utf8");
+ getFlatPrivilegeSet: function(node, callback) {
+ this.getSupportedPrivilegeSet(node, function(err, privs) {
+ if (err)
+ return callback(err);
- var vobj;
- try {
- vobj = jsVObject_Reader.read(data);
- }
- catch (ex) {
- throw new Exc.UnsupportedMediaType("This resource only supports valid vcard data. Parse error: " + ex.message);
- }
+ var flat = {};
- if (vobj.name != "VCARD")
- throw new Exc.UnsupportedMediaType("This collection can only support vcard objects.");
+ // Traverses the privilege set tree for reordering
+ function getFPSTraverse(priv, isConcrete) {
+ var myPriv = {
+ "privilege" : priv.privilege,
+ "abstract" : !!priv.abstract && priv.abstract,
+ "aggregates" : [],
+ "concrete" : !!priv.abstract && priv.abstract ? isConcrete : priv.privilege
+ };
- if (!vobj.UID)
- throw new Exc.BadRequest("Every vcard must have a UID.");
- },
+ if (priv.aggregates) {
+ priv.aggregates.forEach(function(subPriv) {
+ myPriv.aggregates.push(subPriv.privilege);
+ });
+ }
+
+ flat[priv.privilege] = myPriv;
+
+ if (priv.aggregates) {
+ priv.aggregates.forEach(function(subPriv) {
+ getFPSTraverse(subPriv, myPriv.concrete);
+ });
+ }
+ }
+ getFPSTraverse(privs, null);
+ callback(null, flat);
+ });
+ },
/**
- * This function handles the addressbook-query REPORT
+ * Returns the full ACL list.
*
- * This report is used by the client to filter an addressbook based on a
- * complex query.
+ * Either a uri or a DAV\INode may be passed.
*
- * @param DOMNode dom
- * @return void
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode node
+ * @return array
*/
- addressbookQueryReport: function(e, dom) {
- var query = jsCardDAV_AddressBookQueryParser.new(dom);
- query.parse();
-
- var depth = this.handler.getHTTPDepth(0);
+ getACL: function(node, callback) {
var self = this;
-
- if (depth === 0) {
- this.handler.getNodeForPath(this.handler.getRequestUri(), function(err, node) {
- if (err)
- return e.next(err);
- afterCandidates([node]);
- })
- }
- else {
- this.handler.server.tree.getChildren(this.handler.getRequestUri(), function(err, children) {
+
+ if (typeof node == "string") {
+ this.handler.getNodeForPath(node, function(err, n) {
if (err)
- return e.next(err);
- afterCandidates(children);
+ return callback(err);
+ node = n;
+ gotNodeACL();
});
}
-
- function afterCandidates(candidateNodes) {
- var validNodes = [];
- Async.list(candidateNodes)
- .each(function(node, next) {
- if (!node.hasFeature(jsCardDAV_iCard))
- return next();
-
- node.get(function(err, blob) {
- if (err)
- return next(err);
-
- if (!self.validateFilters(blob, query.filters, query.test))
- return next();
-
- validNodes.push(node);
-
- if (query.limit && query.limit <= validNodes.length) {
- // We hit the maximum number of items, we can stop now.
- return next(Async.STOP);
- }
-
- next();
- });
- })
- .end(function(err) {
- if (err)
- return e.next(err);
-
- var result = {};
-
- Async.list(validNodes)
- .each(function(validNode, next) {
- var href = self.handler.getRequestUri();
- if (depth !== 0)
- href = href + "/" + validNode.getName();
-
- self.handler.getPropertiesForPath(href, query.requestedProperties, 0, function(err, props) {
- if (err)
- return next(err);
- Util.extend(result, props);
- next();
- });
- })
- .end(function(err) {
- if (err)
- return e.next(err);
-
- e.stop();
-
- var prefer = self.handler.getHTTPPRefer();
- self.handler.httpResponse.writeHead(207, {
- "content-type": "application/xml; charset=utf-8",
- "vary": "Brief,Prefer"
- });
- self.handler.httpResponse.end(self.handler.generateMultiStatus(result, prefer["return-minimal"]));
- });
+ else
+ gotNodeACL();
+
+ function gotNodeACL() {
+ if (!node.hasFeature(jsDAVACL_iACL))
+ return callback();
+
+ var acl = node.getACL();
+ self.adminPrincipals.forEach(function(adminPrincipal) {
+ acl.push({
+ "principal" : adminPrincipal,
+ "privilege" : "{DAV:}all",
+ "protected" : true
});
+ });
+ callback(null, acl);
}
},
/**
- * Validates if a vcard makes it throught a list of filters.
+ * Returns a list of privileges the current user has
+ * on a particular node.
*
- * @param string vcardData
- * @param array filters
- * @param string test anyof or allof (which means OR or AND)
- * @return bool
+ * Either a uri or a jsDAV_iNode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|jsDAV_iNode node
+ * @return array
*/
- validateFilters: function(vcardData, filters, test) {
- var vcard = jsVObject_Reader.read(vcardData);
- if (!filters)
- return true;
+ getCurrentUserPrivilegeSet: function(node, callback) {
+ var self = this;
+ if (typeof node == "string") {
+ this.handler.getNodeForPath(node, function(err, n) {
+ if (err)
+ return callback(err);
+ node = n;
+ gotNode();
+ });
+ }
+ else
+ gotNode();
-
- var filter, isDefined, success, vProperties, results, texts;
- for (var i = 0, l = filters.length; i < l; ++i) {
- filter = filters[i];
- isDefined = !!vcard[filter.name];
- if (filter["is-not-defined"]) {
- if (isDefined)
- success = false;
- else
- success = true;
- }
- else if ((!filter["param-filters"] && !filter["text-matches"]) || !isDefined) {
- // We only need to check for existence
- success = isDefined;
- }
- else {
- vProperties = vcard.select(filter.name);
-
- results = [];
- if (filter["param-filters"])
- results.push(this.validateParamFilters(vProperties, filter["param-filters"], filter.test));
- if (filter["text-matches"]) {
- texts = vProperties.map(function(vProperty) {
- return vProperty.value;
- });
- results.push(this.validateTextMatches(texts, filter["text-matches"], filter.test));
- }
+ function gotNode() {
+ self.getACL(node, function(err, acl) {
+ if (err)
+ return callback(err);
+ if (!acl)
+ return callback();
- if (results.length === 1) {
- success = results[0];
- }
- else {
- if (filter.test == "anyof")
- success = results[0] || results[1];
- else
- success = results[0] && results[1];
- }
+ self.getCurrentUserPrincipals(function(err, principals) {
+ if (err)
+ return callback(err);
+
+ var collected = [];
+
+ acl.forEach(function(ace) {
+ var principal = ace.principal;
+ switch (principal) {
+ case "{DAV:}owner" :
+ var owner = node.getOwner();
+ if (owner && principals.indexOf(owner) > -1)
+ collected.push(ace);
+ break;
+ // 'all' matches for every user
+ case "{DAV:}all" :
+ // 'authenticated' matched for every user that's logged in.
+ // Since it's not possible to use ACL while not being logged
+ // in, this is also always true.
+ case "{DAV:}authenticated" :
+ collected.push(ace);
+ break;
+ // 'unauthenticated' can never occur either, so we simply
+ // ignore these.
+ case "{DAV:}unauthenticated" :
+ break;
+ default :
+ if (principals.indexOf(ace.principal) > -1)
+ collected.push(ace);
+ break;
+ }
+ });
+
+ // Now we deduct all aggregated privileges.
+ self.getFlatPrivilegeSet(node, function(err, flat) {
+ if (err)
+ return callback(err);
- } // else
-
- // There are two conditions where we can already determine whether
- // or not this filter succeeds.