Skip to content
This repository
Browse code

started porting plugin code

  • Loading branch information...
commit 03333a935b200ede35a74654ce0b38dde88d8de1 1 parent 0ed585e
Mike de Boer authored
6 lib/DAVACL/abstractPrincipalCollection.js
... ... @@ -1,7 +1,7 @@
1 1 /*
2 2 * @package jsDAV
3   - * @subpackage DAV
4   - * @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org>
  3 + * @subpackage DAVACL
  4 + * @copyright Copyright(c) 2013 Mike de Boer. <info AT mikedeboer DOT nl>
5 5 * @author Mike de Boer <info AT mikedeboer DOT nl>
6 6 * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
7 7 */
@@ -83,7 +83,7 @@ var jsDAVACL_AbstractPrincipalCollection = module.exports = jsDAV_Directory.exte
83 83 */
84 84 getName: function() {
85 85 var parts = Util.splitPath(this.principalPrefix);
86   - return name[1];
  86 + return parts[1];
87 87 },
88 88
89 89 /**
1,400 lib/DAVACL/plugin.js
... ... @@ -0,0 +1,1400 @@
  1 +/*
  2 + * @package jsDAV
  3 + * @subpackage DAVACL
  4 + * @copyright Copyright(c) 2013 Mike de Boer. <info AT mikedeboer DOT nl>
  5 + * @author Mike de Boer <info AT mikedeboer DOT nl>
  6 + * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
  7 + */
  8 +"use strict";
  9 +
  10 +var jsDAV_ServerPlugin = require("./../DAV/plugin");
  11 +
  12 +var Util = require("./../shared/util");
  13 +var Exc = require("./../shared/exceptions");
  14 +
  15 +var Async = require("asyncjs");
  16 +
  17 +/**
  18 + * jsDAV ACL Plugin
  19 + *
  20 + * This plugin provides functionality to enforce ACL permissions.
  21 + * ACL is defined in RFC3744.
  22 + *
  23 + * In addition it also provides support for the {DAV:}current-user-principal
  24 + * property, defined in RFC5397 and the {DAV:}expand-property report, as
  25 + * defined in RFC3253.
  26 + */
  27 +var jsDAVACL_Plugin = module.exports = jsDAV_ServerPlugin.extend({
  28 + /**
  29 + * Recursion constants
  30 + *
  31 + * This only checks the base node
  32 + */
  33 + R_PARENT: 1,
  34 +
  35 + /**
  36 + * Recursion constants
  37 + *
  38 + * This checks every node in the tree
  39 + */
  40 + R_RECURSIVE: 2,
  41 +
  42 + /**
  43 + * Recursion constants
  44 + *
  45 + * This checks every parentnode in the tree, but not leaf-nodes.
  46 + */
  47 + R_RECURSIVEPARENTS: 3,
  48 +
  49 + /**
  50 + * Reference to server object.
  51 + *
  52 + * @var jsDAV_Server
  53 + */
  54 + server: null
  55 +
  56 + /**
  57 + * List of urls containing principal collections.
  58 + * Modify this if your principals are located elsewhere.
  59 + *
  60 + * @var array
  61 + */
  62 + principalCollectionSet: [
  63 + "principals"
  64 + ];
  65 +
  66 + /**
  67 + * By default ACL is only enforced for nodes that have ACL support (the
  68 + * ones that implement IACL). For any other node, access is
  69 + * always granted.
  70 + *
  71 + * To override this behaviour you can turn this setting off. This is useful
  72 + * if you plan to fully support ACL in the entire tree.
  73 + *
  74 + * @var bool
  75 + */
  76 + allowAccessToNodesWithoutACL: true,
  77 +
  78 + /**
  79 + * By default nodes that are inaccessible by the user, can still be seen
  80 + * in directory listings (PROPFIND on parent with Depth: 1)
  81 + *
  82 + * In certain cases it's desirable to hide inaccessible nodes. Setting this
  83 + * to true will cause these nodes to be hidden from directory listings.
  84 + *
  85 + * @var bool
  86 + */
  87 + hideNodesFromListings: false,
  88 +
  89 + /**
  90 + * This string is prepended to the username of the currently logged in
  91 + * user. This allows the plugin to determine the principal path based on
  92 + * the username.
  93 + *
  94 + * @var string
  95 + */
  96 + defaultUsernamePath: "principals",
  97 +
  98 + /**
  99 + * This list of properties are the properties a client can search on using
  100 + * the {DAV:}principal-property-search report.
  101 + *
  102 + * The keys are the property names, values are descriptions.
  103 + *
  104 + * @var Object
  105 + */
  106 + principalSearchPropertySet: {
  107 + "{DAV:}displayname": "Display name",
  108 + "{http://ajax.org/2005/aml}email-address": "Email address"
  109 + },
  110 +
  111 + /**
  112 + * Any principal uri's added here, will automatically be added to the list
  113 + * of ACL's. They will effectively receive {DAV:}all privileges, as a
  114 + * protected privilege.
  115 + *
  116 + * @var array
  117 + */
  118 + adminPrincipals: [],
  119 +
  120 + /**
  121 + * Returns a list of features added by this plugin.
  122 + *
  123 + * This list is used in the response of a HTTP OPTIONS request.
  124 + *
  125 + * @return array
  126 + */
  127 + getFeatures: function() {
  128 + return ["access-control", "calendarserver-principal-property-search"];
  129 + },
  130 +
  131 + /**
  132 + * Returns a list of available methods for a given url
  133 + *
  134 + * @param string uri
  135 + * @return array
  136 + */
  137 + getHTTPMethods: function(uri) {
  138 + return ["ACL"];
  139 + },
  140 +
  141 + /**
  142 + * Returns a plugin name.
  143 + *
  144 + * Using this name other plugins will be able to access other plugins
  145 + * using jsDAV_Server#getPlugin
  146 + *
  147 + * @return string
  148 + */
  149 + getPluginName: function() {
  150 + return "acl";
  151 + },
  152 +
  153 + /**
  154 + * Returns a list of reports this plugin supports.
  155 + *
  156 + * This will be used in the {DAV:}supported-report-set property.
  157 + * Note that you still need to subscribe to the 'report' event to actually
  158 + * implement them
  159 + *
  160 + * @param string uri
  161 + * @return array
  162 + */
  163 + getSupportedReportSet: function(uri) {
  164 + return [
  165 + "{DAV:}expand-property",
  166 + "{DAV:}principal-property-search",
  167 + "{DAV:}principal-search-property-set",
  168 + ];
  169 + },
  170 +
  171 + /**
  172 + * Checks if the current user has the specified privilege(s).
  173 + *
  174 + * You can specify a single privilege, or a list of privileges.
  175 + * This method will throw an exception if the privilege is not available
  176 + * and return true otherwise.
  177 + *
  178 + * @param string uri
  179 + * @param array|string privileges
  180 + * @param int recursion
  181 + * @param bool throwExceptions if set to false, this method won't throw exceptions.
  182 + * @throws Sabre\DAVACL\Exception\NeedPrivileges
  183 + * @return bool
  184 + */
  185 + checkPrivileges: function(uri, privileges, recursion, throwExceptions) {
  186 + if (!is_array(privileges)) privileges = array(privileges);
  187 + if (typeof throwExceptions == "undefined")
  188 + throwExceptions = true;
  189 + recursion = recursion || this.R_PARENT;
  190 +
  191 + acl = this.getCurrentUserPrivilegeSet(uri);
  192 +
  193 + if (is_null(acl)) {
  194 + if (this.allowAccessToNodesWithoutACL) {
  195 + return true;
  196 + } else {
  197 + if (throwExceptions)
  198 + throw new Exception\NeedPrivileges(uri,privileges);
  199 + else
  200 + return false;
  201 +
  202 + }
  203 + }
  204 +
  205 + failed = array();
  206 + foreach(privileges as priv) {
  207 +
  208 + if (!in_array(priv, acl)) {
  209 + failed[] = priv;
  210 + }
  211 +
  212 + }
  213 +
  214 + if (failed) {
  215 + if (throwExceptions)
  216 + throw new Exception\NeedPrivileges(uri,failed);
  217 + else
  218 + return false;
  219 + }
  220 + return true;
  221 + },
  222 +
  223 + /**
  224 + * Returns the standard users' principal.
  225 + *
  226 + * This is one authorative principal url for the current user.
  227 + * This method will return null if the user wasn't logged in.
  228 + *
  229 + * @return string|null
  230 + */
  231 + public function getCurrentUserPrincipal() {
  232 +
  233 + authPlugin = this.server.getPlugin("auth");
  234 + if (is_null(authPlugin)) return null;
  235 + /** @var authPlugin Sabre\DAV\Auth\Plugin */
  236 +
  237 + userName = authPlugin.getCurrentUser();
  238 + if (!userName) return null;
  239 +
  240 + return this.defaultUsernamePath . "/" . userName;
  241 +
  242 + }
  243 +
  244 +
  245 + /**
  246 + * Returns a list of principals that's associated to the current
  247 + * user, either directly or through group membership.
  248 + *
  249 + * @return array
  250 + */
  251 + public function getCurrentUserPrincipals() {
  252 +
  253 + currentUser = this.getCurrentUserPrincipal();
  254 +
  255 + if (is_null(currentUser)) return array();
  256 +
  257 + return array_merge(
  258 + array(currentUser),
  259 + this.getPrincipalMembership(currentUser)
  260 + );
  261 +
  262 + }
  263 +
  264 + /**
  265 + * This array holds a cache for all the principals that are associated with
  266 + * a single principal.
  267 + *
  268 + * @var array
  269 + */
  270 + protected principalMembershipCache = array();
  271 +
  272 +
  273 + /**
  274 + * Returns all the principal groups the specified principal is a member of.
  275 + *
  276 + * @param string principal
  277 + * @return array
  278 + */
  279 + public function getPrincipalMembership(mainPrincipal) {
  280 +
  281 + // First check our cache
  282 + if (isset(this.principalMembershipCache[mainPrincipal])) {
  283 + return this.principalMembershipCache[mainPrincipal];
  284 + }
  285 +
  286 + check = array(mainPrincipal);
  287 + principals = array();
  288 +
  289 + while(count(check)) {
  290 +
  291 + principal = array_shift(check);
  292 +
  293 + node = this.server.tree.getNodeForPath(principal);
  294 + if (node instanceof IPrincipal) {
  295 + foreach(node.getGroupMembership() as groupMember) {
  296 +
  297 + if (!in_array(groupMember, principals)) {
  298 +
  299 + check[] = groupMember;
  300 + principals[] = groupMember;
  301 +
  302 + }
  303 +
  304 + }
  305 +
  306 + }
  307 +
  308 + }
  309 +
  310 + // Store the result in the cache
  311 + this.principalMembershipCache[mainPrincipal] = principals;
  312 +
  313 + return principals;
  314 +
  315 + }
  316 +
  317 + /**
  318 + * Returns the supported privilege structure for this ACL plugin.
  319 + *
  320 + * See RFC3744 for more details. Currently we default on a simple,
  321 + * standard structure.
  322 + *
  323 + * You can either get the list of privileges by a uri (path) or by
  324 + * specifying a Node.
  325 + *
  326 + * @param string|DAV\INode node
  327 + * @return array
  328 + */
  329 + public function getSupportedPrivilegeSet(node) {
  330 +
  331 + if (is_string(node)) {
  332 + node = this.server.tree.getNodeForPath(node);
  333 + }
  334 +
  335 + if (node instanceof IACL) {
  336 + result = node.getSupportedPrivilegeSet();
  337 +
  338 + if (result)
  339 + return result;
  340 + }
  341 +
  342 + return self::getDefaultSupportedPrivilegeSet();
  343 +
  344 + }
  345 +
  346 + /**
  347 + * Returns a fairly standard set of privileges, which may be useful for
  348 + * other systems to use as a basis.
  349 + *
  350 + * @return array
  351 + */
  352 + static function getDefaultSupportedPrivilegeSet() {
  353 +
  354 + return array(
  355 + "privilege" => "{DAV:}all",
  356 + "abstract" => true,
  357 + "aggregates" => array(
  358 + array(
  359 + "privilege" => "{DAV:}read",
  360 + "aggregates" => array(
  361 + array(
  362 + "privilege" => "{DAV:}read-acl",
  363 + "abstract" => true,
  364 + ),
  365 + array(
  366 + "privilege" => "{DAV:}read-current-user-privilege-set",
  367 + "abstract" => true,
  368 + ),
  369 + ),
  370 + ), // {DAV:}read
  371 + array(
  372 + "privilege" => "{DAV:}write",
  373 + "aggregates" => array(
  374 + array(
  375 + "privilege" => "{DAV:}write-acl",
  376 + "abstract" => true,
  377 + ),
  378 + array(
  379 + "privilege" => "{DAV:}write-properties",
  380 + "abstract" => true,
  381 + ),
  382 + array(
  383 + "privilege" => "{DAV:}write-content",
  384 + "abstract" => true,
  385 + ),
  386 + array(
  387 + "privilege" => "{DAV:}bind",
  388 + "abstract" => true,
  389 + ),
  390 + array(
  391 + "privilege" => "{DAV:}unbind",
  392 + "abstract" => true,
  393 + ),
  394 + array(
  395 + "privilege" => "{DAV:}unlock",
  396 + "abstract" => true,
  397 + ),
  398 + ),
  399 + ), // {DAV:}write
  400 + ),
  401 + ); // {DAV:}all
  402 +
  403 + }
  404 +
  405 + /**
  406 + * Returns the supported privilege set as a flat list
  407 + *
  408 + * This is much easier to parse.
  409 + *
  410 + * The returned list will be index by privilege name.
  411 + * The value is a struct containing the following properties:
  412 + * - aggregates
  413 + * - abstract
  414 + * - concrete
  415 + *
  416 + * @param string|DAV\INode node
  417 + * @return array
  418 + */
  419 + final public function getFlatPrivilegeSet(node) {
  420 +
  421 + privs = this.getSupportedPrivilegeSet(node);
  422 +
  423 + flat = array();
  424 + this.getFPSTraverse(privs, null, flat);
  425 +
  426 + return flat;
  427 +
  428 + }
  429 +
  430 + /**
  431 + * Traverses the privilege set tree for reordering
  432 + *
  433 + * This function is solely used by getFlatPrivilegeSet, and would have been
  434 + * a closure if it wasn't for the fact I need to support PHP 5.2.
  435 + *
  436 + * @param array priv
  437 + * @param concrete
  438 + * @param array flat
  439 + * @return void
  440 + */
  441 + final private function getFPSTraverse(priv, concrete, &flat) {
  442 +
  443 + myPriv = array(
  444 + "privilege" => priv["privilege"],
  445 + "abstract" => isset(priv["abstract"]) && priv["abstract"],
  446 + "aggregates" => array(),
  447 + "concrete" => isset(priv["abstract"]) && priv["abstract"]?concrete:priv["privilege"],
  448 + );
  449 +
  450 + if (isset(priv["aggregates"]))
  451 + foreach(priv["aggregates"] as subPriv) myPriv["aggregates"][] = subPriv["privilege"];
  452 +
  453 + flat[priv["privilege"]] = myPriv;
  454 +
  455 + if (isset(priv["aggregates"])) {
  456 +
  457 + foreach(priv["aggregates"] as subPriv) {
  458 +
  459 + this.getFPSTraverse(subPriv, myPriv["concrete"], flat);
  460 +
  461 + }
  462 +
  463 + }
  464 +
  465 + }
  466 +
  467 + /**
  468 + * Returns the full ACL list.
  469 + *
  470 + * Either a uri or a DAV\INode may be passed.
  471 + *
  472 + * null will be returned if the node doesn't support ACLs.
  473 + *
  474 + * @param string|DAV\INode node
  475 + * @return array
  476 + */
  477 + public function getACL(node) {
  478 +
  479 + if (is_string(node)) {
  480 + node = this.server.tree.getNodeForPath(node);
  481 + }
  482 + if (!node instanceof IACL) {
  483 + return null;
  484 + }
  485 + acl = node.getACL();
  486 + foreach(this.adminPrincipals as adminPrincipal) {
  487 + acl[] = array(
  488 + "principal" => adminPrincipal,
  489 + "privilege" => "{DAV:}all",
  490 + "protected" => true,
  491 + );
  492 + }
  493 + return acl;
  494 +
  495 + }
  496 +
  497 + /**
  498 + * Returns a list of privileges the current user has
  499 + * on a particular node.
  500 + *
  501 + * Either a uri or a DAV\INode may be passed.
  502 + *
  503 + * null will be returned if the node doesn't support ACLs.
  504 + *
  505 + * @param string|DAV\INode node
  506 + * @return array
  507 + */
  508 + public function getCurrentUserPrivilegeSet(node) {
  509 +
  510 + if (is_string(node)) {
  511 + node = this.server.tree.getNodeForPath(node);
  512 + }
  513 +
  514 + acl = this.getACL(node);
  515 +
  516 + if (is_null(acl)) return null;
  517 +
  518 + principals = this.getCurrentUserPrincipals();
  519 +
  520 + collected = array();
  521 +
  522 + foreach(acl as ace) {
  523 +
  524 + principal = ace["principal"];
  525 +
  526 + switch(principal) {
  527 +
  528 + case "{DAV:}owner" :
  529 + owner = node.getOwner();
  530 + if (owner && in_array(owner, principals)) {
  531 + collected[] = ace;
  532 + }
  533 + break;
  534 +
  535 +
  536 + // 'all' matches for every user
  537 + case "{DAV:}all" :
  538 +
  539 + // 'authenticated' matched for every user that's logged in.
  540 + // Since it's not possible to use ACL while not being logged
  541 + // in, this is also always true.
  542 + case "{DAV:}authenticated" :
  543 + collected[] = ace;
  544 + break;
  545 +
  546 + // 'unauthenticated' can never occur either, so we simply
  547 + // ignore these.
  548 + case "{DAV:}unauthenticated" :
  549 + break;
  550 +
  551 + default :
  552 + if (in_array(ace["principal"], principals)) {
  553 + collected[] = ace;
  554 + }
  555 + break;
  556 +
  557 + }
  558 +
  559 +
  560 + }
  561 +
  562 + // Now we deduct all aggregated privileges.
  563 + flat = this.getFlatPrivilegeSet(node);
  564 +
  565 + collected2 = array();
  566 + while(count(collected)) {
  567 +
  568 + current = array_pop(collected);
  569 + collected2[] = current["privilege"];
  570 +
  571 + foreach(flat[current["privilege"]]["aggregates"] as subPriv) {
  572 + collected2[] = subPriv;
  573 + collected[] = flat[subPriv];
  574 + }
  575 +
  576 + }
  577 +
  578 + return array_values(array_unique(collected2));
  579 +
  580 + }
  581 +
  582 + /**
  583 + * Principal property search
  584 + *
  585 + * This method can search for principals matching certain values in
  586 + * properties.
  587 + *
  588 + * This method will return a list of properties for the matched properties.
  589 + *
  590 + * @param array searchProperties The properties to search on. This is a
  591 + * key-value list. The keys are property
  592 + * names, and the values the strings to
  593 + * match them on.
  594 + * @param array requestedProperties This is the list of properties to
  595 + * return for every match.
  596 + * @param string collectionUri The principal collection to search on.
  597 + * If this is ommitted, the standard
  598 + * principal collection-set will be used.
  599 + * @return array This method returns an array structure similar to
  600 + * Sabre\DAV\Server::getPropertiesForPath. Returned
  601 + * properties are index by a HTTP status code.
  602 + *
  603 + */
  604 + public function principalSearch(array searchProperties, array requestedProperties, collectionUri = null) {
  605 +
  606 + if (!is_null(collectionUri)) {
  607 + uris = array(collectionUri);
  608 + } else {
  609 + uris = this.principalCollectionSet;
  610 + }
  611 +
  612 + lookupResults = array();
  613 + foreach(uris as uri) {
  614 +
  615 + principalCollection = this.server.tree.getNodeForPath(uri);
  616 + if (!principalCollection instanceof IPrincipalCollection) {
  617 + // Not a principal collection, we're simply going to ignore
  618 + // this.
  619 + continue;
  620 + }
  621 +
  622 + results = principalCollection.searchPrincipals(searchProperties);
  623 + foreach(results as result) {
  624 + lookupResults[] = rtrim(uri,"/") . "/" . result;
  625 + }
  626 +
  627 + }
  628 +
  629 + matches = array();
  630 +
  631 + foreach(lookupResults as lookupResult) {
  632 +
  633 + list(matches[]) = this.server.getPropertiesForPath(lookupResult, requestedProperties, 0);
  634 +
  635 + }
  636 +
  637 + return matches;
  638 +
  639 + }
  640 +
  641 + /**
  642 + * Sets up the plugin
  643 + *
  644 + * This method is automatically called by the server class.
  645 + *
  646 + * @param DAV\Server server
  647 + * @return void
  648 + */
  649 + public function initialize(DAV\Server server) {
  650 +
  651 + this.server = server;
  652 + server.subscribeEvent("beforeGetProperties",array(this,"beforeGetProperties"));
  653 +
  654 + server.subscribeEvent("beforeMethod", array(this,"beforeMethod"),20);
  655 + server.subscribeEvent("beforeBind", array(this,"beforeBind"),20);
  656 + server.subscribeEvent("beforeUnbind", array(this,"beforeUnbind"),20);
  657 + server.subscribeEvent("updateProperties",array(this,"updateProperties"));
  658 + server.subscribeEvent("beforeUnlock", array(this,"beforeUnlock"),20);
  659 + server.subscribeEvent("report",array(this,"report"));
  660 + server.subscribeEvent("unknownMethod", array(this, "unknownMethod"));
  661 +
  662 + array_push(server.protectedProperties,
  663 + "{DAV:}alternate-URI-set",
  664 + "{DAV:}principal-URL",
  665 + "{DAV:}group-membership",
  666 + "{DAV:}principal-collection-set",
  667 + "{DAV:}current-user-principal",
  668 + "{DAV:}supported-privilege-set",
  669 + "{DAV:}current-user-privilege-set",
  670 + "{DAV:}acl",
  671 + "{DAV:}acl-restrictions",
  672 + "{DAV:}inherited-acl-set",
  673 + "{DAV:}owner",
  674 + "{DAV:}group"
  675 + );
  676 +
  677 + // Automatically mapping nodes implementing IPrincipal to the
  678 + // {DAV:}principal resourcetype.
  679 + server.resourceTypeMapping["Sabre\\DAVACL\\IPrincipal"] = "{DAV:}principal";
  680 +
  681 + // Mapping the group-member-set property to the HrefList property
  682 + // class.
  683 + server.propertyMap["{DAV:}group-member-set"] = "Sabre\\DAV\\Property\\HrefList";
  684 +
  685 + }
  686 +
  687 +
  688 + /* {{{ Event handlers */
  689 +
  690 + /**
  691 + * Triggered before any method is handled
  692 + *
  693 + * @param string method
  694 + * @param string uri
  695 + * @return void
  696 + */
  697 + public function beforeMethod(method, uri) {
  698 +
  699 + exists = this.server.tree.nodeExists(uri);
  700 +
  701 + // If the node doesn't exists, none of these checks apply
  702 + if (!exists) return;
  703 +
  704 + switch(method) {
  705 +
  706 + case "GET" :
  707 + case "HEAD" :
  708 + case "OPTIONS" :
  709 + // For these 3 we only need to know if the node is readable.
  710 + this.checkPrivileges(uri,"{DAV:}read");
  711 + break;
  712 +
  713 + case "PUT" :
  714 + case "LOCK" :
  715 + case "UNLOCK" :
  716 + // This method requires the write-content priv if the node
  717 + // already exists, and bind on the parent if the node is being
  718 + // created.
  719 + // The bind privilege is handled in the beforeBind event.
  720 + this.checkPrivileges(uri,"{DAV:}write-content");
  721 + break;
  722 +
  723 +
  724 + case "PROPPATCH" :
  725 + this.checkPrivileges(uri,"{DAV:}write-properties");
  726 + break;
  727 +
  728 + case "ACL" :
  729 + this.checkPrivileges(uri,"{DAV:}write-acl");
  730 + break;
  731 +
  732 + case "COPY" :
  733 + case "MOVE" :
  734 + // Copy requires read privileges on the entire source tree.
  735 + // If the target exists write-content normally needs to be
  736 + // checked, however, we're deleting the node beforehand and
  737 + // creating a new one after, so this is handled by the
  738 + // beforeUnbind event.
  739 + //
  740 + // The creation of the new node is handled by the beforeBind
  741 + // event.
  742 + //
  743 + // If MOVE is used beforeUnbind will also be used to check if
  744 + // the sourcenode can be deleted.
  745 + this.checkPrivileges(uri,"{DAV:}read",self::R_RECURSIVE);
  746 +
  747 + break;
  748 +
  749 + }
  750 +
  751 + }
  752 +
  753 + /**
  754 + * Triggered before a new node is created.
  755 + *
  756 + * This allows us to check permissions for any operation that creates a
  757 + * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
  758 + *
  759 + * @param string uri
  760 + * @return void
  761 + */
  762 + public function beforeBind(uri) {
  763 +
  764 + list(parentUri,nodeName) = DAV\URLUtil::splitPath(uri);
  765 + this.checkPrivileges(parentUri,"{DAV:}bind");
  766 +
  767 + }
  768 +
  769 + /**
  770 + * Triggered before a node is deleted
  771 + *
  772 + * This allows us to check permissions for any operation that will delete
  773 + * an existing node.
  774 + *
  775 + * @param string uri
  776 + * @return void
  777 + */
  778 + public function beforeUnbind(uri) {
  779 +
  780 + list(parentUri,nodeName) = DAV\URLUtil::splitPath(uri);
  781 + this.checkPrivileges(parentUri,"{DAV:}unbind",self::R_RECURSIVEPARENTS);
  782 +
  783 + }
  784 +
  785 + /**
  786 + * Triggered before a node is unlocked.
  787 + *
  788 + * @param string uri
  789 + * @param DAV\Locks\LockInfo lock
  790 + * @TODO: not yet implemented
  791 + * @return void
  792 + */
  793 + public function beforeUnlock(uri, DAV\Locks\LockInfo lock) {
  794 +
  795 +
  796 + }
  797 +
  798 + /**
  799 + * Triggered before properties are looked up in specific nodes.
  800 + *
  801 + * @param string uri
  802 + * @param DAV\INode node
  803 + * @param array requestedProperties
  804 + * @param array returnedProperties
  805 + * @TODO really should be broken into multiple methods, or even a class.
  806 + * @return bool
  807 + */
  808 + public function beforeGetProperties(uri, DAV\INode node, &requestedProperties, &returnedProperties) {
  809 +
  810 + // Checking the read permission
  811 + if (!this.checkPrivileges(uri,"{DAV:}read",self::R_PARENT,false)) {
  812 +
  813 + // User is not allowed to read properties
  814 + if (this.hideNodesFromListings) {
  815 + return false;
  816 + }
  817 +
  818 + // Marking all requested properties as '403'.
  819 + foreach(requestedProperties as key=>requestedProperty) {
  820 + unset(requestedProperties[key]);
  821 + returnedProperties[403][requestedProperty] = null;
  822 + }
  823 + return;
  824 +
  825 + }
  826 +
  827 + /* Adding principal properties */
  828 + if (node instanceof IPrincipal) {
  829 +
  830 + if (false !== (index = array_search("{DAV:}alternate-URI-set", requestedProperties))) {
  831 +
  832 + unset(requestedProperties[index]);
  833 + returnedProperties[200]["{DAV:}alternate-URI-set"] = new DAV\Property\HrefList(node.getAlternateUriSet());
  834 +
  835 + }
  836 + if (false !== (index = array_search("{DAV:}principal-URL", requestedProperties))) {
  837 +
  838 + unset(requestedProperties[index]);
  839 + returnedProperties[200]["{DAV:}principal-URL"] = new DAV\Property\Href(node.getPrincipalUrl() . "/");
  840 +
  841 + }
  842 + if (false !== (index = array_search("{DAV:}group-member-set", requestedProperties))) {
  843 +
  844 + unset(requestedProperties[index]);
  845 + returnedProperties[200]["{DAV:}group-member-set"] = new DAV\Property\HrefList(node.getGroupMemberSet());
  846 +
  847 + }
  848 + if (false !== (index = array_search("{DAV:}group-membership", requestedProperties))) {
  849 +
  850 + unset(requestedProperties[index]);
  851 + returnedProperties[200]["{DAV:}group-membership"] = new DAV\Property\HrefList(node.getGroupMembership());
  852 +
  853 + }
  854 +
  855 + if (false !== (index = array_search("{DAV:}displayname", requestedProperties))) {
  856 +
  857 + returnedProperties[200]["{DAV:}displayname"] = node.getDisplayName();
  858 +
  859 + }
  860 +
  861 + }
  862 + if (false !== (index = array_search("{DAV:}principal-collection-set", requestedProperties))) {
  863 +
  864 + unset(requestedProperties[index]);
  865 + val = this.principalCollectionSet;
  866 + // Ensuring all collections end with a slash
  867 + foreach(val as k=>v) val[k] = v . "/";
  868 + returnedProperties[200]["{DAV:}principal-collection-set"] = new DAV\Property\HrefList(val);
  869 +
  870 + }
  871 + if (false !== (index = array_search("{DAV:}current-user-principal", requestedProperties))) {
  872 +
  873 + unset(requestedProperties[index]);
  874 + if (url = this.getCurrentUserPrincipal()) {
  875 + returnedProperties[200]["{DAV:}current-user-principal"] = new Property\Principal(Property\Principal::HREF, url . "/");
  876 + } else {
  877 + returnedProperties[200]["{DAV:}current-user-principal"] = new Property\Principal(Property\Principal::UNAUTHENTICATED);
  878 + }
  879 +
  880 + }
  881 + if (false !== (index = array_search("{DAV:}supported-privilege-set", requestedProperties))) {
  882 +
  883 + unset(requestedProperties[index]);
  884 + returnedProperties[200]["{DAV:}supported-privilege-set"] = new Property\SupportedPrivilegeSet(this.getSupportedPrivilegeSet(node));
  885 +
  886 + }
  887 + if (false !== (index = array_search("{DAV:}current-user-privilege-set", requestedProperties))) {
  888 +
  889 + if (!this.checkPrivileges(uri, "{DAV:}read-current-user-privilege-set", self::R_PARENT, false)) {
  890 + returnedProperties[403]["{DAV:}current-user-privilege-set"] = null;
  891 + unset(requestedProperties[index]);
  892 + } else {
  893 + val = this.getCurrentUserPrivilegeSet(node);
  894 + if (!is_null(val)) {
  895 + unset(requestedProperties[index]);
  896 + returnedProperties[200]["{DAV:}current-user-privilege-set"] = new Property\CurrentUserPrivilegeSet(val);
  897 + }
  898 + }
  899 +
  900 + }
  901 +
  902 + /* The ACL property contains all the permissions */
  903 + if (false !== (index = array_search("{DAV:}acl", requestedProperties))) {
  904 +
  905 + if (!this.checkPrivileges(uri, "{DAV:}read-acl", self::R_PARENT, false)) {
  906 +
  907 + unset(requestedProperties[index]);
  908 + returnedProperties[403]["{DAV:}acl"] = null;
  909 +
  910 + } else {
  911 +
  912 + acl = this.getACL(node);
  913 + if (!is_null(acl)) {
  914 + unset(requestedProperties[index]);
  915 + returnedProperties[200]["{DAV:}acl"] = new Property\Acl(this.getACL(node));
  916 + }
  917 +
  918 + }
  919 +
  920 + }
  921 +
  922 + /* The acl-restrictions property contains information on how privileges
  923 + * must behave.
  924 + */
  925 + if (false !== (index = array_search("{DAV:}acl-restrictions", requestedProperties))) {
  926 + unset(requestedProperties[index]);
  927 + returnedProperties[200]["{DAV:}acl-restrictions"] = new Property\AclRestrictions();
  928 + }
  929 +
  930 + /* Adding ACL properties */
  931 + if (node instanceof IACL) {
  932 +
  933 + if (false !== (index = array_search("{DAV:}owner", requestedProperties))) {
  934 +
  935 + unset(requestedProperties[index]);
  936 + returnedProperties[200]["{DAV:}owner"] = new DAV\Property\Href(node.getOwner() . "/");
  937 +
  938 + }
  939 +
  940 + }
  941 +
  942 + }
  943 +
  944 + /**
  945 + * This method intercepts PROPPATCH methods and make sure the
  946 + * group-member-set is updated correctly.
  947 + *
  948 + * @param array propertyDelta
  949 + * @param array result
  950 + * @param DAV\INode node
  951 + * @return bool
  952 + */
  953 + public function updateProperties(&propertyDelta, &result, DAV\INode node) {
  954 +
  955 + if (!array_key_exists("{DAV:}group-member-set", propertyDelta))
  956 + return;
  957 +
  958 + if (is_null(propertyDelta["{DAV:}group-member-set"])) {
  959 + memberSet = array();
  960 + } elseif (propertyDelta["{DAV:}group-member-set"] instanceof DAV\Property\HrefList) {
  961 + memberSet = array_map(
  962 + array(this.server,"calculateUri"),
  963 + propertyDelta["{DAV:}group-member-set"].getHrefs()
  964 + );
  965 + } else {
  966 + throw new DAV\Exception("The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null");
  967 + }
  968 +
  969 + if (!(node instanceof IPrincipal)) {
  970 + result[403]["{DAV:}group-member-set"] = null;
  971 + unset(propertyDelta["{DAV:}group-member-set"]);
  972 +
  973 + // Returning false will stop the updateProperties process
  974 + return false;
  975 + }
  976 +
  977 + node.setGroupMemberSet(memberSet);
  978 + // We must also clear our cache, just in case
  979 +
  980 + this.principalMembershipCache = array();
  981 +
  982 + result[200]["{DAV:}group-member-set"] = null;
  983 + unset(propertyDelta["{DAV:}group-member-set"]);
  984 +
  985 + }
  986 +
  987 + /**
  988 + * This method handles HTTP REPORT requests
  989 + *
  990 + * @param string reportName
  991 + * @param \DOMNode dom
  992 + * @return bool
  993 + */
  994 + public function report(reportName, dom) {
  995 +
  996 + switch(reportName) {
  997 +
  998 + case "{DAV:}principal-property-search" :
  999 + this.principalPropertySearchReport(dom);
  1000 + return false;
  1001 + case "{DAV:}principal-search-property-set" :
  1002 + this.principalSearchPropertySetReport(dom);
  1003 + return false;
  1004 + case "{DAV:}expand-property" :
  1005 + this.expandPropertyReport(dom);
  1006 + return false;
  1007 +
  1008 + }
  1009 +
  1010 + }
  1011 +
  1012 + /**
  1013 + * This event is triggered for any HTTP method that is not known by the
  1014 + * webserver.
  1015 + *
  1016 + * @param string method
  1017 + * @param string uri
  1018 + * @return bool
  1019 + */
  1020 + public function unknownMethod(method, uri) {
  1021 +
  1022 + if (method!=="ACL") return;
  1023 +
  1024 + this.httpACL(uri);
  1025 + return false;
  1026 +
  1027 + }
  1028 +
  1029 + /**
  1030 + * This method is responsible for handling the 'ACL' event.
  1031 + *
  1032 + * @param string uri
  1033 + * @return void
  1034 + */
  1035 + public function httpACL(uri) {
  1036 +
  1037 + body = this.server.httpRequest.getBody(true);
  1038 + dom = DAV\XMLUtil::loadDOMDocument(body);
  1039 +
  1040 + newAcl =
  1041 + Property\Acl::unserialize(dom.firstChild)
  1042 + .getPrivileges();
  1043 +
  1044 + // Normalizing urls
  1045 + foreach(newAcl as k=>newAce) {
  1046 + newAcl[k]["principal"] = this.server.calculateUri(newAce["principal"]);
  1047 + }
  1048 +
  1049 + node = this.server.tree.getNodeForPath(uri);
  1050 +
  1051 + if (!(node instanceof IACL)) {
  1052 + throw new DAV\Exception\MethodNotAllowed("This node does not support the ACL method");
  1053 + }
  1054 +
  1055 + oldAcl = this.getACL(node);
  1056 +
  1057 + supportedPrivileges = this.getFlatPrivilegeSet(node);
  1058 +
  1059 + /* Checking if protected principals from the existing principal set are
  1060 + not overwritten. */
  1061 + foreach(oldAcl as oldAce) {
  1062 +
  1063 + if (!isset(oldAce["protected"]) || !oldAce["protected"]) continue;
  1064 +
  1065 + found = false;
  1066 + foreach(newAcl as newAce) {
  1067 + if (
  1068 + newAce["privilege"] === oldAce["privilege"] &&
  1069 + newAce["principal"] === oldAce["principal"] &&
  1070 + newAce["protected"]
  1071 + )
  1072 + found = true;
  1073 + }
  1074 +
  1075 + if (!found)
  1076 + throw new Exception\AceConflict("This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request");
  1077 +
  1078 + }
  1079 +
  1080 + foreach(newAcl as newAce) {
  1081 +
  1082 + // Do we recognize the privilege
  1083 + if (!isset(supportedPrivileges[newAce["privilege"]])) {
  1084 + throw new Exception\NotSupportedPrivilege("The privilege you specified (" . newAce["privilege"] . ") is not recognized by this server");
  1085 + }
  1086 +
  1087 + if (supportedPrivileges[newAce["privilege"]]["abstract"]) {
  1088 + throw new Exception\NoAbstract("The privilege you specified (" . newAce["privilege"] . ") is an abstract privilege");
  1089 + }
  1090 +
  1091 + // Looking up the principal
  1092 + try {
  1093 + principal = this.server.tree.getNodeForPath(newAce["principal"]);
  1094 + } catch (DAV\Exception\NotFound e) {
  1095 + throw new Exception\NotRecognizedPrincipal("The specified principal (" . newAce["principal"] . ") does not exist");
  1096 + }
  1097 + if (!(principal instanceof IPrincipal)) {
  1098 + throw new Exception\NotRecognizedPrincipal("The specified uri (" . newAce["principal"] . ") is not a principal");
  1099 + }
  1100 +
  1101 + }
  1102 + node.setACL(newAcl);
  1103 +
  1104 + }
  1105 +
  1106 + /* }}} */
  1107 +
  1108 + /* Reports {{{ */
  1109 +
  1110 + /**
  1111 + * The expand-property report is defined in RFC3253 section 3-8.
  1112 + *
  1113 + * This report is very similar to a standard PROPFIND. The difference is
  1114 + * that it has the additional ability to look at properties containing a
  1115 + * {DAV:}href element, follow that property and grab additional elements
  1116 + * there.
  1117 + *
  1118 + * Other rfc's, such as ACL rely on this report, so it made sense to put
  1119 + * it in this plugin.
  1120 + *
  1121 + * @param \DOMElement dom
  1122 + * @return void
  1123 + */
  1124 + protected function expandPropertyReport(dom) {
  1125 +
  1126 + requestedProperties = this.parseExpandPropertyReportRequest(dom.firstChild.firstChild);
  1127 + depth = this.server.getHTTPDepth(0);
  1128 + requestUri = this.server.getRequestUri();
  1129 +
  1130 + result = this.expandProperties(requestUri,requestedProperties,depth);
  1131 +
  1132 + dom = new \DOMDocument("1.0","utf-8");
  1133 + dom.formatOutput = true;
  1134 + multiStatus = dom.createElement("d:multistatus");
  1135 + dom.appendChild(multiStatus);
  1136 +
  1137 + // Adding in default namespaces
  1138 + foreach(this.server.xmlNamespaces as namespace=>prefix) {
  1139 +
  1140 + multiStatus.setAttribute("xmlns:" . prefix,namespace);
  1141 +
  1142 + }
  1143 +
  1144 + foreach(result as response) {
  1145 + response.serialize(this.server, multiStatus);
  1146 + }
  1147 +
  1148 + xml = dom.saveXML();
  1149 + this.server.httpResponse.setHeader("Content-Type","application/xml; charset=utf-8");
  1150 + this.server.httpResponse.sendStatus(207);
  1151 + this.server.httpResponse.sendBody(xml);
  1152 +
  1153 + }
  1154 +
  1155 + /**
  1156 + * This method is used by expandPropertyReport to parse
  1157 + * out the entire HTTP request.
  1158 + *
  1159 + * @param \DOMElement node
  1160 + * @return array
  1161 + */
  1162 + protected function parseExpandPropertyReportRequest(node) {
  1163 +
  1164 + requestedProperties = array();
  1165 + do {
  1166 +
  1167 + if (DAV\XMLUtil::toClarkNotation(node)!=="{DAV:}property") continue;
  1168 +
  1169 + if (node.firstChild) {
  1170 +
  1171 + children = this.parseExpandPropertyReportRequest(node.firstChild);
  1172 +
  1173 + } else {
  1174 +
  1175 + children = array();
  1176 +
  1177 + }
  1178 +
  1179 + namespace = node.getAttribute("namespace");
  1180 + if (!namespace) namespace = "DAV:";
  1181 +
  1182 + propName = "{".namespace."}" . node.getAttribute("name");
  1183 + requestedProperties[propName] = children;
  1184 +
  1185 + } while (node = node.nextSibling);
  1186 +
  1187 + return requestedProperties;
  1188 +
  1189 + }
  1190 +
  1191 + /**
  1192 + * This method expands all the properties and returns
  1193 + * a list with property values
  1194 + *
  1195 + * @param array path
  1196 + * @param array requestedProperties the list of required properties
  1197 + * @param int depth
  1198 + * @return array
  1199 + */
  1200 + protected function expandProperties(path, array requestedProperties, depth) {
  1201 +