SharingPlugin.php 14 KB

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