Plugin.php 51 KB

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