527 lines
18 KiB
PHP
527 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Sabre\VObject\Component;
|
|
|
|
use DateTime;
|
|
use DateTimeZone;
|
|
use Sabre\VObject;
|
|
use Sabre\VObject\Component;
|
|
use Sabre\VObject\Recur\EventIterator;
|
|
use Sabre\VObject\Recur\NoInstancesException;
|
|
|
|
/**
|
|
* The VCalendar component
|
|
*
|
|
* This component adds functionality to a component, specific for a VCALENDAR.
|
|
*
|
|
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
|
* @author Evert Pot (http://evertpot.com/)
|
|
* @license http://sabre.io/license/ Modified BSD License
|
|
*/
|
|
class VCalendar extends VObject\Document {
|
|
|
|
/**
|
|
* The default name for this component.
|
|
*
|
|
* This should be 'VCALENDAR' or 'VCARD'.
|
|
*
|
|
* @var string
|
|
*/
|
|
static $defaultName = 'VCALENDAR';
|
|
|
|
/**
|
|
* This is a list of components, and which classes they should map to.
|
|
*
|
|
* @var array
|
|
*/
|
|
static $componentMap = array(
|
|
'VALARM' => 'Sabre\\VObject\\Component\\VAlarm',
|
|
'VEVENT' => 'Sabre\\VObject\\Component\\VEvent',
|
|
'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy',
|
|
'VAVAILABILITY' => 'Sabre\\VObject\\Component\\VAvailability',
|
|
'AVAILABLE' => 'Sabre\\VObject\\Component\\Available',
|
|
'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal',
|
|
'VTIMEZONE' => 'Sabre\\VObject\\Component\\VTimeZone',
|
|
'VTODO' => 'Sabre\\VObject\\Component\\VTodo',
|
|
);
|
|
|
|
/**
|
|
* List of value-types, and which classes they map to.
|
|
*
|
|
* @var array
|
|
*/
|
|
static $valueMap = array(
|
|
'BINARY' => 'Sabre\\VObject\\Property\\Binary',
|
|
'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean',
|
|
'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress',
|
|
'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date',
|
|
'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration',
|
|
'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue',
|
|
'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue',
|
|
'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period',
|
|
'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur',
|
|
'TEXT' => 'Sabre\\VObject\\Property\\Text',
|
|
'TIME' => 'Sabre\\VObject\\Property\\Time',
|
|
'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only.
|
|
'URI' => 'Sabre\\VObject\\Property\\Uri',
|
|
'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset',
|
|
);
|
|
|
|
/**
|
|
* List of properties, and which classes they map to.
|
|
*
|
|
* @var array
|
|
*/
|
|
static $propertyMap = array(
|
|
// Calendar properties
|
|
'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'METHOD' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'PRODID' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'VERSION' => 'Sabre\\VObject\\Property\\FlatText',
|
|
|
|
// Component properties
|
|
'ATTACH' => 'Sabre\\VObject\\Property\\Uri',
|
|
'CATEGORIES' => 'Sabre\\VObject\\Property\\Text',
|
|
'CLASS' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'COMMENT' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'GEO' => 'Sabre\\VObject\\Property\\FloatValue',
|
|
'LOCATION' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\IntegerValue',
|
|
'PRIORITY' => 'Sabre\\VObject\\Property\\IntegerValue',
|
|
'RESOURCES' => 'Sabre\\VObject\\Property\\Text',
|
|
'STATUS' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText',
|
|
|
|
// Date and Time Component Properties
|
|
'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration',
|
|
'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period',
|
|
'TRANSP' => 'Sabre\\VObject\\Property\\FlatText',
|
|
|
|
// Time Zone Component Properties
|
|
'TZID' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'TZNAME' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset',
|
|
'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset',
|
|
'TZURL' => 'Sabre\\VObject\\Property\\Uri',
|
|
|
|
// Relationship Component Properties
|
|
'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress',
|
|
'CONTACT' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress',
|
|
'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'URL' => 'Sabre\\VObject\\Property\\Uri',
|
|
'UID' => 'Sabre\\VObject\\Property\\FlatText',
|
|
|
|
// Recurrence Component Properties
|
|
'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur',
|
|
'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545
|
|
|
|
// Alarm Component Properties
|
|
'ACTION' => 'Sabre\\VObject\\Property\\FlatText',
|
|
'REPEAT' => 'Sabre\\VObject\\Property\\IntegerValue',
|
|
'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration',
|
|
|
|
// Change Management Component Properties
|
|
'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'SEQUENCE' => 'Sabre\\VObject\\Property\\IntegerValue',
|
|
|
|
// Request Status
|
|
'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text',
|
|
|
|
// Additions from draft-daboo-valarm-extensions-04
|
|
'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text',
|
|
'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime',
|
|
'PROXIMITY' => 'Sabre\\VObject\\Property\\Text',
|
|
'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean',
|
|
|
|
// Additions from draft-daboo-calendar-availability-05
|
|
'BUSYTYPE' => 'Sabre\\VObject\\Property\\Text',
|
|
|
|
);
|
|
|
|
/**
|
|
* Returns the current document type.
|
|
*
|
|
* @return void
|
|
*/
|
|
function getDocumentType() {
|
|
|
|
return self::ICALENDAR20;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all 'base components'. For instance, if an Event has
|
|
* a recurrence rule, and one instance is overridden, the overridden event
|
|
* will have the same UID, but will be excluded from this list.
|
|
*
|
|
* VTIMEZONE components will always be excluded.
|
|
*
|
|
* @param string $componentName filter by component name
|
|
* @return VObject\Component[]
|
|
*/
|
|
function getBaseComponents($componentName = null) {
|
|
|
|
$components = array();
|
|
foreach($this->children as $component) {
|
|
|
|
if (!$component instanceof VObject\Component)
|
|
continue;
|
|
|
|
if (isset($component->{'RECURRENCE-ID'}))
|
|
continue;
|
|
|
|
if ($componentName && $component->name !== strtoupper($componentName))
|
|
continue;
|
|
|
|
if ($component->name === 'VTIMEZONE')
|
|
continue;
|
|
|
|
$components[] = $component;
|
|
|
|
}
|
|
|
|
return $components;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the first component that is not a VTIMEZONE, and does not have
|
|
* an RECURRENCE-ID.
|
|
*
|
|
* If there is no such component, null will be returned.
|
|
*
|
|
* @param string $componentName filter by component name
|
|
* @return VObject\Component|null
|
|
*/
|
|
function getBaseComponent($componentName = null) {
|
|
|
|
foreach($this->children as $component) {
|
|
|
|
if (!$component instanceof VObject\Component)
|
|
continue;
|
|
|
|
if (isset($component->{'RECURRENCE-ID'}))
|
|
continue;
|
|
|
|
if ($componentName && $component->name !== strtoupper($componentName))
|
|
continue;
|
|
|
|
if ($component->name === 'VTIMEZONE')
|
|
continue;
|
|
|
|
return $component;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
}
|
|
|
|
/**
|
|
* This method should return a list of default property values.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getDefaults() {
|
|
|
|
return array(
|
|
'VERSION' => '2.0',
|
|
'PRODID' => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN',
|
|
'CALSCALE' => 'GREGORIAN',
|
|
);
|
|
|
|
}
|
|
|
|
/**
|
|
* A simple list of validation rules.
|
|
*
|
|
* This is simply a list of properties, and how many times they either
|
|
* must or must not appear.
|
|
*
|
|
* Possible values per property:
|
|
* * 0 - Must not appear.
|
|
* * 1 - Must appear exactly once.
|
|
* * + - Must appear at least once.
|
|
* * * - Can appear any number of times.
|
|
* * ? - May appear, but not more than once.
|
|
*
|
|
* @var array
|
|
*/
|
|
function getValidationRules() {
|
|
|
|
return array(
|
|
'PRODID' => 1,
|
|
'VERSION' => 1,
|
|
|
|
'CALSCALE' => '?',
|
|
'METHOD' => '?',
|
|
);
|
|
|
|
}
|
|
|
|
/**
|
|
* Validates the node for correctness.
|
|
*
|
|
* The following options are supported:
|
|
* Node::REPAIR - May attempt to automatically repair the problem.
|
|
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
|
|
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
|
|
*
|
|
* 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 - A warning.
|
|
* 3 - An error.
|
|
*
|
|
* @param int $options
|
|
* @return array
|
|
*/
|
|
function validate($options = 0) {
|
|
|
|
$warnings = parent::validate($options);
|
|
|
|
if ($ver = $this->VERSION) {
|
|
if ((string)$ver !== '2.0') {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
|
|
'node' => $this,
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
$uidList = array();
|
|
|
|
$componentsFound = 0;
|
|
|
|
$componentTypes = array();
|
|
|
|
foreach($this->children as $child) {
|
|
if($child instanceof Component) {
|
|
$componentsFound++;
|
|
|
|
if (!in_array($child->name, array('VEVENT', 'VTODO', 'VJOURNAL'))) {
|
|
continue;
|
|
}
|
|
$componentTypes[] = $child->name;
|
|
|
|
$uid = (string)$child->UID;
|
|
$isMaster = isset($child->{'RECURRENCE-ID'})?0:1;
|
|
if (isset($uidList[$uid])) {
|
|
$uidList[$uid]['count']++;
|
|
if ($isMaster && $uidList[$uid]['hasMaster']) {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'More than one master object was found for the object with UID ' . $uid,
|
|
'node' => $this,
|
|
);
|
|
}
|
|
$uidList[$uid]['hasMaster']+=$isMaster;
|
|
} else {
|
|
$uidList[$uid] = array(
|
|
'count' => 1,
|
|
'hasMaster' => $isMaster,
|
|
);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if ($componentsFound===0) {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'An iCalendar object must have at least 1 component.',
|
|
'node' => $this,
|
|
);
|
|
}
|
|
|
|
if ($options & self::PROFILE_CALDAV) {
|
|
if (count($uidList)>1) {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
|
|
'node' => $this,
|
|
);
|
|
}
|
|
if (count(array_unique($componentTypes))===0) {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).',
|
|
'node' => $this,
|
|
);
|
|
}
|
|
if (count(array_unique($componentTypes))>1) {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).',
|
|
'node' => $this,
|
|
);
|
|
}
|
|
|
|
if (isset($this->METHOD)) {
|
|
$warnings[] = array(
|
|
'level' => 3,
|
|
'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.',
|
|
'node' => $this,
|
|
);
|
|
}
|
|
}
|
|
|
|
return $warnings;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns all components with a specific UID value.
|
|
*
|
|
* @return array
|
|
*/
|
|
function getByUID($uid) {
|
|
|
|
return array_filter($this->children, function($item) use ($uid) {
|
|
|
|
if (!$item instanceof Component) {
|
|
return false;
|
|
}
|
|
if (!$itemUid = $item->select('UID')) {
|
|
return false;
|
|
}
|
|
$itemUid = current($itemUid)->getValue();
|
|
return $uid === $itemUid;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
}
|