Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117885775
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
88 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/libcalendaring/lib/libcalendaring_vcalendar.php b/plugins/libcalendaring/lib/libcalendaring_vcalendar.php
index 1cd5bc2a..218e3347 100644
--- a/plugins/libcalendaring/lib/libcalendaring_vcalendar.php
+++ b/plugins/libcalendaring/lib/libcalendaring_vcalendar.php
@@ -1,1593 +1,1594 @@
<?php
/**
* iCalendar functions for the libcalendaring plugin
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Sabre\VObject;
use Sabre\VObject\DateTimeParser;
/**
* Class to parse and build vCalendar (iCalendar) files
*
* Uses the Sabre VObject library, version 3.x.
*/
class libcalendaring_vcalendar implements Iterator
{
private $timezone;
private $attach_uri;
private $prodid;
private $type_component_map = ['event' => 'VEVENT', 'task' => 'VTODO'];
private $attendee_keymap = [
'name' => 'CN',
'status' => 'PARTSTAT',
'role' => 'ROLE',
'cutype' => 'CUTYPE',
'rsvp' => 'RSVP',
'delegated-from' => 'DELEGATED-FROM',
'delegated-to' => 'DELEGATED-TO',
'schedule-status' => 'SCHEDULE-STATUS',
'schedule-agent' => 'SCHEDULE-AGENT',
'sent-by' => 'SENT-BY',
];
private $organizer_keymap = [
'name' => 'CN',
'schedule-status' => 'SCHEDULE-STATUS',
'schedule-agent' => 'SCHEDULE-AGENT',
'sent-by' => 'SENT-BY',
];
private $iteratorkey = 0;
private $charset;
private $forward_exceptions;
private $vhead;
private $fp;
private $vtimezones = [];
public $method;
public $agent = '';
public $objects = [];
public $freebusy = [];
public $sender;
public $message_date;
public $mime_id;
/**
* Default constructor
*/
public function __construct($tz = null)
{
$this->timezone = $tz;
$this->prodid = '-//Roundcube ' . RCUBE_VERSION . '//Sabre VObject ' . VObject\Version::VERSION . '//EN';
}
/**
* Setter for timezone information
*/
public function set_timezone($tz)
{
$this->timezone = $tz;
}
/**
* Setter for URI template for attachment links
*/
public function set_attach_uri($uri)
{
$this->attach_uri = $uri;
}
/**
* Setter for a custom PRODID attribute
*/
public function set_prodid($prodid)
{
$this->prodid = $prodid;
}
/**
* Setter for a user-agent string to tweak input/output accordingly
*/
public function set_agent($agent)
{
$this->agent = $agent;
}
/**
* Free resources by clearing member vars
*/
public function reset()
{
$this->vhead = '';
$this->method = '';
$this->objects = [];
$this->freebusy = [];
$this->vtimezones = [];
$this->iteratorkey = 0;
if ($this->fp) {
fclose($this->fp);
$this->fp = null;
}
}
/**
* Import events from iCalendar format
*
* @param string $vcal vCalendar input
* @param string $charset Input charset (from envelope)
* @param bool $forward_exceptions True if parsing exceptions should be forwarded to the caller
*
* @return array List of events extracted from the input
*/
public function import($vcal, $charset = 'UTF-8', $forward_exceptions = false, $memcheck = true)
{
// TODO: convert charset to UTF-8 if other
try {
// estimate the memory usage and try to avoid fatal errors when allowed memory gets exhausted
if ($memcheck) {
$count = substr_count($vcal, 'BEGIN:VEVENT') + substr_count($vcal, 'BEGIN:VTODO');
$expected_memory = $count * 70 * 1024; // assume ~ 70K per event (empirically determined)
if (!rcube_utils::mem_check($expected_memory)) {
throw new Exception("iCal file too big");
}
}
$vobject = VObject\Reader::read($vcal, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobject) {
return $this->import_from_vobject($vobject);
}
} catch (Exception $e) {
if ($forward_exceptions) {
throw $e;
} else {
rcube::raise_error(
[
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "iCal data parse error: " . $e->getMessage()],
true,
false
);
}
}
return [];
}
/**
* Read iCalendar events from a file
*
* @param string $filepath File path to read from
* @param string $charset Input charset (from envelope)
* @param bool $forward_exceptions True if parsing exceptions should be forwarded to the caller
*
* @return array List of events extracted from the file
*/
public function import_from_file($filepath, $charset = 'UTF-8', $forward_exceptions = false)
{
if ($this->fopen($filepath, $charset, $forward_exceptions)) {
while ($this->_parse_next(false)) {
// nop
}
fclose($this->fp);
$this->fp = null;
}
return $this->objects;
}
/**
* Open a file to read iCalendar events sequentially
*
* @param string $filepath File path to read from
* @param string $charset Input charset (from envelope)
* @param bool $forward_exceptions True if parsing exceptions should be forwarded to the caller
*
* @return bool True if file contents are considered valid
*/
public function fopen($filepath, $charset = 'UTF-8', $forward_exceptions = false)
{
$this->reset();
// just to be sure...
@ini_set('auto_detect_line_endings', true);
$this->charset = $charset;
$this->forward_exceptions = $forward_exceptions;
$this->fp = fopen($filepath, 'r');
// check file content first
$begin = fread($this->fp, 1024);
if (!preg_match('/BEGIN:VCALENDAR/i', $begin)) {
return false;
}
fseek($this->fp, 0);
return $this->_parse_next();
}
/**
* Parse the next event/todo/freebusy object from the input file
*/
private function _parse_next($reset = true)
{
if ($reset) {
$this->iteratorkey = 0;
$this->objects = [];
$this->freebusy = [];
}
$next = $this->_next_component();
$buffer = $next;
// load the next component(s) too, as they could contain recurrence exceptions
while (preg_match('/(RRULE|RECURRENCE-ID)[:;]/i', $next)) {
$next = $this->_next_component();
$buffer .= $next;
}
// parse the vevent block surrounded with the vcalendar heading
if (strlen($buffer) && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $buffer)) {
try {
$this->import($this->vhead . $buffer . "END:VCALENDAR", $this->charset, true, false);
} catch (Exception $e) {
if ($this->forward_exceptions) {
throw new VObject\ParseException($e->getMessage() . " in\n" . $buffer);
} else {
// write the failing section to error log
rcube::raise_error(
[
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => $e->getMessage() . " in\n" . $buffer],
true,
false
);
}
// advance to next
return $this->_parse_next($reset);
}
return count($this->objects) > 0;
}
return false;
}
/**
* Helper method to read the next calendar component from the file
*/
private function _next_component()
{
$buffer = '';
$vcalendar_head = false;
while (($line = fgets($this->fp, 1024)) !== false) {
// ignore END:VCALENDAR lines
if (preg_match('/END:VCALENDAR/i', $line)) {
continue;
}
// read vcalendar header (with timezone defintion)
if (preg_match('/BEGIN:VCALENDAR/i', $line)) {
$this->vhead = '';
$vcalendar_head = true;
}
// end of VCALENDAR header part
if ($vcalendar_head && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
$vcalendar_head = false;
}
if ($vcalendar_head) {
$this->vhead .= $line;
} else {
$buffer .= $line;
if (preg_match('/END:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
break;
}
}
}
return $buffer;
}
/**
* Import objects from an already parsed Sabre\VObject\Component object
*
* @param VObject\Component $vobject Component to read from
*
* @return array List of events extracted from the file
*/
public function import_from_vobject($vobject)
{
$seen = [];
$exceptions = [];
if ($vobject->name == 'VCALENDAR') {
$this->method = strval($vobject->METHOD);
$this->agent = strval($vobject->PRODID);
foreach ($vobject->getComponents() as $ve) {
if ($ve->name == 'VEVENT' || $ve->name == 'VTODO') {
// convert to hash array representation
$object = $this->_to_array($ve);
// temporarily store this as exception
if (!empty($object['recurrence_date'])) {
$exceptions[] = $object;
} elseif (empty($seen[$object['uid']])) {
$seen[$object['uid']] = true;
$this->objects[] = $object;
}
} elseif ($ve->name == 'VFREEBUSY') {
$this->objects[] = $this->_parse_freebusy($ve);
}
}
// add exceptions to the according master events
foreach ($exceptions as $exception) {
$uid = $exception['uid'];
// make this exception the master
if (empty($seen[$uid])) {
$seen[$uid] = true;
$this->objects[] = $exception;
} else {
foreach ($this->objects as $i => $object) {
// add as exception to existing entry with a matching UID
if ($object['uid'] == $uid) {
$this->objects[$i]['exceptions'][] = $exception;
if (!empty($object['recurrence'])) {
$this->objects[$i]['recurrence']['EXCEPTIONS'] = &$this->objects[$i]['exceptions'];
}
break;
}
}
}
}
}
return $this->objects;
}
/**
* Getter for free-busy periods
*/
public function get_busy_periods()
{
$out = [];
foreach ((array)$this->freebusy['periods'] as $period) {
if ($period[2] != 'FREE') {
$out[] = $period;
}
}
return $out;
}
/**
* Helper method to determine whether the connected client is an Apple device
*/
private function is_apple()
{
return stripos($this->agent, 'Apple') !== false
|| stripos($this->agent, 'Mac OS X') !== false
|| stripos($this->agent, 'iOS/') !== false;
}
/**
* Convert the given VEvent object to a libkolab compatible array representation
*
* @param VObject\Component\VEvent|VObject\Component\VTodo $ve VEvent object to convert
*
* @return array Hash array with object properties
*/
private function _to_array($ve)
{
$event = [
'uid' => self::convert_string($ve->UID),
'title' => self::convert_string($ve->SUMMARY),
'_type' => $ve->name == 'VTODO' ? 'task' : 'event',
// set defaults
'priority' => 0,
'attendees' => [],
'x-custom' => [],
];
// Catch possible exceptions when date is invalid (Bug #2144)
// We can skip these fields, they aren't critical
foreach (['CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed'] as $attr => $field) {
try {
if (empty($event[$field]) && !empty($ve->{$attr})) {
$event[$field] = $ve->{$attr}->getDateTime();
}
} catch (Exception $e) {
}
}
// map other attributes to internal fields
foreach ($ve->children() as $prop) {
if (!($prop instanceof VObject\Property)) {
continue;
}
$value = strval($prop);
switch ($prop->name) {
case 'DTSTART':
case 'DTEND':
case 'DUE':
$propmap = ['DTSTART' => 'start', 'DTEND' => 'end', 'DUE' => 'due'];
$event[$propmap[$prop->name]] = self::convert_datetime($prop);
break;
case 'TRANSP':
$event['free_busy'] = strval($prop) == 'TRANSPARENT' ? 'free' : 'busy';
break;
case 'STATUS':
if ($value == 'TENTATIVE') {
$event['free_busy'] = 'tentative';
} elseif ($value == 'CANCELLED') {
$event['cancelled'] = true;
} elseif ($value == 'COMPLETED') {
$event['complete'] = 100;
}
$event['status'] = $value;
break;
case 'COMPLETED':
if (self::convert_datetime($prop)) {
$event['status'] = 'COMPLETED';
$event['complete'] = 100;
}
break;
case 'PRIORITY':
if (is_numeric($value)) {
$event['priority'] = $value;
}
break;
case 'RRULE':
$params = !empty($event['recurrence']) && is_array($event['recurrence']) ? $event['recurrence'] : [];
// parse recurrence rule attributes
foreach ($prop->getParts() as $k => $v) {
$params[strtoupper($k)] = is_array($v) ? implode(',', $v) : $v;
}
if (!empty($params['UNTIL'])) {
$params['UNTIL'] = date_create($params['UNTIL']);
}
if (empty($params['INTERVAL'])) {
$params['INTERVAL'] = 1;
}
$event['recurrence'] = array_filter($params);
break;
case 'EXDATE':
if (!empty($value)) {
$exdates = array_map(function ($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true));
if (!empty($event['recurrence']['EXDATE'])) {
$event['recurrence']['EXDATE'] = array_merge($event['recurrence']['EXDATE'], $exdates);
} else {
$event['recurrence']['EXDATE'] = $exdates;
}
}
break;
case 'RDATE':
if (!empty($value)) {
$rdates = array_map(function ($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true));
if (!empty($event['recurrence']['RDATE'])) {
$event['recurrence']['RDATE'] = array_merge($event['recurrence']['RDATE'], $rdates);
} else {
$event['recurrence']['RDATE'] = $rdates;
}
}
break;
case 'RECURRENCE-ID':
$event['recurrence_date'] = self::convert_datetime($prop);
if ($prop->offsetGet('RANGE') == 'THISANDFUTURE' || $prop->offsetGet('THISANDFUTURE') !== null) {
$event['thisandfuture'] = true;
}
break;
case 'RELATED-TO':
$reltype = $prop->offsetGet('RELTYPE');
if ($reltype == 'PARENT' || $reltype === null) {
$event['parent_id'] = $value;
}
break;
case 'SEQUENCE':
$event['sequence'] = intval($value);
break;
case 'PERCENT-COMPLETE':
$event['complete'] = intval($value);
break;
case 'LOCATION':
case 'DESCRIPTION':
case 'URL':
case 'COMMENT':
$event[strtolower($prop->name)] = self::convert_string($prop);
break;
case 'CATEGORY':
case 'CATEGORIES':
if (!empty($event['categories'])) {
$event['categories'] = array_merge((array) $event['categories'], $prop->getParts());
} else {
$event['categories'] = $prop->getParts();
}
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
if ($value == 'OOF') {
$event['free_busy'] = 'outofoffice';
} elseif (in_array($value, ['FREE', 'BUSY', 'TENTATIVE'])) {
$event['free_busy'] = strtolower($value);
}
break;
case 'ATTENDEE':
case 'ORGANIZER':
$params = ['RSVP' => false];
foreach ($prop->parameters() as $pname => $pvalue) {
switch ($pname) {
case 'RSVP': $params[$pname] = strtolower($pvalue) == 'true';
break;
case 'CN': $params[$pname] = self::unescape($pvalue);
break;
default: $params[$pname] = strval($pvalue);
break;
}
}
$attendee = self::map_keys($params, array_flip($this->attendee_keymap));
$attendee['email'] = preg_replace('!^mailto:!i', '', $value);
if ($prop->name == 'ORGANIZER') {
$attendee['role'] = 'ORGANIZER';
$attendee['status'] = 'ACCEPTED';
$event['organizer'] = $attendee;
if (array_key_exists('schedule-agent', $attendee)) {
$schedule_agent = $attendee['schedule-agent'];
}
} elseif (empty($event['organizer']) || $attendee['email'] != $event['organizer']['email']) {
$event['attendees'][] = $attendee;
}
break;
case 'ATTACH':
$params = self::parameters_array($prop);
if (substr($value, 0, 4) == 'http' && !strpos($value, ':attachment:')) {
$event['links'][] = $value;
} elseif (!empty($params['VALUE']) && strtoupper($params['VALUE']) == 'BINARY') {
$attachment = self::map_keys($params, [
'FMTTYPE' => 'mimetype',
'X-LABEL' => 'name',
'X-APPLE-FILENAME' => 'name',
'X-SIZE' => 'size',
]);
$attachment['data'] = $value;
$attachment['size'] = $attachment['size'] ?? strlen($value);
$attachment['id'] = md5(($attachment['mimetype'] ?? 'application/octet-stream') . ($attachment['name'] ?? 'noname'));
$event['attachments'][] = $attachment;
}
break;
default:
if (substr($prop->name, 0, 2) == 'X-') {
$event['x-custom'][] = [$prop->name, strval($value)];
}
break;
}
}
// check DURATION property if no end date is set
if (empty($event['end']) && $ve->DURATION) {
try {
$duration = new DateInterval(strval($ve->DURATION));
$end = clone $event['start'];
$end->add($duration);
$event['end'] = $end;
} catch (\Exception $e) {
trigger_error(strval($e), E_USER_WARNING);
}
}
// validate event dates
if ($event['_type'] == 'event') {
$event['allday'] = !empty($event['start']->_dateonly);
// events may lack the DTEND property, set it to DTSTART (RFC5545 3.6.1)
- if (empty($event['end'])) {
+ // Note: Missing event start date is a problem, but should not throw an error here
+ if (empty($event['end']) && !empty($event['start'])) {
$event['end'] = clone $event['start'];
}
// shift end-date by one day (except Thunderbird)
elseif ($event['allday'] && is_object($event['end'])) {
$event['end']->sub(new \DateInterval('PT23H'));
}
// sanity-check and fix end date
if (!empty($event['end']) && $event['end'] < $event['start']) {
$event['end'] = clone $event['start'];
}
}
// make organizer part of the attendees list for compatibility reasons
if (!empty($event['organizer']) && is_array($event['attendees']) && $event['_type'] == 'event') {
array_unshift($event['attendees'], $event['organizer']);
}
// find alarms
foreach ($ve->select('VALARM') as $valarm) {
$action = 'DISPLAY';
$trigger = null;
$alarm = [];
foreach ($valarm->children() as $prop) {
$value = strval($prop);
switch ($prop->name) {
case 'TRIGGER':
foreach ($prop->parameters as $param) {
if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') {
$trigger = '@' . $prop->getDateTime()->format('U');
$alarm['trigger'] = $prop->getDateTime();
} elseif ($param->name == 'RELATED') {
$alarm['related'] = $param->getValue();
}
}
if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) {
$trigger = $values[2];
}
if (empty($alarm['trigger'])) {
$alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $value), 'T');
// if all 0-values have been stripped, assume 'at time'
if ($alarm['trigger'] == 'P') {
$alarm['trigger'] = 'PT0S';
}
}
break;
case 'ACTION':
$action = $alarm['action'] = strtoupper($value);
break;
case 'SUMMARY':
case 'DESCRIPTION':
case 'DURATION':
$alarm[strtolower($prop->name)] = self::convert_string($prop);
break;
case 'REPEAT':
$alarm['repeat'] = intval($value);
break;
case 'ATTENDEE':
$alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value);
break;
case 'ATTACH':
$params = self::parameters_array($prop);
if (strlen($value) && (preg_match('/^[a-z]+:/', $value) || strtoupper($params['VALUE']) == 'URI')) {
// we only support URI-type of attachments here
$alarm['uri'] = $value;
}
break;
}
}
if ($action != 'NONE') {
// store first alarm in legacy property
if ($trigger && empty($event['alarms'])) {
$event['alarms'] = $trigger . ':' . $action;
}
if (!empty($alarm['trigger'])) {
$event['valarms'][] = $alarm;
}
}
}
// assign current timezone to event start/end
if (!empty($event['start']) && $event['start'] instanceof DateTimeInterface) {
$this->_apply_timezone($event['start']);
} else {
unset($event['start']);
}
if (!empty($event['end']) && $event['end'] instanceof DateTimeInterface) {
$this->_apply_timezone($event['end']);
} else {
unset($event['end']);
}
// some iTip CANCEL messages only contain the start date
if (empty($event['end']) && !empty($event['start']) && $this->method == 'CANCEL') {
$event['end'] = clone $event['start'];
}
// T2531: Remember SCHEDULE-AGENT in custom property to properly
// support event updates via CalDAV when SCHEDULE-AGENT=CLIENT is used
if (isset($schedule_agent)) {
$event['x-custom'][] = ['SCHEDULE-AGENT', $schedule_agent];
}
// minimal validation
if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) {
throw new VObject\ParseException('Object validation failed: missing mandatory object properties');
}
return $event;
}
/**
* Apply user timezone to DateTime object
*
* @param DateTime $date
*/
private function _apply_timezone(&$date)
{
if (empty($this->timezone)) {
return;
}
// For date-only we'll keep the date and time intact
if (!empty($date->_dateonly)) {
$dt = new libcalendaring_datetime(null, $this->timezone);
$dt->setDate($date->format('Y'), $date->format('n'), $date->format('j'));
$dt->setTime($date->format('G'), $date->format('i'), 0);
$date = $dt;
} else {
$date->setTimezone($this->timezone);
}
}
/**
* Parse the given vfreebusy component into an array representation
*/
private function _parse_freebusy($ve)
{
$this->freebusy = ['_type' => 'freebusy', 'periods' => []];
$seen = [];
foreach ($ve->children() as $prop) {
if (!($prop instanceof VObject\Property)) {
continue;
}
$value = strval($prop);
switch ($prop->name) {
case 'CREATED':
case 'LAST-MODIFIED':
case 'DTSTAMP':
case 'DTSTART':
case 'DTEND':
$propmap = [
'DTSTART' => 'start',
'DTEND' => 'end',
'CREATED' => 'created',
'LAST-MODIFIED' => 'changed',
'DTSTAMP' => 'changed',
];
$this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop);
break;
case 'ORGANIZER':
$this->freebusy['organizer'] = preg_replace('!^mailto:!i', '', $value);
break;
case 'FREEBUSY':
// The freebusy component can hold more than 1 value, separated by commas.
$periods = explode(',', $value);
$fbtype = strval($prop['FBTYPE']) ?: 'BUSY';
// skip dupes
if (!empty($seen[$value . ':' . $fbtype])) {
break;
}
$seen[$value . ':' . $fbtype] = true;
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.
[$busyStart, $busyEnd] = explode('/', $period);
$busyStart = DateTimeParser::parse($busyStart);
$busyEnd = DateTimeParser::parse($busyEnd);
if ($busyEnd instanceof \DateInterval) {
$tmp = clone $busyStart;
$busyEnd = $tmp->add($busyEnd);
}
if ($busyEnd && $busyEnd > $busyStart) {
$this->freebusy['periods'][] = [$busyStart, $busyEnd, $fbtype];
}
}
break;
case 'COMMENT':
$this->freebusy['comment'] = $value;
}
}
return $this->freebusy;
}
/**
*
*/
public static function convert_string($prop)
{
return strval($prop);
}
/**
*
*/
public static function unescape($prop)
{
return str_replace('\,', ',', strval($prop));
}
/**
* Helper method to correctly interpret an all-day date value
*/
public static function convert_datetime($prop, $as_array = false)
{
if (empty($prop)) {
return $as_array ? [] : null;
}
if ($prop instanceof VObject\Property\ICalendar\DateTime) {
if (count($prop->getDateTimes()) > 1) {
$dt = [];
$dateonly = !$prop->hasTime();
foreach ($prop->getDateTimes() as $item) {
$item = self::toDateTime($item);
$item->_dateonly = $dateonly;
$dt[] = $item;
}
} else {
$dt = self::toDateTime($prop->getDateTime());
if (!$prop->hasTime()) {
$dt->_dateonly = true;
}
}
} elseif ($prop instanceof VObject\Property\ICalendar\Period) {
$dt = [];
foreach ($prop->getParts() as $val) {
try {
[$start, $end] = explode('/', $val);
$start = self::toDateTime(DateTimeParser::parseDateTime($start));
// This is a duration value.
if ($end[0] === 'P') {
$dur = DateTimeParser::parseDuration($end);
$end = clone $start;
$end->add($dur);
} else {
$end = self::toDateTime(DateTimeParser::parseDateTime($end));
}
$dt[] = [$start, $end];
} catch (Exception $e) {
// ignore single date parse errors
}
}
} elseif ($prop instanceof \DateTimeInterface) {
$dt = self::toDateTime($prop);
}
// force return value to array if requested
if ($as_array) {
if (empty($dt)) {
return [];
}
return is_array($dt) ? $dt : [$dt];
}
return $dt ?? null;
}
/**
* Create a Sabre\VObject\Property instance from a PHP DateTime object
*
* @param VObject\Document $cal Parent node to create property for
* @param string $name Property name
* @param DateTime $dt Date time object
* @param bool $utc Set as UTC date
* @param bool $dateonly Set as VALUE=DATE property
*
* @return VObject\Property
*/
public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null, $set_type = false)
{
$tz = null;
$is_utc = false;
if ($dt) {
if ($utc) {
$dt->setTimeZone(new DateTimeZone('UTC'));
$is_utc = true;
} else {
$is_utc = ($tz = $dt->getTimezone()) && in_array($tz->getName(), ['UTC','GMT','Z']);
}
}
$is_dateonly = $dateonly === null && $dt ? !empty($dt->_dateonly) : (bool) $dateonly;
$vdt = $cal->createProperty($name, $dt, null, $is_dateonly ? 'DATE' : 'DATE-TIME');
if ($is_dateonly) {
$vdt['VALUE'] = 'DATE';
} elseif ($set_type) {
$vdt['VALUE'] = 'DATE-TIME';
}
// register timezone for VTIMEZONE block
if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) {
$ts = $dt->format('U');
if (!empty($this->vtimezones[$tzname])) {
$this->vtimezones[$tzname][0] = min($this->vtimezones[$tzname][0], $ts);
$this->vtimezones[$tzname][1] = max($this->vtimezones[$tzname][1], $ts);
} else {
$this->vtimezones[$tzname] = [$ts, $ts];
}
}
return $vdt;
}
/**
* Copy values from one hash array to another using a key-map
*/
public static function map_keys($values, $map)
{
$out = [];
foreach ($map as $from => $to) {
if (isset($values[$from])) {
$out[$to] = is_array($values[$from]) ? implode(',', $values[$from]) : $values[$from];
}
}
return $out;
}
/**
*
*/
private static function parameters_array($prop)
{
$params = [];
foreach ($prop->parameters() as $name => $value) {
$params[strtoupper($name)] = strval($value);
}
return $params;
}
/**
* Export events to iCalendar format
*
* @param array $objects Events as array
* @param ?string $method VCalendar method to advertise
* @param bool $write Directly send data to stdout instead of returning
* @param ?callable $get_attachment Optional callback function to fetch attachment contents
* @param bool $with_timezones Add VTIMEZONE block with timezone definitions for the included events
*
* @return string|true Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
*/
public function export($objects, $method = null, $write = false, $get_attachment = null, $with_timezones = true)
{
$this->method = $method;
// encapsulate in VCALENDAR container
$vcal = new VObject\Component\VCalendar();
$vcal->VERSION = '2.0';
$vcal->PRODID = $this->prodid;
$vcal->CALSCALE = 'GREGORIAN';
if (!empty($method)) {
$vcal->METHOD = $method;
}
// write vcalendar header
if ($write) {
echo preg_replace('/END:VCALENDAR[\r\n]*$/m', '', $vcal->serialize());
}
foreach ($objects as $object) {
$this->_to_ical($object, !$write ? $vcal : false, $get_attachment);
}
// include timezone information
if ($with_timezones || !empty($method)) {
foreach ($this->vtimezones as $tzid => $range) {
$vt = self::get_vtimezone($tzid, $range[0], $range[1], $vcal);
if (empty($vt)) {
continue; // no timezone information found
}
if ($write) {
echo $vt->serialize();
} else {
$vcal->add($vt);
}
}
}
if ($write) {
echo "END:VCALENDAR\r\n";
return true;
}
return $vcal->serialize();
}
/**
* Converts internal event representation to Sabre component
*
* @param array $object Event
* @param ?callable $get_attachment Optional callback function to fetch attachment contents
*
* @return VObject\Component Sabre component
*/
public function toSabreComponent($object, $get_attachment = null)
{
$vcal = new VObject\Component\VCalendar();
$this->_to_ical($object, $vcal, $get_attachment);
return $vcal->getBaseComponent();
}
/**
* Build a valid iCal format block from the given event
*
* @param array $event Hash array with event/task properties from libkolab
* @param VObject\Component\VCalendar $vcal VCalendar object to append event to or false for directly sending data to stdout
* @param ?callable $get_attachment Optional callback function to fetch attachment contents
* @param VObject\Property $recurrence_id RECURRENCE-ID property when serializing a recurrence exception
*/
private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null)
{
$type = !empty($event['_type']) ? $event['_type'] : 'event';
$cal = $vcal ?: new VObject\Component\VCalendar();
$ve = $cal->create($this->type_component_map[$type]);
$ve->UID = $event['uid'];
// set DTSTAMP according to RFC 5545, 3.8.7.2.
$dtstamp = !empty($event['changed']) && empty($this->method) ? $event['changed'] : new DateTime('now', new DateTimeZone('UTC'));
$ve->DTSTAMP = $this->datetime_prop($cal, 'DTSTAMP', $dtstamp, true);
// all-day events end the next day
if (!empty($event['allday']) && !empty($event['end'])) {
$event['end'] = self::toDateTime($event['end']);
$event['end']->add(new \DateInterval('P1D'));
$event['end']->_dateonly = true;
}
if (!empty($event['created'])) {
$ve->add($this->datetime_prop($cal, 'CREATED', $event['created'], true));
}
if (!empty($event['changed'])) {
$ve->add($this->datetime_prop($cal, 'LAST-MODIFIED', $event['changed'], true));
}
if (!empty($event['start'])) {
$ve->add($this->datetime_prop($cal, 'DTSTART', $event['start'], false, !empty($event['allday'])));
}
if (!empty($event['end'])) {
$ve->add($this->datetime_prop($cal, 'DTEND', $event['end'], false, !empty($event['allday'])));
}
if (!empty($event['due'])) {
$ve->add($this->datetime_prop($cal, 'DUE', $event['due'], false));
}
// we're exporting a recurrence instance only
if (!$recurrence_id && !empty($event['recurrence_date']) && $event['recurrence_date'] instanceof DateTimeInterface) {
$recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $event['recurrence_date'], false, !empty($event['allday']));
if (!empty($event['thisandfuture'])) {
$recurrence_id->add('RANGE', 'THISANDFUTURE');
}
}
if ($recurrence_id) {
$ve->add($recurrence_id);
}
if (!empty($event['title'])) {
$ve->add('SUMMARY', $event['title']);
}
if (!empty($event['location'])) {
$ve->add($this->is_apple() ? new vobject_location_property($cal, 'LOCATION', $event['location']) : $cal->create('LOCATION', $event['location']));
}
if (!empty($event['description'])) {
$ve->add('DESCRIPTION', strtr($event['description'], ["\r\n" => "\n", "\r" => "\n"])); // normalize line endings
}
if (isset($event['sequence'])) {
$ve->add('SEQUENCE', $event['sequence']);
}
if (!empty($event['recurrence']) && !$recurrence_id) {
$exdates = $rdates = null;
if (isset($event['recurrence']['EXDATE'])) {
$exdates = $event['recurrence']['EXDATE'];
unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value
}
if (isset($event['recurrence']['RDATE'])) {
$rdates = $event['recurrence']['RDATE'];
unset($event['recurrence']['RDATE']); // don't serialize RDATEs into RRULE value
}
if (!empty($event['recurrence']['FREQ'])) {
$ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], !empty($event['allday'])));
}
// add EXDATEs each one per line (for Thunderbird Lightning)
if (is_array($exdates)) {
foreach ($exdates as $exdate) {
if ($exdate instanceof DateTimeInterface) {
$ve->add($this->datetime_prop($cal, 'EXDATE', $exdate));
}
}
}
// add RDATEs
if (is_array($rdates)) {
foreach ($rdates as $rdate) {
$ve->add($this->datetime_prop($cal, 'RDATE', $rdate));
}
}
}
if (!empty($event['categories'])) {
$cat = $cal->create('CATEGORIES');
$cat->setParts((array)$event['categories']);
$ve->add($cat);
}
if (!empty($event['free_busy'])) {
$ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
// for Outlook clients we provide the X-MICROSOFT-CDO-BUSYSTATUS property
if (stripos($this->agent, 'outlook') !== false) {
$ve->add('X-MICROSOFT-CDO-BUSYSTATUS', $event['free_busy'] == 'outofoffice' ? 'OOF' : strtoupper($event['free_busy']));
}
}
if (!empty($event['priority'])) {
$ve->add('PRIORITY', $event['priority']);
}
if (!empty($event['cancelled'])) {
$ve->add('STATUS', 'CANCELLED');
} elseif (!empty($event['free_busy']) && $event['free_busy'] == 'tentative') {
$ve->add('STATUS', 'TENTATIVE');
} elseif (!empty($event['complete']) && $event['complete'] == 100) {
$ve->add('STATUS', 'COMPLETED');
} elseif (!empty($event['status'])) {
$ve->add('STATUS', $event['status']);
}
if (!empty($event['complete'])) {
$ve->add('PERCENT-COMPLETE', intval($event['complete']));
}
// Apple iCal and BusyCal required the COMPLETED date to be set in order to consider a task complete
if (
(!empty($event['status']) && $event['status'] == 'COMPLETED')
|| (!empty($event['complete']) && $event['complete'] == 100)
) {
$completed = !empty($event['changed']) ? $event['changed'] : new DateTime('now - 1 hour');
$ve->add($this->datetime_prop($cal, 'COMPLETED', $completed, true));
}
if (!empty($event['valarms'])) {
foreach ($event['valarms'] as $alarm) {
$va = $cal->createComponent('VALARM');
$va->action = $alarm['action'];
if ($alarm['trigger'] instanceof DateTimeInterface) {
$va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true));
} else {
$alarm_props = [];
if (!empty($alarm['related']) && strtoupper($alarm['related']) == 'END') {
$alarm_props['RELATED'] = 'END';
}
$va->add('TRIGGER', $alarm['trigger'], $alarm_props);
}
if (!empty($alarm['action']) && $alarm['action'] == 'EMAIL') {
if (!empty($alarm['attendees'])) {
foreach ((array) $alarm['attendees'] as $attendee) {
$va->add('ATTENDEE', 'mailto:' . $attendee);
}
}
}
if (!empty($alarm['description'])) {
$va->add('DESCRIPTION', $alarm['description']);
}
if (!empty($alarm['summary'])) {
$va->add('SUMMARY', $alarm['summary']);
}
if (!empty($alarm['duration'])) {
$va->add('DURATION', $alarm['duration']);
$va->add('REPEAT', !empty($alarm['repeat']) ? intval($alarm['repeat']) : 0);
}
if (!empty($alarm['uri'])) {
$va->add('ATTACH', $alarm['uri'], ['VALUE' => 'URI']);
}
$ve->add($va);
}
}
// legacy support
elseif (!empty($event['alarms'])) {
$va = $cal->createComponent('VALARM');
[$trigger, $va->action] = explode(':', $event['alarms']);
$val = libcalendaring::parse_alarm_value($trigger);
if (!empty($val[3])) {
$va->add('TRIGGER', $val[3]);
} elseif ($val[0] instanceof DateTimeInterface) {
$va->add($this->datetime_prop($cal, 'TRIGGER', $val[0], true, null, true));
}
$ve->add($va);
}
// Find SCHEDULE-AGENT
if (!empty($event['x-custom'])) {
foreach ((array) $event['x-custom'] as $prop) {
if ($prop[0] === 'SCHEDULE-AGENT') {
$schedule_agent = $prop[1];
}
}
}
if (!empty($event['attendees'])) {
foreach ((array) $event['attendees'] as $attendee) {
if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') {
if (empty($event['organizer'])) {
$event['organizer'] = $attendee;
}
} elseif (!empty($attendee['email'])) {
if (isset($attendee['rsvp'])) {
$attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
}
$mailto = $attendee['email'];
$attendee = array_filter(self::map_keys($attendee, $this->attendee_keymap));
if (isset($schedule_agent) && !isset($attendee['SCHEDULE-AGENT'])) {
$attendee['SCHEDULE-AGENT'] = $schedule_agent;
}
$ve->add('ATTENDEE', 'mailto:' . $mailto, $attendee);
}
}
}
if (!empty($event['organizer'])) {
$organizer = array_filter(self::map_keys($event['organizer'], $this->organizer_keymap));
if (isset($schedule_agent) && !isset($organizer['SCHEDULE-AGENT'])) {
$organizer['SCHEDULE-AGENT'] = $schedule_agent;
}
$ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], $organizer);
}
if (!empty($event['url'])) {
foreach ((array) $event['url'] as $url) {
if (!empty($url)) {
$ve->add('URL', $url);
}
}
}
if (!empty($event['parent_id'])) {
$ve->add('RELATED-TO', $event['parent_id'], ['RELTYPE' => 'PARENT']);
}
if (!empty($event['comment'])) {
$ve->add('COMMENT', $event['comment']);
}
$memory_limit = parse_bytes(ini_get('memory_limit'));
// export attachments
if (!empty($event['attachments'])) {
foreach ((array) $event['attachments'] as $idx => $attach) {
// check available memory and skip attachment export if we can't buffer it
// @todo: use rcube_utils::mem_check()
if (is_callable($get_attachment) && $memory_limit > 0) {
$memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16 * 1024 * 1024;
if (!empty($attach['size']) && $memory_used + $attach['size'] * 3 > $memory_limit) {
continue;
}
}
// embed attachments using the given callback function
if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'] ?? $idx, $event))) {
// embed attachments for iCal
$ve->add(
'ATTACH',
$data,
array_filter([
'VALUE' => 'BINARY',
'ENCODING' => 'BASE64',
'FMTTYPE' => $attach['mimetype'] ?? null,
'X-LABEL' => $attach['name'] ?? null,
'X-SIZE' => $attach['size'] ?? null,
])
);
unset($data); // attempt to free memory
}
// list attachments as absolute URIs
elseif (!empty($this->attach_uri)) {
$ve->add(
'ATTACH',
strtr($this->attach_uri, [
'{{id}}' => urlencode($attach['id'] ?? $idx),
'{{name}}' => urlencode($attach['name']),
'{{mimetype}}' => urlencode($attach['mimetype']),
]),
['FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI']
);
}
}
}
if (!empty($event['links'])) {
foreach ((array) $event['links'] as $uri) {
$ve->add('ATTACH', $uri);
}
}
// add custom properties
if (!empty($event['x-custom'])) {
foreach ((array) $event['x-custom'] as $prop) {
$ve->add($prop[0], $prop[1]);
}
}
// append to vcalendar container
if ($vcal) {
$vcal->add($ve);
} else { // serialize and send to stdout
echo $ve->serialize();
}
// append recurrence exceptions
if (!empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
$exdate = !empty($ex['recurrence_date']) ? $ex['recurrence_date'] : $ex['start'];
$recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $exdate, false, !empty($event['allday']));
if (!empty($ex['thisandfuture'])) {
$recurrence_id->add('RANGE', 'THISANDFUTURE');
}
$ex['uid'] = $ve->UID;
$this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
}
}
}
/**
* Returns a VTIMEZONE component for a Olson timezone identifier
* with daylight transitions covering the given date range.
*
* @param string $tzid Timezone ID as used in PHP's Date functions
* @param int $from Unix timestamp with first date/time in this timezone
* @param int $to Unix timestap with last date/time in this timezone
* @param VObject\Component\VCalendar $cal Optional VCalendar component
*
* @return VObject\Component|false Object representing a VTIMEZONE definition
* or false if no timezone information is available
*/
public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null)
{
// TODO: Consider using tzurl.org database for better interoperability e.g. with Outlook
if (!$from) {
$from = time();
}
if (!$to) {
$to = $from;
}
if (!$cal) {
$cal = new VObject\Component\VCalendar();
}
if (is_string($tzid)) {
try {
$tz = new DateTimeZone($tzid);
} catch (Exception $e) {
// do nothing
}
} elseif ($tzid instanceof DateTimeZone) {
$tz = $tzid;
}
if (empty($tz) || !$tz instanceof DateTimeZone) {
return false;
}
$year = 86400 * 360;
$transitions = $tz->getTransitions($from - $year, $to + $year);
// Make sure VTIMEZONE contains at least one STANDARD/DAYLIGHT component
// when there's only one transition in specified time period (T5626)
if (count($transitions) == 1) {
// Get more transitions and use OFFSET from the previous to last
$more_transitions = $tz->getTransitions(0, $to + $year);
if (count($more_transitions) > 1) {
$index = count($more_transitions) - 2;
$tzfrom = $more_transitions[$index]['offset'] / 3600;
}
}
$vt = $cal->createComponent('VTIMEZONE');
$vt->TZID = $tz->getName();
$std = null;
$dst = null;
$t_dst = $t_std = 0;
foreach ($transitions as $i => $trans) {
if (!isset($tzfrom)) {
$tzfrom = $trans['offset'] / 3600;
continue;
}
if ($trans['isdst']) {
$t_dst = $trans['ts'];
$dst = $cal->createComponent('DAYLIGHT');
$cmp = $dst;
} else {
$t_std = $trans['ts'];
$std = $cal->createComponent('STANDARD');
$cmp = $std;
}
// FIXME: Imho $trans['time'] is UTC, shouldn't we consider timezone offset for DTSTART below?
$dt = new DateTime($trans['time']);
$offset = $trans['offset'] / 3600;
$cmp->DTSTART = $dt->format('Ymd\THis');
$cmp->TZOFFSETFROM = sprintf('%+03d%02d', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
$cmp->TZOFFSETTO = sprintf('%+03d%02d', floor($offset), ($offset - floor($offset)) * 60);
if (!empty($trans['abbr'])) {
$cmp->TZNAME = $trans['abbr'];
}
$tzfrom = $offset;
$vt->add($cmp);
// we covered the entire date range
if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
break;
}
}
// add X-MICROSOFT-CDO-TZID if available
$microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
if (array_key_exists($tz->getName(), $microsoftExchangeMap)) {
$vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
}
return $vt;
}
/**
* Convert DateTime into libcalendaring_datetime
*
* @return libcalendaring_datetime
*/
private static function toDateTime($date)
{
return libcalendaring_datetime::createFromFormat(
'Y-m-d\\TH:i:s',
$date->format('Y-m-d\\TH:i:s'),
$date->getTimezone()
);
}
/*** Implement PHP 5 Iterator interface to make foreach work ***/
#[\ReturnTypeWillChange]
public function current()
{
return $this->objects[$this->iteratorkey];
}
#[\ReturnTypeWillChange]
public function key()
{
return $this->iteratorkey;
}
#[\ReturnTypeWillChange]
public function next()
{
$this->iteratorkey++;
// read next chunk if we're reading from a file
if (empty($this->objects[$this->iteratorkey]) && $this->fp) {
$this->_parse_next(true);
}
}
#[\ReturnTypeWillChange]
public function rewind()
{
$this->iteratorkey = 0;
}
#[\ReturnTypeWillChange]
public function valid()
{
return !empty($this->objects[$this->iteratorkey]);
}
}
/**
* Override Sabre\VObject\Property\Text that quotes commas in the location property
* because Apple clients treat that property as list.
*/
class vobject_location_property extends VObject\Property\Text
{
/**
* List of properties that are considered 'structured'.
*
* @var array
*/
protected $structuredValues = [
// vCard
'N',
'ADR',
'ORG',
'GENDER',
'LOCATION',
// iCalendar
'REQUEST-STATUS',
];
}
diff --git a/plugins/libcalendaring/tests/VcalendarTest.php b/plugins/libcalendaring/tests/VcalendarTest.php
index b9cb68d9..3a0595a1 100644
--- a/plugins/libcalendaring/tests/VcalendarTest.php
+++ b/plugins/libcalendaring/tests/VcalendarTest.php
@@ -1,609 +1,620 @@
<?php
/**
* libcalendaring plugin's iCalendar functions tests
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class VcalendarTest extends PHPUnit\Framework\TestCase
{
private $attachment_data;
public function setUp(): void
{
require_once __DIR__ . '/../libcalendaring.php';
require_once __DIR__ . '/../lib/libcalendaring_vcalendar.php';
require_once __DIR__ . '/../lib/libcalendaring_datetime.php';
}
/**
* Simple iCal parsing test
*/
public function test_import()
{
$ical = new libcalendaring_vcalendar();
$ics = file_get_contents(__DIR__ . '/resources/snd.ics');
$events = $ical->import($ics, 'UTF-8');
$this->assertEquals(1, count($events));
$event = $events[0];
$this->assertInstanceOf('DateTimeInterface', $event['created'], "'created' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $event['changed'], "'changed' property is DateTime object");
$this->assertEquals('UTC', $event['created']->getTimezone()->getName(), "'created' date is in UTC");
$this->assertInstanceOf('DateTimeInterface', $event['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $event['end'], "'end' property is DateTime object");
$this->assertEquals('08-01', $event['start']->format('m-d'), "Start date is August 1st");
$this->assertTrue($event['allday'], "All-day event flag");
$this->assertEquals('B968B885-08FB-40E5-B89E-6DA05F26AA79', $event['uid'], "Event UID");
$this->assertEquals('Swiss National Day', $event['title'], "Event title");
$this->assertEquals('http://en.wikipedia.org/wiki/Swiss_National_Day', $event['url'], "URL property");
$this->assertEquals(2, $event['sequence'], "Sequence number");
$desclines = explode("\n", $event['description']);
$this->assertEquals(4, count($desclines), "Multiline description");
$this->assertEquals("French: Fête nationale Suisse", rtrim($desclines[1]), "UTF-8 encoding");
+
+ $ical->reset();
+
+ // An event without DTSTART should not throw an exception
+ // It's a broken iTip from Exchange 2010
+ $ics = file_get_contents(__DIR__ . '/resources/dummy-dupe.ics');
+ $events = $ical->import($ics, 'UTF-8');
+
+ $this->assertEquals(1, count($events));
+ $event = $events[0];
+ $this->assertSame('Summary', $event['title']);
}
/**
* Test parsing from files
*/
public function test_import_from_file()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
$this->assertEquals(2, count($events));
$events = $ical->import_from_file(__DIR__ . '/resources/invalid.txt', 'UTF-8');
$this->assertEmpty($events);
}
/**
* Test parsing from files with multiple VCALENDAR blocks (#2884)
*/
public function test_import_from_file_multiple()
{
$ical = new libcalendaring_vcalendar();
$ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$events = [];
foreach ($ical as $event) {
$events[] = $event;
}
$this->assertEquals(2, count($events));
$this->assertEquals("AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated", $events[0]['uid']);
$this->assertEquals("AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated", $events[1]['uid']);
}
public function test_invalid_dates()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/invalid-dates.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(1, count($events), "Import event data");
$this->assertInstanceOf('DateTimeInterface', $event['created'], "Created date field");
$this->assertFalse(array_key_exists('changed', $event), "No changed date field");
}
/**
* Test some extended ical properties such as attendees, recurrence rules, alarms and attachments
*/
public function test_extended()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('REQUEST', $ical->method, "iTip method");
// attendees
$this->assertEquals(3, count($event['attendees']), "Attendees list (including organizer)");
$organizer = $event['attendees'][0];
$this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE');
$this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name');
$attendee = $event['attendees'][1];
$this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE');
$this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS');
$this->assertEquals('rolf2@mykolab.com', $attendee['email'], 'Attendee mailto:');
$this->assertEquals('carl@mykolab.com', $attendee['delegated-from'], 'Attendee delegated-from');
$this->assertTrue($attendee['rsvp'], 'Attendee RSVP');
$delegator = $event['attendees'][2];
$this->assertEquals('NON-PARTICIPANT', $delegator['role'], 'Delegator ROLE');
$this->assertEquals('DELEGATED', $delegator['status'], 'Delegator STATUS');
$this->assertEquals('INDIVIDUAL', $delegator['cutype'], 'Delegator CUTYPE');
$this->assertEquals('carl@mykolab.com', $delegator['email'], 'Delegator mailto:');
$this->assertEquals('rolf2@mykolab.com', $delegator['delegated-to'], 'Delegator delegated-to');
$this->assertFalse($delegator['rsvp'], 'Delegator RSVP');
// attachments
$this->assertEquals(1, count($event['attachments']), "Embedded attachments");
$attachment = $event['attachments'][0];
$this->assertEquals('text/html', $attachment['mimetype'], "Attachment mimetype attribute");
$this->assertEquals('calendar.html', $attachment['name'], "Attachment filename (X-LABEL) attribute");
$this->assertStringContainsString('<title>Kalender</title>', $attachment['data'], "Attachment content (decoded)");
// recurrence rules
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
$event = $events[0];
$this->assertTrue(is_array($event['recurrence']), 'Recurrences rule as hash array');
$rrule = $event['recurrence'];
$this->assertEquals('MONTHLY', $rrule['FREQ'], "Recurrence frequency");
$this->assertEquals('1', $rrule['INTERVAL'], "Recurrence interval");
$this->assertEquals('3WE', $rrule['BYDAY'], "Recurrence frequency");
$this->assertInstanceOf('DateTimeInterface', $rrule['UNTIL'], "Recurrence end date");
$this->assertEquals(2, count($rrule['EXDATE']), "Recurrence EXDATEs");
$this->assertInstanceOf('DateTimeInterface', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime");
$this->assertTrue(is_array($rrule['EXCEPTIONS']));
$this->assertEquals(1, count($rrule['EXCEPTIONS']), "Recurrence Exceptions");
$exception = $rrule['EXCEPTIONS'][0];
$this->assertEquals($event['uid'], $event['uid'], "Exception UID");
$this->assertEquals('Recurring Test (Exception)', $exception['title'], "Exception title");
$this->assertInstanceOf('DateTimeInterface', $exception['start'], "Exception start");
// categories, class
$this->assertEquals('libcalendaring tests', implode(',', (array)$event['categories']), "Event categories");
// parse a recurrence chain instance
$events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8');
$this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty");
$this->assertInstanceOf('DateTimeInterface', $events[0]['recurrence_date'], "Recurrence-ID as date");
$this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE");
$this->assertEquals(count($events[0]['exceptions']), 1, "Second VEVENT as exception");
$this->assertEquals($events[0]['exceptions'][0]['uid'], $events[0]['uid'], "Exception UID match");
$this->assertEquals($events[0]['exceptions'][0]['sequence'], '2', "Exception sequence");
}
/**
*
*/
public function test_alarms()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string");
$alarm = libcalendaring::parse_alarm_value($event['alarms']);
$this->assertEquals('12', $alarm[0], "Alarm value");
$this->assertEquals('-H', $alarm[1], "Alarm unit");
$this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)");
$this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
$this->assertEquals('END', $event['valarms'][0]['related'], "Full alarm item (related)");
// alarm trigger with 0 values
$events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('-30M:DISPLAY', $event['alarms'], "Stripped alarm string");
$alarm = libcalendaring::parse_alarm_value($event['alarms']);
$this->assertEquals('30', $alarm[0], "Alarm value");
$this->assertEquals('-M', $alarm[1], "Alarm unit");
$this->assertEquals('-30M', $alarm[2], "Alarm string");
$this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)");
$this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action");
$this->assertTrue(empty($event['valarms'][0]['related']), "First alarm related property");
$this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text");
$this->assertEquals(3, count($event['valarms']), "List all VALARM blocks");
$valarm = $event['valarms'][1];
$this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees");
$this->assertEquals('EMAIL', $valarm['action'], "Second alarm item (action)");
$this->assertEquals('-P1D', $valarm['trigger'], "Second alarm item (trigger)");
$this->assertEquals('This is the reminder message', $valarm['summary'], "Email alarm text");
$this->assertInstanceOf('DateTimeInterface', $event['valarms'][2]['trigger'], "Absolute trigger date/time");
// test alarms export
$ics = $ical->export([$event]);
$this->assertStringContainsString('ACTION:DISPLAY', $ics, "Display alarm block");
$this->assertStringContainsString('ACTION:EMAIL', $ics, "Email alarm block");
$this->assertStringContainsString('DESCRIPTION:This is the first event reminder', $ics, "Alarm description");
$this->assertStringContainsString('SUMMARY:This is the reminder message', $ics, "Email alarm summary");
$this->assertStringContainsString('ATTENDEE:mailto:reminder-recipient@example.org', $ics, "Email alarm recipient");
$this->assertStringContainsString('TRIGGER;VALUE=DATE-TIME:20130812', $ics, "Date-Time trigger");
}
/**
* @depends test_import_from_file
*/
public function test_attachment()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/attachment.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(2, count($events));
$this->assertEquals(1, count($event['attachments']));
$this->assertEquals('image/png', $event['attachments'][0]['mimetype']);
$this->assertEquals('500px-Opensource.svg.png', $event['attachments'][0]['name']);
}
/**
* @depends test_import
*/
public function test_apple_alarms()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/apple-alarms.ics', 'UTF-8');
$event = $events[0];
// alarms
$this->assertEquals('-45M:AUDIO', $event['alarms'], "Relative alarm string");
$alarm = libcalendaring::parse_alarm_value($event['alarms']);
$this->assertEquals('45', $alarm[0], "Alarm value");
$this->assertEquals('-M', $alarm[1], "Alarm unit");
$this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks");
$this->assertEquals('AUDIO', $event['valarms'][0]['action'], "Full alarm item (action)");
$this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
$this->assertEquals('Basso', $event['valarms'][0]['uri'], "Full alarm item (attachment)");
}
/**
*
*/
public function test_escaped_values()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/escaped.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals("House, Street, Zip Place", $event['location'], "Decode escaped commas in location value");
$this->assertEquals("Me, meets Them\nThem, meet Me", $event['description'], "Decode description value");
$this->assertEquals("Kolab, Thomas", $event['attendees'][3]['name'], "Unescaped");
$ics = $ical->export($events);
$this->assertStringContainsString('ATTENDEE;CN="Kolab, Thomas";PARTSTAT=', $ics, "Quoted attendee parameters");
}
/**
* Parse RDATE properties (#2885)
*/
public function test_rdate()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(9, count($event['recurrence']['RDATE']));
$this->assertInstanceOf('DateTimeInterface', $event['recurrence']['RDATE'][0]);
$this->assertInstanceOf('DateTimeInterface', $event['recurrence']['RDATE'][1]);
}
/**
* @depends test_import
*/
public function test_freebusy()
{
$ical = new libcalendaring_vcalendar();
$ical->import_from_file(__DIR__ . '/resources/freebusy.ifb', 'UTF-8');
$freebusy = $ical->freebusy;
$this->assertInstanceOf('DateTimeInterface', $freebusy['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $freebusy['end'], "'end' property is DateTime object");
$this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined");
$periods = $ical->get_busy_periods();
$this->assertEquals(9, count($periods), "Number of busy periods found");
$this->assertEquals('BUSY-TENTATIVE', $periods[8][2], "FBTYPE=BUSY-TENTATIVE");
}
/**
* @depends test_import
*/
public function test_freebusy_dummy()
{
$ical = new libcalendaring_vcalendar();
$ical->import_from_file(__DIR__ . '/resources/dummy.ifb', 'UTF-8');
$freebusy = $ical->freebusy;
$this->assertEquals(0, count($freebusy['periods']), "Ignore 0-length freebudy periods");
$this->assertStringContainsString('dummy', $freebusy['comment'], "Parse comment");
}
public function test_vtodo()
{
$ical = new libcalendaring_vcalendar();
$tasks = $ical->import_from_file(__DIR__ . '/resources/vtodo.ics', 'UTF-8', true);
$task = $tasks[0];
$this->assertInstanceOf('DateTimeInterface', $task['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $task['due'], "'due' property is DateTime object");
$this->assertEquals('-1D:DISPLAY', $task['alarms'], "Taks alarm value");
$this->assertEquals('IN-PROCESS', $task['status'], "Task status property");
$this->assertEquals(1, count($task['x-custom']), "Custom properties");
$this->assertEquals(4, count($task['categories']));
$this->assertEquals('1234567890-12345678-PARENT', $task['parent_id'], "Parent Relation");
$completed = $tasks[1];
$this->assertEquals('COMPLETED', $completed['status'], "Task status=completed when COMPLETED property is present");
$this->assertEquals(100, $completed['complete'], "Task percent complete value");
$ics = $ical->export([$completed]);
$this->assertMatchesRegularExpression('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property");
}
/**
* Test for iCal export from internal hash array representation
*/
public function test_export()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
$event = $events[0];
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
$event += $events[0];
$this->attachment_data = $event['attachments'][0]['data'];
unset($event['attachments'][0]['data']);
$event['attachments'][0]['id'] = '1';
$event['description'] = '*Exported by libcalendaring_vcalendar*';
$event['start']->setTimezone(new DateTimezone('America/Montreal'));
$event['end']->setTimezone(new DateTimezone('Europe/Berlin'));
$ics = $ical->export([$event], 'REQUEST', false, [$this, 'get_attachment_data'], true);
$this->assertStringContainsString('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
$this->assertStringContainsString('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN");
$this->assertStringContainsString('TZID:Europe/Berlin', $ics, "Timezone ID");
$this->assertStringContainsString('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM");
$this->assertStringContainsString('TZOFFSETTO:+0200', $ics, "Timzone transition TO");
$this->assertStringContainsString('TZOFFSETFROM:-0400', $ics, "TZOFFSETFROM with negative offset (Bug T428)");
$this->assertStringContainsString('TZOFFSETTO:-0500', $ics, "TZOFFSETTO with negative offset (Bug T428)");
$this->assertStringContainsString('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
$this->assertStringContainsString('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
$this->assertSame(2, substr_count($ics, 'DTSTAMP'), "Duplicate DTSTAMP (T1148)");
$this->assertStringContainsString('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
$this->assertStringContainsString('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number");
$this->assertStringContainsString('DESCRIPTION:*Exported by', $ics, "Export Description");
$this->assertStringContainsString('ORGANIZER;CN=Rolf Test:mailto:rolf@', $ics, "Export organizer");
$this->assertMatchesRegularExpression('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/', $ics, "Export Attendee ROLE");
$this->assertMatchesRegularExpression('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/', $ics, "Export Attendee Status");
$this->assertMatchesRegularExpression('/ATTENDEE.*;RSVP=TRUE/', $ics, "Export Attendee RSVP");
$this->assertMatchesRegularExpression('/:mailto:rolf2@/', $ics, "Export Attendee mailto:");
$rrule = $event['recurrence'];
$this->assertMatchesRegularExpression('/RRULE:.*FREQ=' . $rrule['FREQ'] . '/', $ics, "Export Recurrence Frequence");
$this->assertMatchesRegularExpression('/RRULE:.*INTERVAL=' . $rrule['INTERVAL'] . '/', $ics, "Export Recurrence Interval");
$this->assertMatchesRegularExpression('/RRULE:.*UNTIL=20140718T215959Z/', $ics, "Export Recurrence End date");
$this->assertMatchesRegularExpression('/RRULE:.*BYDAY=' . $rrule['BYDAY'] . '/', $ics, "Export Recurrence BYDAY");
$this->assertMatchesRegularExpression('/EXDATE.*:20131218/', $ics, "Export Recurrence EXDATE");
$this->assertStringContainsString('BEGIN:VALARM', $ics, "Export VALARM");
$this->assertStringContainsString('TRIGGER;RELATED=END:-PT12H', $ics, "Export Alarm trigger");
$this->assertMatchesRegularExpression('/ATTACH.*;VALUE=BINARY/', $ics, "Embed attachment");
$this->assertMatchesRegularExpression('/ATTACH.*;ENCODING=BASE64/', $ics, "Attachment B64 encoding");
$this->assertMatchesRegularExpression('!ATTACH.*;FMTTYPE=text/html!', $ics, "Attachment mimetype");
$this->assertMatchesRegularExpression('!ATTACH.*;X-LABEL=calendar.html!', $ics, "Attachment filename with X-LABEL");
$this->assertStringContainsString('END:VEVENT', $ics, "VEVENT encapsulation END");
$this->assertStringContainsString('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
}
/**
* @depends test_extended
* @depends test_export
*/
public function test_export_multiple()
{
$ical = new libcalendaring_vcalendar();
$events = array_merge(
$ical->import_from_file(__DIR__ . '/resources/snd.ics', 'UTF-8'),
$ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8')
);
$num = count($events);
$ics = $ical->export($events, null, false);
$this->assertStringContainsString('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
$this->assertStringContainsString('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
$this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
$this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END");
}
/**
* @depends test_export
*/
public function test_export_recurrence_exceptions()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
// add exceptions
$event = $events[0];
unset($event['recurrence']['EXCEPTIONS']);
$exception1 = $event;
$exception1['start'] = clone $event['start'];
$exception1['start']->setDate(2013, 8, 14);
$exception1['end'] = clone $event['end'];
$exception1['end']->setDate(2013, 8, 14);
$exception2 = $event;
$exception2['start'] = clone $event['start'];
$exception2['start']->setDate(2013, 11, 13);
$exception2['end'] = clone $event['end'];
$exception2['end']->setDate(2013, 11, 13);
$exception2['title'] = 'Recurring Exception';
$events[0]['recurrence']['EXCEPTIONS'] = [$exception1, $exception2];
$ics = $ical->export($events, null, false);
$num = count($events[0]['recurrence']['EXCEPTIONS']) + 1;
$this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
$this->assertEquals($num, substr_count($ics, 'UID:' . $event['uid']), "Recurrence Exceptions with same UID");
$this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END");
$this->assertStringContainsString('RECURRENCE-ID;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date");
$this->assertStringContainsString('RECURRENCE-ID;TZID=Europe/Zurich:20131113', $ics, "Recurrence-ID (2) being the exception date");
$this->assertStringContainsString('SUMMARY:' . $exception2['title'], $ics, "Exception title");
}
public function test_export_valid_rrules()
{
$event = [
'uid' => '1234567890',
'start' => new DateTime('now'),
'end' => new DateTime('now + 30min'),
'title' => 'test_export_valid_rrules',
'recurrence' => [
'FREQ' => 'DAILY',
'COUNT' => 5,
'EXDATE' => [],
'RDATE' => [],
],
];
$ical = new libcalendaring_vcalendar();
$ics = $ical->export([$event], null, false, null, false);
$this->assertStringNotContainsString('EXDATE=', $ics);
$this->assertStringNotContainsString('RDATE=', $ics);
}
/**
*
*/
public function test_export_rdate()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$ics = $ical->export($events, null, false);
$this->assertStringContainsString('RDATE:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values");
}
/**
* @depends test_export
*/
public function test_export_direct()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
$num = count($events);
ob_start();
$return = $ical->export($events, null, true);
$output = ob_get_contents();
ob_end_clean();
$this->assertTrue($return, "Return true on successful writing");
$this->assertStringContainsString('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN");
$this->assertStringContainsString('END:VCALENDAR', $output, "VCALENDAR encapsulation END");
$this->assertEquals($num, substr_count($output, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
$this->assertEquals($num, substr_count($output, 'END:VEVENT'), "VEVENT encapsulation END");
}
public function test_datetime()
{
$ical = new libcalendaring_vcalendar();
$cal = new \Sabre\VObject\Component\VCalendar();
$localtime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
$localdate = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
$utctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
$asutctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
$this->assertStringContainsString('TZID=Europe/Berlin', $localtime->serialize());
$this->assertStringContainsString('VALUE=DATE', $localdate->serialize());
$this->assertStringContainsString('20130901T120000Z', $utctime->serialize());
$this->assertStringContainsString('20130901T100000Z', $asutctime->serialize());
}
public function test_get_vtimezone()
{
$vtz = libcalendaring_vcalendar::get_vtimezone('Europe/Berlin', strtotime('2014-08-22T15:00:00+02:00'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz, "VTIMEZONE is a Component object");
$this->assertEquals('Europe/Berlin', $vtz->TZID);
$this->assertEquals('4', $vtz->{'X-MICROSOFT-CDO-TZID'});
// check for transition to daylight saving time which is BEFORE the given date
$dst = array_first($vtz->select('DAYLIGHT'));
$this->assertEquals('DAYLIGHT', $dst->name);
$this->assertEquals('20140330T010000', $dst->DTSTART);
$this->assertEquals('+0100', $dst->TZOFFSETFROM);
$this->assertEquals('+0200', $dst->TZOFFSETTO);
$this->assertEquals('CEST', $dst->TZNAME);
// check (last) transition to standard time which is AFTER the given date
$std = $vtz->select('STANDARD');
$std = end($std);
$this->assertEquals('STANDARD', $std->name);
$this->assertEquals('20141026T010000', $std->DTSTART);
$this->assertEquals('+0200', $std->TZOFFSETFROM);
$this->assertEquals('+0100', $std->TZOFFSETTO);
$this->assertEquals('CET', $std->TZNAME);
// unknown timezone
$vtz = libcalendaring_vcalendar::get_vtimezone('America/Foo Bar');
$this->assertEquals(false, $vtz);
// invalid input data
$vtz = libcalendaring_vcalendar::get_vtimezone(new DateTime());
$this->assertEquals(false, $vtz);
// DateTimezone as input data
$vtz = libcalendaring_vcalendar::get_vtimezone(new DateTimezone('Pacific/Chatham'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
$this->assertStringContainsString('TZOFFSETFROM:+1245', $vtz->serialize());
$this->assertStringContainsString('TZOFFSETTO:+1345', $vtz->serialize());
// Making sure VTIMEZOONE contains at least one STANDARD/DAYLIGHT component
// when there's only one transition in specified time period (T5626)
$vtz = libcalendaring_vcalendar::get_vtimezone('Europe/Warsaw', strtotime('2023-10-04T15:00:00'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
$dst = $vtz->select('DAYLIGHT');
$std = $vtz->select('STANDARD');
$this->assertCount(1, $dst);
$this->assertCount(2, $std);
$std = end($std);
$this->assertSame('STANDARD', $std->name);
$this->assertSame('20231029T010000', (string) $std->DTSTART);
$this->assertSame('+0200', (string) $std->TZOFFSETFROM);
$this->assertSame('+0100', (string) $std->TZOFFSETTO);
$this->assertSame('CET', (string) $std->TZNAME);
}
public function get_attachment_data($id, $event)
{
return $this->attachment_data;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Apr 6, 2:14 AM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831934
Default Alt Text
(88 KB)
Attached To
Mode
rRPK roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline