ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
Plugin.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Sabre\CalDAV;
4 
5 use DateTimeZone;
6 use Sabre\DAV;
11 use Sabre\DAVACL;
12 use Sabre\HTTP;
15 use Sabre\Uri;
16 use Sabre\VObject;
17 
28 class Plugin extends DAV\ServerPlugin {
29 
33  const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
34 
38  const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
39 
44  const CALENDAR_ROOT = 'calendars';
45 
51  protected $server;
52 
58  protected $maxResourceSize = 10000000;
59 
70  function getHTTPMethods($uri) {
71 
72  // The MKCALENDAR is only available on unmapped uri's, whose
73  // parents extend IExtendedCollection
74  list($parent, $name) = Uri\split($uri);
75 
76  $node = $this->server->tree->getNodeForPath($parent);
77 
78  if ($node instanceof DAV\IExtendedCollection) {
79  try {
80  $node->getChild($name);
81  } catch (DAV\Exception\NotFound $e) {
82  return ['MKCALENDAR'];
83  }
84  }
85  return [];
86 
87  }
88 
99  function getCalendarHomeForPrincipal($principalUrl) {
100 
101  // The default behavior for most sabre/dav servers is that there is a
102  // principals root node, which contains users directly under it.
103  //
104  // This function assumes that there are two components in a principal
105  // path. If there's more, we don't return a calendar home. This
106  // excludes things like the calendar-proxy-read principal (which it
107  // should).
108  $parts = explode('/', trim($principalUrl, '/'));
109  if (count($parts) !== 2) return;
110  if ($parts[0] !== 'principals') return;
111 
112  return self::CALENDAR_ROOT . '/' . $parts[1];
113 
114  }
115 
121  function getFeatures() {
122 
123  return ['calendar-access', 'calendar-proxy'];
124 
125  }
126 
135  function getPluginName() {
136 
137  return 'caldav';
138 
139  }
140 
151  function getSupportedReportSet($uri) {
152 
153  $node = $this->server->tree->getNodeForPath($uri);
154 
155  $reports = [];
156  if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) {
157  $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
158  $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
159  }
160  if ($node instanceof ICalendar) {
161  $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
162  }
163  // iCal has a bug where it assumes that sync support is enabled, only
164  // if we say we support it on the calendar-home, even though this is
165  // not actually the case.
166  if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) {
167  $reports[] = '{DAV:}sync-collection';
168  }
169  return $reports;
170 
171  }
172 
179  function initialize(DAV\Server $server) {
180 
181  $this->server = $server;
182 
183  $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']);
184  $server->on('report', [$this, 'report']);
185  $server->on('propFind', [$this, 'propFind']);
186  $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
187  $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
188  $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
189  $server->on('afterMethod:GET', [$this, 'httpAfterGET']);
190  $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
191 
192  $server->xml->namespaceMap[self::NS_CALDAV] = 'cal';
193  $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
194 
195  $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
196  $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
197  $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
198  $server->xml->elementMap['{' . self::NS_CALDAV . '}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
199  $server->xml->elementMap['{' . self::NS_CALDAV . '}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
200  $server->xml->elementMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
201  $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
202 
203  $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
204 
205  $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
206  $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
207 
208  array_push($server->protectedProperties,
209 
210  '{' . self::NS_CALDAV . '}supported-calendar-component-set',
211  '{' . self::NS_CALDAV . '}supported-calendar-data',
212  '{' . self::NS_CALDAV . '}max-resource-size',
213  '{' . self::NS_CALDAV . '}min-date-time',
214  '{' . self::NS_CALDAV . '}max-date-time',
215  '{' . self::NS_CALDAV . '}max-instances',
216  '{' . self::NS_CALDAV . '}max-attendees-per-instance',
217  '{' . self::NS_CALDAV . '}calendar-home-set',
218  '{' . self::NS_CALDAV . '}supported-collation-set',
219  '{' . self::NS_CALDAV . '}calendar-data',
220 
221  // CalendarServer extensions
222  '{' . self::NS_CALENDARSERVER . '}getctag',
223  '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
224  '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'
225 
226  );
227 
228  if ($aclPlugin = $server->getPlugin('acl')) {
229  $aclPlugin->principalSearchPropertySet['{' . self::NS_CALDAV . '}calendar-user-address-set'] = 'Calendar address';
230  }
231  }
232 
241  function report($reportName, $report, $path) {
242 
243  switch ($reportName) {
244  case '{' . self::NS_CALDAV . '}calendar-multiget' :
245  $this->server->transactionType = 'report-calendar-multiget';
246  $this->calendarMultiGetReport($report);
247  return false;
248  case '{' . self::NS_CALDAV . '}calendar-query' :
249  $this->server->transactionType = 'report-calendar-query';
250  $this->calendarQueryReport($report);
251  return false;
252  case '{' . self::NS_CALDAV . '}free-busy-query' :
253  $this->server->transactionType = 'report-free-busy-query';
254  $this->freeBusyQueryReport($report);
255  return false;
256 
257  }
258 
259 
260  }
261 
271 
272  $body = $request->getBodyAsString();
273  $path = $request->getPath();
274 
275  $properties = [];
276 
277  if ($body) {
278 
279  try {
280  $mkcalendar = $this->server->xml->expect(
281  '{urn:ietf:params:xml:ns:caldav}mkcalendar',
282  $body
283  );
284  } catch (\Sabre\Xml\ParseException $e) {
285  throw new BadRequest($e->getMessage(), null, $e);
286  }
287  $properties = $mkcalendar->getProperties();
288 
289  }
290 
291  // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored
292  // subscriptions. Before that it used MKCOL which was the correct way
293  // to do this.
294  //
295  // If the body had a {DAV:}resourcetype, it means we stumbled upon this
296  // request, and we simply use it instead of the pre-defined list.
297  if (isset($properties['{DAV:}resourcetype'])) {
298  $resourceType = $properties['{DAV:}resourcetype']->getValue();
299  } else {
300  $resourceType = ['{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar'];
301  }
302 
303  $this->server->createCollection($path, new MkCol($resourceType, $properties));
304 
305  $response->setStatus(201);
306  $response->setHeader('Content-Length', 0);
307 
308  // This breaks the method chain.
309  return false;
310  }
311 
323  function propFind(DAV\PropFind $propFind, DAV\INode $node) {
324 
325  $ns = '{' . self::NS_CALDAV . '}';
326 
327  if ($node instanceof ICalendarObjectContainer) {
328 
329  $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize);
330  $propFind->handle($ns . 'supported-calendar-data', function() {
332  });
333  $propFind->handle($ns . 'supported-collation-set', function() {
335  });
336 
337  }
338 
339  if ($node instanceof DAVACL\IPrincipal) {
340 
341  $principalUrl = $node->getPrincipalUrl();
342 
343  $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() use ($principalUrl) {
344 
345  $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl);
346  if (is_null($calendarHomePath)) return null;
347  return new LocalHref($calendarHomePath . '/');
348 
349  });
350  // The calendar-user-address-set property is basically mapped to
351  // the {DAV:}alternate-URI-set property.
352  $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function() use ($node) {
353  $addresses = $node->getAlternateUriSet();
354  $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/';
355  return new LocalHref($addresses);
356  });
357  // For some reason somebody thought it was a good idea to add
358  // another one of these properties. We're supporting it too.
359  $propFind->handle('{' . self::NS_CALENDARSERVER . '}email-address-set', function() use ($node) {
360  $addresses = $node->getAlternateUriSet();
361  $emails = [];
362  foreach ($addresses as $address) {
363  if (substr($address, 0, 7) === 'mailto:') {
364  $emails[] = substr($address, 7);
365  }
366  }
367  return new Xml\Property\EmailAddressSet($emails);
368  });
369 
370  // These two properties are shortcuts for ical to easily find
371  // other principals this principal has access to.
372  $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
373  $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
374 
375  if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) {
376 
377  $aclPlugin = $this->server->getPlugin('acl');
378  $membership = $aclPlugin->getPrincipalMembership($propFind->getPath());
379  $readList = [];
380  $writeList = [];
381 
382  foreach ($membership as $group) {
383 
384  $groupNode = $this->server->tree->getNodeForPath($group);
385 
386  $listItem = Uri\split($group)[0] . '/';
387 
388  // If the node is either ap proxy-read or proxy-write
389  // group, we grab the parent principal and add it to the
390  // list.
391  if ($groupNode instanceof Principal\IProxyRead) {
392  $readList[] = $listItem;
393  }
394  if ($groupNode instanceof Principal\IProxyWrite) {
395  $writeList[] = $listItem;
396  }
397 
398  }
399 
400  $propFind->set($propRead, new LocalHref($readList));
401  $propFind->set($propWrite, new LocalHref($writeList));
402 
403  }
404 
405  } // instanceof IPrincipal
406 
407  if ($node instanceof ICalendarObject) {
408 
409  // The calendar-data property is not supposed to be a 'real'
410  // property, but in large chunks of the spec it does act as such.
411  // Therefore we simply expose it as a property.
412  $propFind->handle('{' . self::NS_CALDAV . '}calendar-data', function() use ($node) {
413  $val = $node->get();
414  if (is_resource($val))
415  $val = stream_get_contents($val);
416 
417  // Taking out \r to not screw up the xml output
418  return str_replace("\r", "", $val);
419 
420  });
421 
422  }
423 
424  }
425 
435  function calendarMultiGetReport($report) {
436 
437  $needsJson = $report->contentType === 'application/calendar+json';
438 
439  $timeZones = [];
440  $propertyList = [];
441 
442  $paths = array_map(
443  [$this->server, 'calculateUri'],
444  $report->hrefs
445  );
446 
447  foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) {
448 
449  if (($needsJson || $report->expand) && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
450  $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
451 
452  if ($report->expand) {
453  // We're expanding, and for that we need to figure out the
454  // calendar's timezone.
455  list($calendarPath) = Uri\split($uri);
456  if (!isset($timeZones[$calendarPath])) {
457  // Checking the calendar-timezone property.
458  $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
459  $tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
460  if (isset($tzResult[$tzProp])) {
461  // This property contains a VCALENDAR with a single
462  // VTIMEZONE.
463  $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
464  $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
465  } else {
466  // Defaulting to UTC.
467  $timeZone = new DateTimeZone('UTC');
468  }
469  $timeZones[$calendarPath] = $timeZone;
470  }
471 
472  $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]);
473  }
474  if ($needsJson) {
475  $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
476  } else {
477  $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
478  }
479  // Destroy circular references so PHP will garbage collect the
480  // object.
481  $vObject->destroy();
482  }
483 
484  $propertyList[] = $objProps;
485 
486  }
487 
488  $prefer = $this->server->getHTTPPrefer();
489 
490  $this->server->httpResponse->setStatus(207);
491  $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
492  $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
493  $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal'));
494 
495  }
496 
506  function calendarQueryReport($report) {
507 
508  $path = $this->server->getRequestUri();
509 
510  $needsJson = $report->contentType === 'application/calendar+json';
511 
512  $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
513  $depth = $this->server->getHTTPDepth(0);
514 
515  // The default result is an empty array
516  $result = [];
517 
518  $calendarTimeZone = null;
519  if ($report->expand) {
520  // We're expanding, and for that we need to figure out the
521  // calendar's timezone.
522  $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
523  $tzResult = $this->server->getProperties($path, [$tzProp]);
524  if (isset($tzResult[$tzProp])) {
525  // This property contains a VCALENDAR with a single
526  // VTIMEZONE.
527  $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
528  $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
529 
530  // Destroy circular references so PHP will garbage collect the
531  // object.
532  $vtimezoneObj->destroy();
533  } else {
534  // Defaulting to UTC.
535  $calendarTimeZone = new DateTimeZone('UTC');
536  }
537  }
538 
539  // The calendarobject was requested directly. In this case we handle
540  // this locally.
541  if ($depth == 0 && $node instanceof ICalendarObject) {
542 
543  $requestedCalendarData = true;
544  $requestedProperties = $report->properties;
545 
546  if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
547 
548  // We always retrieve calendar-data, as we need it for filtering.
549  $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
550 
551  // If calendar-data wasn't explicitly requested, we need to remove
552  // it after processing.
553  $requestedCalendarData = false;
554  }
555 
556  $properties = $this->server->getPropertiesForPath(
557  $path,
558  $requestedProperties,
559  0
560  );
561 
562  // This array should have only 1 element, the first calendar
563  // object.
564  $properties = current($properties);
565 
566  // If there wasn't any calendar-data returned somehow, we ignore
567  // this.
568  if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
569 
570  $validator = new CalendarQueryValidator();
571 
572  $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
573  if ($validator->validate($vObject, $report->filters)) {
574 
575  // If the client didn't require the calendar-data property,
576  // we won't give it back.
577  if (!$requestedCalendarData) {
578  unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
579  } else {
580 
581 
582  if ($report->expand) {
583  $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
584  }
585  if ($needsJson) {
586  $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
587  } elseif ($report->expand) {
588  $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
589  }
590  }
591 
592  $result = [$properties];
593 
594  }
595  // Destroy circular references so PHP will garbage collect the
596  // object.
597  $vObject->destroy();
598 
599  }
600 
601  }
602 
603  if ($node instanceof ICalendarObjectContainer && $depth === 0) {
604 
605  if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-') === 0) {
606  // Microsoft clients incorrectly supplied depth as 0, when it actually
607  // should have set depth to 1. We're implementing a workaround here
608  // to deal with this.
609  //
610  // This targets at least the following clients:
611  // Windows 10
612  // Windows Phone 8, 10
613  $depth = 1;
614  } else {
615  throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
616  }
617 
618  }
619 
620  // If we're dealing with a calendar, the calendar itself is responsible
621  // for the calendar-query.
622  if ($node instanceof ICalendarObjectContainer && $depth == 1) {
623 
624  $nodePaths = $node->calendarQuery($report->filters);
625 
626  foreach ($nodePaths as $path) {
627 
628  list($properties) =
629  $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $report->properties);
630 
631  if (($needsJson || $report->expand)) {
632  $vObject = VObject\Reader::read($properties[200]['{' . self::NS_CALDAV . '}calendar-data']);
633 
634  if ($report->expand) {
635  $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
636  }
637 
638  if ($needsJson) {
639  $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
640  } else {
641  $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
642  }
643 
644  // Destroy circular references so PHP will garbage collect the
645  // object.
646  $vObject->destroy();
647  }
648  $result[] = $properties;
649 
650  }
651 
652  }
653 
654  $prefer = $this->server->getHTTPPrefer();
655 
656  $this->server->httpResponse->setStatus(207);
657  $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
658  $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
659  $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal'));
660 
661  }
662 
670  protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) {
671 
672  $uri = $this->server->getRequestUri();
673 
674  $acl = $this->server->getPlugin('acl');
675  if ($acl) {
676  $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy');
677  }
678 
679  $calendar = $this->server->tree->getNodeForPath($uri);
680  if (!$calendar instanceof ICalendar) {
681  throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
682  }
683 
684  $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
685 
686  // Figuring out the default timezone for the calendar, for floating
687  // times.
688  $calendarProps = $this->server->getProperties($uri, [$tzProp]);
689 
690  if (isset($calendarProps[$tzProp])) {
691  $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]);
692  $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
693  // Destroy circular references so PHP will garbage collect the object.
694  $vtimezoneObj->destroy();
695  } else {
696  $calendarTimeZone = new DateTimeZone('UTC');
697  }
698 
699  // Doing a calendar-query first, to make sure we get the most
700  // performance.
701  $urls = $calendar->calendarQuery([
702  'name' => 'VCALENDAR',
703  'comp-filters' => [
704  [
705  'name' => 'VEVENT',
706  'comp-filters' => [],
707  'prop-filters' => [],
708  'is-not-defined' => false,
709  'time-range' => [
710  'start' => $report->start,
711  'end' => $report->end,
712  ],
713  ],
714  ],
715  'prop-filters' => [],
716  'is-not-defined' => false,
717  'time-range' => null,
718  ]);
719 
720  $objects = array_map(function($url) use ($calendar) {
721  $obj = $calendar->getChild($url)->get();
722  return $obj;
723  }, $urls);
724 
725  $generator = new VObject\FreeBusyGenerator();
726  $generator->setObjects($objects);
727  $generator->setTimeRange($report->start, $report->end);
728  $generator->setTimeZone($calendarTimeZone);
729  $result = $generator->getResult();
730  $result = $result->serialize();
731 
732  $this->server->httpResponse->setStatus(200);
733  $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
734  $this->server->httpResponse->setHeader('Content-Length', strlen($result));
735  $this->server->httpResponse->setBody($result);
736 
737  }
738 
752  function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) {
753 
754  if (!$node instanceof ICalendarObject)
755  return;
756 
757  // We're onyl interested in ICalendarObject nodes that are inside of a
758  // real calendar. This is to avoid triggering validation and scheduling
759  // for non-calendars (such as an inbox).
760  list($parent) = Uri\split($path);
761  $parentNode = $this->server->tree->getNodeForPath($parent);
762 
763  if (!$parentNode instanceof ICalendar)
764  return;
765 
766  $this->validateICalendar(
767  $data,
768  $path,
769  $modified,
770  $this->server->httpRequest,
771  $this->server->httpResponse,
772  false
773  );
774 
775  }
776 
790  function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) {
791 
792  if (!$parentNode instanceof ICalendar)
793  return;
794 
795  $this->validateICalendar(
796  $data,
797  $path,
798  $modified,
799  $this->server->httpRequest,
800  $this->server->httpResponse,
801  true
802  );
803 
804  }
805 
820  protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) {
821 
822  // If it's a stream, we convert it to a string first.
823  if (is_resource($data)) {
824  $data = stream_get_contents($data);
825  }
826 
827  $before = $data;
828 
829  try {
830 
831  // If the data starts with a [, we can reasonably assume we're dealing
832  // with a jCal object.
833  if (substr($data, 0, 1) === '[') {
835 
836  // Converting $data back to iCalendar, as that's what we
837  // technically support everywhere.
838  $data = $vobj->serialize();
839  $modified = true;
840  } else {
842  }
843 
844  } catch (VObject\ParseException $e) {
845 
846  throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
847 
848  }
849 
850  if ($vobj->name !== 'VCALENDAR') {
851  throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
852  }
853 
854  $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
855 
856  // Get the Supported Components for the target calendar
857  list($parentPath) = Uri\split($path);
858  $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
859 
860  if (isset($calendarProperties[$sCCS])) {
861  $supportedComponents = $calendarProperties[$sCCS]->getValue();
862  } else {
863  $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
864  }
865 
866  $foundType = null;
867 
868  foreach ($vobj->getComponents() as $component) {
869  switch ($component->name) {
870  case 'VTIMEZONE' :
871  continue 2;
872  case 'VEVENT' :
873  case 'VTODO' :
874  case 'VJOURNAL' :
875  $foundType = $component->name;
876  break;
877  }
878 
879  }
880 
881  if (!$foundType || !in_array($foundType, $supportedComponents)) {
882  throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type ' . implode(', ', $supportedComponents));
883  }
884 
886  $prefer = $this->server->getHTTPPrefer();
887 
888  if ($prefer['handling'] !== 'strict') {
890  }
891 
892  $messages = $vobj->validate($options);
893 
894  $highestLevel = 0;
895  $warningMessage = null;
896 
897  // $messages contains a list of problems with the vcard, along with
898  // their severity.
899  foreach ($messages as $message) {
900 
901  if ($message['level'] > $highestLevel) {
902  // Recording the highest reported error level.
903  $highestLevel = $message['level'];
904  $warningMessage = $message['message'];
905  }
906  switch ($message['level']) {
907 
908  case 1 :
909  // Level 1 means that there was a problem, but it was repaired.
910  $modified = true;
911  break;
912  case 2 :
913  // Level 2 means a warning, but not critical
914  break;
915  case 3 :
916  // Level 3 means a critical error
917  throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: ' . $message['message']);
918 
919  }
920 
921  }
922  if ($warningMessage) {
923  $response->setHeader(
924  'X-Sabre-Ew-Gross',
925  'iCalendar validation warning: ' . $warningMessage
926  );
927  }
928 
929  // We use an extra variable to allow event handles to tell us whether
930  // the object was modified or not.
931  //
932  // This helps us determine if we need to re-serialize the object.
933  $subModified = false;
934 
935  $this->server->emit(
936  'calendarObjectChange',
937  [
938  $request,
939  $response,
940  $vobj,
941  $parentPath,
942  &$subModified,
943  $isNew
944  ]
945  );
946 
947  if ($modified || $subModified) {
948  // An event handler told us that it modified the object.
949  $data = $vobj->serialize();
950 
951  // Using md5 to figure out if there was an *actual* change.
952  if (!$modified && strcmp($data, $before) !== 0) {
953  $modified = true;
954  }
955 
956  }
957 
958  // Destroy circular references so PHP will garbage collect the object.
959  $vobj->destroy();
960 
961  }
962 
970  function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) {
971 
972  if ($node instanceof ICalendar) {
973  $supportedPrivilegeSet['{DAV:}read']['aggregates']['{' . self::NS_CALDAV . '}read-free-busy'] = [
974  'abstract' => false,
975  'aggregates' => [],
976  ];
977  }
978  }
979 
989  function htmlActionsPanel(DAV\INode $node, &$output) {
990 
991  if (!$node instanceof CalendarHome)
992  return;
993 
994  $output .= '<tr><td colspan="2"><form method="post" action="">
995  <h3>Create new calendar</h3>
996  <input type="hidden" name="sabreAction" value="mkcol" />
997  <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV . '}calendar" />
998  <label>Name (uri):</label> <input type="text" name="name" /><br />
999  <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
1000  <input type="submit" value="create" />
1001  </form>
1002  </td></tr>';
1003 
1004  return false;
1005 
1006  }
1007 
1018 
1019  if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) {
1020  return;
1021  }
1022 
1024  $request->getHeader('Accept'),
1025  ['text/calendar', 'application/calendar+json']
1026  );
1027 
1028  if ($result !== 'application/calendar+json') {
1029  // Do nothing
1030  return;
1031  }
1032 
1033  // Transforming.
1034  $vobj = VObject\Reader::read($response->getBody());
1035 
1036  $jsonBody = json_encode($vobj->jsonSerialize());
1037  $response->setBody($jsonBody);
1038 
1039  // Destroy circular references so PHP will garbage collect the object.
1040  $vobj->destroy();
1041 
1042  $response->setHeader('Content-Type', 'application/calendar+json');
1043  $response->setHeader('Content-Length', strlen($jsonBody));
1044 
1045  }
1046 
1058  function getPluginInfo() {
1059 
1060  return [
1061  'name' => $this->getPluginName(),
1062  'description' => 'Adds support for CalDAV (rfc4791)',
1063  'link' => 'http://sabre.io/dav/caldav/',
1064  ];
1065 
1066  }
1067 
1068 }
CalDAV plugin.
Definition: Plugin.php:28
This interface represents a HTTP response.
getCalendarHomeForPrincipal($principalUrl)
Returns the path to a principal&#39;s calendar home.
Definition: Plugin.php:99
The RequestInterface represents a HTTP request.
$path
Definition: aliased.php:25
getPluginInfo()
Returns a bunch of meta-data about the plugin.
Definition: Plugin.php:1058
htmlActionsPanel(DAV\INode $node, &$output)
This method is used to generate HTML output for the DAV.
Definition: Plugin.php:989
The baseclass for all server plugins.
setBody($body)
Updates the body resource with a new stream.
$result
if($argc< 2) $paths
Definition: migrateto20.php:44
const CALENDAR_ROOT
The hardcoded root for calendar objects.
Definition: Plugin.php:44
foreach($paths as $path) $request
Definition: asyncclient.php:32
static negotiate($acceptHeaderValue, array $availableOptions)
Deprecated! Use negotiateContentType.
Definition: Util.php:38
The Request class represents a single HTTP request.
Definition: Request.php:18
LocalHref property.
Definition: LocalHref.php:25
This class helps with generating FREEBUSY reports based on existing sets of objects.
getBodyAsString()
Returns the body as a string.
This interface represents a node that may contain calendar objects.
split($path)
Returns the &#39;dirname&#39; and &#39;basename&#39; for a path.
Definition: functions.php:279
The CalendarHome represents a node that is usually in a users&#39; calendar-homeset.
Principal class.
Definition: Principal.php:23
This class holds all the information about a PROPFIND request.
Definition: PropFind.php:11
initialize(DAV\Server $server)
Initializes the plugin.
Definition: Plugin.php:179
beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
This method is triggered before a new file is created.
Definition: Plugin.php:790
calendarMultiGetReport($report)
This function handles the calendar-multiget REPORT.
Definition: Plugin.php:435
validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew)
Checks if the submitted iCalendar data is in fact, valid.
Definition: Plugin.php:820
beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
This method is triggered before a file gets updated with new content.
Definition: Plugin.php:752
The IExtendedCollection interface.
The ICollection Interface.
Definition: ICollection.php:14
Calendar interface.
Definition: ICalendar.php:16
httpMkCalendar(RequestInterface $request, ResponseInterface $response)
This function handles the MKCALENDAR HTTP method, which creates a new calendar.
Definition: Plugin.php:270
$messages
Definition: en.php:5
$urls
Definition: croninfo.php:28
getPluginName()
Returns a plugin name.
Definition: Plugin.php:135
calendarQueryReport($report)
This function handles the calendar-query REPORT.
Definition: Plugin.php:506
catch(Exception $e) $message
report($reportName, $report, $path)
This functions handles REPORT requests specific to CalDAV.
Definition: Plugin.php:241
const NS_CALENDARSERVER
This is the namespace for the proprietary calendarserver extensions.
Definition: Plugin.php:38
setStatus($status)
Sets the HTTP status code.
Main DAV server class.
Definition: Server.php:23
getHeader($name)
Returns a specific HTTP header, based on it&#39;s name.
This interface represents a file in the directory tree.
Definition: IFile.php:16
freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report)
This method is responsible for parsing the request and generating the response for the CALDAV:free-bu...
Definition: Plugin.php:670
The INode interface is the base interface, and the parent class of both ICollection and IFile...
Definition: INode.php:12
$vobj
Definition: rrulebench.php:21
propFind(DAV\PropFind $propFind, DAV\INode $node)
PropFind.
Definition: Plugin.php:323
static readJson($data, $options=0)
Parses a jCard or jCal object, and returns the top component.
Definition: Reader.php:67
CalendarObject interface.
const REPAIR
The following constants are used by the validate() method.
Definition: Node.php:27
getPath()
Returns the relative path.
This class represents a MKCOL operation.
Definition: MkCol.php:23
$maxResourceSize
The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data, which can hold up to 2^24 = 16777...
Definition: Plugin.php:58
IPrincipal interface.
Definition: IPrincipal.php:16
static read($data, $options=0, $charset='UTF-8')
Parses a vCard or iCalendar object, and returns the top component.
Definition: Reader.php:42
getSupportedReportSet($uri)
Returns a list of reports this plugin supports.
Definition: Plugin.php:151
getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
This method is triggered whenever a subsystem reqeuests the privileges that are supported on a partic...
Definition: Plugin.php:970
httpAfterGet(RequestInterface $request, ResponseInterface $response)
This event is triggered after GET requests.
Definition: Plugin.php:1017
Exception thrown by Reader if an invalid object was attempted to be parsed.
const PROFILE_CALDAV
If this option is set, the validator will operate on iCalendar objects on the assumption that the vca...
Definition: Node.php:45
getHTTPMethods($uri)
Use this method to tell the server this plugin defines additional HTTP methods.
Definition: Plugin.php:70
$url
$response
getFeatures()
Returns a list of features for the DAV: HTTP header.
Definition: Plugin.php:121
const NS_CALDAV
This is the official CalDAV namespace.
Definition: Plugin.php:33
setHeader($name, $value)
Updates a HTTP header.
getBody()
Returns the message body, as it&#39;s internal representation.
$aclPlugin
$data
Definition: bench.php:6