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