Ejemplo n.º 1
0
 /**
  * Intercepts GET requests on calendar urls ending with ?export.
  *
  * @param RequestInterface $request
  * @param ResponseInterface $response
  * @return bool
  */
 function httpGet(RequestInterface $request, ResponseInterface $response)
 {
     $queryParams = $request->getQueryParameters();
     if (!array_key_exists('export', $queryParams)) {
         return;
     }
     $path = $request->getPath();
     $node = $this->server->getProperties($path, ['{DAV:}resourcetype', '{DAV:}displayname', '{http://sabredav.org/ns}sync-token', '{http://apple.com/ns/ical/}calendar-color']);
     if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
         return;
     }
     // Marking the transactionType, for logging purposes.
     $this->server->transactionType = 'get-calendar-export';
     $properties = $node;
     $start = null;
     $end = null;
     $expand = false;
     if (isset($queryParams['start'])) {
         if (!ctype_digit($queryParams['start'])) {
             throw new BadRequest('The start= parameter must contain a unix timestamp');
         }
         $start = DateTime::createFromFormat('U', $queryParams['start']);
     }
     if (isset($queryParams['end'])) {
         if (!ctype_digit($queryParams['end'])) {
             throw new BadRequest('The end= parameter must contain a unix timestamp');
         }
         $end = DateTime::createFromFormat('U', $queryParams['end']);
     }
     if (isset($queryParams['expand']) && !!$queryParams['expand']) {
         if (!$start || !$end) {
             throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
         }
         $expand = true;
     }
     $format = \Sabre\HTTP\Util::Negotiate($request->getHeader('Accept'), ['text/calendar', 'application/calendar+json']);
     if (isset($queryParams['accept'])) {
         if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') {
             $format = 'application/calendar+json';
         }
     }
     if (!$format) {
         $format = 'text/calendar';
     }
     $this->generateResponse($path, $start, $end, $expand, $format, $properties, $response);
     // Returning false to break the event chain
     return false;
 }
Ejemplo n.º 2
0
 /**
  * This method is called when properties are retrieved.
  *
  * This specific handler is called very late in the process, because we
  * want other systems to first have a chance to handle the properties.
  *
  * @param PropFind $propFind
  * @param INode $node
  * @return void
  */
 function propFindLate(PropFind $propFind, INode $node)
 {
     $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use($propFind) {
         // If we already have a sync-token from the current propFind
         // request, we can re-use that.
         $val = $propFind->get('{http://sabredav.org/ns}sync-token');
         if ($val) {
             return $val;
         }
         $val = $propFind->get('{DAV:}sync-token');
         if ($val && is_scalar($val)) {
             return $val;
         }
         if ($val && $val instanceof Property\IHref) {
             return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
         }
         // If we got here, the earlier two properties may simply not have
         // been part of the earlier request. We're going to fetch them.
         $result = $this->server->getProperties($propFind->getPath(), ['{http://sabredav.org/ns}sync-token', '{DAV:}sync-token']);
         if (isset($result['{http://sabredav.org/ns}sync-token'])) {
             return $result['{http://sabredav.org/ns}sync-token'];
         }
         if (isset($result['{DAV:}sync-token'])) {
             $val = $result['{DAV:}sync-token'];
             if (is_scalar($val)) {
                 return $val;
             } elseif ($val instanceof Property\IHref) {
                 return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
             }
         }
     });
 }
