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