SharingPlugin.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\CalDAV;
  4. use Sabre\DAV;
  5. use Sabre\DAV\Xml\Property\LocalHref;
  6. use Sabre\HTTP\RequestInterface;
  7. use Sabre\HTTP\ResponseInterface;
  8. /**
  9. * This plugin implements support for caldav sharing.
  10. *
  11. * This spec is defined at:
  12. * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
  13. *
  14. * See:
  15. * Sabre\CalDAV\Backend\SharingSupport for all the documentation.
  16. *
  17. * Note: This feature is experimental, and may change in between different
  18. * SabreDAV versions.
  19. *
  20. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  21. * @author Evert Pot (http://evertpot.com/)
  22. * @license http://sabre.io/license/ Modified BSD License
  23. */
  24. class SharingPlugin extends DAV\ServerPlugin
  25. {
  26. /**
  27. * Reference to SabreDAV server object.
  28. *
  29. * @var DAV\Server
  30. */
  31. protected $server;
  32. /**
  33. * This method should return a list of server-features.
  34. *
  35. * This is for example 'versioning' and is added to the DAV: header
  36. * in an OPTIONS response.
  37. *
  38. * @return array
  39. */
  40. public function getFeatures()
  41. {
  42. return ['calendarserver-sharing'];
  43. }
  44. /**
  45. * Returns a plugin name.
  46. *
  47. * Using this name other plugins will be able to access other plugins
  48. * using Sabre\DAV\Server::getPlugin
  49. *
  50. * @return string
  51. */
  52. public function getPluginName()
  53. {
  54. return 'caldav-sharing';
  55. }
  56. /**
  57. * This initializes the plugin.
  58. *
  59. * This function is called by Sabre\DAV\Server, after
  60. * addPlugin is called.
  61. *
  62. * This method should set up the required event subscriptions.
  63. */
  64. public function initialize(DAV\Server $server)
  65. {
  66. $this->server = $server;
  67. if (is_null($this->server->getPlugin('sharing'))) {
  68. throw new \LogicException('The generic "sharing" plugin must be loaded before the caldav sharing plugin. Call $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); before this one.');
  69. }
  70. array_push(
  71. $this->server->protectedProperties,
  72. '{'.Plugin::NS_CALENDARSERVER.'}invite',
  73. '{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes',
  74. '{'.Plugin::NS_CALENDARSERVER.'}shared-url'
  75. );
  76. $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share';
  77. $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply';
  78. $this->server->on('propFind', [$this, 'propFindEarly']);
  79. $this->server->on('propFind', [$this, 'propFindLate'], 150);
  80. $this->server->on('propPatch', [$this, 'propPatch'], 40);
  81. $this->server->on('method:POST', [$this, 'httpPost']);
  82. }
  83. /**
  84. * This event is triggered when properties are requested for a certain
  85. * node.
  86. *
  87. * This allows us to inject any properties early.
  88. */
  89. public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
  90. {
  91. if ($node instanceof ISharedCalendar) {
  92. $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) {
  93. return new Xml\Property\Invite(
  94. $node->getInvites()
  95. );
  96. });
  97. }
  98. }
  99. /**
  100. * This method is triggered *after* all properties have been retrieved.
  101. * This allows us to inject the correct resourcetype for calendars that
  102. * have been shared.
  103. */
  104. public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
  105. {
  106. if ($node instanceof ISharedCalendar) {
  107. $shareAccess = $node->getShareAccess();
  108. if ($rt = $propFind->get('{DAV:}resourcetype')) {
  109. switch ($shareAccess) {
  110. case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER:
  111. $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner');
  112. break;
  113. case \Sabre\DAV\Sharing\Plugin::ACCESS_READ:
  114. case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE:
  115. $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared');
  116. break;
  117. }
  118. }
  119. $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', function () {
  120. return new Xml\Property\AllowedSharingModes(true, false);
  121. });
  122. }
  123. }
  124. /**
  125. * This method is trigged when a user attempts to update a node's
  126. * properties.
  127. *
  128. * A previous draft of the sharing spec stated that it was possible to use
  129. * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
  130. * the calendar.
  131. *
  132. * Even though this is no longer in the current spec, we keep this around
  133. * because OS X 10.7 may still make use of this feature.
  134. *
  135. * @param string $path
  136. */
  137. public function propPatch($path, DAV\PropPatch $propPatch)
  138. {
  139. $node = $this->server->tree->getNodeForPath($path);
  140. if (!$node instanceof ISharedCalendar) {
  141. return;
  142. }
  143. if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) {
  144. $propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) {
  145. if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) {
  146. return false;
  147. }
  148. $shares = $node->getInvites();
  149. foreach ($shares as $share) {
  150. $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS;
  151. }
  152. $node->updateInvites($shares);
  153. return true;
  154. });
  155. }
  156. }
  157. /**
  158. * We intercept this to handle POST requests on calendars.
  159. *
  160. * @return bool|null
  161. */
  162. public function httpPost(RequestInterface $request, ResponseInterface $response)
  163. {
  164. $path = $request->getPath();
  165. // Only handling xml
  166. $contentType = $request->getHeader('Content-Type');
  167. if (null === $contentType) {
  168. return;
  169. }
  170. if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) {
  171. return;
  172. }
  173. // Making sure the node exists
  174. try {
  175. $node = $this->server->tree->getNodeForPath($path);
  176. } catch (DAV\Exception\NotFound $e) {
  177. return;
  178. }
  179. $requestBody = $request->getBodyAsString();
  180. // If this request handler could not deal with this POST request, it
  181. // will return 'null' and other plugins get a chance to handle the
  182. // request.
  183. //
  184. // However, we already requested the full body. This is a problem,
  185. // because a body can only be read once. This is why we preemptively
  186. // re-populated the request body with the existing data.
  187. $request->setBody($requestBody);
  188. $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
  189. switch ($documentType) {
  190. // Both the DAV:share-resource and CALENDARSERVER:share requests
  191. // behave identically.
  192. case '{'.Plugin::NS_CALENDARSERVER.'}share':
  193. $sharingPlugin = $this->server->getPlugin('sharing');
  194. $sharingPlugin->shareResource($path, $message->sharees);
  195. $response->setStatus(200);
  196. // Adding this because sending a response body may cause issues,
  197. // and I wanted some type of indicator the response was handled.
  198. $response->setHeader('X-Sabre-Status', 'everything-went-well');
  199. // Breaking the event chain
  200. return false;
  201. // The invite-reply document is sent when the user replies to an
  202. // invitation of a calendar share.
  203. case '{'.Plugin::NS_CALENDARSERVER.'}invite-reply':
  204. // This only works on the calendar-home-root node.
  205. if (!$node instanceof CalendarHome) {
  206. return;
  207. }
  208. $this->server->transactionType = 'post-invite-reply';
  209. // Getting ACL info
  210. $acl = $this->server->getPlugin('acl');
  211. // If there's no ACL support, we allow everything
  212. if ($acl) {
  213. $acl->checkPrivileges($path, '{DAV:}write');
  214. }
  215. $url = $node->shareReply(
  216. $message->href,
  217. $message->status,
  218. $message->calendarUri,
  219. $message->inReplyTo,
  220. $message->summary
  221. );
  222. $response->setStatus(200);
  223. // Adding this because sending a response body may cause issues,
  224. // and I wanted some type of indicator the response was handled.
  225. $response->setHeader('X-Sabre-Status', 'everything-went-well');
  226. if ($url) {
  227. $writer = $this->server->xml->getWriter();
  228. $writer->contextUri = $request->getUrl();
  229. $writer->openMemory();
  230. $writer->startDocument();
  231. $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as');
  232. $writer->write(new LocalHref($url));
  233. $writer->endElement();
  234. $response->setHeader('Content-Type', 'application/xml');
  235. $response->setBody($writer->outputMemory());
  236. }
  237. // Breaking the event chain
  238. return false;
  239. case '{'.Plugin::NS_CALENDARSERVER.'}publish-calendar':
  240. // We can only deal with IShareableCalendar objects
  241. if (!$node instanceof ISharedCalendar) {
  242. return;
  243. }
  244. $this->server->transactionType = 'post-publish-calendar';
  245. // Getting ACL info
  246. $acl = $this->server->getPlugin('acl');
  247. // If there's no ACL support, we allow everything
  248. if ($acl) {
  249. $acl->checkPrivileges($path, '{DAV:}share');
  250. }
  251. $node->setPublishStatus(true);
  252. // iCloud sends back the 202, so we will too.
  253. $response->setStatus(202);
  254. // Adding this because sending a response body may cause issues,
  255. // and I wanted some type of indicator the response was handled.
  256. $response->setHeader('X-Sabre-Status', 'everything-went-well');
  257. // Breaking the event chain
  258. return false;
  259. case '{'.Plugin::NS_CALENDARSERVER.'}unpublish-calendar':
  260. // We can only deal with IShareableCalendar objects
  261. if (!$node instanceof ISharedCalendar) {
  262. return;
  263. }
  264. $this->server->transactionType = 'post-unpublish-calendar';
  265. // Getting ACL info
  266. $acl = $this->server->getPlugin('acl');
  267. // If there's no ACL support, we allow everything
  268. if ($acl) {
  269. $acl->checkPrivileges($path, '{DAV:}share');
  270. }
  271. $node->setPublishStatus(false);
  272. $response->setStatus(200);
  273. // Adding this because sending a response body may cause issues,
  274. // and I wanted some type of indicator the response was handled.
  275. $response->setHeader('X-Sabre-Status', 'everything-went-well');
  276. // Breaking the event chain
  277. return false;
  278. }
  279. }
  280. /**
  281. * Returns a bunch of meta-data about the plugin.
  282. *
  283. * Providing this information is optional, and is mainly displayed by the
  284. * Browser plugin.
  285. *
  286. * The description key in the returned array may contain html and will not
  287. * be sanitized.
  288. *
  289. * @return array
  290. */
  291. public function getPluginInfo()
  292. {
  293. return [
  294. 'name' => $this->getPluginName(),
  295. 'description' => 'Adds support for caldav-sharing.',
  296. 'link' => 'http://sabre.io/dav/caldav-sharing/',
  297. ];
  298. }
  299. }