Ejemplo n.º 3
0
 /**
  * This method is responsible for generating the actual, full response.
  *
  * @param string $path
  * @param DateTime|null $start
  * @param DateTime|null $end
  * @param bool $expand
  * @param string $componentType
  * @param string $format
  * @param array $properties
  * @param ResponseInterface $response
  */
 protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response)
 {
     $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
     $blobs = [];
     if ($start || $end || $componentType) {
         // If there was a start or end filter, we need to enlist
         // calendarQuery for speed.
         $calendarNode = $this->server->tree->getNodeForPath($path);
         $queryResult = $calendarNode->calendarQuery(['name' => 'VCALENDAR', 'comp-filters' => [['name' => $componentType, 'comp-filters' => [], 'prop-filters' => [], 'is-not-defined' => false, 'time-range' => ['start' => $start, 'end' => $end]]], 'prop-filters' => [], 'is-not-defined' => false, 'time-range' => null]);
         // queryResult is just a list of base urls. We need to prefix the
         // calendar path.
         $queryResult = array_map(function ($item) use($path) {
             return $path . '/' . $item;
         }, $queryResult);
         $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
         unset($queryResult);
     } else {
         $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
     }
     // Flattening the arrays
     foreach ($nodes as $node) {
         if (isset($node[200][$calDataProp])) {
             $blobs[$node['href']] = $node[200][$calDataProp];
         }
     }
     unset($nodes);
     $mergedCalendar = $this->mergeObjects($properties, $blobs);
     if ($expand) {
         $calendarTimeZone = null;
         // We're expanding, and for that we need to figure out the
         // calendar's timezone.
         $tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone';
         $tzResult = $this->server->getProperties($path, [$tzProp]);
         if (isset($tzResult[$tzProp])) {
             // This property contains a VCALENDAR with a single
             // VTIMEZONE.
             $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
             $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
             // Destroy circular references to PHP will GC the object.
             $vtimezoneObj->destroy();
             unset($vtimezoneObj);
         } else {
             // Defaulting to UTC.
             $calendarTimeZone = new DateTimeZone('UTC');
         }
         $mergedCalendar->expand($start, $end, $calendarTimeZone);
     }
     $response->setHeader('Content-Type', $format);
     switch ($format) {
         case 'text/calendar':
             $mergedCalendar = $mergedCalendar->serialize();
             break;
         case 'application/calendar+json':
             $mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
             break;
     }
     $response->setStatus(200);
     $response->setBody($mergedCalendar);
 }
Ejemplo n.º 4
0
 /**
  * This method is responsible for parsing a free-busy query request and
  * returning it's result.
  *
  * @param IOutbox $outbox
  * @param VObject\Component $vObject
  * @param RequestInterface $request
  * @param ResponseInterface $response
  * @return string
  */
 protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response)
 {
     $vFreeBusy = $vObject->VFREEBUSY;
     $organizer = $vFreeBusy->organizer;
     $organizer = (string) $organizer;
     // Validating if the organizer matches the owner of the inbox.
     $owner = $outbox->getOwner();
     $caldavNS = '{' . self::NS_CALDAV . '}';
     $uas = $caldavNS . 'calendar-user-address-set';
     $props = $this->server->getProperties($owner, [$uas]);
     if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
         throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
     }
     if (!isset($vFreeBusy->ATTENDEE)) {
         throw new BadRequest('You must at least specify 1 attendee');
     }
     $attendees = [];
     foreach ($vFreeBusy->ATTENDEE as $attendee) {
         $attendees[] = (string) $attendee;
     }
     if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
         throw new BadRequest('DTSTART and DTEND must both be specified');
     }
     $startRange = $vFreeBusy->DTSTART->getDateTime();
     $endRange = $vFreeBusy->DTEND->getDateTime();
     $results = [];
     foreach ($attendees as $attendee) {
         $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
     }
     $dom = new \DOMDocument('1.0', 'utf-8');
     $dom->formatOutput = true;
     $scheduleResponse = $dom->createElement('cal:schedule-response');
     foreach ($this->server->xmlNamespaces as $namespace => $prefix) {
         $scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
     }
     $dom->appendChild($scheduleResponse);
     foreach ($results as $result) {
         $xresponse = $dom->createElement('cal:response');
         $recipient = $dom->createElement('cal:recipient');
         $recipientHref = $dom->createElement('d:href');
         $recipientHref->appendChild($dom->createTextNode($result['href']));
         $recipient->appendChild($recipientHref);
         $xresponse->appendChild($recipient);
         $reqStatus = $dom->createElement('cal:request-status');
         $reqStatus->appendChild($dom->createTextNode($result['request-status']));
         $xresponse->appendChild($reqStatus);
         if (isset($result['calendar-data'])) {
             $calendardata = $dom->createElement('cal:calendar-data');
             $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
             $xresponse->appendChild($calendardata);
         }
         $scheduleResponse->appendChild($xresponse);
     }
     $response->setStatus(200);
     $response->setHeader('Content-Type', 'application/xml');
     $response->setBody($dom->saveXML());
 }
