1<?php
2
3declare(strict_types=1);
4
5namespace Sabre\DAVACL;
6
7use Sabre\DAV;
8use Sabre\DAV\Exception\BadRequest;
9use Sabre\DAV\Exception\Forbidden;
10use Sabre\DAV\Exception\NotAuthenticated;
11use Sabre\DAV\Exception\NotFound;
12use Sabre\DAV\INode;
13use Sabre\DAV\Xml\Property\Href;
14use Sabre\DAVACL\Exception\NeedPrivileges;
15use Sabre\HTTP\RequestInterface;
16use Sabre\HTTP\ResponseInterface;
17use Sabre\Uri;
18
19/**
20 * SabreDAV ACL Plugin.
21 *
22 * This plugin provides functionality to enforce ACL permissions.
23 * ACL is defined in RFC3744.
24 *
25 * In addition it also provides support for the {DAV:}current-user-principal
26 * property, defined in RFC5397 and the {DAV:}expand-property report, as
27 * defined in RFC3253.
28 *
29 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
30 * @author Evert Pot (http://evertpot.com/)
31 * @license http://sabre.io/license/ Modified BSD License
32 */
33class Plugin extends DAV\ServerPlugin
34{
35    /**
36     * Recursion constants.
37     *
38     * This only checks the base node
39     */
40    const R_PARENT = 1;
41
42    /**
43     * Recursion constants.
44     *
45     * This checks every node in the tree
46     */
47    const R_RECURSIVE = 2;
48
49    /**
50     * Recursion constants.
51     *
52     * This checks every parentnode in the tree, but not leaf-nodes.
53     */
54    const R_RECURSIVEPARENTS = 3;
55
56    /**
57     * Reference to server object.
58     *
59     * @var DAV\Server
60     */
61    protected $server;
62
63    /**
64     * List of urls containing principal collections.
65     * Modify this if your principals are located elsewhere.
66     *
67     * @var array
68     */
69    public $principalCollectionSet = [
70        'principals',
71    ];
72
73    /**
74     * By default nodes that are inaccessible by the user, can still be seen
75     * in directory listings (PROPFIND on parent with Depth: 1).
76     *
77     * In certain cases it's desirable to hide inaccessible nodes. Setting this
78     * to true will cause these nodes to be hidden from directory listings.
79     *
80     * @var bool
81     */
82    public $hideNodesFromListings = false;
83
84    /**
85     * This list of properties are the properties a client can search on using
86     * the {DAV:}principal-property-search report.
87     *
88     * The keys are the property names, values are descriptions.
89     *
90     * @var array
91     */
92    public $principalSearchPropertySet = [
93        '{DAV:}displayname' => 'Display name',
94        '{http://sabredav.org/ns}email-address' => 'Email address',
95    ];
96
97    /**
98     * Any principal uri's added here, will automatically be added to the list
99     * of ACL's. They will effectively receive {DAV:}all privileges, as a
100     * protected privilege.
101     *
102     * @var array
103     */
104    public $adminPrincipals = [];
105
106    /**
107     * The ACL plugin allows privileges to be assigned to users that are not
108     * logged in. To facilitate that, it modifies the auth plugin's behavior
109     * to only require login when a privileged operation was denied.
110     *
111     * Unauthenticated access can be considered a security concern, so it's
112     * possible to turn this feature off to harden the server's security.
113     *
114     * @var bool
115     */
116    public $allowUnauthenticatedAccess = true;
117
118    /**
119     * Returns a list of features added by this plugin.
120     *
121     * This list is used in the response of a HTTP OPTIONS request.
122     *
123     * @return array
124     */
125    public function getFeatures()
126    {
127        return ['access-control', 'calendarserver-principal-property-search'];
128    }
129
130    /**
131     * Returns a list of available methods for a given url.
132     *
133     * @param string $uri
134     *
135     * @return array
136     */
137    public function getMethods($uri)
138    {
139        return ['ACL'];
140    }
141
142    /**
143     * Returns a plugin name.
144     *
145     * Using this name other plugins will be able to access other plugins
146     * using Sabre\DAV\Server::getPlugin
147     *
148     * @return string
149     */
150    public function getPluginName()
151    {
152        return 'acl';
153    }
154
155    /**
156     * Returns a list of reports this plugin supports.
157     *
158     * This will be used in the {DAV:}supported-report-set property.
159     * Note that you still need to subscribe to the 'report' event to actually
160     * implement them
161     *
162     * @param string $uri
163     *
164     * @return array
165     */
166    public function getSupportedReportSet($uri)
167    {
168        return [
169            '{DAV:}expand-property',
170            '{DAV:}principal-match',
171            '{DAV:}principal-property-search',
172            '{DAV:}principal-search-property-set',
173        ];
174    }
175
176    /**
177     * Checks if the current user has the specified privilege(s).
178     *
179     * You can specify a single privilege, or a list of privileges.
180     * This method will throw an exception if the privilege is not available
181     * and return true otherwise.
182     *
183     * @param string       $uri
184     * @param array|string $privileges
185     * @param int          $recursion
186     * @param bool         $throwExceptions if set to false, this method won't throw exceptions
187     *
188     * @throws NeedPrivileges
189     * @throws NotAuthenticated
190     *
191     * @return bool
192     */
193    public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true)
194    {
195        if (!is_array($privileges)) {
196            $privileges = [$privileges];
197        }
198
199        $acl = $this->getCurrentUserPrivilegeSet($uri);
200
201        $failed = [];
202        foreach ($privileges as $priv) {
203            if (!in_array($priv, $acl)) {
204                $failed[] = $priv;
205            }
206        }
207
208        if ($failed) {
209            if ($this->allowUnauthenticatedAccess && is_null($this->getCurrentUserPrincipal())) {
210                // We are not authenticated. Kicking in the Auth plugin.
211                $authPlugin = $this->server->getPlugin('auth');
212                $reasons = $authPlugin->getLoginFailedReasons();
213                $authPlugin->challenge(
214                    $this->server->httpRequest,
215                    $this->server->httpResponse
216                );
217                throw new NotAuthenticated(implode(', ', $reasons).'. Login was needed for privilege: '.implode(', ', $failed).' on '.$uri);
218            }
219            if ($throwExceptions) {
220                throw new NeedPrivileges($uri, $failed);
221            } else {
222                return false;
223            }
224        }
225
226        return true;
227    }
228
229    /**
230     * Returns the standard users' principal.
231     *
232     * This is one authoritative principal url for the current user.
233     * This method will return null if the user wasn't logged in.
234     *
235     * @return string|null
236     */
237    public function getCurrentUserPrincipal()
238    {
239        /** @var $authPlugin \Sabre\DAV\Auth\Plugin */
240        $authPlugin = $this->server->getPlugin('auth');
241        if (!$authPlugin) {
242            return null;
243        }
244
245        return $authPlugin->getCurrentPrincipal();
246    }
247
248    /**
249     * Returns a list of principals that's associated to the current
250     * user, either directly or through group membership.
251     *
252     * @return array
253     */
254    public function getCurrentUserPrincipals()
255    {
256        $currentUser = $this->getCurrentUserPrincipal();
257
258        if (is_null($currentUser)) {
259            return [];
260        }
261
262        return array_merge(
263            [$currentUser],
264            $this->getPrincipalMembership($currentUser)
265        );
266    }
267
268    /**
269     * Sets the default ACL rules.
270     *
271     * These rules are used for all nodes that don't implement the IACL interface.
272     */
273    public function setDefaultAcl(array $acl)
274    {
275        $this->defaultAcl = $acl;
276    }
277
278    /**
279     * Returns the default ACL rules.
280     *
281     * These rules are used for all nodes that don't implement the IACL interface.
282     *
283     * @return array
284     */
285    public function getDefaultAcl()
286    {
287        return $this->defaultAcl;
288    }
289
290    /**
291     * The default ACL rules.
292     *
293     * These rules are used for nodes that don't implement IACL. These default
294     * set of rules allow anyone to do anything, as long as they are
295     * authenticated.
296     *
297     * @var array
298     */
299    protected $defaultAcl = [
300        [
301            'principal' => '{DAV:}authenticated',
302            'protected' => true,
303            'privilege' => '{DAV:}all',
304        ],
305    ];
306
307    /**
308     * This array holds a cache for all the principals that are associated with
309     * a single principal.
310     *
311     * @var array
312     */
313    protected $principalMembershipCache = [];
314
315    /**
316     * Returns all the principal groups the specified principal is a member of.
317     *
318     * @param string $mainPrincipal
319     *
320     * @return array
321     */
322    public function getPrincipalMembership($mainPrincipal)
323    {
324        // First check our cache
325        if (isset($this->principalMembershipCache[$mainPrincipal])) {
326            return $this->principalMembershipCache[$mainPrincipal];
327        }
328
329        $check = [$mainPrincipal];
330        $principals = [];
331
332        while (count($check)) {
333            $principal = array_shift($check);
334
335            $node = $this->server->tree->getNodeForPath($principal);
336            if ($node instanceof IPrincipal) {
337                foreach ($node->getGroupMembership() as $groupMember) {
338                    if (!in_array($groupMember, $principals)) {
339                        $check[] = $groupMember;
340                        $principals[] = $groupMember;
341                    }
342                }
343            }
344        }
345
346        // Store the result in the cache
347        $this->principalMembershipCache[$mainPrincipal] = $principals;
348
349        return $principals;
350    }
351
352    /**
353     * Find out of a principal equals another principal.
354     *
355     * This is a quick way to find out whether a principal URI is part of a
356     * group, or any subgroups.
357     *
358     * The first argument is the principal URI you want to check against. For
359     * example the principal group, and the second argument is the principal of
360     * which you want to find out of it is the same as the first principal, or
361     * in a member of the first principal's group or subgroups.
362     *
363     * So the arguments are not interchangeable. If principal A is in group B,
364     * passing 'B', 'A' will yield true, but 'A', 'B' is false.
365     *
366     * If the second argument is not passed, we will use the current user
367     * principal.
368     *
369     * @param string $checkPrincipal
370     * @param string $currentPrincipal
371     *
372     * @return bool
373     */
374    public function principalMatchesPrincipal($checkPrincipal, $currentPrincipal = null)
375    {
376        if (is_null($currentPrincipal)) {
377            $currentPrincipal = $this->getCurrentUserPrincipal();
378        }
379        if ($currentPrincipal === $checkPrincipal) {
380            return true;
381        }
382        if (is_null($currentPrincipal)) {
383            return false;
384        }
385
386        return in_array(
387            $checkPrincipal,
388            $this->getPrincipalMembership($currentPrincipal)
389        );
390    }
391
392    /**
393     * Returns a tree of supported privileges for a resource.
394     *
395     * The returned array structure should be in this form:
396     *
397     * [
398     *    [
399     *       'privilege' => '{DAV:}read',
400     *       'abstract'  => false,
401     *       'aggregates' => []
402     *    ]
403     * ]
404     *
405     * Privileges can be nested using "aggregates". Doing so means that
406     * if you assign someone the aggregating privilege, all the
407     * sub-privileges will automatically be granted.
408     *
409     * Marking a privilege as abstract means that the privilege cannot be
410     * directly assigned, but must be assigned via the parent privilege.
411     *
412     * So a more complex version might look like this:
413     *
414     * [
415     *    [
416     *       'privilege' => '{DAV:}read',
417     *       'abstract'  => false,
418     *       'aggregates' => [
419     *          [
420     *              'privilege'  => '{DAV:}read-acl',
421     *              'abstract'   => false,
422     *              'aggregates' => [],
423     *          ]
424     *       ]
425     *    ]
426     * ]
427     *
428     * @param string|INode $node
429     *
430     * @return array
431     */
432    public function getSupportedPrivilegeSet($node)
433    {
434        if (is_string($node)) {
435            $node = $this->server->tree->getNodeForPath($node);
436        }
437
438        $supportedPrivileges = null;
439        if ($node instanceof IACL) {
440            $supportedPrivileges = $node->getSupportedPrivilegeSet();
441        }
442
443        if (is_null($supportedPrivileges)) {
444            // Default
445            $supportedPrivileges = [
446                '{DAV:}read' => [
447                    'abstract' => false,
448                    'aggregates' => [
449                        '{DAV:}read-acl' => [
450                            'abstract' => false,
451                            'aggregates' => [],
452                        ],
453                        '{DAV:}read-current-user-privilege-set' => [
454                            'abstract' => false,
455                            'aggregates' => [],
456                        ],
457                    ],
458                ],
459                '{DAV:}write' => [
460                    'abstract' => false,
461                    'aggregates' => [
462                        '{DAV:}write-properties' => [
463                            'abstract' => false,
464                            'aggregates' => [],
465                        ],
466                        '{DAV:}write-content' => [
467                            'abstract' => false,
468                            'aggregates' => [],
469                        ],
470                        '{DAV:}unlock' => [
471                            'abstract' => false,
472                            'aggregates' => [],
473                        ],
474                    ],
475                ],
476            ];
477            if ($node instanceof DAV\ICollection) {
478                $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}bind'] = [
479                    'abstract' => false,
480                    'aggregates' => [],
481                ];
482                $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}unbind'] = [
483                    'abstract' => false,
484                    'aggregates' => [],
485                ];
486            }
487            if ($node instanceof IACL) {
488                $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}write-acl'] = [
489                    'abstract' => false,
490                    'aggregates' => [],
491                ];
492            }
493        }
494
495        $this->server->emit(
496            'getSupportedPrivilegeSet',
497            [$node, &$supportedPrivileges]
498        );
499
500        return $supportedPrivileges;
501    }
502
503    /**
504     * Returns the supported privilege set as a flat list.
505     *
506     * This is much easier to parse.
507     *
508     * The returned list will be index by privilege name.
509     * The value is a struct containing the following properties:
510     *   - aggregates
511     *   - abstract
512     *   - concrete
513     *
514     * @param string|INode $node
515     *
516     * @return array
517     */
518    final public function getFlatPrivilegeSet($node)
519    {
520        $privs = [
521            'abstract' => false,
522            'aggregates' => $this->getSupportedPrivilegeSet($node),
523        ];
524
525        $fpsTraverse = null;
526        $fpsTraverse = function ($privName, $privInfo, $concrete, &$flat) use (&$fpsTraverse) {
527            $myPriv = [
528                'privilege' => $privName,
529                'abstract' => isset($privInfo['abstract']) && $privInfo['abstract'],
530                'aggregates' => [],
531                'concrete' => isset($privInfo['abstract']) && $privInfo['abstract'] ? $concrete : $privName,
532            ];
533
534            if (isset($privInfo['aggregates'])) {
535                foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) {
536                    $myPriv['aggregates'][] = $subPrivName;
537                }
538            }
539
540            $flat[$privName] = $myPriv;
541
542            if (isset($privInfo['aggregates'])) {
543                foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) {
544                    $fpsTraverse($subPrivName, $subPrivInfo, $myPriv['concrete'], $flat);
545                }
546            }
547        };
548
549        $flat = [];
550        $fpsTraverse('{DAV:}all', $privs, null, $flat);
551
552        return $flat;
553    }
554
555    /**
556     * Returns the full ACL list.
557     *
558     * Either a uri or a INode may be passed.
559     *
560     * null will be returned if the node doesn't support ACLs.
561     *
562     * @param string|DAV\INode $node
563     *
564     * @return array
565     */
566    public function getAcl($node)
567    {
568        if (is_string($node)) {
569            $node = $this->server->tree->getNodeForPath($node);
570        }
571        if (!$node instanceof IACL) {
572            return $this->getDefaultAcl();
573        }
574        $acl = $node->getACL();
575        foreach ($this->adminPrincipals as $adminPrincipal) {
576            $acl[] = [
577                'principal' => $adminPrincipal,
578                'privilege' => '{DAV:}all',
579                'protected' => true,
580            ];
581        }
582
583        return $acl;
584    }
585
586    /**
587     * Returns a list of privileges the current user has
588     * on a particular node.
589     *
590     * Either a uri or a DAV\INode may be passed.
591     *
592     * null will be returned if the node doesn't support ACLs.
593     *
594     * @param string|DAV\INode $node
595     *
596     * @return array
597     */
598    public function getCurrentUserPrivilegeSet($node)
599    {
600        if (is_string($node)) {
601            $node = $this->server->tree->getNodeForPath($node);
602        }
603
604        $acl = $this->getACL($node);
605
606        $collected = [];
607
608        $isAuthenticated = null !== $this->getCurrentUserPrincipal();
609
610        foreach ($acl as $ace) {
611            $principal = $ace['principal'];
612
613            switch ($principal) {
614                case '{DAV:}owner':
615                    $owner = $node->getOwner();
616                    if ($owner && $this->principalMatchesPrincipal($owner)) {
617                        $collected[] = $ace;
618                    }
619                    break;
620
621                // 'all' matches for every user
622                case '{DAV:}all':
623                    $collected[] = $ace;
624                    break;
625
626                case '{DAV:}authenticated':
627                    // Authenticated users only
628                    if ($isAuthenticated) {
629                        $collected[] = $ace;
630                    }
631                    break;
632
633                case '{DAV:}unauthenticated':
634                    // Unauthenticated users only
635                    if (!$isAuthenticated) {
636                        $collected[] = $ace;
637                    }
638                    break;
639
640                default:
641                    if ($this->principalMatchesPrincipal($ace['principal'])) {
642                        $collected[] = $ace;
643                    }
644                    break;
645            }
646        }
647
648        // Now we deduct all aggregated privileges.
649        $flat = $this->getFlatPrivilegeSet($node);
650
651        $collected2 = [];
652        while (count($collected)) {
653            $current = array_pop($collected);
654            $collected2[] = $current['privilege'];
655
656            if (!isset($flat[$current['privilege']])) {
657                // Ignoring privileges that are not in the supported-privileges list.
658                $this->server->getLogger()->debug('A node has the "'.$current['privilege'].'" in its ACL list, but this privilege was not reported in the supportedPrivilegeSet list. This will be ignored.');
659                continue;
660            }
661            foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) {
662                $collected2[] = $subPriv;
663                $collected[] = $flat[$subPriv];
664            }
665        }
666
667        return array_values(array_unique($collected2));
668    }
669
670    /**
671     * Returns a principal based on its uri.
672     *
673     * Returns null if the principal could not be found.
674     *
675     * @param string $uri
676     *
677     * @return string|null
678     */
679    public function getPrincipalByUri($uri)
680    {
681        $result = null;
682        $collections = $this->principalCollectionSet;
683        foreach ($collections as $collection) {
684            try {
685                $principalCollection = $this->server->tree->getNodeForPath($collection);
686            } catch (NotFound $e) {
687                // Ignore and move on
688                continue;
689            }
690
691            if (!$principalCollection instanceof IPrincipalCollection) {
692                // Not a principal collection, we're simply going to ignore
693                // this.
694                continue;
695            }
696
697            $result = $principalCollection->findByUri($uri);
698            if ($result) {
699                return $result;
700            }
701        }
702    }
703
704    /**
705     * Principal property search.
706     *
707     * This method can search for principals matching certain values in
708     * properties.
709     *
710     * This method will return a list of properties for the matched properties.
711     *
712     * @param array  $searchProperties    The properties to search on. This is a
713     *                                    key-value list. The keys are property
714     *                                    names, and the values the strings to
715     *                                    match them on.
716     * @param array  $requestedProperties this is the list of properties to
717     *                                    return for every match
718     * @param string $collectionUri       the principal collection to search on.
719     *                                    If this is ommitted, the standard
720     *                                    principal collection-set will be used
721     * @param string $test                "allof" to use AND to search the
722     *                                    properties. 'anyof' for OR.
723     *
724     * @return array This method returns an array structure similar to
725     *               Sabre\DAV\Server::getPropertiesForPath. Returned
726     *               properties are index by a HTTP status code.
727     */
728    public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null, $test = 'allof')
729    {
730        if (!is_null($collectionUri)) {
731            $uris = [$collectionUri];
732        } else {
733            $uris = $this->principalCollectionSet;
734        }
735
736        $lookupResults = [];
737        foreach ($uris as $uri) {
738            $principalCollection = $this->server->tree->getNodeForPath($uri);
739            if (!$principalCollection instanceof IPrincipalCollection) {
740                // Not a principal collection, we're simply going to ignore
741                // this.
742                continue;
743            }
744
745            $results = $principalCollection->searchPrincipals($searchProperties, $test);
746            foreach ($results as $result) {
747                $lookupResults[] = rtrim($uri, '/').'/'.$result;
748            }
749        }
750
751        $matches = [];
752
753        foreach ($lookupResults as $lookupResult) {
754            list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0);
755        }
756
757        return $matches;
758    }
759
760    /**
761     * Sets up the plugin.
762     *
763     * This method is automatically called by the server class.
764     */
765    public function initialize(DAV\Server $server)
766    {
767        if ($this->allowUnauthenticatedAccess) {
768            $authPlugin = $server->getPlugin('auth');
769            if (!$authPlugin) {
770                throw new \Exception('The Auth plugin must be loaded before the ACL plugin if you want to allow unauthenticated access.');
771            }
772            $authPlugin->autoRequireLogin = false;
773        }
774
775        $this->server = $server;
776        $server->on('propFind', [$this, 'propFind'], 20);
777        $server->on('beforeMethod:*', [$this, 'beforeMethod'], 20);
778        $server->on('beforeBind', [$this, 'beforeBind'], 20);
779        $server->on('beforeUnbind', [$this, 'beforeUnbind'], 20);
780        $server->on('propPatch', [$this, 'propPatch']);
781        $server->on('beforeUnlock', [$this, 'beforeUnlock'], 20);
782        $server->on('report', [$this, 'report']);
783        $server->on('method:ACL', [$this, 'httpAcl']);
784        $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
785        $server->on('getPrincipalByUri', function ($principal, &$uri) {
786            $uri = $this->getPrincipalByUri($principal);
787
788            // Break event chain
789            if ($uri) {
790                return false;
791            }
792        });
793
794        array_push($server->protectedProperties,
795            '{DAV:}alternate-URI-set',
796            '{DAV:}principal-URL',
797            '{DAV:}group-membership',
798            '{DAV:}principal-collection-set',
799            '{DAV:}current-user-principal',
800            '{DAV:}supported-privilege-set',
801            '{DAV:}current-user-privilege-set',
802            '{DAV:}acl',
803            '{DAV:}acl-restrictions',
804            '{DAV:}inherited-acl-set',
805            '{DAV:}owner',
806            '{DAV:}group'
807        );
808
809        // Automatically mapping nodes implementing IPrincipal to the
810        // {DAV:}principal resourcetype.
811        $server->resourceTypeMapping['Sabre\\DAVACL\\IPrincipal'] = '{DAV:}principal';
812
813        // Mapping the group-member-set property to the HrefList property
814        // class.
815        $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href';
816        $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl';
817        $server->xml->elementMap['{DAV:}acl-principal-prop-set'] = 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport';
818        $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport';
819        $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport';
820        $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport';
821        $server->xml->elementMap['{DAV:}principal-match'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport';
822    }
823
824    /* {{{ Event handlers */
825
826    /**
827     * Triggered before any method is handled.
828     */
829    public function beforeMethod(RequestInterface $request, ResponseInterface $response)
830    {
831        $method = $request->getMethod();
832        $path = $request->getPath();
833
834        $exists = $this->server->tree->nodeExists($path);
835
836        // If the node doesn't exists, none of these checks apply
837        if (!$exists) {
838            return;
839        }
840
841        switch ($method) {
842            case 'GET':
843            case 'HEAD':
844            case 'OPTIONS':
845                // For these 3 we only need to know if the node is readable.
846                $this->checkPrivileges($path, '{DAV:}read');
847                break;
848
849            case 'PUT':
850            case 'LOCK':
851                // This method requires the write-content priv if the node
852                // already exists, and bind on the parent if the node is being
853                // created.
854                // The bind privilege is handled in the beforeBind event.
855                $this->checkPrivileges($path, '{DAV:}write-content');
856                break;
857
858            case 'UNLOCK':
859                // Unlock is always allowed at the moment.
860                break;
861
862            case 'PROPPATCH':
863                $this->checkPrivileges($path, '{DAV:}write-properties');
864                break;
865
866            case 'ACL':
867                $this->checkPrivileges($path, '{DAV:}write-acl');
868                break;
869
870            case 'COPY':
871            case 'MOVE':
872                // Copy requires read privileges on the entire source tree.
873                // If the target exists write-content normally needs to be
874                // checked, however, we're deleting the node beforehand and
875                // creating a new one after, so this is handled by the
876                // beforeUnbind event.
877                //
878                // The creation of the new node is handled by the beforeBind
879                // event.
880                //
881                // If MOVE is used beforeUnbind will also be used to check if
882                // the sourcenode can be deleted.
883                $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE);
884                break;
885        }
886    }
887
888    /**
889     * Triggered before a new node is created.
890     *
891     * This allows us to check permissions for any operation that creates a
892     * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
893     *
894     * @param string $uri
895     */
896    public function beforeBind($uri)
897    {
898        list($parentUri) = Uri\split($uri);
899        $this->checkPrivileges($parentUri, '{DAV:}bind');
900    }
901
902    /**
903     * Triggered before a node is deleted.
904     *
905     * This allows us to check permissions for any operation that will delete
906     * an existing node.
907     *
908     * @param string $uri
909     */
910    public function beforeUnbind($uri)
911    {
912        list($parentUri) = Uri\split($uri);
913        $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS);
914    }
915
916    /**
917     * Triggered before a node is unlocked.
918     *
919     * @param string $uri
920     * @TODO: not yet implemented
921     */
922    public function beforeUnlock($uri, DAV\Locks\LockInfo $lock)
923    {
924    }
925
926    /**
927     * Triggered before properties are looked up in specific nodes.
928     *
929     * @TODO really should be broken into multiple methods, or even a class.
930     *
931     * @return bool
932     */
933    public function propFind(DAV\PropFind $propFind, DAV\INode $node)
934    {
935        $path = $propFind->getPath();
936
937        // Checking the read permission
938        if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) {
939            // User is not allowed to read properties
940
941            // Returning false causes the property-fetching system to pretend
942            // that the node does not exist, and will cause it to be hidden
943            // from listings such as PROPFIND or the browser plugin.
944            if ($this->hideNodesFromListings) {
945                return false;
946            }
947
948            // Otherwise we simply mark every property as 403.
949            foreach ($propFind->getRequestedProperties() as $requestedProperty) {
950                $propFind->set($requestedProperty, null, 403);
951            }
952
953            return;
954        }
955
956        /* Adding principal properties */
957        if ($node instanceof IPrincipal) {
958            $propFind->handle('{DAV:}alternate-URI-set', function () use ($node) {
959                return new Href($node->getAlternateUriSet());
960            });
961            $propFind->handle('{DAV:}principal-URL', function () use ($node) {
962                return new Href($node->getPrincipalUrl().'/');
963            });
964            $propFind->handle('{DAV:}group-member-set', function () use ($node) {
965                $members = $node->getGroupMemberSet();
966                foreach ($members as $k => $member) {
967                    $members[$k] = rtrim($member, '/').'/';
968                }
969
970                return new Href($members);
971            });
972            $propFind->handle('{DAV:}group-membership', function () use ($node) {
973                $members = $node->getGroupMembership();
974                foreach ($members as $k => $member) {
975                    $members[$k] = rtrim($member, '/').'/';
976                }
977
978                return new Href($members);
979            });
980            $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']);
981        }
982
983        $propFind->handle('{DAV:}principal-collection-set', function () {
984            $val = $this->principalCollectionSet;
985            // Ensuring all collections end with a slash
986            foreach ($val as $k => $v) {
987                $val[$k] = $v.'/';
988            }
989
990            return new Href($val);
991        });
992        $propFind->handle('{DAV:}current-user-principal', function () {
993            if ($url = $this->getCurrentUserPrincipal()) {
994                return new Xml\Property\Principal(Xml\Property\Principal::HREF, $url.'/');
995            } else {
996                return new Xml\Property\Principal(Xml\Property\Principal::UNAUTHENTICATED);
997            }
998        });
999        $propFind->handle('{DAV:}supported-privilege-set', function () use ($node) {
1000            return new Xml\Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node));
1001        });
1002        $propFind->handle('{DAV:}current-user-privilege-set', function () use ($node, $propFind, $path) {
1003            if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) {
1004                $propFind->set('{DAV:}current-user-privilege-set', null, 403);
1005            } else {
1006                $val = $this->getCurrentUserPrivilegeSet($node);
1007
1008                return new Xml\Property\CurrentUserPrivilegeSet($val);
1009            }
1010        });
1011        $propFind->handle('{DAV:}acl', function () use ($node, $propFind, $path) {
1012            /* The ACL property contains all the permissions */
1013            if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) {
1014                $propFind->set('{DAV:}acl', null, 403);
1015            } else {
1016                $acl = $this->getACL($node);
1017
1018                return new Xml\Property\Acl($this->getACL($node));
1019            }
1020        });
1021        $propFind->handle('{DAV:}acl-restrictions', function () {
1022            return new Xml\Property\AclRestrictions();
1023        });
1024
1025        /* Adding ACL properties */
1026        if ($node instanceof IACL) {
1027            $propFind->handle('{DAV:}owner', function () use ($node) {
1028                return new Href($node->getOwner().'/');
1029            });
1030        }
1031    }
1032
1033    /**
1034     * This method intercepts PROPPATCH methods and make sure the
1035     * group-member-set is updated correctly.
1036     *
1037     * @param string $path
1038     */
1039    public function propPatch($path, DAV\PropPatch $propPatch)
1040    {
1041        $propPatch->handle('{DAV:}group-member-set', function ($value) use ($path) {
1042            if (is_null($value)) {
1043                $memberSet = [];
1044            } elseif ($value instanceof Href) {
1045                $memberSet = array_map(
1046                    [$this->server, 'calculateUri'],
1047                    $value->getHrefs()
1048                );
1049            } else {
1050                throw new DAV\Exception('The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null');
1051            }
1052            $node = $this->server->tree->getNodeForPath($path);
1053            if (!($node instanceof IPrincipal)) {
1054                // Fail
1055                return false;
1056            }
1057
1058            $node->setGroupMemberSet($memberSet);
1059            // We must also clear our cache, just in case
1060
1061            $this->principalMembershipCache = [];
1062
1063            return true;
1064        });
1065    }
1066
1067    /**
1068     * This method handles HTTP REPORT requests.
1069     *
1070     * @param string $reportName
1071     * @param mixed  $report
1072     * @param mixed  $path
1073     *
1074     * @return bool
1075     */
1076    public function report($reportName, $report, $path)
1077    {
1078        switch ($reportName) {
1079            case '{DAV:}principal-property-search':
1080                $this->server->transactionType = 'report-principal-property-search';
1081                $this->principalPropertySearchReport($path, $report);
1082
1083                return false;
1084            case '{DAV:}principal-search-property-set':
1085                $this->server->transactionType = 'report-principal-search-property-set';
1086                $this->principalSearchPropertySetReport($path, $report);
1087
1088                return false;
1089            case '{DAV:}expand-property':
1090                $this->server->transactionType = 'report-expand-property';
1091                $this->expandPropertyReport($path, $report);
1092
1093                return false;
1094            case '{DAV:}principal-match':
1095                $this->server->transactionType = 'report-principal-match';
1096                $this->principalMatchReport($path, $report);
1097
1098                return false;
1099            case '{DAV:}acl-principal-prop-set':
1100                $this->server->transactionType = 'acl-principal-prop-set';
1101                $this->aclPrincipalPropSetReport($path, $report);
1102
1103                return false;
1104        }
1105    }
1106
1107    /**
1108     * This method is responsible for handling the 'ACL' event.
1109     *
1110     * @return bool
1111     */
1112    public function httpAcl(RequestInterface $request, ResponseInterface $response)
1113    {
1114        $path = $request->getPath();
1115        $body = $request->getBodyAsString();
1116
1117        if (!$body) {
1118            throw new DAV\Exception\BadRequest('XML body expected in ACL request');
1119        }
1120
1121        $acl = $this->server->xml->expect('{DAV:}acl', $body);
1122        $newAcl = $acl->getPrivileges();
1123
1124        // Normalizing urls
1125        foreach ($newAcl as $k => $newAce) {
1126            $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']);
1127        }
1128        $node = $this->server->tree->getNodeForPath($path);
1129
1130        if (!$node instanceof IACL) {
1131            throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method');
1132        }
1133
1134        $oldAcl = $this->getACL($node);
1135
1136        $supportedPrivileges = $this->getFlatPrivilegeSet($node);
1137
1138        /* Checking if protected principals from the existing principal set are
1139           not overwritten. */
1140        foreach ($oldAcl as $oldAce) {
1141            if (!isset($oldAce['protected']) || !$oldAce['protected']) {
1142                continue;
1143            }
1144
1145            $found = false;
1146            foreach ($newAcl as $newAce) {
1147                if (
1148                    $newAce['privilege'] === $oldAce['privilege'] &&
1149                    $newAce['principal'] === $oldAce['principal'] &&
1150                    $newAce['protected']
1151                ) {
1152                    $found = true;
1153                }
1154            }
1155
1156            if (!$found) {
1157                throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request');
1158            }
1159        }
1160
1161        foreach ($newAcl as $newAce) {
1162            // Do we recognize the privilege
1163            if (!isset($supportedPrivileges[$newAce['privilege']])) {
1164                throw new Exception\NotSupportedPrivilege('The privilege you specified ('.$newAce['privilege'].') is not recognized by this server');
1165            }
1166
1167            if ($supportedPrivileges[$newAce['privilege']]['abstract']) {
1168                throw new Exception\NoAbstract('The privilege you specified ('.$newAce['privilege'].') is an abstract privilege');
1169            }
1170
1171            // Looking up the principal
1172            try {
1173                $principal = $this->server->tree->getNodeForPath($newAce['principal']);
1174            } catch (NotFound $e) {
1175                throw new Exception\NotRecognizedPrincipal('The specified principal ('.$newAce['principal'].') does not exist');
1176            }
1177            if (!($principal instanceof IPrincipal)) {
1178                throw new Exception\NotRecognizedPrincipal('The specified uri ('.$newAce['principal'].') is not a principal');
1179            }
1180        }
1181        $node->setACL($newAcl);
1182
1183        $response->setStatus(200);
1184
1185        // Breaking the event chain, because we handled this method.
1186        return false;
1187    }
1188
1189    /* }}} */
1190
1191    /* Reports {{{ */
1192
1193    /**
1194     * The principal-match report is defined in RFC3744, section 9.3.
1195     *
1196     * This report allows a client to figure out based on the current user,
1197     * or a principal URL, the principal URL and principal URLs of groups that
1198     * principal belongs to.
1199     *
1200     * @param string $path
1201     */
1202    protected function principalMatchReport($path, Xml\Request\PrincipalMatchReport $report)
1203    {
1204        $depth = $this->server->getHTTPDepth(0);
1205        if (0 !== $depth) {
1206            throw new BadRequest('The principal-match report is only defined on Depth: 0');
1207        }
1208
1209        $currentPrincipals = $this->getCurrentUserPrincipals();
1210
1211        $result = [];
1212
1213        if (Xml\Request\PrincipalMatchReport::SELF === $report->type) {
1214            // Finding all principals under the request uri that match the
1215            // current principal.
1216            foreach ($currentPrincipals as $currentPrincipal) {
1217                if ($currentPrincipal === $path || 0 === strpos($currentPrincipal, $path.'/')) {
1218                    $result[] = $currentPrincipal;
1219                }
1220            }
1221        } else {
1222            // We need to find all resources that have a property that matches
1223            // one of the current principals.
1224            $candidates = $this->server->getPropertiesForPath(
1225                $path,
1226                [$report->principalProperty],
1227                1
1228            );
1229
1230            foreach ($candidates as $candidate) {
1231                if (!isset($candidate[200][$report->principalProperty])) {
1232                    continue;
1233                }
1234
1235                $hrefs = $candidate[200][$report->principalProperty];
1236
1237                if (!$hrefs instanceof Href) {
1238                    continue;
1239                }
1240
1241                foreach ($hrefs->getHrefs() as $href) {
1242                    if (in_array(trim($href, '/'), $currentPrincipals)) {
1243                        $result[] = $candidate['href'];
1244                        continue 2;
1245                    }
1246                }
1247            }
1248        }
1249
1250        $responses = [];
1251
1252        foreach ($result as $item) {
1253            $properties = [];
1254
1255            if ($report->properties) {
1256                $foo = $this->server->getPropertiesForPath($item, $report->properties);
1257                $foo = $foo[0];
1258                $item = $foo['href'];
1259                unset($foo['href']);
1260                $properties = $foo;
1261            }
1262
1263            $responses[] = new DAV\Xml\Element\Response(
1264                $item,
1265                $properties,
1266                '200'
1267            );
1268        }
1269
1270        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
1271        $this->server->httpResponse->setStatus(207);
1272        $this->server->httpResponse->setBody(
1273            $this->server->xml->write(
1274                '{DAV:}multistatus',
1275                $responses,
1276                $this->server->getBaseUri()
1277            )
1278        );
1279    }
1280
1281    /**
1282     * The expand-property report is defined in RFC3253 section 3.8.
1283     *
1284     * This report is very similar to a standard PROPFIND. The difference is
1285     * that it has the additional ability to look at properties containing a
1286     * {DAV:}href element, follow that property and grab additional elements
1287     * there.
1288     *
1289     * Other rfc's, such as ACL rely on this report, so it made sense to put
1290     * it in this plugin.
1291     *
1292     * @param string                           $path
1293     * @param Xml\Request\ExpandPropertyReport $report
1294     */
1295    protected function expandPropertyReport($path, $report)
1296    {
1297        $depth = $this->server->getHTTPDepth(0);
1298
1299        $result = $this->expandProperties($path, $report->properties, $depth);
1300
1301        $xml = $this->server->xml->write(
1302            '{DAV:}multistatus',
1303            new DAV\Xml\Response\MultiStatus($result),
1304            $this->server->getBaseUri()
1305        );
1306        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
1307        $this->server->httpResponse->setStatus(207);
1308        $this->server->httpResponse->setBody($xml);
1309    }
1310
1311    /**
1312     * This method expands all the properties and returns
1313     * a list with property values.
1314     *
1315     * @param array $path
1316     * @param array $requestedProperties the list of required properties
1317     * @param int   $depth
1318     *
1319     * @return array
1320     */
1321    protected function expandProperties($path, array $requestedProperties, $depth)
1322    {
1323        $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth);
1324
1325        $result = [];
1326
1327        foreach ($foundProperties as $node) {
1328            foreach ($requestedProperties as $propertyName => $childRequestedProperties) {
1329                // We're only traversing if sub-properties were requested
1330                if (!is_array($childRequestedProperties) || 0 === count($childRequestedProperties)) {
1331                    continue;
1332                }
1333
1334                // We only have to do the expansion if the property was found
1335                // and it contains an href element.
1336                if (!array_key_exists($propertyName, $node[200])) {
1337                    continue;
1338                }
1339
1340                if (!$node[200][$propertyName] instanceof DAV\Xml\Property\Href) {
1341                    continue;
1342                }
1343
1344                $childHrefs = $node[200][$propertyName]->getHrefs();
1345                $childProps = [];
1346
1347                foreach ($childHrefs as $href) {
1348                    // Gathering the result of the children
1349                    $childProps[] = [
1350                        'name' => '{DAV:}response',
1351                        'value' => $this->expandProperties($href, $childRequestedProperties, 0)[0],
1352                    ];
1353                }
1354
1355                // Replacing the property with its expanded form.
1356                $node[200][$propertyName] = $childProps;
1357            }
1358            $result[] = new DAV\Xml\Element\Response($node['href'], $node);
1359        }
1360
1361        return $result;
1362    }
1363
1364    /**
1365     * principalSearchPropertySetReport.
1366     *
1367     * This method responsible for handing the
1368     * {DAV:}principal-search-property-set report. This report returns a list
1369     * of properties the client may search on, using the
1370     * {DAV:}principal-property-search report.
1371     *
1372     * @param string                                       $path
1373     * @param Xml\Request\PrincipalSearchPropertySetReport $report
1374     */
1375    protected function principalSearchPropertySetReport($path, $report)
1376    {
1377        $httpDepth = $this->server->getHTTPDepth(0);
1378        if (0 !== $httpDepth) {
1379            throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0');
1380        }
1381
1382        $writer = $this->server->xml->getWriter();
1383        $writer->openMemory();
1384        $writer->startDocument();
1385
1386        $writer->startElement('{DAV:}principal-search-property-set');
1387
1388        foreach ($this->principalSearchPropertySet as $propertyName => $description) {
1389            $writer->startElement('{DAV:}principal-search-property');
1390            $writer->startElement('{DAV:}prop');
1391
1392            $writer->writeElement($propertyName);
1393
1394            $writer->endElement(); // prop
1395
1396            if ($description) {
1397                $writer->write([[
1398                    'name' => '{DAV:}description',
1399                    'value' => $description,
1400                    'attributes' => ['xml:lang' => 'en'],
1401                ]]);
1402            }
1403
1404            $writer->endElement(); // principal-search-property
1405        }
1406
1407        $writer->endElement(); // principal-search-property-set
1408
1409        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
1410        $this->server->httpResponse->setStatus(200);
1411        $this->server->httpResponse->setBody($writer->outputMemory());
1412    }
1413
1414    /**
1415     * principalPropertySearchReport.
1416     *
1417     * This method is responsible for handing the
1418     * {DAV:}principal-property-search report. This report can be used for
1419     * clients to search for groups of principals, based on the value of one
1420     * or more properties.
1421     *
1422     * @param string $path
1423     */
1424    protected function principalPropertySearchReport($path, Xml\Request\PrincipalPropertySearchReport $report)
1425    {
1426        if ($report->applyToPrincipalCollectionSet) {
1427            $path = null;
1428        }
1429        if (0 !== $this->server->getHttpDepth('0')) {
1430            throw new BadRequest('Depth must be 0');
1431        }
1432        $result = $this->principalSearch(
1433            $report->searchProperties,
1434            $report->properties,
1435            $path,
1436            $report->test
1437        );
1438
1439        $prefer = $this->server->getHTTPPrefer();
1440
1441        $this->server->httpResponse->setStatus(207);
1442        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
1443        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
1444        $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
1445    }
1446
1447    /**
1448     * aclPrincipalPropSet REPORT.
1449     *
1450     * This method is responsible for handling the {DAV:}acl-principal-prop-set
1451     * REPORT, as defined in:
1452     *
1453     * https://tools.ietf.org/html/rfc3744#section-9.2
1454     *
1455     * This REPORT allows a user to quickly fetch information about all
1456     * principals specified in the access control list. Most commonly this
1457     * is used to for example generate a UI with ACL rules, allowing you
1458     * to show names for principals for every entry.
1459     *
1460     * @param string $path
1461     */
1462    protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report)
1463    {
1464        if (0 !== $this->server->getHTTPDepth(0)) {
1465            throw new BadRequest('The {DAV:}acl-principal-prop-set REPORT only supports Depth 0');
1466        }
1467
1468        // Fetching ACL rules for the given path. We're using the property
1469        // API and not the local getACL, because it will ensure that all
1470        // business rules and restrictions are applied.
1471        $acl = $this->server->getProperties($path, '{DAV:}acl');
1472
1473        if (!$acl || !isset($acl['{DAV:}acl'])) {
1474            throw new Forbidden('Could not fetch ACL rules for this path');
1475        }
1476
1477        $principals = [];
1478        foreach ($acl['{DAV:}acl']->getPrivileges() as $ace) {
1479            if ('{' === $ace['principal'][0]) {
1480                // It's not a principal, it's one of the special rules such as {DAV:}authenticated
1481                continue;
1482            }
1483
1484            $principals[] = $ace['principal'];
1485        }
1486
1487        $properties = $this->server->getPropertiesForMultiplePaths(
1488            $principals,
1489            $report->properties
1490        );
1491
1492        $this->server->httpResponse->setStatus(207);
1493        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
1494        $this->server->httpResponse->setBody(
1495            $this->server->generateMultiStatus($properties)
1496        );
1497    }
1498
1499    /* }}} */
1500
1501    /**
1502     * This method is used to generate HTML output for the
1503     * DAV\Browser\Plugin. This allows us to generate an interface users
1504     * can use to create new calendars.
1505     *
1506     * @param string $output
1507     *
1508     * @return bool
1509     */
1510    public function htmlActionsPanel(DAV\INode $node, &$output)
1511    {
1512        if (!$node instanceof PrincipalCollection) {
1513            return;
1514        }
1515
1516        $output .= '<tr><td colspan="2"><form method="post" action="">
1517            <h3>Create new principal</h3>
1518            <input type="hidden" name="sabreAction" value="mkcol" />
1519            <input type="hidden" name="resourceType" value="{DAV:}principal" />
1520            <label>Name (uri):</label> <input type="text" name="name" /><br />
1521            <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
1522            <label>Email address:</label> <input type="text" name="{http://sabredav*DOT*org/ns}email-address" /><br />
1523            <input type="submit" value="create" />
1524            </form>
1525            </td></tr>';
1526
1527        return false;
1528    }
1529
1530    /**
1531     * Returns a bunch of meta-data about the plugin.
1532     *
1533     * Providing this information is optional, and is mainly displayed by the
1534     * Browser plugin.
1535     *
1536     * The description key in the returned array may contain html and will not
1537     * be sanitized.
1538     *
1539     * @return array
1540     */
1541    public function getPluginInfo()
1542    {
1543        return [
1544            'name' => $this->getPluginName(),
1545            'description' => 'Adds support for WebDAV ACL (rfc3744)',
1546            'link' => 'http://sabre.io/dav/acl/',
1547        ];
1548    }
1549}
1550