/** * The deserialize method is called during xml parsing. * * This method is called statictly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * You are responsible for advancing the reader to the next element. Not * doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseInnerTree() will parse the entire sub-tree, and advance to * the next element. * * @param Reader $reader * @return mixed */ static function xmlDeserialize(Reader $reader) { $timeRange = '{' . Plugin::NS_CALDAV . '}time-range'; $start = null; $end = null; foreach ((array) $reader->parseInnerTree([]) as $elem) { if ($elem['name'] !== $timeRange) { continue; } $start = empty($elem['attributes']['start']) ?: $elem['attributes']['start']; $end = empty($elem['attributes']['end']) ?: $elem['attributes']['end']; } if (!$start && !$end) { throw new BadRequest('The freebusy report must have a time-range element'); } if ($start) { $start = DateTimeParser::parseDateTime($start); } if ($end) { $end = DateTimeParser::parseDateTime($end); } $result = new self(); $result->start = $start; $result->end = $end; return $result; }
/** * The deserialize method is called during xml parsing. * * This method is called statictly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * You are responsible for advancing the reader to the next element. Not * doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseInnerTree() will parse the entire sub-tree, and advance to * the next element. * * @param Reader $reader * @return mixed */ static function xmlDeserialize(Reader $reader) { $result = ['name' => null, 'is-not-defined' => false, 'param-filters' => [], 'text-match' => null, 'time-range' => false]; $att = $reader->parseAttributes(); $result['name'] = $att['name']; $elems = $reader->parseInnerTree(); if (is_array($elems)) { foreach ($elems as $elem) { switch ($elem['name']) { case '{' . Plugin::NS_CALDAV . '}param-filter': $result['param-filters'][] = $elem['value']; break; case '{' . Plugin::NS_CALDAV . '}is-not-defined': $result['is-not-defined'] = true; break; case '{' . Plugin::NS_CALDAV . '}time-range': $result['time-range'] = ['start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null]; if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { throw new BadRequest('The end-date must be larger than the start-date'); } break; case '{' . Plugin::NS_CALDAV . '}text-match': $result['text-match'] = ['negate-condition' => isset($elem['attributes']['negate-condition']) && $elem['attributes']['negate-condition'] === 'yes', 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', 'value' => $elem['value']]; break; } } } return $result; }
/** * The deserialize method is called during xml parsing. * * This method is called statictly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * You are responsible for advancing the reader to the next element. Not * doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseInnerTree() will parse the entire sub-tree, and advance to * the next element. * * @param Reader $reader * @return mixed */ static function xmlDeserialize(Reader $reader) { $result = ['name' => null, 'is-not-defined' => false, 'comp-filters' => [], 'prop-filters' => [], 'time-range' => false]; $att = $reader->parseAttributes(); $result['name'] = $att['name']; $elems = $reader->parseInnerTree(); if (is_array($elems)) { foreach ($elems as $elem) { switch ($elem['name']) { case '{' . Plugin::NS_CALDAV . '}comp-filter': $result['comp-filters'][] = $elem['value']; break; case '{' . Plugin::NS_CALDAV . '}prop-filter': $result['prop-filters'][] = $elem['value']; break; case '{' . Plugin::NS_CALDAV . '}is-not-defined': $result['is-not-defined'] = true; break; case '{' . Plugin::NS_CALDAV . '}time-range': if ($result['name'] === 'VCALENDAR') { throw new BadRequest('You cannot add time-range filters on the VCALENDAR component'); } $result['time-range'] = ['start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null]; if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { throw new BadRequest('The end-date must be larger than the start-date'); } break; } } } return $result; }
/** * 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 VObject\RecurrenceIterator($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->getDateType() == VObject\Property\DateTime::DATE) { $effectiveEnd = clone $effectiveStart; $effectiveEnd->modify('+1 day'); } else { $effectiveEnd = clone $effectiveStart; } return $start <= $effectiveEnd && $end > $effectiveStart; }
/** * 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) { $dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null; $duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null; $due = isset($this->DUE) ? $this->DUE->getDateTime() : null; $completed = isset($this->COMPLETED) ? $this->COMPLETED->getDateTime() : null; $created = isset($this->CREATED) ? $this->CREATED->getDateTime() : null; if ($dtstart) { if ($duration) { $effectiveEnd = clone $dtstart; $effectiveEnd->add($duration); return $start <= $effectiveEnd && $end > $dtstart; } elseif ($due) { return ($start < $due || $start <= $dtstart) && ($end > $dtstart || $end >= $due); } else { return $start <= $dtstart && $end > $dtstart; } } if ($due) { return $start < $due && $end >= $due; } if ($completed && $created) { return ($start <= $created || $start <= $completed) && ($end >= $created || $end >= $completed); } if ($completed) { return $start <= $completed && $end >= $completed; } if ($created) { return $end > $created; } return true; }
/** * Checks based on the contained FREEBUSY information, if a timeslot is * available. * * @param DateTime $start * @param Datetime $end * @return bool */ public function isFree(\DateTime $start, \Datetime $end) { foreach ($this->select('FREEBUSY') as $freebusy) { // We are only interested in FBTYPE=BUSY (the default), // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. if (isset($freebusy['FBTYPE']) && strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4)) !== 'BUSY') { continue; } // The freebusy component can hold more than 1 value, separated by // commas. $periods = explode(',', (string) $freebusy); foreach ($periods as $period) { // Every period is formatted as [start]/[end]. The start is an // absolute UTC time, the end may be an absolute UTC time, or // duration (relative) value. list($busyStart, $busyEnd) = explode('/', $period); $busyStart = VObject\DateTimeParser::parse($busyStart); $busyEnd = VObject\DateTimeParser::parse($busyEnd); if ($busyEnd instanceof \DateInterval) { $tmp = clone $busyStart; $tmp->add($busyEnd); $busyEnd = $tmp; } if ($start < $busyEnd && $end > $busyStart) { return false; } } } return true; }
/** * Goes on to the next iteration. * * @return void */ public function next() { $this->counter++; if (!$this->valid()) { return; } $this->currentDate = DateTimeParser::parse($this->dates[$this->counter - 1]); }
/** * Goes on to the next iteration. * * @return void */ function next() { $this->counter++; if (!$this->valid()) { return; } $this->currentDate = DateTimeParser::parse($this->dates[$this->counter - 1], $this->startDate->getTimezone()); }
/** * Returns the 'effective start' and 'effective end' of this VAVAILABILITY * component. * * We use the DTSTART and DTEND or DURATION to determine this. * * The returned value is an array containing DateTimeImmutable instances. * If either the start or end is 'unbounded' its value will be null * instead. * * @return array */ function getEffectiveStartEnd() { $effectiveStart = $this->DTSTART->getDateTime(); if (isset($this->DTEND)) { $effectiveEnd = $this->DTEND->getDateTime(); } else { $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); } return [$effectiveStart, $effectiveEnd]; }
/** * Returns the value, in the format it should be encoded for json. * * This method must always return an array. * * @return array */ public function getJsonValue() { $parts = DateTimeParser::parseVCardDateTime($this->getValue()); $dateStr = $parts['year'] . '-' . $parts['month'] . '-' . $parts['date'] . 'T' . $parts['hour'] . ':' . $parts['minute'] . ':' . $parts['second']; // Timezone if (!is_null($parts['timezone'])) { $dateStr .= $parts['timezone']; } return array($dateStr); }
/** * Returns the value, in the format it should be encoded for json. * * This method must always return an array. * * @return array */ public function getJsonValue() { $return = array(); foreach ($this->getParts() as $item) { list($start, $end) = explode('/', $item, 2); $start = DateTimeParser::parseDateTime($start); // This is a duration value. if ($end[0] === 'P') { $return[] = array($start->format('Y-m-d\\TH:i:s'), $end); } else { $end = DateTimeParser::parseDateTime($end); $return[] = array($start->format('Y-m-d\\TH:i:s'), $end->format('Y-m-d\\TH:i:s')); } } return $return; }
/** * The deserialize method is called during xml parsing. * * This method is called statictly, this is because in theory this method * may be used as a type of constructor, or factory method. * * Often you want to return an instance of the current class, but you are * free to return other data as well. * * You are responsible for advancing the reader to the next element. Not * doing anything will result in a never-ending loop. * * If you just want to skip parsing for this element altogether, you can * just call $reader->next(); * * $reader->parseInnerTree() will parse the entire sub-tree, and advance to * the next element. * * @param Reader $reader * @return mixed */ static function xmlDeserialize(Reader $reader) { $result = ['contentType' => $reader->getAttribute('content-type') ?: 'text/calendar', 'version' => $reader->getAttribute('version') ?: '2.0']; $elems = (array) $reader->parseInnerTree(); foreach ($elems as $elem) { switch ($elem['name']) { case '{' . Plugin::NS_CALDAV . '}expand': $result['expand'] = ['start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null]; if (!$result['expand']['start'] || !$result['expand']['end']) { throw new BadRequest('The "start" and "end" attributes are required when expanding calendar-data'); } if ($result['expand']['end'] <= $result['expand']['start']) { throw new BadRequest('The end-date must be larger than the start-date when expanding calendar-data'); } break; } } return $result; }
/** * 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) { $effectiveTrigger = $this->getEffectiveTriggerTime(); if (isset($this->DURATION)) { $duration = VObject\DateTimeParser::parseDuration($this->DURATION); $repeat = (string) $this->repeat; if (!$repeat) { $repeat = 1; } $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat); foreach ($period as $occurrence) { if ($start <= $occurrence && $end > $occurrence) { return true; } } return false; } else { return $start <= $effectiveTrigger && $end > $effectiveTrigger; } }
/** * Returns the value, in the format it should be encoded for json. * * This method must always return an array. * * @return array */ function getJsonValue() { $parts = DateTimeParser::parseVCardTime($this->getValue()); $timeStr = ''; // Hour if (!is_null($parts['hour'])) { $timeStr .= $parts['hour']; if (!is_null($parts['minute'])) { $timeStr .= ':'; } } else { // We know either minute or second _must_ be set, so we insert a // dash for an empty value. $timeStr .= '-'; } // Minute if (!is_null($parts['minute'])) { $timeStr .= $parts['minute']; if (!is_null($parts['second'])) { $timeStr .= ':'; } } else { if (isset($parts['second'])) { // Dash for empty minute $timeStr .= '-'; } } // Second if (!is_null($parts['second'])) { $timeStr .= $parts['second']; } // Timezone if (!is_null($parts['timezone'])) { if ($parts['timezone'] === 'Z') { $timeStr .= 'Z'; } else { $timeStr .= preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); } } return [$timeStr]; }
/** * 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; }
/** * Returns the value, in the format it should be encoded for json. * * This method must always return an array. * * @return array */ public function getJsonValue() { $parts = DateTimeParser::parseVCardTime($this->getValue()); $timeStr = ''; // Hour if (!is_null($parts['hour'])) { $timeStr .= $parts['hour']; if (!is_null($parts['minute'])) { $timeStr .= ':'; } } else { // We know either minute or second _must_ be set, so we insert a // dash for an empty value. $timeStr .= '-'; } // Minute if (!is_null($parts['minute'])) { $timeStr .= $parts['minute']; if (!is_null($parts['second'])) { $timeStr .= ':'; } } else { if (isset($parts['second'])) { // Dash for empty minute $timeStr .= '-'; } } // Second if (!is_null($parts['second'])) { $timeStr .= $parts['second']; } // Timezone if (!is_null($parts['timezone'])) { $timeStr .= $parts['timezone']; } return array($timeStr); }
protected function assertDateAndOrTimeEqualsTo($date, $parts) { $this->assertSame(DateTimeParser::parseVCardDateAndOrTime($date), array_merge(['year' => null, 'month' => null, 'date' => null, 'hour' => null, 'minute' => null, 'second' => null, 'timezone' => null], $parts)); }
public static function arrayForJSON($id, $vtodo, $user_timezone, $calendarId) { $task = array('id' => $id); $task['calendarid'] = $calendarId; $task['type'] = 'task'; $task['name'] = (string) $vtodo->SUMMARY; $task['created'] = (string) $vtodo->CREATED; $task['note'] = (string) $vtodo->DESCRIPTION; $task['location'] = (string) $vtodo->LOCATION; $categories = $vtodo->CATEGORIES; if ($categories) { $task['categories'] = $categories->getParts(); } $start = $vtodo->DTSTART; if ($start) { try { $start = $start->getDateTime(); $start->setTimezone(new \DateTimeZone($user_timezone)); $task['start'] = $start->format('Ymd\\THis'); } catch (\Exception $e) { $task['start'] = null; \OCP\Util::writeLog('tasks', $e->getMessage(), \OCP\Util::ERROR); } } else { $task['start'] = null; } $due = $vtodo->DUE; if ($due) { try { $due = $due->getDateTime(); $due->setTimezone(new \DateTimeZone($user_timezone)); $task['due'] = $due->format('Ymd\\THis'); } catch (\Exception $e) { $task['due'] = null; \OCP\Util::writeLog('tasks', $e->getMessage(), \OCP\Util::ERROR); } } else { $task['due'] = null; } $reminder = $vtodo->VALARM; if ($reminder) { try { $reminderType = $reminder->TRIGGER['VALUE']->getValue(); $reminderAction = $reminder->ACTION->getValue(); $reminderDate = null; $reminderDuration = null; if ($reminderType == 'DATE-TIME') { $reminderDate = $reminder->TRIGGER->getDateTime(); $reminderDate->setTimezone(new \DateTimeZone($user_timezone)); $reminderDate = $reminderDate->format('Ymd\\THis'); } elseif ($reminderType == 'DURATION' && ($start || $due)) { $parsed = VObject\DateTimeParser::parseDuration($reminder->TRIGGER, true); // Calculate the reminder date from duration and start date $related = null; if (is_object($reminder->TRIGGER['RELATED'])) { $related = $reminder->TRIGGER['RELATED']->getValue(); if ($related == 'END' && $due) { $reminderDate = $due->modify($parsed)->format('Ymd\\THis'); } else { throw new \Exception('Reminder duration related to not available date.'); } } elseif ($start) { $reminderDate = $start->modify($parsed)->format('Ymd\\THis'); } else { throw new \Exception('Reminder duration related to not available date.'); } preg_match('/^(?P<plusminus>\\+|-)?P((?P<week>\\d+)W)?((?P<day>\\d+)D)?(T((?P<hour>\\d+)H)?((?P<minute>\\d+)M)?((?P<second>\\d+)S)?)?$/', $reminder->TRIGGER, $matches); $invert = false; if ($matches['plusminus'] === '-') { $invert = true; } $parts = array('week', 'day', 'hour', 'minute', 'second'); $reminderDuration = array('token' => null); foreach ($parts as $part) { $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; $reminderDuration[$part] = $matches[$part]; if ($matches[$part] && !$reminderDuration['token']) { $reminderDuration['token'] = $part; } } if ($reminderDuration['token'] == null) { $reminderDuration['token'] = $parts[0]; } $reminderDuration['params'] = array('id' => (int) $invert . (int) ($related == 'END'), 'related' => $related ? $related : 'START', 'invert' => $invert); } else { $reminderDate = null; $reminderDuration = null; } $task['reminder'] = array('type' => $reminderType, 'action' => $reminderAction, 'date' => $reminderDate, 'duration' => $reminderDuration); } catch (\Exception $e) { $task['reminder'] = null; \OCP\Util::writeLog('tasks', $e->getMessage(), \OCP\Util::ERROR); } } else { $task['reminder'] = null; } $priority = $vtodo->PRIORITY; if (isset($priority)) { $priority = (10 - $priority->getValue()) % 10; $task['priority'] = (string) $priority; if ($priority > 5) { $task['starred'] = true; } } else { $task['priority'] = '0'; $task['starred'] = false; } $completed = $vtodo->COMPLETED; if ($completed) { try { $completed = $completed->getDateTime(); $completed->setTimezone(new \DateTimeZone($user_timezone)); $task['completed_date'] = $completed->format('Ymd\\THis'); $task['completed'] = true; } catch (\Exception $e) { $task['completed'] = false; \OCP\Util::writeLog('tasks', $e->getMessage(), \OCP\Util::ERROR); } } else { $task['completed'] = false; } $percentComplete = $vtodo->{'PERCENT-COMPLETE'}; if ($percentComplete) { $task['complete'] = $percentComplete->getValue(); } else { $task['complete'] = '0'; } $comments = $vtodo->COMMENT; if ($comments) { $comments_parsed = array(); foreach ($comments as $com) { // parse time $time = $com['X-OC-DATE-TIME']; if ($time) { $time = new \DateTime($time); $time->setTimezone(new \DateTimeZone($user_timezone)); $time = $time->format('Ymd\\THis'); } // parse comment ID $comID = $com['X-OC-ID']; if ($comID) { $comID = $com['X-OC-ID']->getValue(); } // parse user ID $userID = $com['X-OC-USERID']; if ($userID) { $userID = (string) $com['X-OC-USERID']->getValue(); } $user = \OC::$server->getUserManager()->get($userID); $userName = $userID; if ($user) { $userName = $user->getDisplayName(); } $comments_parsed[] = array('id' => $comID, 'userID' => $userID, 'name' => $userName, 'comment' => $com->getValue(), 'time' => $time); } $task['comments'] = $comments_parsed; } return $task; }
/** * Parse an event update for an attendee. * * This function figures out if we need to send a reply to an organizer. * * @param VCalendar $calendar * @param array $eventInfo * @param array $oldEventInfo * @param string $attendee * @return Message[] */ protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) { if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent'] === 'CLIENT') { return array(); } // Don't bother generating messages for events that have already been // cancelled. if ($eventInfo['status'] === 'CANCELLED') { return array(); } $instances = array(); foreach ($oldEventInfo['attendees'][$attendee]['instances'] as $instance) { $instances[$instance['id']] = array('id' => $instance['id'], 'oldstatus' => $instance['partstat'], 'newstatus' => null); } foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) { if (isset($instances[$instance['id']])) { $instances[$instance['id']]['newstatus'] = $instance['partstat']; } else { $instances[$instance['id']] = array('id' => $instance['id'], 'oldstatus' => null, 'newstatus' => $instance['partstat']); } } // We need to also look for differences in EXDATE. If there are new // items in EXDATE, it means that an attendee deleted instances of an // event, which means we need to send DECLINED specifically for those // instances. // We only need to do that though, if the master event is not declined. if ($instances['master']['newstatus'] !== 'DECLINED') { foreach ($eventInfo['exdate'] as $exDate) { if (!in_array($exDate, $oldEventInfo['exdate'])) { if (isset($instances[$exDate])) { $instances[$exDate]['newstatus'] = 'DECLINED'; } else { $instances[$exDate] = array('id' => $exDate, 'oldstatus' => null, 'newstatus' => 'DECLINED'); } } } } // Gathering a few extra properties for each instance. foreach ($instances as $recurId => $instanceInfo) { if (isset($eventInfo['instances'][$recurId])) { $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; } else { $instances[$recurId]['dtstart'] = $recurId; } } $message = new Message(); $message->uid = $eventInfo['uid']; $message->method = 'REPLY'; $message->component = 'VEVENT'; $message->sequence = $eventInfo['sequence']; $message->sender = $attendee; $message->senderName = $eventInfo['attendees'][$attendee]['name']; $message->recipient = $eventInfo['organizer']; $message->recipientName = $eventInfo['organizerName']; $icalMsg = new VCalendar(); $icalMsg->METHOD = 'REPLY'; $hasReply = false; foreach ($instances as $instance) { if ($instance['oldstatus'] == $instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') { // Skip continue; } $event = $icalMsg->add('VEVENT', array('UID' => $message->uid, 'SEQUENCE' => $message->sequence)); $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : ''; // Adding properties from the correct source instance if (isset($eventInfo['instances'][$instance['id']])) { $instanceObj = $eventInfo['instances'][$instance['id']]; $event->add(clone $instanceObj->DTSTART); if (isset($instanceObj->SUMMARY)) { $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); } elseif ($summary) { $event->add('SUMMARY', $summary); } } else { // This branch of the code is reached, when a reply is // generated for an instance of a recurring event, through the // fact that the instance has disappeared by showing up in // EXDATE $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); // Treat is as a DATE field if (strlen($instance['id']) <= 8) { $recur = $event->add('DTSTART', $dt, array('VALUE' => 'DATE')); } else { $recur = $event->add('DTSTART', $dt); } if ($summary) { $event->add('SUMMARY', $summary); } } if ($instance['id'] !== 'master') { $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); // Treat is as a DATE field if (strlen($instance['id']) <= 8) { $recur = $event->add('RECURRENCE-ID', $dt, array('VALUE' => 'DATE')); } else { $recur = $event->add('RECURRENCE-ID', $dt); } } $organizer = $event->add('ORGANIZER', $message->recipient); if ($message->recipientName) { $organizer['CN'] = $message->recipientName; } $attendee = $event->add('ATTENDEE', $message->sender, array('PARTSTAT' => $instance['newstatus'])); if ($message->senderName) { $attendee['CN'] = $message->senderName; } $hasReply = true; } if ($hasReply) { $message->message = $icalMsg; return array($message); } else { return array(); } }
/** * @depends testParseICalendarDate * @expectedException LogicException */ function testParseICalendarDateBadFormat() { $dateTime = DateTimeParser::parseDate('20100316T141405'); }
/** * Returns a DateInterval representation of the Duration property. * * If the property has more than one value, only the first is returned. * * @return \DateInterval */ public function getDateInterval() { $parts = $this->getParts(); $value = $parts[0]; return DateTimeParser::parseDuration($value); }
/** * 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; } } } }
/** * Validates the node for correctness. * * The following options are supported: * Node::REPAIR - May attempt to automatically repair the problem. * * This method returns an array with detected problems. * Every element has the following properties: * * * level - problem level. * * message - A human-readable string describing the issue. * * node - A reference to the problematic node. * * The level means: * 1 - The issue was repaired (only happens if REPAIR was turned on) * 2 - An inconsequential issue * 3 - A severe issue. * * @param int $options * * @return array */ function validate($options = 0) { $messages = parent::validate($options); $valueType = $this->getValueType(); $values = $this->getParts(); try { foreach ($values as $value) { switch ($valueType) { case 'DATE': DateTimeParser::parseDate($value); break; case 'DATE-TIME': DateTimeParser::parseDateTime($value); break; } } } catch (InvalidDataException $e) { $messages[] = ['level' => 3, 'message' => 'The supplied value (' . $value . ') is not a correct ' . $valueType, 'node' => $this]; } return $messages; }
/** * This method receives a string from an RRULE property, and populates this * class with all the values. * * @param string|array $rrule * @return void */ protected function parseRRule($rrule) { if (is_string($rrule)) { $rrule = Property\ICalendar\Recur::stringToArray($rrule); } foreach ($rrule as $key => $value) { $key = strtoupper($key); switch ($key) { case 'FREQ': $value = strtolower($value); if (!in_array($value, array('secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'))) { throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); } $this->frequency = $value; break; case 'UNTIL': $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone()); // In some cases events are generated with an UNTIL= // parameter before the actual start of the event. // // Not sure why this is happening. We assume that the // intention was that the event only recurs once. // // So we are modifying the parameter so our code doesn't // break. if ($this->until < $this->startDate) { $this->until = $this->startDate; } break; case 'INTERVAL': // No break // No break case 'COUNT': $val = (int) $value; if ($val < 1) { throw new \InvalidArgumentException(strtoupper($key) . ' in RRULE must be a positive integer!'); } $key = strtolower($key); $this->{$key} = $val; break; case 'BYSECOND': $this->bySecond = (array) $value; break; case 'BYMINUTE': $this->byMinute = (array) $value; break; case 'BYHOUR': $this->byHour = (array) $value; break; case 'BYDAY': $value = (array) $value; foreach ($value as $part) { if (!preg_match('#^ (-|\\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { throw new \InvalidArgumentException('Invalid part in BYDAY clause: ' . $part); } } $this->byDay = $value; break; case 'BYMONTHDAY': $this->byMonthDay = (array) $value; break; case 'BYYEARDAY': $this->byYearDay = (array) $value; break; case 'BYWEEKNO': $this->byWeekNo = (array) $value; break; case 'BYMONTH': $this->byMonth = (array) $value; break; case 'BYSETPOS': $this->bySetPos = (array) $value; break; case 'WKST': $this->weekStart = strtoupper($value); break; default: throw new \InvalidArgumentException('Not supported: ' . strtoupper($key)); } } }
/** * 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; }
/** * Parses the input data and returns a VCALENDAR. * * @return Component/VCalendar */ function getResult() { $calendar = new VCalendar(); foreach ($this->objects as $object) { // Skip if there is no BDAY property. if (!$object->select('BDAY')) { continue; } // We've seen clients (ez-vcard) putting "BDAY:" properties // without a value into vCards. If we come across those, we'll // skip them. if (empty($object->BDAY->getValue())) { continue; } // We're always converting to vCard 4.0 so we can rely on the // VCardConverter handling the X-APPLE-OMIT-YEAR property for us. $object = $object->convert(Document::VCARD40); // Skip if the card has no FN property. if (!isset($object->FN)) { continue; } // Skip if the BDAY property is not of the right type. if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) { continue; } // Skip if we can't parse the BDAY value. try { $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); } catch (\InvalidArgumentException $e) { continue; } // Set a year if it's not set. $unknownYear = false; if (!$dateParts['year']) { $object->BDAY = self::DEFAULT_YEAR . '-' . $dateParts['month'] . '-' . $dateParts['date']; $unknownYear = true; } // Create event. $event = $calendar->add('VEVENT', ['SUMMARY' => sprintf($this->format, $object->FN->getValue()), 'DTSTART' => new \DateTime($object->BDAY->getValue()), 'RRULE' => 'FREQ=YEARLY', 'TRANSP' => 'TRANSPARENT']); // add VALUE=date $event->DTSTART['VALUE'] = 'DATE'; // Add X-SABRE-BDAY property. if ($unknownYear) { $event->add('X-SABRE-BDAY', 'BDAY', ['X-SABRE-VCARD-UID' => $object->UID->getValue(), 'X-SABRE-VCARD-FN' => $object->FN->getValue(), 'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR]); } else { $event->add('X-SABRE-BDAY', 'BDAY', ['X-SABRE-VCARD-UID' => $object->UID->getValue(), 'X-SABRE-VCARD-FN' => $object->FN->getValue()]); } } return $calendar; }
/** * Parses the CALDAV:expand element * * @param \DOMElement $parentNode * @return void */ protected function parseExpand(\DOMElement $parentNode) { $start = $parentNode->getAttribute('start'); if (!$start) { throw new \Sabre\DAV\Exception\BadRequest('The "start" attribute is required for the CALDAV:expand element'); } $start = VObject\DateTimeParser::parseDateTime($start); $end = $parentNode->getAttribute('end'); if (!$end) { throw new \Sabre\DAV\Exception\BadRequest('The "end" attribute is required for the CALDAV:expand element'); } $end = VObject\DateTimeParser::parseDateTime($end); if ($end <= $start) { throw new \Sabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.'); } return array('start' => $start, 'end' => $end); }
/** * Validates the node for correctness. * * The following options are supported: * Node::REPAIR - May attempt to automatically repair the problem. * * This method returns an array with detected problems. * Every element has the following properties: * * * level - problem level. * * message - A human-readable string describing the issue. * * node - A reference to the problematic node. * * The level means: * 1 - The issue was repaired (only happens if REPAIR was turned on) * 2 - An inconsequential issue * 3 - A severe issue. * * @param int $options * @return array */ public function validate($options = 0) { $messages = parent::validate($options); $value = $this->getValue(); try { DateTimeParser::parseVCardDateTime($value); } catch (\InvalidArgumentException $e) { $messages[] = array('level' => 3, 'message' => 'The supplied value (' . $value . ') is not a correct DATE-AND-OR-TIME property', 'node' => $this); } return $messages; }
/** * Parses some information from calendar objects, used for optimized * calendar-queries. * * Returns an array with the following keys: * * etag * * size * * componentType * * firstOccurence * * lastOccurence * * @param string $calendarData * @return array */ protected function getDenormalizedData($calendarData) { $vObject = VObject\Reader::read($calendarData); $componentType = null; $component = null; $firstOccurence = null; $lastOccurence = null; foreach ($vObject->getComponents() as $component) { if ($component->name !== 'VTIMEZONE') { $componentType = $component->name; 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->add(VObject\DateTimeParser::parse($component->DURATION->getValue())); $lastOccurence = $endDate->getTimeStamp(); } elseif (!$component->DTSTART->hasTime()) { $endDate = clone $component->DTSTART->getDateTime(); $endDate->modify('+1 day'); $lastOccurence = $endDate->getTimeStamp(); } else { $lastOccurence = $firstOccurence; } } else { $it = new VObject\RecurrenceIterator($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(); } } } return array('etag' => md5($calendarData), 'size' => strlen($calendarData), 'componentType' => $componentType, 'firstOccurence' => $firstOccurence, 'lastOccurence' => $lastOccurence); }
/** * 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]; }