Plugin.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\CalDAV;
  4. use DateTimeZone;
  5. use Sabre\CalDAV\Xml\Request\CalendarMultiGetReport;
  6. use Sabre\DAV;
  7. use Sabre\DAV\Exception\BadRequest;
  8. use Sabre\DAV\INode;
  9. use Sabre\DAV\MkCol;
  10. use Sabre\DAV\Xml\Property\LocalHref;
  11. use Sabre\DAVACL;
  12. use Sabre\HTTP;
  13. use Sabre\HTTP\RequestInterface;
  14. use Sabre\HTTP\ResponseInterface;
  15. use Sabre\Uri;
  16. use Sabre\VObject;
  17. /**
  18. * CalDAV plugin.
  19. *
  20. * This plugin provides functionality added by CalDAV (RFC 4791)
  21. * It implements new reports, and the MKCALENDAR method.
  22. *
  23. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  24. * @author Evert Pot (http://evertpot.com/)
  25. * @license http://sabre.io/license/ Modified BSD License
  26. */
  27. class Plugin extends DAV\ServerPlugin
  28. {
  29. /**
  30. * This is the official CalDAV namespace.
  31. */
  32. const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
  33. /**
  34. * This is the namespace for the proprietary calendarserver extensions.
  35. */
  36. const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
  37. /**
  38. * The hardcoded root for calendar objects. It is unfortunate
  39. * that we're stuck with it, but it will have to do for now.
  40. */
  41. const CALENDAR_ROOT = 'calendars';
  42. /**
  43. * Reference to server object.
  44. *
  45. * @var DAV\Server
  46. */
  47. protected $server;
  48. /**
  49. * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
  50. * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
  51. * capping it to 10M here.
  52. */
  53. protected $maxResourceSize = 10000000;
  54. /**
  55. * Use this method to tell the server this plugin defines additional
  56. * HTTP methods.
  57. *
  58. * This method is passed a uri. It should only return HTTP methods that are
  59. * available for the specified uri.
  60. *
  61. * @param string $uri
  62. *
  63. * @return array
  64. */
  65. public function getHTTPMethods($uri)
  66. {
  67. // The MKCALENDAR is only available on unmapped uri's, whose
  68. // parents extend IExtendedCollection
  69. list($parent, $name) = Uri\split($uri);
  70. if ('' === $uri) {
  71. $parent = '';
  72. }
  73. $node = $this->server->tree->getNodeForPath($parent);
  74. if ($node instanceof DAV\IExtendedCollection) {
  75. try {
  76. $node->getChild($name);
  77. } catch (DAV\Exception\NotFound $e) {
  78. return ['MKCALENDAR'];
  79. }
  80. }
  81. return [];
  82. }
  83. /**
  84. * Returns the path to a principal's calendar home.
  85. *
  86. * The return url must not end with a slash.
  87. * This function should return null in case a principal did not have
  88. * a calendar home.
  89. *
  90. * @param string $principalUrl
  91. *
  92. * @return string
  93. */
  94. public function getCalendarHomeForPrincipal($principalUrl)
  95. {
  96. // The default behavior for most sabre/dav servers is that there is a
  97. // principals root node, which contains users directly under it.
  98. //
  99. // This function assumes that there are two components in a principal
  100. // path. If there's more, we don't return a calendar home. This
  101. // excludes things like the calendar-proxy-read principal (which it
  102. // should).
  103. $parts = explode('/', trim($principalUrl, '/'));
  104. if (2 !== count($parts)) {
  105. return;
  106. }
  107. if ('principals' !== $parts[0]) {
  108. return;
  109. }
  110. return self::CALENDAR_ROOT.'/'.$parts[1];
  111. }
  112. /**
  113. * Returns a list of features for the DAV: HTTP header.
  114. *
  115. * @return array
  116. */
  117. public function getFeatures()
  118. {
  119. return ['calendar-access', 'calendar-proxy'];
  120. }
  121. /**
  122. * Returns a plugin name.
  123. *
  124. * Using this name other plugins will be able to access other plugins
  125. * using DAV\Server::getPlugin
  126. *
  127. * @return string
  128. */
  129. public function getPluginName()
  130. {
  131. return 'caldav';
  132. }
  133. /**
  134. * Returns a list of reports this plugin supports.
  135. *
  136. * This will be used in the {DAV:}supported-report-set property.
  137. * Note that you still need to subscribe to the 'report' event to actually
  138. * implement them
  139. *
  140. * @param string $uri
  141. *
  142. * @return array
  143. */
  144. public function getSupportedReportSet($uri)
  145. {
  146. $node = $this->server->tree->getNodeForPath($uri);
  147. $reports = [];
  148. if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) {
  149. $reports[] = '{'.self::NS_CALDAV.'}calendar-multiget';
  150. $reports[] = '{'.self::NS_CALDAV.'}calendar-query';
  151. }
  152. if ($node instanceof ICalendar) {
  153. $reports[] = '{'.self::NS_CALDAV.'}free-busy-query';
  154. }
  155. // iCal has a bug where it assumes that sync support is enabled, only
  156. // if we say we support it on the calendar-home, even though this is
  157. // not actually the case.
  158. if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) {
  159. $reports[] = '{DAV:}sync-collection';
  160. }
  161. return $reports;
  162. }
  163. /**
  164. * Initializes the plugin.
  165. */
  166. public function initialize(DAV\Server $server)
  167. {
  168. $this->server = $server;
  169. $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']);
  170. $server->on('report', [$this, 'report']);
  171. $server->on('propFind', [$this, 'propFind']);
  172. $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
  173. $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
  174. $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
  175. $server->on('afterMethod:GET', [$this, 'httpAfterGET']);
  176. $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
  177. $server->xml->namespaceMap[self::NS_CALDAV] = 'cal';
  178. $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
  179. $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
  180. $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
  181. $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
  182. $server->xml->elementMap['{'.self::NS_CALDAV.'}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
  183. $server->xml->elementMap['{'.self::NS_CALDAV.'}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
  184. $server->xml->elementMap['{'.self::NS_CALDAV.'}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
  185. $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
  186. $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
  187. $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
  188. $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
  189. array_push($server->protectedProperties,
  190. '{'.self::NS_CALDAV.'}supported-calendar-component-set',
  191. '{'.self::NS_CALDAV.'}supported-calendar-data',
  192. '{'.self::NS_CALDAV.'}max-resource-size',
  193. '{'.self::NS_CALDAV.'}min-date-time',
  194. '{'.self::NS_CALDAV.'}max-date-time',
  195. '{'.self::NS_CALDAV.'}max-instances',
  196. '{'.self::NS_CALDAV.'}max-attendees-per-instance',
  197. '{'.self::NS_CALDAV.'}calendar-home-set',
  198. '{'.self::NS_CALDAV.'}supported-collation-set',
  199. '{'.self::NS_CALDAV.'}calendar-data',
  200. // CalendarServer extensions
  201. '{'.self::NS_CALENDARSERVER.'}getctag',
  202. '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for',
  203. '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for'
  204. );
  205. if ($aclPlugin = $server->getPlugin('acl')) {
  206. $aclPlugin->principalSearchPropertySet['{'.self::NS_CALDAV.'}calendar-user-address-set'] = 'Calendar address';
  207. }
  208. }
  209. /**
  210. * This functions handles REPORT requests specific to CalDAV.
  211. *
  212. * @param string $reportName
  213. * @param mixed $report
  214. * @param mixed $path
  215. *
  216. * @return bool|null
  217. */
  218. public function report($reportName, $report, $path)
  219. {
  220. switch ($reportName) {
  221. case '{'.self::NS_CALDAV.'}calendar-multiget':
  222. $this->server->transactionType = 'report-calendar-multiget';
  223. $this->calendarMultiGetReport($report);
  224. return false;
  225. case '{'.self::NS_CALDAV.'}calendar-query':
  226. $this->server->transactionType = 'report-calendar-query';
  227. $this->calendarQueryReport($report);
  228. return false;
  229. case '{'.self::NS_CALDAV.'}free-busy-query':
  230. $this->server->transactionType = 'report-free-busy-query';
  231. $this->freeBusyQueryReport($report);
  232. return false;
  233. }
  234. }
  235. /**
  236. * This function handles the MKCALENDAR HTTP method, which creates
  237. * a new calendar.
  238. *
  239. * @return bool
  240. */
  241. public function httpMkCalendar(RequestInterface $request, ResponseInterface $response)
  242. {
  243. $body = $request->getBodyAsString();
  244. $path = $request->getPath();
  245. $properties = [];
  246. if ($body) {
  247. try {
  248. $mkcalendar = $this->server->xml->expect(
  249. '{urn:ietf:params:xml:ns:caldav}mkcalendar',
  250. $body
  251. );
  252. } catch (\Sabre\Xml\ParseException $e) {
  253. throw new BadRequest($e->getMessage(), 0, $e);
  254. }
  255. $properties = $mkcalendar->getProperties();
  256. }
  257. // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored
  258. // subscriptions. Before that it used MKCOL which was the correct way
  259. // to do this.
  260. //
  261. // If the body had a {DAV:}resourcetype, it means we stumbled upon this
  262. // request, and we simply use it instead of the pre-defined list.
  263. if (isset($properties['{DAV:}resourcetype'])) {
  264. $resourceType = $properties['{DAV:}resourcetype']->getValue();
  265. } else {
  266. $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar'];
  267. }
  268. $this->server->createCollection($path, new MkCol($resourceType, $properties));
  269. $response->setStatus(201);
  270. $response->setHeader('Content-Length', 0);
  271. // This breaks the method chain.
  272. return false;
  273. }
  274. /**
  275. * PropFind.
  276. *
  277. * This method handler is invoked before any after properties for a
  278. * resource are fetched. This allows us to add in any CalDAV specific
  279. * properties.
  280. */
  281. public function propFind(DAV\PropFind $propFind, DAV\INode $node)
  282. {
  283. $ns = '{'.self::NS_CALDAV.'}';
  284. if ($node instanceof ICalendarObjectContainer) {
  285. $propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
  286. $propFind->handle($ns.'supported-calendar-data', function () {
  287. return new Xml\Property\SupportedCalendarData();
  288. });
  289. $propFind->handle($ns.'supported-collation-set', function () {
  290. return new Xml\Property\SupportedCollationSet();
  291. });
  292. }
  293. if ($node instanceof DAVACL\IPrincipal) {
  294. $principalUrl = $node->getPrincipalUrl();
  295. $propFind->handle('{'.self::NS_CALDAV.'}calendar-home-set', function () use ($principalUrl) {
  296. $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl);
  297. if (is_null($calendarHomePath)) {
  298. return null;
  299. }
  300. return new LocalHref($calendarHomePath.'/');
  301. });
  302. // The calendar-user-address-set property is basically mapped to
  303. // the {DAV:}alternate-URI-set property.
  304. $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-address-set', function () use ($node) {
  305. $addresses = $node->getAlternateUriSet();
  306. $addresses[] = $this->server->getBaseUri().$node->getPrincipalUrl().'/';
  307. return new LocalHref($addresses);
  308. });
  309. // For some reason somebody thought it was a good idea to add
  310. // another one of these properties. We're supporting it too.
  311. $propFind->handle('{'.self::NS_CALENDARSERVER.'}email-address-set', function () use ($node) {
  312. $addresses = $node->getAlternateUriSet();
  313. $emails = [];
  314. foreach ($addresses as $address) {
  315. if ('mailto:' === substr($address, 0, 7)) {
  316. $emails[] = substr($address, 7);
  317. }
  318. }
  319. return new Xml\Property\EmailAddressSet($emails);
  320. });
  321. // These two properties are shortcuts for ical to easily find
  322. // other principals this principal has access to.
  323. $propRead = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for';
  324. $propWrite = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for';
  325. if (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) {
  326. $aclPlugin = $this->server->getPlugin('acl');
  327. $membership = $aclPlugin->getPrincipalMembership($propFind->getPath());
  328. $readList = [];
  329. $writeList = [];
  330. foreach ($membership as $group) {
  331. $groupNode = $this->server->tree->getNodeForPath($group);
  332. $listItem = Uri\split($group)[0].'/';
  333. // If the node is either ap proxy-read or proxy-write
  334. // group, we grab the parent principal and add it to the
  335. // list.
  336. if ($groupNode instanceof Principal\IProxyRead) {
  337. $readList[] = $listItem;
  338. }
  339. if ($groupNode instanceof Principal\IProxyWrite) {
  340. $writeList[] = $listItem;
  341. }
  342. }
  343. $propFind->set($propRead, new LocalHref($readList));
  344. $propFind->set($propWrite, new LocalHref($writeList));
  345. }
  346. } // instanceof IPrincipal
  347. if ($node instanceof ICalendarObject) {
  348. // The calendar-data property is not supposed to be a 'real'
  349. // property, but in large chunks of the spec it does act as such.
  350. // Therefore we simply expose it as a property.
  351. $propFind->handle('{'.self::NS_CALDAV.'}calendar-data', function () use ($node) {
  352. $val = $node->get();
  353. if (is_resource($val)) {
  354. $val = stream_get_contents($val);
  355. }
  356. // Taking out \r to not screw up the xml output
  357. return str_replace("\r", '', $val);
  358. });
  359. }
  360. }
  361. /**
  362. * This function handles the calendar-multiget REPORT.
  363. *
  364. * This report is used by the client to fetch the content of a series
  365. * of urls. Effectively avoiding a lot of redundant requests.
  366. *
  367. * @param CalendarMultiGetReport $report
  368. */
  369. public function calendarMultiGetReport($report)
  370. {
  371. $needsJson = 'application/calendar+json' === $report->contentType;
  372. $timeZones = [];
  373. $propertyList = [];
  374. $paths = array_map(
  375. [$this->server, 'calculateUri'],
  376. $report->hrefs
  377. );
  378. foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) {
  379. if (($needsJson || $report->expand) && isset($objProps[200]['{'.self::NS_CALDAV.'}calendar-data'])) {
  380. $vObject = VObject\Reader::read($objProps[200]['{'.self::NS_CALDAV.'}calendar-data']);
  381. if ($report->expand) {
  382. // We're expanding, and for that we need to figure out the
  383. // calendar's timezone.
  384. list($calendarPath) = Uri\split($uri);
  385. if (!isset($timeZones[$calendarPath])) {
  386. // Checking the calendar-timezone property.
  387. $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone';
  388. $tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
  389. if (isset($tzResult[$tzProp])) {
  390. // This property contains a VCALENDAR with a single
  391. // VTIMEZONE.
  392. $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
  393. $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
  394. } else {
  395. // Defaulting to UTC.
  396. $timeZone = new DateTimeZone('UTC');
  397. }
  398. $timeZones[$calendarPath] = $timeZone;
  399. }
  400. $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]);
  401. }
  402. if ($needsJson) {
  403. $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize());
  404. } else {
  405. $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize();
  406. }
  407. // Destroy circular references so PHP will garbage collect the
  408. // object.
  409. $vObject->destroy();
  410. }
  411. $propertyList[] = $objProps;
  412. }
  413. $prefer = $this->server->getHTTPPrefer();
  414. $this->server->httpResponse->setStatus(207);
  415. $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
  416. $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
  417. $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return']));
  418. }
  419. /**
  420. * This function handles the calendar-query REPORT.
  421. *
  422. * This report is used by clients to request calendar objects based on
  423. * complex conditions.
  424. *
  425. * @param Xml\Request\CalendarQueryReport $report
  426. */
  427. public function calendarQueryReport($report)
  428. {
  429. $path = $this->server->getRequestUri();
  430. $needsJson = 'application/calendar+json' === $report->contentType;
  431. $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
  432. $depth = $this->server->getHTTPDepth(0);
  433. // The default result is an empty array
  434. $result = [];
  435. $calendarTimeZone = null;
  436. if ($report->expand) {
  437. // We're expanding, and for that we need to figure out the
  438. // calendar's timezone.
  439. $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone';
  440. $tzResult = $this->server->getProperties($path, [$tzProp]);
  441. if (isset($tzResult[$tzProp])) {
  442. // This property contains a VCALENDAR with a single
  443. // VTIMEZONE.
  444. $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
  445. $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
  446. // Destroy circular references so PHP will garbage collect the
  447. // object.
  448. $vtimezoneObj->destroy();
  449. } else {
  450. // Defaulting to UTC.
  451. $calendarTimeZone = new DateTimeZone('UTC');
  452. }
  453. }
  454. // The calendarobject was requested directly. In this case we handle
  455. // this locally.
  456. if (0 == $depth && $node instanceof ICalendarObject) {
  457. $requestedCalendarData = true;
  458. $requestedProperties = $report->properties;
  459. if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
  460. // We always retrieve calendar-data, as we need it for filtering.
  461. $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
  462. // If calendar-data wasn't explicitly requested, we need to remove
  463. // it after processing.
  464. $requestedCalendarData = false;
  465. }
  466. $properties = $this->server->getPropertiesForPath(
  467. $path,
  468. $requestedProperties,
  469. 0
  470. );
  471. // This array should have only 1 element, the first calendar
  472. // object.
  473. $properties = current($properties);
  474. // If there wasn't any calendar-data returned somehow, we ignore
  475. // this.
  476. if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
  477. $validator = new CalendarQueryValidator();
  478. $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
  479. if ($validator->validate($vObject, $report->filters)) {
  480. // If the client didn't require the calendar-data property,
  481. // we won't give it back.
  482. if (!$requestedCalendarData) {
  483. unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
  484. } else {
  485. if ($report->expand) {
  486. $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
  487. }
  488. if ($needsJson) {
  489. $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize());
  490. } elseif ($report->expand) {
  491. $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize();
  492. }
  493. }
  494. $result = [$properties];
  495. }
  496. // Destroy circular references so PHP will garbage collect the
  497. // object.
  498. $vObject->destroy();
  499. }
  500. }
  501. if ($node instanceof ICalendarObjectContainer && 0 === $depth) {
  502. if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) {
  503. // Microsoft clients incorrectly supplied depth as 0, when it actually
  504. // should have set depth to 1. We're implementing a workaround here
  505. // to deal with this.
  506. //
  507. // This targets at least the following clients:
  508. // Windows 10
  509. // Windows Phone 8, 10
  510. $depth = 1;
  511. } else {
  512. throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
  513. }
  514. }
  515. // If we're dealing with a calendar, the calendar itself is responsible
  516. // for the calendar-query.
  517. if ($node instanceof ICalendarObjectContainer && 1 == $depth) {
  518. $nodePaths = $node->calendarQuery($report->filters);
  519. foreach ($nodePaths as $path) {
  520. list($properties) =
  521. $this->server->getPropertiesForPath($this->server->getRequestUri().'/'.$path, $report->properties);
  522. if (($needsJson || $report->expand)) {
  523. $vObject = VObject\Reader::read($properties[200]['{'.self::NS_CALDAV.'}calendar-data']);
  524. if ($report->expand) {
  525. $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
  526. }
  527. if ($needsJson) {
  528. $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize());
  529. } else {
  530. $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize();
  531. }
  532. // Destroy circular references so PHP will garbage collect the
  533. // object.
  534. $vObject->destroy();
  535. }
  536. $result[] = $properties;
  537. }
  538. }
  539. $prefer = $this->server->getHTTPPrefer();
  540. $this->server->httpResponse->setStatus(207);
  541. $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
  542. $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
  543. $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
  544. }
  545. /**
  546. * This method is responsible for parsing the request and generating the
  547. * response for the CALDAV:free-busy-query REPORT.
  548. */
  549. protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report)
  550. {
  551. $uri = $this->server->getRequestUri();
  552. $acl = $this->server->getPlugin('acl');
  553. if ($acl) {
  554. $acl->checkPrivileges($uri, '{'.self::NS_CALDAV.'}read-free-busy');
  555. }
  556. $calendar = $this->server->tree->getNodeForPath($uri);
  557. if (!$calendar instanceof ICalendar) {
  558. throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
  559. }
  560. $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone';
  561. // Figuring out the default timezone for the calendar, for floating
  562. // times.
  563. $calendarProps = $this->server->getProperties($uri, [$tzProp]);
  564. if (isset($calendarProps[$tzProp])) {
  565. $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]);
  566. $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
  567. // Destroy circular references so PHP will garbage collect the object.
  568. $vtimezoneObj->destroy();
  569. } else {
  570. $calendarTimeZone = new DateTimeZone('UTC');
  571. }
  572. // Doing a calendar-query first, to make sure we get the most
  573. // performance.
  574. $urls = $calendar->calendarQuery([
  575. 'name' => 'VCALENDAR',
  576. 'comp-filters' => [
  577. [
  578. 'name' => 'VEVENT',
  579. 'comp-filters' => [],
  580. 'prop-filters' => [],
  581. 'is-not-defined' => false,
  582. 'time-range' => [
  583. 'start' => $report->start,
  584. 'end' => $report->end,
  585. ],
  586. ],
  587. ],
  588. 'prop-filters' => [],
  589. 'is-not-defined' => false,
  590. 'time-range' => null,
  591. ]);
  592. $objects = array_map(function ($url) use ($calendar) {
  593. $obj = $calendar->getChild($url)->get();
  594. return $obj;
  595. }, $urls);
  596. $generator = new VObject\FreeBusyGenerator();
  597. $generator->setObjects($objects);
  598. $generator->setTimeRange($report->start, $report->end);
  599. $generator->setTimeZone($calendarTimeZone);
  600. $result = $generator->getResult();
  601. $result = $result->serialize();
  602. $this->server->httpResponse->setStatus(200);
  603. $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
  604. $this->server->httpResponse->setHeader('Content-Length', strlen($result));
  605. $this->server->httpResponse->setBody($result);
  606. }
  607. /**
  608. * This method is triggered before a file gets updated with new content.
  609. *
  610. * This plugin uses this method to ensure that CalDAV objects receive
  611. * valid calendar data.
  612. *
  613. * @param string $path
  614. * @param resource $data
  615. * @param bool $modified should be set to true, if this event handler
  616. * changed &$data
  617. */
  618. public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
  619. {
  620. if (!$node instanceof ICalendarObject) {
  621. return;
  622. }
  623. // We're onyl interested in ICalendarObject nodes that are inside of a
  624. // real calendar. This is to avoid triggering validation and scheduling
  625. // for non-calendars (such as an inbox).
  626. list($parent) = Uri\split($path);
  627. $parentNode = $this->server->tree->getNodeForPath($parent);
  628. if (!$parentNode instanceof ICalendar) {
  629. return;
  630. }
  631. $this->validateICalendar(
  632. $data,
  633. $path,
  634. $modified,
  635. $this->server->httpRequest,
  636. $this->server->httpResponse,
  637. false
  638. );
  639. }
  640. /**
  641. * This method is triggered before a new file is created.
  642. *
  643. * This plugin uses this method to ensure that newly created calendar
  644. * objects contain valid calendar data.
  645. *
  646. * @param string $path
  647. * @param resource $data
  648. * @param bool $modified should be set to true, if this event handler
  649. * changed &$data
  650. */
  651. public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
  652. {
  653. if (!$parentNode instanceof ICalendar) {
  654. return;
  655. }
  656. $this->validateICalendar(
  657. $data,
  658. $path,
  659. $modified,
  660. $this->server->httpRequest,
  661. $this->server->httpResponse,
  662. true
  663. );
  664. }
  665. /**
  666. * Checks if the submitted iCalendar data is in fact, valid.
  667. *
  668. * An exception is thrown if it's not.
  669. *
  670. * @param resource|string $data
  671. * @param string $path
  672. * @param bool $modified should be set to true, if this event handler
  673. * changed &$data
  674. * @param RequestInterface $request the http request
  675. * @param ResponseInterface $response the http response
  676. * @param bool $isNew is the item a new one, or an update
  677. */
  678. protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew)
  679. {
  680. // If it's a stream, we convert it to a string first.
  681. if (is_resource($data)) {
  682. $data = stream_get_contents($data);
  683. }
  684. $before = $data;
  685. try {
  686. // If the data starts with a [, we can reasonably assume we're dealing
  687. // with a jCal object.
  688. if ('[' === substr($data, 0, 1)) {
  689. $vobj = VObject\Reader::readJson($data);
  690. // Converting $data back to iCalendar, as that's what we
  691. // technically support everywhere.
  692. $data = $vobj->serialize();
  693. $modified = true;
  694. } else {
  695. $vobj = VObject\Reader::read($data);
  696. }
  697. } catch (VObject\ParseException $e) {
  698. throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: '.$e->getMessage());
  699. }
  700. if ('VCALENDAR' !== $vobj->name) {
  701. throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
  702. }
  703. $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
  704. // Get the Supported Components for the target calendar
  705. list($parentPath) = Uri\split($path);
  706. $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
  707. if (isset($calendarProperties[$sCCS])) {
  708. $supportedComponents = $calendarProperties[$sCCS]->getValue();
  709. } else {
  710. $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
  711. }
  712. $foundType = null;
  713. foreach ($vobj->getComponents() as $component) {
  714. switch ($component->name) {
  715. case 'VTIMEZONE':
  716. continue 2;
  717. case 'VEVENT':
  718. case 'VTODO':
  719. case 'VJOURNAL':
  720. $foundType = $component->name;
  721. break;
  722. }
  723. }
  724. if (!$foundType || !in_array($foundType, $supportedComponents)) {
  725. throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents));
  726. }
  727. $options = VObject\Node::PROFILE_CALDAV;
  728. $prefer = $this->server->getHTTPPrefer();
  729. if ('strict' !== $prefer['handling']) {
  730. $options |= VObject\Node::REPAIR;
  731. }
  732. $messages = $vobj->validate($options);
  733. $highestLevel = 0;
  734. $warningMessage = null;
  735. // $messages contains a list of problems with the vcard, along with
  736. // their severity.
  737. foreach ($messages as $message) {
  738. if ($message['level'] > $highestLevel) {
  739. // Recording the highest reported error level.
  740. $highestLevel = $message['level'];
  741. $warningMessage = $message['message'];
  742. }
  743. switch ($message['level']) {
  744. case 1:
  745. // Level 1 means that there was a problem, but it was repaired.
  746. $modified = true;
  747. break;
  748. case 2:
  749. // Level 2 means a warning, but not critical
  750. break;
  751. case 3:
  752. // Level 3 means a critical error
  753. throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']);
  754. }
  755. }
  756. if ($warningMessage) {
  757. $response->setHeader(
  758. 'X-Sabre-Ew-Gross',
  759. 'iCalendar validation warning: '.$warningMessage
  760. );
  761. }
  762. // We use an extra variable to allow event handles to tell us whether
  763. // the object was modified or not.
  764. //
  765. // This helps us determine if we need to re-serialize the object.
  766. $subModified = false;
  767. $this->server->emit(
  768. 'calendarObjectChange',
  769. [
  770. $request,
  771. $response,
  772. $vobj,
  773. $parentPath,
  774. &$subModified,
  775. $isNew,
  776. ]
  777. );
  778. if ($modified || $subModified) {
  779. // An event handler told us that it modified the object.
  780. $data = $vobj->serialize();
  781. // Using md5 to figure out if there was an *actual* change.
  782. if (!$modified && 0 !== strcmp($data, $before)) {
  783. $modified = true;
  784. }
  785. }
  786. // Destroy circular references so PHP will garbage collect the object.
  787. $vobj->destroy();
  788. }
  789. /**
  790. * This method is triggered whenever a subsystem reqeuests the privileges
  791. * that are supported on a particular node.
  792. */
  793. public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
  794. {
  795. if ($node instanceof ICalendar) {
  796. $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [
  797. 'abstract' => false,
  798. 'aggregates' => [],
  799. ];
  800. }
  801. }
  802. /**
  803. * This method is used to generate HTML output for the
  804. * DAV\Browser\Plugin. This allows us to generate an interface users
  805. * can use to create new calendars.
  806. *
  807. * @param string $output
  808. *
  809. * @return bool
  810. */
  811. public function htmlActionsPanel(DAV\INode $node, &$output)
  812. {
  813. if (!$node instanceof CalendarHome) {
  814. return;
  815. }
  816. $output .= '<tr><td colspan="2"><form method="post" action="">
  817. <h3>Create new calendar</h3>
  818. <input type="hidden" name="sabreAction" value="mkcol" />
  819. <input type="hidden" name="resourceType" value="{DAV:}collection,{'.self::NS_CALDAV.'}calendar" />
  820. <label>Name (uri):</label> <input type="text" name="name" /><br />
  821. <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
  822. <input type="submit" value="create" />
  823. </form>
  824. </td></tr>';
  825. return false;
  826. }
  827. /**
  828. * This event is triggered after GET requests.
  829. *
  830. * This is used to transform data into jCal, if this was requested.
  831. */
  832. public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
  833. {
  834. $contentType = $response->getHeader('Content-Type');
  835. if (null === $contentType || false === strpos($contentType, 'text/calendar')) {
  836. return;
  837. }
  838. $result = HTTP\negotiateContentType(
  839. $request->getHeader('Accept'),
  840. ['text/calendar', 'application/calendar+json']
  841. );
  842. if ('application/calendar+json' !== $result) {
  843. // Do nothing
  844. return;
  845. }
  846. // Transforming.
  847. $vobj = VObject\Reader::read($response->getBody());
  848. $jsonBody = json_encode($vobj->jsonSerialize());
  849. $response->setBody($jsonBody);
  850. // Destroy circular references so PHP will garbage collect the object.
  851. $vobj->destroy();
  852. $response->setHeader('Content-Type', 'application/calendar+json');
  853. $response->setHeader('Content-Length', strlen($jsonBody));
  854. }
  855. /**
  856. * Returns a bunch of meta-data about the plugin.
  857. *
  858. * Providing this information is optional, and is mainly displayed by the
  859. * Browser plugin.
  860. *
  861. * The description key in the returned array may contain html and will not
  862. * be sanitized.
  863. *
  864. * @return array
  865. */
  866. public function getPluginInfo()
  867. {
  868. return [
  869. 'name' => $this->getPluginName(),
  870. 'description' => 'Adds support for CalDAV (rfc4791)',
  871. 'link' => 'http://sabre.io/dav/caldav/',
  872. ];
  873. }
  874. }