PageRenderTime 52ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sabre/DAVACL/Plugin.php

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