Ejemplo n.º 5
0
 function testResourceType()
 {
     $tree = array(new DirectoryMock('directory'));
     $server = new DAV\Server($tree);
     $plugin = new Plugin();
     $server->addPlugin($plugin);
     $props = $server->getProperties('directory', array('{DAV:}resourcetype'));
     $this->assertTrue($props['{DAV:}resourcetype']->is('{' . Plugin::NS_CARDDAV . '}directory'));
 }
Ejemplo n.º 6
0
 /**
  * Checks if the submitted iCalendar data is in fact, valid.
  *
  * An exception is thrown if it's not.
  *
  * @param resource|string $data
  * @param string $path
  * @param bool $modified Should be set to true, if this event handler
  *                       changed &$data.
  * @param RequestInterface $request The http request.
  * @param ResponseInterface $response The http response.
  * @param bool $isNew Is the item a new one, or an update.
  * @return void
  */
 protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew)
 {
     // If it's a stream, we convert it to a string first.
     if (is_resource($data)) {
         $data = stream_get_contents($data);
     }
     $before = md5($data);
     // Converting the data to unicode, if needed.
     $data = DAV\StringUtil::ensureUTF8($data);
     if ($before !== md5($data)) {
         $modified = true;
     }
     try {
         // If the data starts with a [, we can reasonably assume we're dealing
         // with a jCal object.
         if (substr($data, 0, 1) === '[') {
             $vobj = VObject\Reader::readJson($data);
             // Converting $data back to iCalendar, as that's what we
             // technically support everywhere.
             $data = $vobj->serialize();
             $modified = true;
         } else {
             $vobj = VObject\Reader::read($data);
         }
     } catch (VObject\ParseException $e) {
         throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
     }
     if ($vobj->name !== 'VCALENDAR') {
         throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
     }
     $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
     // Get the Supported Components for the target calendar
     list($parentPath) = Uri\split($path);
     $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
     if (isset($calendarProperties[$sCCS])) {
         $supportedComponents = $calendarProperties[$sCCS]->getValue();
     } else {
         $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
     }
     $foundType = null;
     $foundUID = null;
     foreach ($vobj->getComponents() as $component) {
         switch ($component->name) {
             case 'VTIMEZONE':
                 continue 2;
             case 'VEVENT':
             case 'VTODO':
             case 'VJOURNAL':
                 if (is_null($foundType)) {
                     $foundType = $component->name;
                     if (!in_array($foundType, $supportedComponents)) {
                         throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
                     }
                     if (!isset($component->UID)) {
                         throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
                     }
                     $foundUID = (string) $component->UID;
                 } else {
                     if ($foundType !== $component->name) {
                         throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
                     }
                     if ($foundUID !== (string) $component->UID) {
                         throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
                     }
                 }
                 break;
             default:
                 throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
         }
     }
     if (!$foundType) {
         throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
     }
     // We use an extra variable to allow event handles to tell us wether
     // the object was modified or not.
     //
     // This helps us determine if we need to re-serialize the object.
     $subModified = false;
     $this->server->emit('calendarObjectChange', [$request, $response, $vobj, $parentPath, &$subModified, $isNew]);
     if ($subModified) {
         // An event handler told us that it modified the object.
         $data = $vobj->serialize();
         // Using md5 to figure out if there was an *actual* change.
         if (!$modified && $before !== md5($data)) {
             $modified = true;
         }
     }
 }
