/** * 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 VObject\RecurrenceIterator($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]); } foreach ($newEvents as $newEvent) { foreach ($newEvent->children as $child) { if ($child instanceof VObject\Property\DateTime && $child->getDateType() == VObject\Property\DateTime::LOCALTZ) { $child->setDateTime($child->getDateTime(), VObject\Property\DateTime::UTC); } } $this->add($newEvent); } // Removing all VTIMEZONE components unset($this->VTIMEZONE); }
/** * @depends testValues */ function testOverridenEventNoValuesExpected() { $vcal = Component::create('VCALENDAR'); $ev1 = Component::create('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 = Component::create('VEVENT'); $ev2->UID = 'overridden'; $ev2->{'RECURRENCE-ID'} = '20120131T120000Z'; $ev2->DTSTART = '20120125T120000Z'; $ev2->SUMMARY = 'Override!'; $vcal->add($ev2); $it = new RecurrenceIterator($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); }
/** * 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 VObject\RecurrenceIterator($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); }
/** * 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\RecurrenceIterator($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'); } }