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\Schedule;
4 
5 use DateTimeZone;
13 use Sabre\DAV\INode;
20 use Sabre\DAVACL;
23 use Sabre\VObject;
28 
56 class Plugin extends ServerPlugin {
57 
61  const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
62 
68  protected $server;
69 
75  function getFeatures() {
76 
77  return ['calendar-auto-schedule', 'calendar-availability'];
78 
79  }
80 
89  function getPluginName() {
90 
91  return 'caldav-schedule';
92 
93  }
94 
102 
103  $this->server = $server;
104  $server->on('method:POST', [$this, 'httpPost']);
105  $server->on('propFind', [$this, 'propFind']);
106  $server->on('propPatch', [$this, 'propPatch']);
107  $server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
108  $server->on('beforeUnbind', [$this, 'beforeUnbind']);
109  $server->on('schedule', [$this, 'scheduleLocalDelivery']);
110  $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
111 
112  $ns = '{' . self::NS_CALDAV . '}';
113 
118  $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox';
119  $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox';
120 
124  array_push($server->protectedProperties,
125  $ns . 'schedule-inbox-URL',
126  $ns . 'schedule-outbox-URL',
127  $ns . 'calendar-user-address-set',
128  $ns . 'calendar-user-type',
129  $ns . 'schedule-default-calendar-URL'
130  );
131 
132  }
133 
144  function getHTTPMethods($uri) {
145 
146  try {
147  $node = $this->server->tree->getNodeForPath($uri);
148  } catch (NotFound $e) {
149  return [];
150  }
151 
152  if ($node instanceof IOutbox) {
153  return ['POST'];
154  }
155 
156  return [];
157 
158  }
159 
168 
169  // Checking if this is a text/calendar content type
170  $contentType = $request->getHeader('Content-Type');
171  if (strpos($contentType, 'text/calendar') !== 0) {
172  return;
173  }
174 
175  $path = $request->getPath();
176 
177  // Checking if we're talking to an outbox
178  try {
179  $node = $this->server->tree->getNodeForPath($path);
180  } catch (NotFound $e) {
181  return;
182  }
183  if (!$node instanceof IOutbox)
184  return;
185 
186  $this->server->transactionType = 'post-caldav-outbox';
187  $this->outboxRequest($node, $request, $response);
188 
189  // Returning false breaks the event chain and tells the server we've
190  // handled the request.
191  return false;
192 
193  }
194 
204  function propFind(PropFind $propFind, INode $node) {
205 
206  if ($node instanceof DAVACL\IPrincipal) {
207 
208  $caldavPlugin = $this->server->getPlugin('caldav');
209  $principalUrl = $node->getPrincipalUrl();
210 
211  // schedule-outbox-URL property
212  $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) {
213 
214  $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
215  if (!$calendarHomePath) {
216  return null;
217  }
218  $outboxPath = $calendarHomePath . '/outbox/';
219 
220  return new LocalHref($outboxPath);
221 
222  });
223  // schedule-inbox-URL property
224  $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) {
225 
226  $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
227  if (!$calendarHomePath) {
228  return null;
229  }
230  $inboxPath = $calendarHomePath . '/inbox/';
231 
232  return new LocalHref($inboxPath);
233 
234  });
235 
236  $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) {
237 
238  // We don't support customizing this property yet, so in the
239  // meantime we just grab the first calendar in the home-set.
240  $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
241 
242  if (!$calendarHomePath) {
243  return null;
244  }
245 
246  $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set';
247 
248  $result = $this->server->getPropertiesForPath($calendarHomePath, [
249  '{DAV:}resourcetype',
250  '{DAV:}share-access',
251  $sccs,
252  ], 1);
253 
254  foreach ($result as $child) {
255  if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar')) {
256  // Node is either not a calendar
257  continue;
258  }
259  if (isset($child[200]['{DAV:}share-access'])) {
260  $shareAccess = $child[200]['{DAV:}share-access']->getValue();
261  if ($shareAccess !== Sharing\Plugin::ACCESS_NOTSHARED && $shareAccess !== Sharing\Plugin::ACCESS_SHAREDOWNER) {
262  // Node is a shared node, not owned by the relevant
263  // user.
264  continue;
265  }
266 
267  }
268  if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
269  // Either there is no supported-calendar-component-set
270  // (which is fine) or we found one that supports VEVENT.
271  return new LocalHref($child['href']);
272  }
273  }
274 
275  });
276 
277  // The server currently reports every principal to be of type
278  // 'INDIVIDUAL'
279  $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function() {
280 
281  return 'INDIVIDUAL';
282 
283  });
284 
285  }
286 
287  // Mapping the old property to the new property.
288  $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function() use ($propFind, $node) {
289 
290  // In case it wasn't clear, the only difference is that we map the
291  // old property to a different namespace.
292  $availProp = '{' . self::NS_CALDAV . '}calendar-availability';
293  $subPropFind = new PropFind(
294  $propFind->getPath(),
295  [$availProp]
296  );
297 
298  $this->server->getPropertiesByNode(
299  $subPropFind,
300  $node
301  );
302 
303  $propFind->set(
304  '{http://calendarserver.org/ns/}calendar-availability',
305  $subPropFind->get($availProp),
306  $subPropFind->getStatus($availProp)
307  );
308 
309  });
310 
311  }
312 
320  function propPatch($path, PropPatch $propPatch) {
321 
322  // Mapping the old property to the new property.
323  $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function($value) use ($path) {
324 
325  $availProp = '{' . self::NS_CALDAV . '}calendar-availability';
326  $subPropPatch = new PropPatch([$availProp => $value]);
327  $this->server->emit('propPatch', [$path, $subPropPatch]);
328  $subPropPatch->commit();
329 
330  return $subPropPatch->getResult()[$availProp];
331 
332  });
333 
334  }
335 
348  function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
349 
350  if (!$this->scheduleReply($this->server->httpRequest)) {
351  return;
352  }
353 
354  $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
355 
356  $addresses = $this->getAddressesForPrincipal(
357  $calendarNode->getOwner()
358  );
359 
360  if (!$isNew) {
361  $node = $this->server->tree->getNodeForPath($request->getPath());
362  $oldObj = Reader::read($node->get());
363  } else {
364  $oldObj = null;
365  }
366 
367  $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
368 
369  if ($oldObj) {
370  // Destroy circular references so PHP will GC the object.
371  $oldObj->destroy();
372  }
373 
374  }
375 
382  function deliver(ITip\Message $iTipMessage) {
383 
384  $this->server->emit('schedule', [$iTipMessage]);
385  if (!$iTipMessage->scheduleStatus) {
386  $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
387  }
388  // In case the change was considered 'insignificant', we are going to
389  // remove any error statuses, if any. See ticket #525.
390  list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
391  if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
392  $iTipMessage->scheduleStatus = null;
393  }
394 
395  }
396 
406  function beforeUnbind($path) {
407 
408  // FIXME: We shouldn't trigger this functionality when we're issuing a
409  // MOVE. This is a hack.
410  if ($this->server->httpRequest->getMethod() === 'MOVE') return;
411 
412  $node = $this->server->tree->getNodeForPath($path);
413 
414  if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
415  return;
416  }
417 
418  if (!$this->scheduleReply($this->server->httpRequest)) {
419  return;
420  }
421 
422  $addresses = $this->getAddressesForPrincipal(
423  $node->getOwner()
424  );
425 
426  $broker = new ITip\Broker();
427  $messages = $broker->parseEvent(null, $addresses, $node->get());
428 
429  foreach ($messages as $message) {
430  $this->deliver($message);
431  }
432 
433  }
434 
444  function scheduleLocalDelivery(ITip\Message $iTipMessage) {
445 
446  $aclPlugin = $this->server->getPlugin('acl');
447 
448  // Local delivery is not available if the ACL plugin is not loaded.
449  if (!$aclPlugin) {
450  return;
451  }
452 
453  $caldavNS = '{' . self::NS_CALDAV . '}';
454 
455  $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
456  if (!$principalUri) {
457  $iTipMessage->scheduleStatus = '3.7;Could not find principal.';
458  return;
459  }
460 
461  // We found a principal URL, now we need to find its inbox.
462  // Unfortunately we may not have sufficient privileges to find this, so
463  // we are temporarily turning off ACL to let this come through.
464  //
465  // Once we support PHP 5.5, this should be wrapped in a try..finally
466  // block so we can ensure that this privilege gets added again after.
467  $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
468 
469  $result = $this->server->getProperties(
470  $principalUri,
471  [
472  '{DAV:}principal-URL',
473  $caldavNS . 'calendar-home-set',
474  $caldavNS . 'schedule-inbox-URL',
475  $caldavNS . 'schedule-default-calendar-URL',
476  '{http://sabredav.org/ns}email-address',
477  ]
478  );
479 
480  // Re-registering the ACL event
481  $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
482 
483  if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) {
484  $iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
485  return;
486  }
487  if (!isset($result[$caldavNS . 'calendar-home-set'])) {
488  $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
489  return;
490  }
491  if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) {
492  $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
493  return;
494  }
495 
496  $calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref();
497  $homePath = $result[$caldavNS . 'calendar-home-set']->getHref();
498  $inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref();
499 
500  if ($iTipMessage->method === 'REPLY') {
501  $privilege = 'schedule-deliver-reply';
502  } else {
503  $privilege = 'schedule-deliver-invite';
504  }
505 
506  if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) {
507  $iTipMessage->scheduleStatus = '3.8;insufficient privileges: ' . $privilege . ' is required on the recipient schedule inbox.';
508  return;
509  }
510 
511  // Next, we're going to find out if the item already exits in one of
512  // the users' calendars.
513  $uid = $iTipMessage->uid;
514 
515  $newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics';
516 
517  $home = $this->server->tree->getNodeForPath($homePath);
518  $inbox = $this->server->tree->getNodeForPath($inboxPath);
519 
520  $currentObject = null;
521  $objectNode = null;
522  $isNewNode = false;
523 
524  $result = $home->getCalendarObjectByUID($uid);
525  if ($result) {
526  // There was an existing object, we need to update probably.
527  $objectPath = $homePath . '/' . $result;
528  $objectNode = $this->server->tree->getNodeForPath($objectPath);
529  $oldICalendarData = $objectNode->get();
530  $currentObject = Reader::read($oldICalendarData);
531  } else {
532  $isNewNode = true;
533  }
534 
535  $broker = new ITip\Broker();
536  $newObject = $broker->processMessage($iTipMessage, $currentObject);
537 
538  $inbox->createFile($newFileName, $iTipMessage->message->serialize());
539 
540  if (!$newObject) {
541  // We received an iTip message referring to a UID that we don't
542  // have in any calendars yet, and processMessage did not give us a
543  // calendarobject back.
544  //
545  // The implication is that processMessage did not understand the
546  // iTip message.
547  $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
548  return;
549  }
550 
551  // Note that we are bypassing ACL on purpose by calling this directly.
552  // We may need to look a bit deeper into this later. Supporting ACL
553  // here would be nice.
554  if ($isNewNode) {
555  $calendar = $this->server->tree->getNodeForPath($calendarPath);
556  $calendar->createFile($newFileName, $newObject->serialize());
557  } else {
558  // If the message was a reply, we may have to inform other
559  // attendees of this attendees status. Therefore we're shooting off
560  // another itipMessage.
561  if ($iTipMessage->method === 'REPLY') {
562  $this->processICalendarChange(
563  $oldICalendarData,
564  $newObject,
565  [$iTipMessage->recipient],
566  [$iTipMessage->sender]
567  );
568  }
569  $objectNode->put($newObject->serialize());
570  }
571  $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
572 
573  }
574 
584  function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) {
585 
586  $ns = '{' . self::NS_CALDAV . '}';
587  if ($node instanceof IOutbox) {
588  $supportedPrivilegeSet[$ns . 'schedule-send'] = [
589  'abstract' => false,
590  'aggregates' => [
591  $ns . 'schedule-send-invite' => [
592  'abstract' => false,
593  'aggregates' => [],
594  ],
595  $ns . 'schedule-send-reply' => [
596  'abstract' => false,
597  'aggregates' => [],
598  ],
599  $ns . 'schedule-send-freebusy' => [
600  'abstract' => false,
601  'aggregates' => [],
602  ],
603  // Privilege from an earlier scheduling draft, but still
604  // used by some clients.
605  $ns . 'schedule-post-vevent' => [
606  'abstract' => false,
607  'aggregates' => [],
608  ],
609  ]
610  ];
611  }
612  if ($node instanceof IInbox) {
613  $supportedPrivilegeSet[$ns . 'schedule-deliver'] = [
614  'abstract' => false,
615  'aggregates' => [
616  $ns . 'schedule-deliver-invite' => [
617  'abstract' => false,
618  'aggregates' => [],
619  ],
620  $ns . 'schedule-deliver-reply' => [
621  'abstract' => false,
622  'aggregates' => [],
623  ],
624  $ns . 'schedule-query-freebusy' => [
625  'abstract' => false,
626  'aggregates' => [],
627  ],
628  ]
629  ];
630  }
631 
632  }
633 
652  protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) {
653 
654  $broker = new ITip\Broker();
655  $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
656 
657  if ($messages) $modified = true;
658 
659  foreach ($messages as $message) {
660 
661  if (in_array($message->recipient, $ignore)) {
662  continue;
663  }
664 
665  $this->deliver($message);
666 
667  if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
668  if ($message->scheduleStatus) {
669  $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
670  }
671  unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
672 
673  } else {
674 
675  if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
676 
677  if ($attendee->getNormalizedValue() === $message->recipient) {
678  if ($message->scheduleStatus) {
679  $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
680  }
681  unset($attendee['SCHEDULE-FORCE-SEND']);
682  break;
683  }
684 
685  }
686 
687  }
688 
689  }
690 
691  }
692 
699  protected function getAddressesForPrincipal($principal) {
700 
701  $CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set';
702 
703  $properties = $this->server->getProperties(
704  $principal,
705  [$CUAS]
706  );
707 
708  // If we can't find this information, we'll stop processing
709  if (!isset($properties[$CUAS])) {
710  return;
711  }
712 
713  $addresses = $properties[$CUAS]->getHrefs();
714  return $addresses;
715 
716  }
717 
735 
736  $outboxPath = $request->getPath();
737 
738  // Parsing the request body
739  try {
740  $vObject = VObject\Reader::read($request->getBody());
741  } catch (VObject\ParseException $e) {
742  throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
743  }
744 
745  // The incoming iCalendar object must have a METHOD property, and a
746  // component. The combination of both determines what type of request
747  // this is.
748  $componentType = null;
749  foreach ($vObject->getComponents() as $component) {
750  if ($component->name !== 'VTIMEZONE') {
751  $componentType = $component->name;
752  break;
753  }
754  }
755  if (is_null($componentType)) {
756  throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
757  }
758 
759  // Validating the METHOD
760  $method = strtoupper((string)$vObject->METHOD);
761  if (!$method) {
762  throw new BadRequest('A METHOD property must be specified in iTIP messages');
763  }
764 
765  // So we support one type of request:
766  //
767  // REQUEST with a VFREEBUSY component
768 
769  $acl = $this->server->getPlugin('acl');
770 
771  if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
772 
773  $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-send-freebusy');
774  $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
775 
776  // Destroy circular references so PHP can GC the object.
777  $vObject->destroy();
778  unset($vObject);
779 
780  } else {
781 
782  throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
783 
784  }
785 
786  }
787 
799 
800  $vFreeBusy = $vObject->VFREEBUSY;
801  $organizer = $vFreeBusy->ORGANIZER;
802 
803  $organizer = (string)$organizer;
804 
805  // Validating if the organizer matches the owner of the inbox.
806  $owner = $outbox->getOwner();
807 
808  $caldavNS = '{' . self::NS_CALDAV . '}';
809 
810  $uas = $caldavNS . 'calendar-user-address-set';
811  $props = $this->server->getProperties($owner, [$uas]);
812 
813  if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
814  throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
815  }
816 
817  if (!isset($vFreeBusy->ATTENDEE)) {
818  throw new BadRequest('You must at least specify 1 attendee');
819  }
820 
821  $attendees = [];
822  foreach ($vFreeBusy->ATTENDEE as $attendee) {
823  $attendees[] = (string)$attendee;
824  }
825 
826 
827  if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
828  throw new BadRequest('DTSTART and DTEND must both be specified');
829  }
830 
831  $startRange = $vFreeBusy->DTSTART->getDateTime();
832  $endRange = $vFreeBusy->DTEND->getDateTime();
833 
834  $results = [];
835  foreach ($attendees as $attendee) {
836  $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
837  }
838 
839  $dom = new \DOMDocument('1.0', 'utf-8');
840  $dom->formatOutput = true;
841  $scheduleResponse = $dom->createElement('cal:schedule-response');
842  foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
843 
844  $scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
845 
846  }
847  $dom->appendChild($scheduleResponse);
848 
849  foreach ($results as $result) {
850  $xresponse = $dom->createElement('cal:response');
851 
852  $recipient = $dom->createElement('cal:recipient');
853  $recipientHref = $dom->createElement('d:href');
854 
855  $recipientHref->appendChild($dom->createTextNode($result['href']));
856  $recipient->appendChild($recipientHref);
857  $xresponse->appendChild($recipient);
858 
859  $reqStatus = $dom->createElement('cal:request-status');
860  $reqStatus->appendChild($dom->createTextNode($result['request-status']));
861  $xresponse->appendChild($reqStatus);
862 
863  if (isset($result['calendar-data'])) {
864 
865  $calendardata = $dom->createElement('cal:calendar-data');
866  $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
867  $xresponse->appendChild($calendardata);
868 
869  }
870  $scheduleResponse->appendChild($xresponse);
871  }
872 
873  $response->setStatus(200);
874  $response->setHeader('Content-Type', 'application/xml');
875  $response->setBody($dom->saveXML());
876 
877  }
878 
898 
899  $caldavNS = '{' . self::NS_CALDAV . '}';
900 
901  $aclPlugin = $this->server->getPlugin('acl');
902  if (substr($email, 0, 7) === 'mailto:') $email = substr($email, 7);
903 
904  $result = $aclPlugin->principalSearch(
905  ['{http://sabredav.org/ns}email-address' => $email],
906  [
907  '{DAV:}principal-URL',
908  $caldavNS . 'calendar-home-set',
909  $caldavNS . 'schedule-inbox-URL',
910  '{http://sabredav.org/ns}email-address',
911 
912  ]
913  );
914 
915  if (!count($result)) {
916  return [
917  'request-status' => '3.7;Could not find principal',
918  'href' => 'mailto:' . $email,
919  ];
920  }
921 
922  if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
923  return [
924  'request-status' => '3.7;No calendar-home-set property found',
925  'href' => 'mailto:' . $email,
926  ];
927  }
928  if (!isset($result[0][200][$caldavNS . 'schedule-inbox-URL'])) {
929  return [
930  'request-status' => '3.7;No schedule-inbox-URL property found',
931  'href' => 'mailto:' . $email,
932  ];
933  }
934  $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
935  $inboxUrl = $result[0][200][$caldavNS . 'schedule-inbox-URL']->getHref();
936 
937  // Do we have permission?
938  $aclPlugin->checkPrivileges($inboxUrl, $caldavNS . 'schedule-query-freebusy');
939 
940  // Grabbing the calendar list
941  $objects = [];
942  $calendarTimeZone = new DateTimeZone('UTC');
943 
944  foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
945  if (!$node instanceof ICalendar) {
946  continue;
947  }
948 
949  $sct = $caldavNS . 'schedule-calendar-transp';
950  $ctz = $caldavNS . 'calendar-timezone';
951  $props = $node->getProperties([$sct, $ctz]);
952 
953  if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) {
954  // If a calendar is marked as 'transparent', it means we must
955  // ignore it for free-busy purposes.
956  continue;
957  }
958 
959  if (isset($props[$ctz])) {
960  $vtimezoneObj = VObject\Reader::read($props[$ctz]);
961  $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
962 
963  // Destroy circular references so PHP can garbage collect the object.
964  $vtimezoneObj->destroy();
965 
966  }
967 
968  // Getting the list of object uris within the time-range
969  $urls = $node->calendarQuery([
970  'name' => 'VCALENDAR',
971  'comp-filters' => [
972  [
973  'name' => 'VEVENT',
974  'comp-filters' => [],
975  'prop-filters' => [],
976  'is-not-defined' => false,
977  'time-range' => [
978  'start' => $start,
979  'end' => $end,
980  ],
981  ],
982  ],
983  'prop-filters' => [],
984  'is-not-defined' => false,
985  'time-range' => null,
986  ]);
987 
988  $calObjects = array_map(function($url) use ($node) {
989  $obj = $node->getChild($url)->get();
990  return $obj;
991  }, $urls);
992 
993  $objects = array_merge($objects, $calObjects);
994 
995  }
996 
997  $inboxProps = $this->server->getProperties(
998  $inboxUrl,
999  $caldavNS . 'calendar-availability'
1000  );
1001 
1002  $vcalendar = new VObject\Component\VCalendar();
1003  $vcalendar->METHOD = 'REPLY';
1004 
1005  $generator = new VObject\FreeBusyGenerator();
1006  $generator->setObjects($objects);
1007  $generator->setTimeRange($start, $end);
1008  $generator->setBaseObject($vcalendar);
1009  $generator->setTimeZone($calendarTimeZone);
1010 
1011  if ($inboxProps) {
1012  $generator->setVAvailability(
1013  VObject\Reader::read(
1014  $inboxProps[$caldavNS . 'calendar-availability']
1015  )
1016  );
1017  }
1018 
1019  $result = $generator->getResult();
1020 
1021  $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
1022  $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
1023  $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
1024 
1025  return [
1026  'calendar-data' => $result,
1027  'request-status' => '2.0;Success',
1028  'href' => 'mailto:' . $email,
1029  ];
1030  }
1031 
1040 
1041  $scheduleReply = $request->getHeader('Schedule-Reply');
1042  return $scheduleReply !== 'F';
1043 
1044  }
1045 
1057  function getPluginInfo() {
1058 
1059  return [
1060  'name' => $this->getPluginName(),
1061  'description' => 'Adds calendar-auto-schedule, as defined in rfc6638',
1062  'link' => 'http://sabre.io/dav/scheduling/',
1063  ];
1064 
1065  }
1066 }
if($err=$client->getError()) $namespace
CalDAV plugin.
Definition: Plugin.php:28
This interface represents a HTTP response.
The RequestInterface represents a HTTP request.
if($orgName !==null) if($spconfig->hasValue('contacts')) $email
Definition: metadata.php:201
handle($propertyName, $valueOrCallBack)
Handles a specific property.
Definition: PropFind.php:94
processICalendarChange($oldObject=null, VCalendar $newObject, array $addresses, array $ignore=[], &$modified=false)
This method looks at an old iCalendar object, a new iCalendar object and starts sending scheduling me...
Definition: Plugin.php:652
$path
Definition: aliased.php:25
beforeUnbind($path)
This method is triggered before a file gets deleted.
Definition: Plugin.php:406
The baseclass for all server plugins.
The ITip class is a utility class that helps with processing so-called iTip messages.
Definition: Broker.php:38
getOwner()
Returns the owner principal.
setBody($body)
Updates the body resource with a new stream.
on($eventName, callable $callBack, $priority=100)
Subscribe to an event.
This class represents a set of properties that are going to be updated.
Definition: PropPatch.php:20
$result
This is the abstract base class for both the Request and Response objects.
Definition: Message.php:14
foreach($paths as $path) $request
Definition: asyncclient.php:32
outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response)
This method handles POST requests to the schedule-outbox.
Definition: Plugin.php:734
LocalHref property.
Definition: LocalHref.php:25
Implement this interface to have a node be recognized as a CalDAV scheduling inbox.
Definition: IInbox.php:13
getPath()
Returns the path this PROPFIND request is for.
Definition: PropFind.php:187
deliver(ITip\Message $iTipMessage)
This method is responsible for delivering the ITip message.
Definition: Plugin.php:382
This class helps with generating FREEBUSY reports based on existing sets of objects.
calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew)
This method is triggered whenever there was a calendar object gets created or updated.
Definition: Plugin.php:348
This class holds all the information about a PROPFIND request.
Definition: PropFind.php:11
scheduleLocalDelivery(ITip\Message $iTipMessage)
Event handler for the &#39;schedule&#39; event.
Definition: Plugin.php:444
set($propertyName, $value, $status=null)
Sets the value of the property.
Definition: PropFind.php:121
handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response)
This method is responsible for parsing a free-busy query request and returning it&#39;s result...
Definition: Plugin.php:798
propFind(PropFind $propFind, INode $node)
This method handler is invoked during fetching of properties.
Definition: Plugin.php:204
getPluginName()
Returns the name of the plugin.
Definition: Plugin.php:89
The VCalendar component.
Definition: VCalendar.php:23
Calendar interface.
Definition: ICalendar.php:16
The SchedulingObject represents a scheduling object in the Inbox collection.
$start
Definition: bench.php:8
$messages
Definition: en.php:5
$urls
Definition: croninfo.php:28
getPluginName()
Returns a plugin name.
Definition: Plugin.php:135
catch(Exception $e) $message
getPluginInfo()
Returns a bunch of meta-data about the plugin.
Definition: Plugin.php:1057
getFeatures()
Returns a list of features for the DAV: HTTP header.
Definition: Plugin.php:75
getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
Returns free-busy information for a specific address.
Definition: Plugin.php:897
setStatus($status)
Sets the HTTP status code.
getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
This method is triggered whenever a subsystem requests the privileges that are supported on a particu...
Definition: Plugin.php:584
handle($properties, callable $callback)
Call this function if you wish to handle updating certain properties.
Definition: PropPatch.php:87
httpPost(RequestInterface $request, ResponseInterface $response)
This method handles POST request for the outbox.
Definition: Plugin.php:167
Main DAV server class.
Definition: Server.php:23
getHeader($name)
Returns a specific HTTP header, based on it&#39;s name.
initialize(Server $server)
Initializes the plugin.
Definition: Plugin.php:101
The INode interface is the base interface, and the parent class of both ICollection and IFile...
Definition: INode.php:12
getHTTPMethods($uri)
Use this method to tell the server this plugin defines additional HTTP methods.
Definition: Plugin.php:144
propPatch($path, PropPatch $propPatch)
This method is called during property updates.
Definition: Plugin.php:320
CalendarObject interface.
getPath()
Returns the relative path.
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
static getUUID()
Returns a pseudo-random v4 UUID.
Definition: UUIDUtil.php:26
$results
Definition: svg-scanner.php:47
scheduleReply(RequestInterface $request)
This method checks the &#39;Schedule-Reply&#39; header and returns false if it&#39;s &#39;F&#39;, otherwise true...
Definition: Plugin.php:1039
Exception thrown by Reader if an invalid object was attempted to be parsed.
$url
if($path[strlen($path) - 1]==='/') if(is_dir($path)) if(!file_exists($path)) if(preg_match('#\.php$#D', mb_strtolower($path, 'UTF-8'))) $contentType
Definition: module.php:144
while(false !==($line=fgets($in))) if(! $columns) $ignore
Definition: Utf8Test.php:63
$response
$caldavPlugin
const NS_CALDAV
This is the official CalDAV namespace.
Definition: Plugin.php:33
Implement this interface to have a node be recognized as a CalDAV scheduling outbox.
Definition: IOutbox.php:13
setHeader($name, $value)
Updates a HTTP header.
getAddressesForPrincipal($principal)
Returns a list of addresses that are associated with a principal.
Definition: Plugin.php:699
getBody()
Returns the message body, as it&#39;s internal representation.
$aclPlugin