Ejemplo n.º 7
0
 /**
  * Returns free-busy information for a specific address. The returned
  * data is an array containing the following properties:
  *
  * calendar-data : A VFREEBUSY VObject
  * request-status : an iTip status code.
  * href: The principal's email address, as requested
  *
  * The following request status codes may be returned:
  *   * 2.0;description
  *   * 3.7;description
  *
  * @param string $email address
  * @param DateTimeInterface $start
  * @param DateTimeInterface $end
  * @param VObject\Component $request
  * @return array
  */
 protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
 {
     $caldavNS = '{' . self::NS_CALDAV . '}';
     $aclPlugin = $this->server->getPlugin('acl');
     if (substr($email, 0, 7) === 'mailto:') {
         $email = substr($email, 7);
     }
     $result = $aclPlugin->principalSearch(['{http://sabredav.org/ns}email-address' => $email], ['{DAV:}principal-URL', $caldavNS . 'calendar-home-set', $caldavNS . 'schedule-inbox-URL', '{http://sabredav.org/ns}email-address']);
     if (!count($result)) {
         return ['request-status' => '3.7;Could not find principal', 'href' => 'mailto:' . $email];
     }
     if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
         return ['request-status' => '3.7;No calendar-home-set property found', 'href' => 'mailto:' . $email];
     }
     if (!isset($result[0][200][$caldavNS . 'schedule-inbox-URL'])) {
         return ['request-status' => '3.7;No schedule-inbox-URL property found', 'href' => 'mailto:' . $email];
     }
     $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
     $inboxUrl = $result[0][200][$caldavNS . 'schedule-inbox-URL']->getHref();
     // Grabbing the calendar list
     $objects = [];
     $calendarTimeZone = new DateTimeZone('UTC');
     foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
         if (!$node instanceof ICalendar) {
             continue;
         }
         $sct = $caldavNS . 'schedule-calendar-transp';
         $ctz = $caldavNS . 'calendar-timezone';
         $props = $node->getProperties([$sct, $ctz]);
         if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) {
             // If a calendar is marked as 'transparent', it means we must
             // ignore it for free-busy purposes.
             continue;
         }
         $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy');
         if (isset($props[$ctz])) {
             $vtimezoneObj = VObject\Reader::read($props[$ctz]);
             $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
             // Destroy circular references so PHP can garbage collect the object.
             $vtimezoneObj->destroy();
         }
         // Getting the list of object uris within the time-range
         $urls = $node->calendarQuery(['name' => 'VCALENDAR', 'comp-filters' => [['name' => 'VEVENT', 'comp-filters' => [], 'prop-filters' => [], 'is-not-defined' => false, 'time-range' => ['start' => $start, 'end' => $end]]], 'prop-filters' => [], 'is-not-defined' => false, 'time-range' => null]);
         $calObjects = array_map(function ($url) use($node) {
             $obj = $node->getChild($url)->get();
             return $obj;
         }, $urls);
         $objects = array_merge($objects, $calObjects);
     }
     $inboxProps = $this->server->getProperties($inboxUrl, $caldavNS . 'calendar-availability');
     $vcalendar = new VObject\Component\VCalendar();
     $vcalendar->METHOD = 'REPLY';
     $generator = new VObject\FreeBusyGenerator();
     $generator->setObjects($objects);
     $generator->setTimeRange($start, $end);
     $generator->setBaseObject($vcalendar);
     $generator->setTimeZone($calendarTimeZone);
     if ($inboxProps) {
         $generator->setVAvailability(VObject\Reader::read($inboxProps[$caldavNS . 'calendar-availability']));
     }
     $result = $generator->getResult();
     $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
     $vcalendar->VFREEBUSY->UID = (string) $request->VFREEBUSY->UID;
     $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
     return ['calendar-data' => $result, 'request-status' => '2.0;Success', 'href' => 'mailto:' . $email];
 }
Ejemplo n.º 8
0
 /**
  * @depends testSupportedReportSetPropertyNonCalendar
  */
 function testCalendarProperties()
 {
     $ns = '{urn:ietf:params:xml:ns:caldav}';
     $props = $this->server->getProperties('calendars/user1/UUID-123467', [$ns . 'max-resource-size', $ns . 'supported-calendar-data', $ns . 'supported-collation-set']);
     $this->assertEquals([$ns . 'max-resource-size' => 10000000, $ns . 'supported-calendar-data' => new Xml\Property\SupportedCalendarData(), $ns . 'supported-collation-set' => new Xml\Property\SupportedCollationSet()], $props);
 }
Ejemplo n.º 9
0
 /**
  * Checks if the submitted iCalendar data is in fact, valid.
  *
  * An exception is thrown if it's not.
  *
  * @param resource|string $data
  * @param string $path
  * @param bool $modified Should be set to true, if this event handler
  *                       changed &$data.
  * @param RequestInterface $request The http request.
  * @param ResponseInterface $response The http response.
  * @param bool $isNew Is the item a new one, or an update.
  * @return void
  */
 protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew)
 {
     // If it's a stream, we convert it to a string first.
     if (is_resource($data)) {
         $data = stream_get_contents($data);
     }
     $before = $data;
     try {
         // If the data starts with a [, we can reasonably assume we're dealing
         // with a jCal object.
         if (substr($data, 0, 1) === '[') {
             $vobj = VObject\Reader::readJson($data);
             // Converting $data back to iCalendar, as that's what we
             // technically support everywhere.
             $data = $vobj->serialize();
             $modified = true;
         } else {
             $vobj = VObject\Reader::read($data);
         }
     } catch (VObject\ParseException $e) {
         throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
     }
     if ($vobj->name !== 'VCALENDAR') {
         throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
     }
     $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
     // Get the Supported Components for the target calendar
     list($parentPath) = Uri\split($path);
     $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
     if (isset($calendarProperties[$sCCS])) {
         $supportedComponents = $calendarProperties[$sCCS]->getValue();
     } else {
         $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
     }
     $foundType = null;
     foreach ($vobj->getComponents() as $component) {
         switch ($component->name) {
             case 'VTIMEZONE':
                 continue 2;
             case 'VEVENT':
             case 'VTODO':
             case 'VJOURNAL':
                 $foundType = $component->name;
                 break;
         }
     }
     if (!$foundType || !in_array($foundType, $supportedComponents)) {
         throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type ' . implode(', ', $supportedComponents));
     }
     $options = VObject\Node::PROFILE_CALDAV;
     $prefer = $this->server->getHTTPPrefer();
     if ($prefer['handling'] !== 'strict') {
         $options |= VObject\Node::REPAIR;
     }
     $messages = $vobj->validate($options);
     $highestLevel = 0;
     $warningMessage = null;
     // $messages contains a list of problems with the vcard, along with
     // their severity.
     foreach ($messages as $message) {
         if ($message['level'] > $highestLevel) {
             // Recording the highest reported error level.
             $highestLevel = $message['level'];
             $warningMessage = $message['message'];
         }
         switch ($message['level']) {
             case 1:
                 // Level 1 means that there was a problem, but it was repaired.
                 $modified = true;
                 break;
             case 2:
                 // Level 2 means a warning, but not critical
                 break;
             case 3:
                 // Level 3 means a critical error
                 throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: ' . $message['message']);
         }
     }
     if ($warningMessage) {
         $response->setHeader('X-Sabre-Ew-Gross', 'iCalendar validation warning: ' . $warningMessage);
     }
     // We use an extra variable to allow event handles to tell us wether
     // the object was modified or not.
     //
     // This helps us determine if we need to re-serialize the object.
     $subModified = false;
     $this->server->emit('calendarObjectChange', [$request, $response, $vobj, $parentPath, &$subModified, $isNew]);
     if ($modified || $subModified) {
         // An event handler told us that it modified the object.
         $data = $vobj->serialize();
         // Using md5 to figure out if there was an *actual* change.
         if (!$modified && strcmp($data, $before) !== 0) {
             $modified = true;
         }
     }
     // Destroy circular references so PHP will garbage collect the object.
     $vobj->destroy();
 }