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