Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F174618
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
77 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 37f62241..195f99da 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -1,1349 +1,1362 @@
<?php
/**
* iCalendar functions for the libcalendaring plugin
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, 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;
// load Sabre\VObject classes
if (!class_exists('\Sabre\VObject\Reader')) {
require_once __DIR__ . '/lib/Sabre/VObject/includes.php';
}
/**
* Class to parse and build vCalendar (iCalendar) files
*
* Uses the SabreTooth VObject library, version 2.1.
*
* Download from https://github.com/fruux/sabre-vobject/archive/2.1.0.zip
* and place the lib files in this plugin's lib directory
*
*/
class libvcalendar implements Iterator
{
private $timezone;
private $attach_uri = null;
private $prodid = '-//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE',
'cutype' => 'CUTYPE', 'rsvp' => 'RSVP', 'delegated-from' => 'DELEGATED-FROM', 'delegated-to' => 'DELEGATED-TO');
private $iteratorkey = 0;
private $charset;
private $forward_exceptions;
private $vhead;
private $fp;
private $vtimezones = array();
public $method;
public $agent = '';
public $objects = array();
public $freebusy = array();
/**
* Default constructor
*/
function __construct($tz = null)
{
$this->timezone = $tz;
$this->prodid = '-//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//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 = array();
$this->freebusy = array();
$this->vtimezones = array();
$this->iteratorkey = 0;
if ($this->fp) {
fclose($this->fp);
$this->fp = null;
}
}
/**
* Import events from iCalendar format
*
* @param string vCalendar input
* @param string Input charset (from envelope)
* @param boolean 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(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "iCal data parse error: " . $e->getMessage()),
true, false);
}
}
return array();
}
/**
* Read iCalendar events from a file
*
* @param string File path to read from
* @param string Input charset (from envelope)
* @param boolean 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 File path to read from
* @param string Input charset (from envelope)
* @param boolean True if parsing exceptions should be forwarded to the caller
* @return boolean 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 = array();
$this->freebusy = array();
}
$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(array(
'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 object Sabre\VObject\Component to read from
* @return array List of events extracted from the file
*/
public function import_from_vobject($vobject)
{
$seen = array();
+ $exceptions = array();
if ($vobject->name == 'VCALENDAR') {
$this->method = strval($vobject->METHOD);
$this->agent = strval($vobject->PRODID);
foreach ($vobject->getBaseComponents() ?: $vobject->getComponents() as $ve) {
if ($ve->name == 'VEVENT' || $ve->name == 'VTODO') {
// convert to hash array representation
$object = $this->_to_array($ve);
- if (!$seen[$object['uid']]++) {
- // parse recurrence exceptions
- if ($object['recurrence']) {
- $object['recurrence']['EXCEPTIONS'] = array();
- foreach ($vobject->children as $component) {
- if ($component->name == 'VEVENT' && isset($component->{'RECURRENCE-ID'})) {
- try {
- $object['recurrence']['EXCEPTIONS'][] = $this->_to_array($component);
- }
- catch (Exception $e) {
- console("iCal data parse error: " . $e->getMessage(), $component->serialize());
- }
- }
- }
- }
-
+ // temporarily store this as exception
+ if ($object['recurrence_date']) {
+ $exceptions[] = $object;
+ }
+ else if (!$seen[$object['uid']]++) {
$this->objects[] = $object;
}
}
else if ($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 (!$seen[$uid]++) {
+ $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 = array();
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 object Vevent object to convert
* @return array Hash array with object properties
*/
private function _to_array($ve)
{
$event = array(
'uid' => self::convert_string($ve->UID),
'title' => self::convert_string($ve->SUMMARY),
'_type' => $ve->name == 'VTODO' ? 'task' : 'event',
// set defaults
'priority' => 0,
'attendees' => array(),
'x-custom' => array(),
);
// Catch possible exceptions when date is invalid (Bug #2144)
// We can skip these fields, they aren't critical
foreach (array('CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed') as $attr => $field) {
try {
if (!$event[$field] && $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;
switch ($prop->name) {
case 'DTSTART':
case 'DTEND':
case 'DUE':
$propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'DUE' => 'due');
$event[$propmap[$prop->name]] = self::convert_datetime($prop);
break;
case 'TRANSP':
$event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy';
break;
case 'STATUS':
if ($prop->value == 'TENTATIVE')
$event['free_busy'] = 'tentative';
else if ($prop->value == 'CANCELLED')
$event['cancelled'] = true;
else if ($prop->value == 'COMPLETED')
$event['complete'] = 100;
$event['status'] = strval($prop->value);
break;
case 'PRIORITY':
if (is_numeric($prop->value))
$event['priority'] = $prop->value;
break;
case 'RRULE':
$params = is_array($event['recurrence']) ? $event['recurrence'] : array();
// parse recurrence rule attributes
foreach (explode(';', $prop->value) as $par) {
list($k, $v) = explode('=', $par);
$params[$k] = $v;
}
if ($params['UNTIL'])
$params['UNTIL'] = date_create($params['UNTIL']);
if (!$params['INTERVAL'])
$params['INTERVAL'] = 1;
$event['recurrence'] = array_filter($params);
break;
case 'EXDATE':
if (!empty($prop->value))
$event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], self::convert_datetime($prop, true));
break;
case 'RDATE':
if (!empty($prop->value))
$event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], self::convert_datetime($prop, true));
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'] = $prop->value;
}
break;
case 'SEQUENCE':
$event['sequence'] = intval($prop->value);
break;
case 'PERCENT-COMPLETE':
$event['complete'] = intval($prop->value);
break;
case 'LOCATION':
case 'DESCRIPTION':
case 'URL':
case 'COMMENT':
$event[strtolower($prop->name)] = self::convert_string($prop);
break;
case 'CATEGORY':
case 'CATEGORIES':
$event['categories'] = array_merge((array)$event['categories'], $prop->getParts());
break;
case 'CLASS':
case 'X-CALENDARSERVER-ACCESS':
$event['sensitivity'] = strtolower($prop->value);
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
if ($prop->value == 'OOF')
$event['free_busy'] = 'outofoffice';
else if (in_array($prop->value, array('FREE', 'BUSY', 'TENTATIVE')))
$event['free_busy'] = strtolower($prop->value);
break;
case 'ATTENDEE':
case 'ORGANIZER':
$params = array('rsvp' => false);
foreach ($prop->parameters as $param) {
switch ($param->name) {
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
default: $params[$param->name] = $param->value; break;
}
}
$attendee = self::map_keys($params, array_flip($this->attendee_keymap));
$attendee['email'] = preg_replace('/^mailto:/i', '', $prop->value);
if ($prop->name == 'ORGANIZER') {
$attendee['role'] = 'ORGANIZER';
$attendee['status'] = 'ACCEPTED';
$event['organizer'] = $attendee;
}
else if ($attendee['email'] != $event['organizer']['email']) {
$event['attendees'][] = $attendee;
}
break;
case 'ATTACH':
$params = self::parameters_array($prop);
if (substr($prop->value, 0, 4) == 'http' && !strpos($prop->value, ':attachment:')) {
$event['links'][] = $prop->value;
}
else if (strlen($prop->value) && strtoupper($params['VALUE']) == 'BINARY') {
$attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name'));
$attachment['data'] = base64_decode($prop->value);
$attachment['size'] = strlen($attachment['data']);
$event['attachments'][] = $attachment;
}
break;
default:
if (substr($prop->name, 0, 2) == 'X-')
$event['x-custom'][] = array($prop->name, strval($prop->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') {
// check for all-day dates
if ($event['start']->_dateonly) {
$event['allday'] = true;
}
// all-day events may lack the DTEND property
if ($event['allday'] && empty($event['end'])) {
$event['end'] = clone $event['start'];
}
// shift end-date by one day (except Thunderbird)
else if ($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 = array();
foreach ($valarm->children as $prop) {
switch ($prop->name) {
case 'TRIGGER':
foreach ($prop->parameters as $param) {
if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') {
$trigger = '@' . $prop->getDateTime()->format('U');
$alarm['trigger'] = $prop->getDateTime();
}
}
if (!$trigger && ($values = libcalendaring::parse_alarm_value($prop->value))) {
$trigger = $values[2];
}
if (!$alarm['trigger']) {
$alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $prop->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($prop->value);
break;
case 'SUMMARY':
case 'DESCRIPTION':
case 'DURATION':
$alarm[strtolower($prop->name)] = self::convert_string($prop);
break;
case 'REPEAT':
$alarm['repeat'] = intval($prop->value);
break;
case 'ATTENDEE':
$alarm['attendees'][] = preg_replace('/^mailto:/i', '', $prop->value);
break;
case 'ATTACH':
$params = self::parameters_array($prop);
if (strlen($prop->value) && (preg_match('/^[a-z]+:/', $prop->value) || strtoupper($params['VALUE']) == 'URI')) {
// we only support URI-type of attachments here
$alarm['uri'] = $prop->value;
}
break;
}
}
if ($action != 'NONE') {
if ($trigger && !$event['alarms']) // store first alarm in legacy property
$event['alarms'] = $trigger . ':' . $action;
if ($alarm['trigger'])
$event['valarms'][] = $alarm;
}
}
// assign current timezone to event start/end
if ($event['start'] instanceof DateTime) {
if ($this->timezone)
$event['start']->setTimezone($this->timezone);
}
else {
unset($event['start']);
}
if ($event['end'] instanceof DateTime) {
if ($this->timezone)
$event['end']->setTimezone($this->timezone);
}
else {
unset($event['end']);
}
// 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;
}
/**
* Parse the given vfreebusy component into an array representation
*/
private function _parse_freebusy($ve)
{
$this->freebusy = array('_type' => 'freebusy', 'periods' => array());
$seen = array();
foreach ($ve->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
switch ($prop->name) {
case 'CREATED':
case 'LAST-MODIFIED':
case 'DTSTAMP':
case 'DTSTART':
case 'DTEND':
$propmap = array('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', '', $prop->value);
break;
case 'FREEBUSY':
// The freebusy component can hold more than 1 value, separated by commas.
$periods = explode(',', $prop->value);
$fbtype = strval($prop['FBTYPE']) ?: 'BUSY';
// skip dupes
if ($seen[$prop->value.':'.$fbtype]++)
continue;
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 ($busyEnd && $busyEnd > $busyStart)
$this->freebusy['periods'][] = array($busyStart, $busyEnd, $fbtype);
}
break;
case 'COMMENT':
$this->freebusy['comment'] = $prop->value;
}
}
return $this->freebusy;
}
/**
*
*/
public static function convert_string($prop)
{
return str_replace('\,', ',', strval($prop->value));
}
/**
* 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 ? array() : null;
}
else if ($prop instanceof VObject\Property\MultiDateTime) {
$dt = array();
$dateonly = ($prop->getDateType() & VObject\Property\DateTime::DATE);
foreach ($prop->getDateTimes() as $item) {
$item->_dateonly = $dateonly;
$dt[] = $item;
}
}
else if ($prop instanceof VObject\Property\DateTime) {
$dt = $prop->getDateTime();
if ($prop->getDateType() & VObject\Property\DateTime::DATE) {
$dt->_dateonly = true;
}
}
else if ($prop instanceof VObject\Property && ($prop['VALUE'] == 'DATE' || $prop['VALUE'] == 'DATE-TIME')) {
try {
list($type, $dt) = VObject\Property\DateTime::parseData($prop->value, $prop);
$dt->_dateonly = ($type & VObject\Property\DateTime::DATE);
}
catch (Exception $e) {
// ignore date parse errors
}
}
else if ($prop instanceof VObject\Property && $prop['VALUE'] == 'PERIOD') {
$dt = array();
foreach(explode(',', $prop->value) as $val) {
try {
list($start, $end) = explode('/', $val);
list($type, $item) = VObject\Property\DateTime::parseData($start, $prop);
$item->_dateonly = ($type & VObject\Property\DateTime::DATE);
$dt[] = $item;
}
catch (Exception $e) {
// ignore single date parse errors
}
}
}
else if ($prop instanceof DateTime) {
$dt = $prop;
}
// force return value to array if requested
if ($as_array && !is_array($dt)) {
$dt = empty($dt) ? array() : array($dt);
}
return $dt;
}
/**
* Create a Sabre\VObject\Property instance from a PHP DateTime object
*
* @param string Property name
* @param object DateTime
*/
public function datetime_prop($name, $dt, $utc = false, $dateonly = null)
{
$is_utc = $utc || (($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z')));
$is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly;
$vdt = new VObject\Property\DateTime($name);
$vdt->setDateTime($dt, $is_dateonly ? VObject\Property\DateTime::DATE :
($is_utc ? VObject\Property\DateTime::UTC : VObject\Property\DateTime::LOCALTZ));
// register timezone for VTIMEZONE block
if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) {
$ts = $dt->format('U');
if (is_array($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] = array($ts, $ts);
}
}
return $vdt;
}
/**
* Copy values from one hash array to another using a key-map
*/
public static function map_keys($values, $map)
{
$out = array();
foreach ($map as $from => $to) {
if (isset($values[$from]))
$out[$to] = is_array($values[$from]) ? join(',', $values[$from]) : $values[$from];
}
return $out;
}
/**
*
*/
private static function parameters_array($prop)
{
$params = array();
foreach ($prop->parameters as $param) {
$params[strtoupper($param->name)] = $param->value;
}
return $params;
}
/**
* Export events to iCalendar format
*
* @param array Events as array
* @param string VCalendar method to advertise
* @param boolean Directly send data to stdout instead of returning
* @param callable Callback function to fetch attachment contents, false if no attachment export
* @param boolean Add VTIMEZONE block with timezone definitions for the included events
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
*/
public function export($objects, $method = null, $write = false, $get_attachment = false, $with_timezones = true)
{
$this->method = $method;
// encapsulate in VCALENDAR container
$vcal = VObject\Component::create('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]);
if (empty($vt)) {
continue; // no timezone information found
}
if ($vcal) {
$vcal->add($vt);
}
else {
echo $vt->serialize();
}
}
}
if ($write) {
echo "END:VCALENDAR\r\n";
return true;
}
else {
return $vcal->serialize();
}
}
/**
* Build a valid iCal format block from the given event
*
* @param array Hash array with event/task properties from libkolab
* @param object VCalendar object to append event to or false for directly sending data to stdout
* @param callable Callback function to fetch attachment contents, false if no attachment export
* @param object RECURRENCE-ID property when serializing a recurrence exception
*/
private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null)
{
$type = $event['_type'] ?: 'event';
$ve = VObject\Component::create($this->type_component_map[$type]);
$ve->add('UID', $event['uid']);
// set DTSTAMP according to RFC 5545, 3.8.7.2.
$dtstamp = !empty($event['changed']) && !empty($this->method) ? $event['changed'] : new DateTime();
$ve->add($this->datetime_prop('DTSTAMP', $dtstamp, true));
// all-day events end the next day
if ($event['allday'] && !empty($event['end'])) {
$event['end'] = clone $event['end'];
$event['end']->add(new \DateInterval('P1D'));
$event['end']->_dateonly = true;
}
if (!empty($event['created']))
$ve->add($this->datetime_prop('CREATED', $event['created'], true));
if (!empty($event['changed']))
$ve->add($this->datetime_prop('LAST-MODIFIED', $event['changed'], true));
if (!empty($event['start']))
$ve->add($this->datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
if (!empty($event['end']))
$ve->add($this->datetime_prop('DTEND', $event['end'], false, (bool)$event['allday']));
if (!empty($event['due']))
$ve->add($this->datetime_prop('DUE', $event['due'], false));
// we're exporting a recurrence instance only
if (!$recurrence_id && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) {
$recurrence_id = $this->datetime_prop('RECURRENCE-ID', $event['recurrence_date'], false, (bool)$event['allday']);
if ($event['thisandfuture'])
$recurrence_id->add('RANGE', 'THISANDFUTURE');
}
if ($recurrence_id)
$ve->add($recurrence_id);
$ve->add('SUMMARY', $event['title']);
if ($event['location'])
$ve->add($this->is_apple() ? new vobject_location_property('LOCATION', $event['location']) : new VObject\Property('LOCATION', $event['location']));
if ($event['description'])
$ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
if (isset($event['sequence']))
$ve->add('SEQUENCE', $event['sequence']);
if ($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 ($event['recurrence']['FREQ']) {
$ve->add('RRULE', libcalendaring::to_rrule($event['recurrence']));
}
// add EXDATEs each one per line (for Thunderbird Lightning)
if (is_array($exdates)) {
foreach ($exdates as $ex) {
if ($ex instanceof \DateTime) {
$exd = clone $event['start'];
$exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
$exd->setTimeZone(new \DateTimeZone('UTC'));
$ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
}
}
}
// add RDATEs
if (is_array($rdates) && !empty($rdates)) {
$sample = $this->datetime_prop('RDATE', $rdates[0]);
$rdprop = new VObject\Property\MultiDateTime('RDATE', null);
$rdprop->setDateTimes($rdates, $sample->getDateType());
$ve->add($rdprop);
}
}
if ($event['categories']) {
$cat = VObject\Property::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 ($event['priority'])
$ve->add('PRIORITY', $event['priority']);
if ($event['cancelled'])
$ve->add('STATUS', 'CANCELLED');
else if ($event['free_busy'] == 'tentative')
$ve->add('STATUS', 'TENTATIVE');
else if ($event['complete'] == 100)
$ve->add('STATUS', 'COMPLETED');
else if (!empty($event['status']))
$ve->add('STATUS', $event['status']);
if (!empty($event['sensitivity']))
$ve->add('CLASS', strtoupper($event['sensitivity']));
if (!empty($event['complete'])) {
$ve->add('PERCENT-COMPLETE', intval($event['complete']));
// Apple iCal required the COMPLETED date to be set in order to consider a task complete
if ($event['complete'] == 100)
$ve->add($this->datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
}
if ($event['valarms']) {
foreach ($event['valarms'] as $alarm) {
$va = VObject\Component::create('VALARM');
$va->action = $alarm['action'];
if ($alarm['trigger'] instanceof DateTime) {
$va->add($this->datetime_prop('TRIGGER', $alarm['trigger'], true));
}
else {
$va->add('TRIGGER', $alarm['trigger']);
}
if ($alarm['action'] == 'EMAIL') {
foreach ((array)$alarm['attendees'] as $attendee) {
$va->add('ATTENDEE', 'mailto:' . $attendee);
}
}
if ($alarm['description']) {
$va->add('DESCRIPTION', $alarm['description'] ?: $event['title']);
}
if ($alarm['summary']) {
$va->add('SUMMARY', $alarm['summary']);
}
if ($alarm['duration']) {
$va->add('DURATION', $alarm['duration']);
$va->add('REPEAT', intval($alarm['repeat']));
}
if ($alarm['uri']) {
$va->add('ATTACH', $alarm['uri'], array('VALUE' => 'URI'));
}
$ve->add($va);
}
}
// legacy support
else if ($event['alarms']) {
$va = VObject\Component::create('VALARM');
list($trigger, $va->action) = explode(':', $event['alarms']);
$val = libcalendaring::parse_alarm_value($trigger);
if ($val[3])
$va->add('TRIGGER', $val[3]);
else if ($val[0] instanceof DateTime)
$va->add($this->datetime_prop('TRIGGER', $val[0]));
$ve->add($va);
}
foreach ((array)$event['attendees'] as $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
if (empty($event['organizer']))
$event['organizer'] = $attendee;
}
else if (!empty($attendee['email'])) {
if (isset($attendee['rsvp']))
$attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
$ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap)));
}
}
if ($event['organizer']) {
$ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN')));
}
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'], array('RELTYPE' => 'PARENT'));
}
if ($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 $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)
&& $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'], $event))) {
// embed attachments for iCal
$ve->add('ATTACH',
base64_encode($data),
array_filter(array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name'])));
unset($data); // attempt to free memory
}
// list attachments as absolute URIs
else if (!empty($this->attach_uri)) {
$ve->add('ATTACH',
strtr($this->attach_uri, array(
'{{id}}' => urlencode($attach['id']),
'{{name}}' => urlencode($attach['name']),
'{{mimetype}}' => urlencode($attach['mimetype']),
)),
array('FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI'));
}
}
}
foreach ((array)$event['links'] as $uri) {
$ve->add('ATTACH', $uri);
}
// add custom properties
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 (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) {
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
$exdate = $ex['recurrence_date'] ?: $ex['start'];
$recurrence_id = $this->datetime_prop('RECURRENCE-ID', $exdate, false, (bool)$event['allday']);
if ($ex['thisandfuture'])
$recurrence_id->add('RANGE', 'THISANDFUTURE');
$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 Timezone ID as used in PHP's Date functions
* @param integer Unix timestamp with first date/time in this timezone
* @param integer Unix timestap with last date/time in this timezone
*
* @return mixed A Sabre\VObject\Component object representing a VTIMEZONE definition
* or false if no timezone information is available
*/
public static function get_vtimezone($tzid, $from = 0, $to = 0)
{
if (!$from) $from = time();
if (!$to) $to = $from;
if (is_string($tzid)) {
try {
$tz = new \DateTimeZone($tzid);
}
catch (\Exception $e) {
return false;
}
}
else if (is_a($tzid, '\\DateTimeZone')) {
$tz = $tzid;
}
if (!is_a($tz, '\\DateTimeZone')) {
return false;
}
$year = 86400 * 360;
$transitions = $tz->getTransitions($from - $year, $to + $year);
$vt = new VObject\Component('VTIMEZONE');
$vt->TZID = $tz->getName();
$std = null; $dst = null;
foreach ($transitions as $i => $trans) {
$cmp = null;
if ($i == 0) {
$tzfrom = $trans['offset'] / 3600;
continue;
}
if ($trans['isdst']) {
$t_dst = $trans['ts'];
$dst = new VObject\Component('DAYLIGHT');
$cmp = $dst;
}
else {
$t_std = $trans['ts'];
$std = new VObject\Component('STANDARD');
$cmp = $std;
}
if ($cmp) {
$dt = new DateTime($trans['time']);
$offset = $trans['offset'] / 3600;
$cmp->DTSTART = $dt->format('Ymd\THis');
$cmp->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
$cmp->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '', 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;
}
/*** Implement PHP 5 Iterator interface to make foreach work ***/
function current()
{
return $this->objects[$this->iteratorkey];
}
function key()
{
return $this->iteratorkey;
}
function next()
{
$this->iteratorkey++;
// read next chunk if we're reading from a file
if (!$this->objects[$this->iteratorkey] && $this->fp) {
$this->_parse_next(true);
}
return $this->valid();
}
function rewind()
{
$this->iteratorkey = 0;
}
function valid()
{
return !empty($this->objects[$this->iteratorkey]);
}
}
/**
* Override Sabre\VObject\Property that quotes commas in the location property
* because Apple clients treat that property as list.
*/
class vobject_location_property extends VObject\Property
{
/**
* Turns the object back into a serialized blob.
*
* @return string
*/
public function serialize()
{
$str = $this->name;
foreach ($this->parameters as $param) {
$str.=';' . $param->serialize();
}
$src = array(
'\\',
"\n",
',',
);
$out = array(
'\\\\',
'\n',
'\,',
);
$str.=':' . str_replace($src, $out, $this->value);
$out = '';
while (strlen($str) > 0) {
if (strlen($str) > 75) {
$out.= mb_strcut($str, 0, 75, 'utf-8') . "\r\n";
$str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8');
} else {
$out.= $str . "\r\n";
$str = '';
break;
}
}
return $out;
}
}
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index 9180e915..e0e87636 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -1,560 +1,562 @@
<?php
/**
* libcalendaring plugin's iCalendar functions tests
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, 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 libvcalendar_test extends PHPUnit_Framework_TestCase
{
function setUp()
{
require_once __DIR__ . '/../libvcalendar.php';
require_once __DIR__ . '/../libcalendaring.php';
}
/**
* Simple iCal parsing test
*/
function test_import()
{
$ical = new libvcalendar();
$ics = file_get_contents(__DIR__ . '/resources/snd.ics');
$events = $ical->import($ics, 'UTF-8');
$this->assertEquals(1, count($events));
$event = $events[0];
$this->assertInstanceOf('DateTime', $event['created'], "'created' property is DateTime object");
$this->assertInstanceOf('DateTime', $event['changed'], "'changed' property is DateTime object");
$this->assertEquals('UTC', $event['created']->getTimezone()->getName(), "'created' date is in UTC");
$this->assertInstanceOf('DateTime', $event['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTime', $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");
}
/**
* Test parsing from files
*/
function test_import_from_file()
{
$ical = new libvcalendar();
$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)
*/
function test_import_from_file_multiple()
{
$ical = new libvcalendar();
$ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$events = array();
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']);
}
function test_invalid_dates()
{
$ical = new libvcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/invalid-dates.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(1, count($events), "Import event data");
$this->assertFalse(array_key_exists('created', $event), "No created date field");
$this->assertFalse(array_key_exists('changed', $event), "No changed date field");
}
function test_invalid_vevent()
{
$this->setExpectedException('\Sabre\VObject\ParseException');
$ical = new libvcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/invalid-event.ics', 'UTF-8', true);
}
/**
* Test some extended ical properties such as attendees, recurrence rules, alarms and attachments
- *
- * @depends test_import_from_file
*/
function test_extended()
{
$ical = new libvcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('REQUEST', $ical->method, "iTip method");
// attendees
$this->assertEquals(2, 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->assertTrue($attendee['rsvp'], 'Attendee 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->assertContains('<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('DateTime', $rrule['UNTIL'], "Recurrence end date");
$this->assertEquals(2, count($rrule['EXDATE']), "Recurrence EXDATEs");
$this->assertInstanceOf('DateTime', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime");
// categories, class
$this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories");
$this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential");
- // parse a reccuence chain instance
+ // 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('DateTime', $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");
}
/**
*
*/
function test_alarms()
{
$ical = new libvcalendar();
$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)");
// 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->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text");
$this->assertEquals(2, 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");
// test alarms export
$ics = $ical->export(array($event));
$this->assertContains('ACTION:DISPLAY', $ics, "Display alarm block");
$this->assertContains('ACTION:EMAIL', $ics, "Email alarm block");
$this->assertContains('DESCRIPTION:This is the first event reminder', $ics, "Alarm description");
$this->assertContains('SUMMARY:This is the reminder message', $ics, "Email alarm summary");
$this->assertContains('ATTENDEE:mailto:reminder-recipient@example.org', $ics, "Email alarm recipient");
}
/**
* @depends test_import_from_file
*/
function test_attachment()
{
$ical = new libvcalendar();
$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
*/
function test_apple_alarms()
{
$ical = new libvcalendar();
$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)");
}
/**
*
*/
function test_escaped_values()
{
$ical = new libvcalendar();
$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->assertContains('ATTENDEE;CN="Kolab, Thomas";PARTSTAT=', $ics, "Quoted attendee parameters");
}
/**
* Parse RDATE properties (#2885)
*/
function test_rdate()
{
$ical = new libvcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(9, count($event['recurrence']['RDATE']));
$this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]);
}
/**
* @depends test_import
*/
function test_freebusy()
{
$ical = new libvcalendar();
$ical->import_from_file(__DIR__ . '/resources/freebusy.ifb', 'UTF-8');
$freebusy = $ical->freebusy;
$this->assertInstanceOf('DateTime', $freebusy['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTime', $freebusy['end'], "'end' property is DateTime object");
$this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined");
$this->assertEquals(9, count($ical->get_busy_periods()), "Number of busy periods found");
}
/**
* @depends test_import
*/
function test_freebusy_dummy()
{
$ical = new libvcalendar();
$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->assertContains('dummy', $freebusy['comment'], "Parse comment");
}
function test_vtodo()
{
$ical = new libvcalendar();
$tasks = $ical->import_from_file(__DIR__ . '/resources/vtodo.ics', 'UTF-8', true);
$task = $tasks[0];
$this->assertInstanceOf('DateTime', $task['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTime', $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");
}
/**
* Test for iCal export from internal hash array representation
*
*
*/
function test_export()
{
$ical = new libvcalendar();
$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 libvcalendar*';
$event['start']->setTimezone(new DateTimezone('Europe/Berlin'));
$event['end']->setTimezone(new DateTimezone('Europe/Berlin'));
$ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'), true);
$this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
$this->assertContains('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN");
$this->assertContains('TZID:Europe/Berlin', $ics, "Timezone ID");
$this->assertContains('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM");
$this->assertContains('TZOFFSETTO:+0200', $ics, "Timzone transition TO");
$this->assertContains('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
$this->assertContains('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
$this->assertContains('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
$this->assertContains('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number");
$this->assertContains('CLASS:CONFIDENTIAL', $ics, "Sensitivity => Class");
$this->assertContains('DESCRIPTION:*Exported by', $ics, "Export Description");
$this->assertContains('ORGANIZER;CN=Rolf Test:mailto:rolf@', $ics, "Export organizer");
$this->assertRegExp('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/', $ics, "Export Attendee ROLE");
$this->assertRegExp('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/', $ics, "Export Attendee Status");
$this->assertRegExp('/ATTENDEE.*;RSVP=TRUE/', $ics, "Export Attendee RSVP");
$this->assertRegExp('/ATTENDEE.*:mailto:rolf2@/', $ics, "Export Attendee mailto:");
$rrule = $event['recurrence'];
$this->assertRegExp('/RRULE:.*FREQ='.$rrule['FREQ'].'/', $ics, "Export Recurrence Frequence");
$this->assertRegExp('/RRULE:.*INTERVAL='.$rrule['INTERVAL'].'/', $ics, "Export Recurrence Interval");
$this->assertRegExp('/RRULE:.*UNTIL=20140718T215959Z/', $ics, "Export Recurrence End date");
$this->assertRegExp('/RRULE:.*BYDAY='.$rrule['BYDAY'].'/', $ics, "Export Recurrence BYDAY");
$this->assertRegExp('/EXDATE.*:20131218/', $ics, "Export Recurrence EXDATE");
$this->assertContains('BEGIN:VALARM', $ics, "Export VALARM");
$this->assertContains('TRIGGER:-PT12H', $ics, "Export Alarm trigger");
$this->assertRegExp('/ATTACH.*;VALUE=BINARY/', $ics, "Embed attachment");
$this->assertRegExp('/ATTACH.*;ENCODING=BASE64/', $ics, "Attachment B64 encoding");
$this->assertRegExp('!ATTACH.*;FMTTYPE=text/html!', $ics, "Attachment mimetype");
$this->assertRegExp('!ATTACH.*;X-LABEL=calendar.html!', $ics, "Attachment filename with X-LABEL");
$this->assertContains('END:VEVENT', $ics, "VEVENT encapsulation END");
$this->assertContains('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
}
/**
* @depends test_extended
* @depends test_export
*/
function test_export_multiple()
{
$ical = new libvcalendar();
$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->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
$this->assertContains('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
*/
function test_export_recurrence_exceptions()
{
$ical = new libvcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
// add exceptions
$event = $events[0];
$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'] = array($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->assertContains('RECURRENCE-ID;VALUE=DATE-TIME;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date");
$this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME;TZID=Europe/Zurich:20131113', $ics, "Recurrence-ID (2) being the exception date");
$this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
}
function test_export_valid_rrules()
{
$event = array(
'uid' => '1234567890',
'start' => new DateTime('now'),
'end' => new DateTime('now + 30min'),
'title' => 'test_export_valid_rrules',
'recurrence' => array(
'FREQ' => 'DAILY',
'COUNT' => 5,
'EXDATE' => array(),
'RDATE' => array(),
),
);
$ical = new libvcalendar();
$ics = $ical->export(array($event), null, false, null, false);
$this->assertNotContains('EXDATE=', $ics);
$this->assertNotContains('RDATE=', $ics);
}
/**
*
*/
function test_export_rdate()
{
$ical = new libvcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$ics = $ical->export($events, null, false);
$this->assertContains('RDATE;VALUE=DATE-TIME:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values");
}
/**
* @depends test_export
*/
function test_export_direct()
{
$ical = new libvcalendar();
$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->assertContains('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN");
$this->assertContains('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");
}
function test_datetime()
{
$ical = new libvcalendar();
$localtime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
$localdate = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
$utctime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
$asutctime = $ical->datetime_prop('DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
$this->assertContains('TZID=Europe/Berlin', $localtime->serialize());
$this->assertContains('VALUE=DATE', $localdate->serialize());
$this->assertContains('20130901T120000Z', $utctime->serialize());
$this->assertContains('20130901T100000Z', $asutctime->serialize());
}
function test_get_vtimezone()
{
$vtz = libvcalendar::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 = reset($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 = end($vtz->select('STANDARD'));
$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 = libvcalendar::get_vtimezone('America/Foo Bar');
$this->assertEquals(false, $vtz);
// invalid input data
$vtz = libvcalendar::get_vtimezone(new DateTime());
$this->assertEquals(false, $vtz);
// DateTimezone as input data
$vtz = libvcalendar::get_vtimezone(new DateTimezone('Pacific/Chatham'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
$this->assertContains('TZOFFSETFROM:+1245', $vtz->serialize());
$this->assertContains('TZOFFSETTO:+1345', $vtz->serialize());
}
function get_attachment_data($id, $event)
{
return $this->attachment_data;
}
}
diff --git a/plugins/libcalendaring/tests/resources/recurrence-id.ics b/plugins/libcalendaring/tests/resources/recurrence-id.ics
index 8229da28..af880934 100644
--- a/plugins/libcalendaring/tests/resources/recurrence-id.ics
+++ b/plugins/libcalendaring/tests/resources/recurrence-id.ics
@@ -1,31 +1,39 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C
METHOD:REQUEST
BEGIN:VTIMEZONE
TZID:W. Europe
BEGIN:STANDARD
DTSTART:19501029T020000
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19500326T020000
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID="W. Europe":20140230T150000
DTEND;TZID="W. Europe":20140230T163000
TRANSP:OPAQUE
-RDATE;TZID="W. Europe";VALUE=PERIOD:20140227T140000/20140227T153000
RECURRENCE-ID;RANGE=THISANDFUTURE:20140227T130000Z
SEQUENCE:0
UID:7e93e8e8eef16f28aa33b78cd73613ebff
DTSTAMP:20140120T105609Z
SUMMARY:Invitation with Recurrence-ID
END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20140305T150000
+DTEND;TZID="W. Europe":20140305T163000
+RECURRENCE-ID;TZID="W. Europe":20140305T150000
+SEQUENCE:2
+UID:7e93e8e8eef16f28aa33b78cd73613ebff
+DTSTAMP:20140120T105609Z
+SUMMARY:Invitation with Recurrence-ID #2
+END:VEVENT
END:VCALENDAR
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Jan 18, 4:32 PM (5 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
119976
Default Alt Text
(77 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment