This iterator may loop infinitely in the future, therefore it is important that if you use this class, you set hard limits for the amount of iterations you want to handle. Note that currently there is not full support for the entire iCalendar specification, as it's very complex and contains a lot of permutations that's not yet used very often in software. For the focus has been on features as they actually appear in Calendaring software, but this may well get expanded as needed / on demand The following RRULE properties are supported * UNTIL * INTERVAL * COUNT * FREQ=DAILY * BYDAY * BYHOUR * BYMONTH * FREQ=WEEKLY * BYDAY * BYHOUR * WKST * FREQ=MONTHLY * BYMONTHDAY * BYDAY * BYSETPOS * FREQ=YEARLY * BYMONTH * BYMONTHDAY (only if BYMONTH is also set) * BYDAY (only if BYMONTH is also set) Anything beyond this is 'undefined', which means that it may get ignored, or you may get unexpected results. The effect is that in some applications the specified recurrence may look incorrect, or is missing. The recurrence iterator also does not yet support THISANDFUTURE.
Author: Evert Pot (http://evertpot.com/)
Inheritance: implements Iterator
Exemple #1
0
 /**
  * Returns true or false depending on if the event falls in the specified
  * time-range. This is used for filtering purposes.
  *
  * The rules used to determine if an event falls within the specified
  * time-range is based on the CalDAV specification.
  *
  * @param \DateTime $start
  * @param \DateTime $end
  * @return bool
  */
 public function isInTimeRange(\DateTime $start, \DateTime $end)
 {
     if ($this->RRULE) {
         $it = new EventIterator($this);
         $it->fastForward($start);
         // We fast-forwarded to a spot where the end-time of the
         // recurrence instance exceeded the start of the requested
         // time-range.
         //
         // If the starttime of the recurrence did not exceed the
         // end of the time range as well, we have a match.
         return $it->getDTStart() < $end && $it->getDTEnd() > $start;
     }
     $effectiveStart = $this->DTSTART->getDateTime();
     if (isset($this->DTEND)) {
         // The DTEND property is considered non inclusive. So for a 3 day
         // event in july, dtstart and dtend would have to be July 1st and
         // July 4th respectively.
         //
         // See:
         // http://tools.ietf.org/html/rfc5545#page-54
         $effectiveEnd = $this->DTEND->getDateTime();
     } elseif (isset($this->DURATION)) {
         $effectiveEnd = clone $effectiveStart;
         $effectiveEnd->add(VObject\DateTimeParser::parseDuration($this->DURATION));
     } elseif (!$this->DTSTART->hasTime()) {
         $effectiveEnd = clone $effectiveStart;
         $effectiveEnd->modify('+1 day');
     } else {
         $effectiveEnd = clone $effectiveStart;
     }
     return $start <= $effectiveEnd && $end > $effectiveStart;
 }
    /**
     * A pretty slow test. Had to be marked as 'medium' for phpunit to not die
     * after 1 second. Would be good to optimize later.
     *
     * @medium
     */
    function testGetDTEnd()
    {
        $ics = <<<ICS
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.4//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
TRANSP:OPAQUE
DTEND;TZID=America/New_York:20070925T170000
UID:uuid
DTSTAMP:19700101T000000Z
LOCATION:
DESCRIPTION:
STATUS:CONFIRMED
SEQUENCE:18
SUMMARY:Stuff
DTSTART;TZID=America/New_York:20070925T160000
CREATED:20071004T144642Z
RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20071030T035959Z;BYDAY=5TU
END:VEVENT
END:VCALENDAR
ICS;
        $vObject = Reader::read($ics);
        $it = new Recur\EventIterator($vObject, (string) $vObject->VEVENT->UID);
        while ($it->valid()) {
            $it->next();
        }
        // If we got here, it means we were successful. The bug that was in the
        // system before would fail on the 5th tuesday of the month, if the 5th
        // tuesday did not exist.
        $this->assertTrue(true);
    }
 /**
  * Something, somewhere produced an ics with an interval set to 0. Because
  * this means we increase the current day (or week, month) by 0, this also
  * results in an infinite loop.
  *
  * @expectedException InvalidArgumentException
  * @return void
  */
 function testZeroInterval()
 {
     $ev = $this->vcal->createComponent('VEVENT');
     $ev->UID = 'uuid';
     $ev->DTSTART = '20120824T145700Z';
     $ev->RRULE = 'FREQ=YEARLY;INTERVAL=0';
     $this->vcal->add($ev);
     $it = new Recur\EventIterator($this->vcal, 'uuid');
     $it->fastForward(new DateTime('2013-01-01 23:00:00', new DateTimeZone('UTC')));
     // if we got this far.. it means we are no longer infinitely looping
 }
Exemple #4
0
 /**
  * Returns true or false depending on if the event falls in the specified
  * time-range. This is used for filtering purposes.
  *
  * The rules used to determine if an event falls within the specified
  * time-range is based on the CalDAV specification.
  *
  * @param DateTimeInterface $start
  * @param DateTimeInterface $end
  *
  * @return bool
  */
 function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
 {
     if ($this->RRULE) {
         try {
             $it = new EventIterator($this, null, $start->getTimezone());
         } catch (NoInstancesException $e) {
             // If we've catched this exception, there are no instances
             // for the event that fall into the specified time-range.
             return false;
         }
         $it->fastForward($start);
         // We fast-forwarded to a spot where the end-time of the
         // recurrence instance exceeded the start of the requested
         // time-range.
         //
         // If the starttime of the recurrence did not exceed the
         // end of the time range as well, we have a match.
         return $it->getDTStart() < $end && $it->getDTEnd() > $start;
     }
     if (!isset($this->DTSTART)) {
         return false;
     }
     $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone());
     if (isset($this->DTEND)) {
         // The DTEND property is considered non inclusive. So for a 3 day
         // event in july, dtstart and dtend would have to be July 1st and
         // July 4th respectively.
         //
         // See:
         // http://tools.ietf.org/html/rfc5545#page-54
         $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone());
     } elseif (isset($this->DURATION)) {
         $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
     } elseif (!$this->DTSTART->hasTime()) {
         $effectiveEnd = $effectiveStart->modify('+1 day');
     } else {
         $effectiveEnd = $effectiveStart;
     }
     return $start < $effectiveEnd && $end > $effectiveStart;
 }
Exemple #5
0
 /**
  * Processes incoming REPLY messages.
  *
  * The message is a reply. This is for example an attendee telling
  * an organizer he accepted the invite, or declined it.
  *
  * @param Message $itipMessage
  * @param VCalendar $existingObject
  * @return VCalendar|null
  */
 protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null)
 {
     // A reply can only be processed based on an existing object.
     // If the object is not available, the reply is ignored.
     if (!$existingObject) {
         return null;
     }
     $instances = array();
     $requestStatus = '2.0';
     // Finding all the instances the attendee replied to.
     foreach ($itipMessage->message->VEVENT as $vevent) {
         $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
         $attendee = $vevent->ATTENDEE;
         $instances[$recurId] = $attendee['PARTSTAT']->getValue();
         if (isset($vevent->{'REQUEST-STATUS'})) {
             $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
             list($requestStatus) = explode(';', $requestStatus);
         }
     }
     // Now we need to loop through the original organizer event, to find
     // all the instances where we have a reply for.
     $masterObject = null;
     foreach ($existingObject->VEVENT as $vevent) {
         $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
         if ($recurId === 'master') {
             $masterObject = $vevent;
         }
         if (isset($instances[$recurId])) {
             $attendeeFound = false;
             if (isset($vevent->ATTENDEE)) {
                 foreach ($vevent->ATTENDEE as $attendee) {
                     if ($attendee->getValue() === $itipMessage->sender) {
                         $attendeeFound = true;
                         $attendee['PARTSTAT'] = $instances[$recurId];
                         $attendee['SCHEDULE-STATUS'] = $requestStatus;
                         // Un-setting the RSVP status, because we now know
                         // that the attende already replied.
                         unset($attendee['RSVP']);
                         break;
                     }
                 }
             }
             if (!$attendeeFound) {
                 // Adding a new attendee. The iTip documentation calls this
                 // a party crasher.
                 $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, array('PARTSTAT' => $instances[$recurId]));
                 if ($itipMessage->senderName) {
                     $attendee['CN'] = $itipMessage->senderName;
                 }
             }
             unset($instances[$recurId]);
         }
     }
     if (!$masterObject) {
         // No master object, we can't add new instances.
         return null;
     }
     // If we got replies to instances that did not exist in the
     // original list, it means that new exceptions must be created.
     foreach ($instances as $recurId => $partstat) {
         $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
         $found = false;
         $iterations = 1000;
         do {
             $newObject = $recurrenceIterator->getEventObject();
             $recurrenceIterator->next();
             if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
                 $found = true;
             }
             $iterations--;
         } while ($recurrenceIterator->valid() && !$found && $iterations);
         // Invalid recurrence id. Skipping this object.
         if (!$found) {
             continue;
         }
         unset($newObject->RRULE, $newObject->EXDATE, $newObject->RDATE);
         $attendeeFound = false;
         if (isset($newObject->ATTENDEE)) {
             foreach ($newObject->ATTENDEE as $attendee) {
                 if ($attendee->getValue() === $itipMessage->sender) {
                     $attendeeFound = true;
                     $attendee['PARTSTAT'] = $partstat;
                     break;
                 }
             }
         }
         if (!$attendeeFound) {
             // Adding a new attendee
             $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, array('PARTSTAT' => $partstat));
             if ($itipMessage->senderName) {
                 $attendee['CN'] = $itipMessage->senderName;
             }
         }
         $existingObject->add($newObject);
     }
     return $existingObject;
 }
Exemple #6
0
 /**
  * If this calendar object, has events with recurrence rules, this method
  * can be used to expand the event into multiple sub-events.
  *
  * Each event will be stripped from it's recurrence information, and only
  * the instances of the event in the specified timerange will be left
  * alone.
  *
  * In addition, this method will cause timezone information to be stripped,
  * and normalized to UTC.
  *
  * This method will alter the VCalendar. This cannot be reversed.
  *
  * This functionality is specifically used by the CalDAV standard. It is
  * possible for clients to request expand events, if they are rather simple
  * clients and do not have the possibility to calculate recurrences.
  *
  * @param DateTime $start
  * @param DateTime $end
  * @param DateTimeZone $timeZone reference timezone for floating dates and
  *                     times.
  * @return void
  */
 function expand(DateTime $start, DateTime $end, DateTimeZone $timeZone = null)
 {
     $newEvents = array();
     if (!$timeZone) {
         $timeZone = new DateTimeZone('UTC');
     }
     // An array of events. Events are indexed by UID. Each item in this
     // array is a list of one or more events that match the UID.
     $recurringEvents = array();
     foreach ($this->select('VEVENT') as $key => $vevent) {
         $uid = (string) $vevent->UID;
         if (!$uid) {
             throw new \LogicException('Event did not have a UID!');
         }
         if (isset($vevent->{'RECURRENCE-ID'}) || isset($vevent->RRULE)) {
             if (isset($recurringEvents[$uid])) {
                 $recurringEvents[$uid][] = $vevent;
             } else {
                 $recurringEvents[$uid] = array($vevent);
             }
             continue;
         }
         if (!isset($vevent->RRULE)) {
             if ($vevent->isInTimeRange($start, $end)) {
                 $newEvents[] = $vevent;
             }
             continue;
         }
     }
     foreach ($recurringEvents as $events) {
         try {
             $it = new EventIterator($events, $timeZone);
         } catch (NoInstancesException $e) {
             // This event is recurring, but it doesn't have a single
             // instance. We are skipping this event from the output
             // entirely.
             continue;
         }
         $it->fastForward($start);
         while ($it->valid() && $it->getDTStart() < $end) {
             if ($it->getDTEnd() > $start) {
                 $newEvents[] = $it->getEventObject();
             }
             $it->next();
         }
     }
     // Wiping out all old VEVENT objects
     unset($this->VEVENT);
     // Setting all properties to UTC time.
     foreach ($newEvents as $newEvent) {
         foreach ($newEvent->children as $child) {
             if ($child instanceof VObject\Property\ICalendar\DateTime && $child->hasTime()) {
                 $dt = $child->getDateTimes($timeZone);
                 // We only need to update the first timezone, because
                 // setDateTimes will match all other timezones to the
                 // first.
                 $dt[0]->setTimeZone(new DateTimeZone('UTC'));
                 $child->setDateTimes($dt);
             }
         }
         $this->add($newEvent);
     }
     // Removing all VTIMEZONE components
     unset($this->VTIMEZONE);
 }
Exemple #7
0
 /**
  * @depends testValues
  */
 function testOverridenEventNoValuesExpected()
 {
     $vcal = new VCalendar();
     $ev1 = $vcal->createComponent('VEVENT');
     $ev1->UID = 'overridden';
     $ev1->RRULE = 'FREQ=WEEKLY;COUNT=3';
     $ev1->DTSTART = '20120124T120000Z';
     $ev1->SUMMARY = 'baseEvent';
     $vcal->add($ev1);
     // ev2 overrides an event, and puts it 6 days earlier instead.
     $ev2 = $vcal->createComponent('VEVENT');
     $ev2->UID = 'overridden';
     $ev2->{'RECURRENCE-ID'} = '20120131T120000Z';
     $ev2->DTSTART = '20120125T120000Z';
     $ev2->SUMMARY = 'Override!';
     $vcal->add($ev2);
     $it = new EventIterator($vcal, 'overridden');
     $dates = array();
     $summaries = array();
     // The reported problem was specifically related to the VCALENDAR
     // expansion. In this parcitular case, we had to forward to the 28th of
     // january.
     $it->fastForward(new DateTime('2012-01-28 23:00:00'));
     // We stop the loop when it hits the 6th of februari. Normally this
     // iterator would hit 24, 25 (overriden from 31) and 7 feb but because
     // we 'filter' from the 28th till the 6th, we should get 0 results.
     while ($it->valid() && $it->getDTSTart() < new DateTime('2012-02-06 23:00:00')) {
         $dates[] = $it->getDTStart();
         $summaries[] = (string) $it->getEventObject()->SUMMARY;
         $it->next();
     }
     $this->assertEquals(array(), $dates);
     $this->assertEquals(array(), $summaries);
 }
 /**
  * Parses the input data and returns a correct VFREEBUSY object, wrapped in
  * a VCALENDAR.
  *
  * @return Component
  */
 function getResult()
 {
     $busyTimes = [];
     foreach ($this->objects as $key => $object) {
         foreach ($object->getBaseComponents() as $component) {
             switch ($component->name) {
                 case 'VEVENT':
                     $FBTYPE = 'BUSY';
                     if (isset($component->TRANSP) && strtoupper($component->TRANSP) === 'TRANSPARENT') {
                         break;
                     }
                     if (isset($component->STATUS)) {
                         $status = strtoupper($component->STATUS);
                         if ($status === 'CANCELLED') {
                             break;
                         }
                         if ($status === 'TENTATIVE') {
                             $FBTYPE = 'BUSY-TENTATIVE';
                         }
                     }
                     $times = [];
                     if ($component->RRULE) {
                         try {
                             $iterator = new EventIterator($object, (string) $component->uid, $this->timeZone);
                         } catch (NoInstancesException $e) {
                             // This event is recurring, but it doesn't have a single
                             // instance. We are skipping this event from the output
                             // entirely.
                             unset($this->objects[$key]);
                             continue;
                         }
                         if ($this->start) {
                             $iterator->fastForward($this->start);
                         }
                         $maxRecurrences = 200;
                         while ($iterator->valid() && --$maxRecurrences) {
                             $startTime = $iterator->getDTStart();
                             if ($this->end && $startTime > $this->end) {
                                 break;
                             }
                             $times[] = [$iterator->getDTStart(), $iterator->getDTEnd()];
                             $iterator->next();
                         }
                     } else {
                         $startTime = $component->DTSTART->getDateTime($this->timeZone);
                         if ($this->end && $startTime > $this->end) {
                             break;
                         }
                         $endTime = null;
                         if (isset($component->DTEND)) {
                             $endTime = $component->DTEND->getDateTime($this->timeZone);
                         } elseif (isset($component->DURATION)) {
                             $duration = DateTimeParser::parseDuration((string) $component->DURATION);
                             $endTime = clone $startTime;
                             $endTime = $endTime->add($duration);
                         } elseif (!$component->DTSTART->hasTime()) {
                             $endTime = clone $startTime;
                             $endTime = $endTime->modify('+1 day');
                         } else {
                             // The event had no duration (0 seconds)
                             break;
                         }
                         $times[] = [$startTime, $endTime];
                     }
                     foreach ($times as $time) {
                         if ($this->end && $time[0] > $this->end) {
                             break;
                         }
                         if ($this->start && $time[1] < $this->start) {
                             break;
                         }
                         $busyTimes[] = [$time[0], $time[1], $FBTYPE];
                     }
                     break;
                 case 'VFREEBUSY':
                     foreach ($component->FREEBUSY as $freebusy) {
                         $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY';
                         // Skipping intervals marked as 'free'
                         if ($fbType === 'FREE') {
                             continue;
                         }
                         $values = explode(',', $freebusy);
                         foreach ($values as $value) {
                             list($startTime, $endTime) = explode('/', $value);
                             $startTime = DateTimeParser::parseDateTime($startTime);
                             if (substr($endTime, 0, 1) === 'P' || substr($endTime, 0, 2) === '-P') {
                                 $duration = DateTimeParser::parseDuration($endTime);
                                 $endTime = clone $startTime;
                                 $endTime = $endTime->add($duration);
                             } else {
                                 $endTime = DateTimeParser::parseDateTime($endTime);
                             }
                             if ($this->start && $this->start > $endTime) {
                                 continue;
                             }
                             if ($this->end && $this->end < $startTime) {
                                 continue;
                             }
                             $busyTimes[] = [$startTime, $endTime, $fbType];
                         }
                     }
                     break;
             }
         }
     }
     if ($this->baseObject) {
         $calendar = $this->baseObject;
     } else {
         $calendar = new VCalendar();
     }
     $vfreebusy = $calendar->createComponent('VFREEBUSY');
     $calendar->add($vfreebusy);
     if ($this->start) {
         $dtstart = $calendar->createProperty('DTSTART');
         $dtstart->setDateTime($this->start);
         $vfreebusy->add($dtstart);
     }
     if ($this->end) {
         $dtend = $calendar->createProperty('DTEND');
         $dtend->setDateTime($this->end);
         $vfreebusy->add($dtend);
     }
     $dtstamp = $calendar->createProperty('DTSTAMP');
     $dtstamp->setDateTime(new DateTimeImmutable('now', new \DateTimeZone('UTC')));
     $vfreebusy->add($dtstamp);
     foreach ($busyTimes as $busyTime) {
         $busyTime[0] = $busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
         $busyTime[1] = $busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
         $prop = $calendar->createProperty('FREEBUSY', $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z'));
         $prop['FBTYPE'] = $busyTime[2];
         $vfreebusy->add($prop);
     }
     return $calendar;
 }
 /**
  * If this calendar object, has events with recurrence rules, this method
  * can be used to expand the event into multiple sub-events.
  *
  * Each event will be stripped from it's recurrence information, and only
  * the instances of the event in the specified timerange will be left
  * alone.
  *
  * In addition, this method will cause timezone information to be stripped,
  * and normalized to UTC.
  *
  * This method will alter the VCalendar. This cannot be reversed.
  *
  * This functionality is specifically used by the CalDAV standard. It is
  * possible for clients to request expand events, if they are rather simple
  * clients and do not have the possibility to calculate recurrences.
  *
  * @param DateTime $start
  * @param DateTime $end
  * @return void
  */
 public function expand(\DateTime $start, \DateTime $end)
 {
     $newEvents = array();
     foreach ($this->select('VEVENT') as $key => $vevent) {
         if (isset($vevent->{'RECURRENCE-ID'})) {
             unset($this->children[$key]);
             continue;
         }
         if (!$vevent->rrule) {
             unset($this->children[$key]);
             if ($vevent->isInTimeRange($start, $end)) {
                 $newEvents[] = $vevent;
             }
             continue;
         }
         $uid = (string) $vevent->uid;
         if (!$uid) {
             throw new \LogicException('Event did not have a UID!');
         }
         $it = new EventIterator($this, $vevent->uid);
         $it->fastForward($start);
         while ($it->valid() && $it->getDTStart() < $end) {
             if ($it->getDTEnd() > $start) {
                 $newEvents[] = $it->getEventObject();
             }
             $it->next();
         }
         unset($this->children[$key]);
     }
     // Setting all properties to UTC time.
     foreach ($newEvents as $newEvent) {
         foreach ($newEvent->children as $child) {
             if ($child instanceof VObject\Property\ICalendar\DateTime && $child->hasTime()) {
                 $dt = $child->getDateTimes();
                 // We only need to update the first timezone, because
                 // setDateTimes will match all other timezones to the
                 // first.
                 $dt[0]->setTimeZone(new \DateTimeZone('UTC'));
                 $child->setDateTimes($dt);
             }
         }
         $this->add($newEvent);
     }
     // Removing all VTIMEZONE components
     unset($this->VTIMEZONE);
 }
 /**
  * Parses some information from calendar objects, used for optimized
  * calendar-queries.
  *
  * Returns an array with the following keys:
  *   * etag - An md5 checksum of the object without the quotes.
  *   * size - Size of the object in bytes
  *   * componentType - VEVENT, VTODO or VJOURNAL
  *   * firstOccurence
  *   * lastOccurence
  *   * uid - value of the UID property
  *
  * @param string $calendarData
  * @return array
  */
 public function getDenormalizedData($calendarData)
 {
     $vObject = Reader::read($calendarData);
     $componentType = null;
     $component = null;
     $firstOccurrence = null;
     $lastOccurrence = null;
     $uid = null;
     $classification = self::CLASSIFICATION_PUBLIC;
     foreach ($vObject->getComponents() as $component) {
         if ($component->name !== 'VTIMEZONE') {
             $componentType = $component->name;
             $uid = (string) $component->UID;
             break;
         }
     }
     if (!$componentType) {
         throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
     }
     if ($componentType === 'VEVENT' && $component->DTSTART) {
         $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
         // Finding the last occurrence is a bit harder
         if (!isset($component->RRULE)) {
             if (isset($component->DTEND)) {
                 $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
             } elseif (isset($component->DURATION)) {
                 $endDate = clone $component->DTSTART->getDateTime();
                 $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
                 $lastOccurrence = $endDate->getTimeStamp();
             } elseif (!$component->DTSTART->hasTime()) {
                 $endDate = clone $component->DTSTART->getDateTime();
                 $endDate->modify('+1 day');
                 $lastOccurrence = $endDate->getTimeStamp();
             } else {
                 $lastOccurrence = $firstOccurrence;
             }
         } else {
             $it = new EventIterator($vObject, (string) $component->UID);
             $maxDate = new \DateTime(self::MAX_DATE);
             if ($it->isInfinite()) {
                 $lastOccurrence = $maxDate->getTimeStamp();
             } else {
                 $end = $it->getDtEnd();
                 while ($it->valid() && $end < $maxDate) {
                     $end = $it->getDtEnd();
                     $it->next();
                 }
                 $lastOccurrence = $end->getTimeStamp();
             }
         }
     }
     if ($component->CLASS) {
         $classification = CalDavBackend::CLASSIFICATION_PRIVATE;
         switch ($component->CLASS->getValue()) {
             case 'PUBLIC':
                 $classification = CalDavBackend::CLASSIFICATION_PUBLIC;
                 break;
             case 'CONFIDENTIAL':
                 $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
                 break;
         }
     }
     return ['etag' => md5($calendarData), 'size' => strlen($calendarData), 'componentType' => $componentType, 'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence), 'lastOccurence' => $lastOccurrence, 'uid' => $uid, 'classification' => $classification];
 }
 /**
  * This method takes an array of iCalendar objects and applies its busy
  * times on fbData.
  *
  * @param FreeBusyData $fbData
  * @param VCalendar[] $objects
  */
 protected function calculateBusy(FreeBusyData $fbData, array $objects)
 {
     foreach ($objects as $key => $object) {
         foreach ($object->getBaseComponents() as $component) {
             switch ($component->name) {
                 case 'VEVENT':
                     $FBTYPE = 'BUSY';
                     if (isset($component->TRANSP) && strtoupper($component->TRANSP) === 'TRANSPARENT') {
                         break;
                     }
                     if (isset($component->STATUS)) {
                         $status = strtoupper($component->STATUS);
                         if ($status === 'CANCELLED') {
                             break;
                         }
                         if ($status === 'TENTATIVE') {
                             $FBTYPE = 'BUSY-TENTATIVE';
                         }
                     }
                     $times = [];
                     if ($component->RRULE) {
                         try {
                             $iterator = new EventIterator($object, (string) $component->uid, $this->timeZone);
                         } catch (NoInstancesException $e) {
                             // This event is recurring, but it doesn't have a single
                             // instance. We are skipping this event from the output
                             // entirely.
                             unset($this->objects[$key]);
                             continue;
                         }
                         if ($this->start) {
                             $iterator->fastForward($this->start);
                         }
                         $maxRecurrences = Settings::$maxRecurrences;
                         while ($iterator->valid() && --$maxRecurrences) {
                             $startTime = $iterator->getDTStart();
                             if ($this->end && $startTime > $this->end) {
                                 break;
                             }
                             $times[] = [$iterator->getDTStart(), $iterator->getDTEnd()];
                             $iterator->next();
                         }
                     } else {
                         $startTime = $component->DTSTART->getDateTime($this->timeZone);
                         if ($this->end && $startTime > $this->end) {
                             break;
                         }
                         $endTime = null;
                         if (isset($component->DTEND)) {
                             $endTime = $component->DTEND->getDateTime($this->timeZone);
                         } elseif (isset($component->DURATION)) {
                             $duration = DateTimeParser::parseDuration((string) $component->DURATION);
                             $endTime = clone $startTime;
                             $endTime = $endTime->add($duration);
                         } elseif (!$component->DTSTART->hasTime()) {
                             $endTime = clone $startTime;
                             $endTime = $endTime->modify('+1 day');
                         } else {
                             // The event had no duration (0 seconds)
                             break;
                         }
                         $times[] = [$startTime, $endTime];
                     }
                     foreach ($times as $time) {
                         if ($this->end && $time[0] > $this->end) {
                             break;
                         }
                         if ($this->start && $time[1] < $this->start) {
                             break;
                         }
                         $fbData->add($time[0]->getTimeStamp(), $time[1]->getTimeStamp(), $FBTYPE);
                     }
                     break;
                 case 'VFREEBUSY':
                     foreach ($component->FREEBUSY as $freebusy) {
                         $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY';
                         // Skipping intervals marked as 'free'
                         if ($fbType === 'FREE') {
                             continue;
                         }
                         $values = explode(',', $freebusy);
                         foreach ($values as $value) {
                             list($startTime, $endTime) = explode('/', $value);
                             $startTime = DateTimeParser::parseDateTime($startTime);
                             if (substr($endTime, 0, 1) === 'P' || substr($endTime, 0, 2) === '-P') {
                                 $duration = DateTimeParser::parseDuration($endTime);
                                 $endTime = clone $startTime;
                                 $endTime = $endTime->add($duration);
                             } else {
                                 $endTime = DateTimeParser::parseDateTime($endTime);
                             }
                             if ($this->start && $this->start > $endTime) {
                                 continue;
                             }
                             if ($this->end && $this->end < $startTime) {
                                 continue;
                             }
                             $fbData->add($startTime->getTimeStamp(), $endTime->getTimeStamp(), $fbType);
                         }
                     }
                     break;
             }
         }
     }
 }
Exemple #12
0
 /**
  * Parses some information from calendar objects, used for optimized
  * calendar-queries.
  *
  * Returns an array with the following keys:
  *   * etag - An md5 checksum of the object without the quotes.
  *   * size - Size of the object in bytes
  *   * componentType - VEVENT, VTODO or VJOURNAL
  *   * firstOccurence
  *   * lastOccurence
  *   * uid - value of the UID property
  *
  * @param string $calendarData
  * @return array
  */
 protected function getDenormalizedData($calendarData)
 {
     $vObject = VObject\Reader::read($calendarData);
     $componentType = null;
     $component = null;
     $firstOccurence = null;
     $lastOccurence = null;
     $uid = null;
     foreach ($vObject->getComponents() as $component) {
         if ($component->name !== 'VTIMEZONE') {
             $componentType = $component->name;
             $uid = (string) $component->UID;
             break;
         }
     }
     if (!$componentType) {
         throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
     }
     if ($componentType === 'VEVENT') {
         $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
         // Finding the last occurence is a bit harder
         if (!isset($component->RRULE)) {
             if (isset($component->DTEND)) {
                 $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
             } elseif (isset($component->DURATION)) {
                 $endDate = clone $component->DTSTART->getDateTime();
                 $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
                 $lastOccurence = $endDate->getTimeStamp();
             } elseif (!$component->DTSTART->hasTime()) {
                 $endDate = clone $component->DTSTART->getDateTime();
                 $endDate = $endDate->modify('+1 day');
                 $lastOccurence = $endDate->getTimeStamp();
             } else {
                 $lastOccurence = $firstOccurence;
             }
         } else {
             $it = new VObject\Recur\EventIterator($vObject, (string) $component->UID);
             $maxDate = new \DateTime(self::MAX_DATE);
             if ($it->isInfinite()) {
                 $lastOccurence = $maxDate->getTimeStamp();
             } else {
                 $end = $it->getDtEnd();
                 while ($it->valid() && $end < $maxDate) {
                     $end = $it->getDtEnd();
                     $it->next();
                 }
                 $lastOccurence = $end->getTimeStamp();
             }
         }
     }
     // Destroy circular references to PHP will GC the object.
     $vObject->destroy();
     return ['etag' => md5($calendarData), 'size' => strlen($calendarData), 'componentType' => $componentType, 'firstOccurence' => $firstOccurence, 'lastOccurence' => $lastOccurence, 'uid' => $uid];
 }
Exemple #13
0
 /**
  * Expand all events in this VCalendar object and return a new VCalendar
  * with the expanded events.
  *
  * If this calendar object, has events with recurrence rules, this method
  * can be used to expand the event into multiple sub-events.
  *
  * Each event will be stripped from it's recurrence information, and only
  * the instances of the event in the specified timerange will be left
  * alone.
  *
  * In addition, this method will cause timezone information to be stripped,
  * and normalized to UTC.
  *
  * @param DateTimeInterface $start
  * @param DateTimeInterface $end
  * @param DateTimeZone $timeZone reference timezone for floating dates and
  *                     times.
  * @return VCalendar
  */
 function expand(DateTimeInterface $start, DateTimeInterface $end, DateTimeZone $timeZone = null)
 {
     $newChildren = [];
     $recurringEvents = [];
     if (!$timeZone) {
         $timeZone = new DateTimeZone('UTC');
     }
     $stripTimezones = function (Component $component) use($timeZone, &$stripTimezones) {
         foreach ($component->children() as $componentChild) {
             if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) {
                 $dt = $componentChild->getDateTimes($timeZone);
                 // We only need to update the first timezone, because
                 // setDateTimes will match all other timezones to the
                 // first.
                 $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC'));
                 $componentChild->setDateTimes($dt);
             } elseif ($componentChild instanceof Component) {
                 $stripTimezones($componentChild);
             }
         }
         return $component;
     };
     foreach ($this->children() as $child) {
         if ($child instanceof Property && $child->name !== 'PRODID') {
             // We explictly want to ignore PRODID, because we want to
             // overwrite it with our own.
             $newChildren[] = clone $child;
         } elseif ($child instanceof Component && $child->name !== 'VTIMEZONE') {
             // We're also stripping all VTIMEZONE objects because we're
             // converting everything to UTC.
             if ($child->name === 'VEVENT' && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) {
                 // Handle these a bit later.
                 $uid = (string) $child->UID;
                 if (!$uid) {
                     throw new InvalidDataException('Every VEVENT object must have a UID property');
                 }
                 if (isset($recurringEvents[$uid])) {
                     $recurringEvents[$uid][] = clone $child;
                 } else {
                     $recurringEvents[$uid] = [clone $child];
                 }
             } elseif ($child->name === 'VEVENT' && $child->isInTimeRange($start, $end)) {
                 $newChildren[] = $stripTimezones(clone $child);
             }
         }
     }
     foreach ($recurringEvents as $events) {
         try {
             $it = new EventIterator($events, $timeZone);
         } catch (NoInstancesException $e) {
             // This event is recurring, but it doesn't have a single
             // instance. We are skipping this event from the output
             // entirely.
             continue;
         }
         $it->fastForward($start);
         while ($it->valid() && $it->getDTStart() < $end) {
             if ($it->getDTEnd() > $start) {
                 $newChildren[] = $stripTimezones($it->getEventObject());
             }
             $it->next();
         }
     }
     return new self($newChildren);
 }
 /**
  * Validates if a component matches the given time range.
  *
  * This is all based on the rules specified in rfc4791, which are quite
  * complex.
  *
  * @param VObject\Node $component
  * @param DateTime $start
  * @param DateTime $end
  * @return bool
  */
 protected function validateTimeRange(VObject\Node $component, $start, $end)
 {
     if (is_null($start)) {
         $start = new DateTime('1900-01-01');
     }
     if (is_null($end)) {
         $end = new DateTime('3000-01-01');
     }
     switch ($component->name) {
         case 'VEVENT':
         case 'VTODO':
         case 'VJOURNAL':
             return $component->isInTimeRange($start, $end);
         case 'VALARM':
             // If the valarm is wrapped in a recurring event, we need to
             // expand the recursions, and validate each.
             //
             // Our datamodel doesn't easily allow us to do this straight
             // in the VALARM component code, so this is a hack, and an
             // expensive one too.
             if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
                 // Fire up the iterator!
                 $it = new VObject\Recur\EventIterator($component->parent->parent, (string) $component->parent->UID);
                 while ($it->valid()) {
                     $expandedEvent = $it->getEventObject();
                     // We need to check from these expanded alarms, which
                     // one is the first to trigger. Based on this, we can
                     // determine if we can 'give up' expanding events.
                     $firstAlarm = null;
                     if ($expandedEvent->VALARM !== null) {
                         foreach ($expandedEvent->VALARM as $expandedAlarm) {
                             $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
                             if ($expandedAlarm->isInTimeRange($start, $end)) {
                                 return true;
                             }
                             if ((string) $expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
                                 // This is an alarm with a non-relative trigger
                                 // time, likely created by a buggy client. The
                                 // implication is that every alarm in this
                                 // recurring event trigger at the exact same
                                 // time. It doesn't make sense to traverse
                                 // further.
                             } else {
                                 // We store the first alarm as a means to
                                 // figure out when we can stop traversing.
                                 if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
                                     $firstAlarm = $effectiveTrigger;
                                 }
                             }
                         }
                     }
                     if (is_null($firstAlarm)) {
                         // No alarm was found.
                         //
                         // Or technically: No alarm that will change for
                         // every instance of the recurrence was found,
                         // which means we can assume there was no match.
                         return false;
                     }
                     if ($firstAlarm > $end) {
                         return false;
                     }
                     $it->next();
                 }
                 return false;
             } else {
                 return $component->isInTimeRange($start, $end);
             }
         case 'VFREEBUSY':
             throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
         case 'COMPLETED':
         case 'CREATED':
         case 'DTEND':
         case 'DTSTAMP':
         case 'DTSTART':
         case 'DUE':
         case 'LAST-MODIFIED':
             return $start <= $component->getDateTime() && $end >= $component->getDateTime();
         default:
             throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');
     }
 }
Exemple #15
0
 /**
  * Processes incoming REPLY messages.
  *
  * The message is a reply. This is for example an attendee telling
  * an organizer he accepted the invite, or declined it.
  *
  * @param Message $itipMessage
  * @param VCalendar $existingObject
  * @return VCalendar|null
  */
 protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null)
 {
     // A reply can only be processed based on an existing object.
     // If the object is not available, the reply is ignored.
     if (!$existingObject) {
         return null;
     }
     $instances = array();
     foreach ($itipMessage->message->VEVENT as $vevent) {
         $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
         $attendee = $vevent->ATTENDEE;
         $instances[$recurId] = $attendee['PARTSTAT']->getValue();
     }
     $masterObject = null;
     foreach ($existingObject->VEVENT as $vevent) {
         $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
         if ($recurId === 'master') {
             $masterObject = $vevent;
         }
         if (isset($instances[$recurId])) {
             $attendeeFound = false;
             if (isset($vevent->ATTENDEE)) {
                 foreach ($vevent->ATTENDEE as $attendee) {
                     if ($attendee->getValue() === $itipMessage->sender) {
                         $attendeeFound = true;
                         $attendee['PARTSTAT'] = $instances[$recurId];
                         break;
                     }
                 }
             }
             if (!$attendeeFound) {
                 // Adding a new attendee. The iTip documentation calls this
                 // a party crasher.
                 $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, array('PARTSTAT' => $instances[$recurId]));
                 if ($itipMessage->senderName) {
                     $attendee['CN'] = $itipMessage->senderName;
                 }
             }
             unset($instances[$recurId]);
         }
     }
     if (!$masterObject) {
         // No master object, we can't add new instances.
         return null;
     }
     // If we got replies to instances that did not exist in the
     // original list, it means that new exceptions must be created.
     foreach ($instances as $recurId => $partstat) {
         $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
         $found = false;
         $iterations = 1000;
         do {
             $newObject = $recurrenceIterator->getEventObject();
             $recurrenceIterator->next();
             if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
                 $found = true;
             }
             $iterations--;
         } while ($recurrenceIterator->valid() && !$found && $iterations);
         // Invalid recurrence id. Skipping this object.
         if (!$found) {
             continue;
         }
         unset($newObject->RRULE, $newObject->EXDATE, $newObject->RDATE);
         $newObject->{'RECURRENCE-ID'} = $recurId;
         $attendeeFound = false;
         if (isset($newObject->ATTENDEE)) {
             foreach ($newObject->ATTENDEE as $attendee) {
                 if ($attendee->getValue() === $itipMessage->sender) {
                     $attendeeFound = true;
                     $attendee['PARTSTAT'] = $partstat;
                     break;
                 }
             }
         }
         if (!$attendeeFound) {
             // Adding a new attendee
             $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, array('PARTSTAT' => $partstat));
             if ($itipMessage->senderName) {
                 $attendee['CN'] = $itipMessage->senderName;
             }
         }
         $existingObject->add($newObject);
     }
     return $existingObject;
 }