Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2571540
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
202 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 6a02b9e3..379bd945 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1,1532 +1,1551 @@
<?php
/**
* Library providing common functions for calendaring plugins
*
* Provides utility functions for calendar-related modules such as
* - alarms display and dismissal
* - attachment handling
* - recurrence computation and UI elements
* - ical parsing and exporting
* - itip scheduling protocol
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-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/>.
*/
class libcalendaring extends rcube_plugin
{
public $rc;
public $timezone;
public $gmt_offset;
public $dst_active;
public $timezone_offset;
public $ical_parts = array();
public $ical_message;
public $defaults = array(
'calendar_date_format' => "Y-m-d",
'calendar_date_short' => "M-j",
'calendar_date_long' => "F j Y",
'calendar_date_agenda' => "l M-d",
'calendar_time_format' => "H:m",
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'calendar_date_format_sets' => array(
'Y-m-d' => array('d M Y', 'm-d', 'l m-d'),
'Y/m/d' => array('d M Y', 'm/d', 'l m/d'),
'Y.m.d' => array('d M Y', 'm.d', 'l m.d'),
'd-m-Y' => array('d M Y', 'd-m', 'l d-m'),
'd/m/Y' => array('d M Y', 'd/m', 'l d/m'),
'd.m.Y' => array('d M Y', 'd.m', 'l d.m'),
'j.n.Y' => array('d M Y', 'd.m', 'l d.m'),
'm/d/Y' => array('M d Y', 'm/d', 'l m/d'),
),
);
private static $instance;
private $mail_ical_parser;
/**
* Singleton getter to allow direct access from other plugins
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new libcalendaring(rcube::get_instance()->plugins);
self::$instance->init_instance();
}
return self::$instance;
}
/**
* Initializes class properties
*/
public function init_instance()
{
$this->rc = rcube::get_instance();
// set user's timezone
try {
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
$this->timezone = new DateTimeZone('GMT');
}
$now = new DateTime('now', $this->timezone);
$this->gmt_offset = $now->getOffset();
$this->dst_active = $now->format('I');
$this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
$this->add_texts('localization/', false);
}
/**
* Required plugin startup method
*/
public function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
$this->init_instance();
// include client scripts and styles
if ($this->rc->output) {
// add hook to display alarms
$this->add_hook('refresh', array($this, 'refresh'));
$this->register_action('plugin.alarms', array($this, 'alarms_action'));
$this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
}
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
if ($this->rc->output && $this->rc->output->type == 'html') {
$this->rc->output->set_env('libcal_settings', $this->load_settings());
$this->include_script('libcalendaring.js');
$this->include_stylesheet($this->local_skin_path() . '/libcal.css');
$this->add_label(
'itipaccepted', 'itiptentative', 'itipdeclined',
'itipdelegated', 'expandattendeegroup', 'expandattendeegroupnodata',
'statusorganizer', 'statusaccepted', 'statusdeclined',
'statusdelegated', 'statusunknown', 'statusneeds-action',
'statustentative', 'statuscompleted', 'statusin-process',
'delegatedto', 'delegatedfrom', 'showmore'
);
}
if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
}
}
}
/**
* Load iCalendar functions
*/
public static function get_ical()
{
$self = self::get_instance();
require_once __DIR__ . '/libvcalendar.php';
return new libvcalendar();
}
/**
* Load iTip functions
*/
public static function get_itip($domain = 'libcalendaring')
{
$self = self::get_instance();
require_once __DIR__ . '/lib/libcalendaring_itip.php';
return new libcalendaring_itip($self, $domain);
}
/**
* Load recurrence computation engine
*/
public static function get_recurrence()
{
$self = self::get_instance();
require_once __DIR__ . '/lib/libcalendaring_recurrence.php';
return new libcalendaring_recurrence($self);
}
/**
* Shift dates into user's current timezone
*
* @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
* @return object DateTime object in user's timezone
*/
public function adjust_timezone($dt, $dateonly = false)
{
if (is_numeric($dt))
$dt = new DateTime('@'.$dt);
else if (is_string($dt))
$dt = rcube_utils::anytodatetime($dt);
if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
$dt->setTimezone($this->timezone);
}
return $dt;
}
/**
*
*/
public function load_settings()
{
$this->date_format_defaults();
$settings = array();
$keys = array('date_format', 'time_format', 'date_short', 'date_long', 'date_agenda');
foreach ($keys as $key) {
$settings[$key] = (string)$this->rc->config->get('calendar_' . $key, $this->defaults['calendar_' . $key]);
$settings[$key] = self::from_php_date_format($settings[$key]);
}
$settings['dates_long'] = $settings['date_long'];
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['timezone'] = $this->timezone_offset;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
$this->rc->gettext('sunday'), $this->rc->gettext('monday'),
$this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'),
$this->rc->gettext('thursday'), $this->rc->gettext('friday'),
$this->rc->gettext('saturday')
);
$settings['days_short'] = array(
$this->rc->gettext('sun'), $this->rc->gettext('mon'),
$this->rc->gettext('tue'), $this->rc->gettext('wed'),
$this->rc->gettext('thu'), $this->rc->gettext('fri'),
$this->rc->gettext('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
return $settings;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private function date_format_defaults()
{
static $defaults = array();
// nothing to be done
if (isset($defaults['date_format']))
return;
$defaults['date_format'] = $this->rc->config->get('calendar_date_format', $this->rc->config->get('date_format'));
$defaults['time_format'] = $this->rc->config->get('calendar_time_format', $this->rc->config->get('time_format'));
// override defaults
if ($defaults['date_format'])
$this->defaults['calendar_date_format'] = $defaults['date_format'];
if ($defaults['time_format'])
$this->defaults['calendar_time_format'] = $defaults['time_format'];
// derive format variants from basic date format
$format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
$this->defaults['calendar_date_long'] = $format_set[0];
$this->defaults['calendar_date_short'] = $format_set[1];
$this->defaults['calendar_date_agenda'] = $format_set[2];
}
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$fromto = '--';
// handle task objects
if ($event['_type'] == 'task' && is_object($event['due'])) {
$date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
$fromto = $this->rc->format_date($event['due'], $date_format, false);
// add timezone information
if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
// abort if no valid event dates are given
if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
return $fromto;
}
$duration = $event['start']->diff($event['end'])->format('s');
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = $this->rc->format_date($event['start'], $date_format, false);
if (($todate = $this->rc->format_date($event['end'], $date_format, false)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
$fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
' - ' . $this->rc->format_date($event['end'], $time_format);
}
else {
$fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
' - ' . $this->rc->format_date($event['end'], $date_format) . ' ' . $this->rc->format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
/**
* Render HTML form for alarm configuration
*/
public function alarm_select($attrib, $alarm_types, $absolute_time = true)
{
unset($attrib['name']);
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value form-control', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date form-control', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time form-control', 'size' => 6));
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type form-control', 'id' => $attrib['id']));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset form-control'));
$select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related form-control'));
$object_type = $attrib['_type'] ?: 'event';
$select_type->add($this->gettext('none'), '');
foreach ($alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
$select_offset->add($this->gettext('trigger0'), '0');
if ($absolute_time)
$select_offset->add($this->gettext('trigger@'), '@');
$select_related->add($this->gettext('relatedstart'), 'start');
$select_related->add($this->gettext('relatedend' . $object_type), 'end');
// pre-set with default values from user settings
$preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$hidden = array('style' => 'display:none');
return html::span('edit-alarm-set',
$select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
html::span(array('class' => 'edit-alarm-values input-group', 'style' => 'display:none'),
$input_value->show($preset[0]) . ' ' .
$select_offset->show($preset[1]) . ' ' .
$select_related->show() . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
}
/**
* Get a list of email addresses of the given user (from login and identities)
*
* @param string User Email (default to current user)
*
* @return array Email addresses related to the user
*/
public function get_user_emails($user = null)
{
static $_emails = array();
if (empty($user)) {
$user = $this->rc->user->get_username();
}
// return cached result
if (is_array($_emails[$user])) {
return $_emails[$user];
}
$emails = array($user);
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
$emails = array_map('strtolower', $plugin['emails']);
// add all emails from the current user's identities
if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
foreach ($this->rc->user->list_emails() as $identity) {
$emails[] = strtolower($identity['email']);
}
}
$_emails[$user] = array_unique($emails);
return $_emails[$user];
}
/**
* Set the given participant status to the attendee matching the current user's identities
* Unsets 'rsvp' flag too.
*
* @param array &$event Event data
* @param string $status The PARTSTAT value to set
* @param bool $recursive Recurive call
*
* @return mixed Email address of the updated attendee or False if none matching found
*/
public function set_partstat(&$event, $status, $recursive = true)
{
$success = false;
$emails = $this->get_user_emails();
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
unset($event['attendees'][$i]['rsvp']);
$success = $attendee['email'];
}
}
// apply partstat update to each existing exception
if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false);
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
return $success;
}
/********* Alarms handling *********/
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public static function parse_alarm_value($val)
{
if ($val[0] == '@') {
return array(new DateTime($val));
}
else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
if ($m[1] == '')
$m[1] = '+';
foreach ($m2 as $seg) {
$prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
if ($seg[1] > 0) { // ignore zero values
// convert seconds to minutes
if ($seg[2] == 'S') {
$seg[2] = 'M';
$seg[1] = max(1, round($seg[1]/60));
}
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
}
// return zero value nevertheless
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
return false;
}
/**
* Convert the alarms list items to be processed on the client
*/
public static function to_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'] instanceof DateTime) {
$alarm['trigger'] = '@' . $alarm['trigger']->format('U');
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[2];
}
return $alarm;
}, (array)$valarms);
}
/**
* Process the alarms values submitted by the client
*/
public static function from_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'][0] == '@') {
try {
$alarm['trigger'] = new DateTime($alarm['trigger']);
$alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
}
catch (Exception $e) { /* handle this ? */ }
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[3];
}
return $alarm;
}, (array)$valarms);
}
/**
* Render localized text for alarm settings
*/
public static function alarms_text($alarms)
{
if (is_array($alarms) && is_array($alarms[0])) {
$texts = array();
foreach ($alarms as $alarm) {
if ($text = self::alarm_text($alarm))
$texts[] = $text;
}
return join(', ', $texts);
}
else {
return self::alarm_text($alarms);
}
}
/**
* Render localized text for a single alarm property
*/
public static function alarm_text($alarm)
{
if (is_string($alarm)) {
list($trigger, $action) = explode(':', $alarm);
}
else {
$trigger = $alarm['trigger'];
$action = $alarm['action'];
$related = $alarm['related'];
}
$text = '';
$rcube = rcube::get_instance();
switch ($action) {
case 'EMAIL':
$text = $rcube->gettext('libcalendaring.alarmemail');
break;
case 'DISPLAY':
$text = $rcube->gettext('libcalendaring.alarmdisplay');
break;
case 'AUDIO':
$text = $rcube->gettext('libcalendaring.alarmaudio');
break;
}
if ($trigger instanceof DateTime) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($trigger))
));
}
else if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($m[1]))
));
}
else if ($val = self::parse_alarm_value($trigger)) {
$r = strtoupper($related ?: 'start') == 'END' ? 'end' : '';
// TODO: for all-day events say 'on date of event at XX' ?
if ($val[0] == 0) {
$text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r);
}
else {
$label = 'libcalendaring.trigger' . $r . $val[1];
$text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label);
}
}
else {
return false;
}
return $text;
}
/**
* Get the next alarm (time & action) for the given event
*
* @param array Record data
* @return array Hash array with alarm time/type or null if no alarms are configured
*/
public static function get_next_alarm($rec, $type = 'event')
{
- if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
+ if (
+ (empty($rec['valarms']) && empty($rec['alarms']))
+ || !empty($rec['cancelled'])
+ || (!empty($rec['status']) && $rec['status'] == 'CANCELLED')
+ ) {
return null;
+ }
if ($type == 'task') {
$timezone = self::get_instance()->timezone;
- if ($rec['startdate'])
- $rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
- if ($rec['date'])
- $rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
+ if (!empty($rec['startdate'])) {
+ $time = !empty($rec['starttime']) ? $rec['starttime'] : '12:00';
+ $rec['start'] = new DateTime($rec['startdate'] . ' ' . $time, $timezone);
+ }
+ if (!empty($rec['date'])) {
+ $time = !empty($rec['time']) ? $rec['time'] : '12:00';
+ $rec[!empty($rec['start']) ? 'end' : 'start'] = new DateTime($rec['date'] . ' ' . $time, $timezone);
+ }
}
- if (!$rec['end'])
+ if (empty($rec['end'])) {
$rec['end'] = $rec['start'];
+ }
// support legacy format
- if (!$rec['valarms']) {
+ if (empty($rec['valarms'])) {
list($trigger, $action) = explode(':', $rec['alarms'], 2);
if ($alarm = self::parse_alarm_value($trigger)) {
$rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
}
}
- $expires = new DateTime('now - 12 hours');
- $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
+ // alarm ID eq. record ID by default to keep backwards compatibility
+ $alarm_id = isset($rec['id']) ? $rec['id'] : null;
+ $alarm_prop = null;
+ $expires = new DateTime('now - 12 hours');
+ $notify_at = null;
// handle multiple alarms
- $notify_at = null;
foreach ($rec['valarms'] as $alarm) {
$notify_time = null;
if ($alarm['trigger'] instanceof DateTime) {
$notify_time = $alarm['trigger'];
}
else if (is_string($alarm['trigger'])) {
- $refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start'];
+ $refdate = !empty($alarm['related']) && $alarm['related'] == 'END' ? $rec['end'] : $rec['start'];
// abort if no reference date is available to compute notification time
- if (!is_a($refdate, 'DateTime'))
+ if (!is_a($refdate, 'DateTime')) {
continue;
+ }
// TODO: for all-day events, take start @ 00:00 as reference date ?
try {
$interval = new DateInterval(trim($alarm['trigger'], '+-'));
$interval->invert = $alarm['trigger'][0] == '-';
$notify_time = clone $refdate;
$notify_time->add($interval);
}
catch (Exception $e) {
rcube::raise_error($e, true);
continue;
}
}
if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
$notify_at = $notify_time;
- $action = $alarm['action'];
+ $action = isset($alarm['action']) ? $alarm['action'] : null;
$alarm_prop = $alarm;
// generate a unique alarm ID if multiple alarms are set
if (count($rec['valarms']) > 1) {
- $alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
+ $rec_id = substr(md5(isset($rec['id']) ? $rec['id'] : 'none'), 0, 16);
+ $alarm_id = $rec_id . '-' . $notify_at->format('Ymd\THis');
}
}
}
return !$notify_at ? null : array(
'time' => $notify_at->format('U'),
- 'action' => $action ? strtoupper($action) : 'DISPLAY',
+ 'action' => !empty($action) ? strtoupper($action) : 'DISPLAY',
'id' => $alarm_id,
'prop' => $alarm_prop,
);
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
public function refresh($attr)
{
// collect pending alarms from all providers (e.g. calendar, tasks)
$plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
'time' => time(),
'alarms' => array(),
));
if (!$plugin['abort'] && !empty($plugin['alarms'])) {
// make sure texts and env vars are available on client
$this->add_texts('localization/', true);
$this->rc->output->add_label('close');
$this->rc->output->set_env('snooze_select', $this->snooze_select());
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
}
}
/**
* Handler for alarm dismiss/snooze requests
*/
public function alarms_action()
{
// $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$data['ids'] = explode(',', $data['id']);
$plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
- if ($plugin['success'])
+ if (!empty($plugin['success'])) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
- else
+ }
+ else {
$this->rc->output->show_message('calendar.errorsaving', 'error');
+ }
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private function _alarms_output($alarms)
{
$out = array();
foreach ($alarms as $alarm) {
$out[] = array(
'id' => $alarm['id'],
- 'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
- 'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '',
- 'allDay' => $alarm['allday'] == 1,
+ 'start' => !empty($alarm['start']) ? $this->adjust_timezone($alarm['start'])->format('c') : '',
+ 'end' => !empty($alarm['end'])? $this->adjust_timezone($alarm['end'])->format('c') : '',
+ 'allDay' => !empty($alarm['allday']),
'action' => $alarm['action'],
'title' => $alarm['title'],
'location' => $alarm['location'],
'calendar' => $alarm['calendar'],
);
}
return $out;
}
/**
* Render a dropdown menu to choose snooze time
*/
private function snooze_select($attrib = array())
{
$steps = array(
5 => 'repeatinmin',
10 => 'repeatinmin',
15 => 'repeatinmin',
20 => 'repeatinmin',
30 => 'repeatinmin',
60 => 'repeatinhr',
120 => 'repeatinhrs',
1440 => 'repeattomorrow',
10080 => 'repeatinweek',
);
$items = array();
foreach ($steps as $n => $label) {
$items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
$this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
}
return html::tag('ul', $attrib + array('class' => 'toolbarmenu menu'), join("\n", $items), html::$common_attrib);
}
/********* Recurrence rules handling ********/
/**
* Render localized text describing the recurrence rule of an event
*/
public function recurrence_text($rrule)
{
$limit = 10;
$exdates = array();
$format = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$format = self::to_php_date_format($format);
$format_fn = function($dt) use ($format) {
return rcmail::get_instance()->format_date($dt, $format);
};
if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
$exdates = array_map($format_fn, $rrule['EXDATE']);
}
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
$rdates = array_map($format_fn, $rrule['RDATE']);
if (!empty($exdates)) {
$rdates = array_diff($rdates, $exdates);
}
if (count($rdates) > $limit) {
$rdates = array_slice($rdates, 0, $limit);
$more = true;
}
return $this->gettext('ondate') . ' ' . join(', ', $rdates)
. ($more ? '...' : '');
}
$output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1);
switch ($rrule['FREQ']) {
case 'DAILY':
$output .= $this->gettext('days');
break;
case 'WEEKLY':
$output .= $this->gettext('weeks');
break;
case 'MONTHLY':
$output .= $this->gettext('months');
break;
case 'YEARLY':
$output .= $this->gettext('years');
break;
}
if ($rrule['COUNT']) {
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
}
else if ($rrule['UNTIL']) {
$until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format);
}
else {
$until = $this->gettext('forever');
}
$output .= ', ' . $until;
if (!empty($exdates)) {
if (count($exdates) > $limit) {
$exdates = array_slice($exdates, 0, $limit);
$more = true;
}
$output .= '; ' . $this->gettext('except') . ' ' . join(', ', $exdates)
. ($more ? '...' : '');
}
return $output;
}
/**
* Generate the form for recurrence settings
*/
public function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency', 'class' => 'form-control'));
$select->add($this->gettext('never'), '');
$select->add($this->gettext('daily'), 'DAILY');
$select->add($this->gettext('weekly'), 'WEEKLY');
$select->add($this->gettext('monthly'), 'MONTHLY');
$select->add($this->gettext('yearly'), 'YEARLY');
$select->add($this->gettext('rdate'), 'RDATE');
$html = html::label(array('for' => 'edit-recurrence-frequency', 'class' => 'col-form-label col-sm-2'), $this->gettext('frequency'))
. html::div('col-sm-10', $select->show(''));
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-daily', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
. html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('days')))));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-weekly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
. html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('weeks')))));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'),
$checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
$this->gettext($daymap[$d])
) . ' ';
}
$html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bydays'))
. html::div('col-sm-10 form-control-plaintext', $weekdays));
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-monthly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
. html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('months')))));
$checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('every')));
$table->add('recurrence-onevery', $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bydays'))
. html::div('col-sm-10 form-control-plaintext', $table->show()));
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-yearly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
. html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('years')))));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bymonths'))
. html::div('col-sm-10 form-control-plaintext',
html::div(array('id' => 'edit-recurrence-yearly-bymonthblock'), $months)
. html::div('recurrence-onevery', $this->rrule_selectors($attrib['part'], '---'))
));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times', 'class' => 'form-control'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => '10', 'class' => 'form-control datepicker'));
$html = html::div('line first',
$radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever'))
. ' ' . html::label('edit-recurrence-repeat-forever', $this->gettext('forever'))
);
$label = $this->gettext('ntimes');
if (strpos($label, '$') === 0) {
$label = str_replace('$n', '', $label);
$group = $select->show(1)
. html::span('input-group-append', html::span('input-group-text', rcube::Q($label)));
}
else {
$label = str_replace('$n', '', $label);
$group = html::span('input-group-prepend', html::span('input-group-text', rcube::Q($label)))
. $select->show(1);
}
$html .= html::div('line',
$radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count'))
. ' ' . html::label('edit-recurrence-repeat-count', $this->gettext('for'))
. ' ' . html::span('input-group', $group)
);
$html .= html::div('line',
$radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate')))
. ' ' . html::label('edit-recurrence-repeat-until', $this->gettext('untildate'))
. ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
);
$html = html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), ucfirst($this->gettext('recurrencend')))
. html::div('col-sm-10', $html));
break;
case 'rdate':
$ul = html::tag('ul', array('id' => 'edit-recurrence-rdates', 'class' => 'recurrence-rdates'), '');
$input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10", 'class' => 'form-control datepicker'));
$button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
$html = html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2', 'for' => 'edit-recurrence-rdate-input'), $this->gettext('bydates'))
. html::div('col-sm-10', $ul . html::div('inputform', $input->show() . $button->show())));
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix", 'class' => 'form-control'));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->gettext('first'),
$this->gettext('second'),
$this->gettext('third'),
$this->gettext('fourth'),
$this->gettext('last')
),
array(1, 2, 3, 4, -1));
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday", 'class' => 'form-control'));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
return $select_prefix->show() . ' ' . $select_wday->show();
}
/**
* Convert the recurrence settings to be processed on the client
*/
public function to_client_recurrence($recurrence, $allday = false)
{
if ($recurrence['UNTIL']) {
$recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
}
// format RDATE values
if (is_array($recurrence['RDATE'])) {
$libcal = $this;
$recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
}, $recurrence['RDATE']);
}
unset($recurrence['EXCEPTIONS']);
return $recurrence;
}
/**
* Process the alarms values submitted by the client
*/
public function from_client_recurrence($recurrence, $start = null)
{
if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
$recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
}
if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
$tz = $this->timezone;
$recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
$dt = new DateTime($rdate, $tz);
if (is_a($start, 'DateTime'))
$dt->setTime($start->format('G'), $start->format('i'));
return $dt;
}
catch (Exception $e) {
return null;
}
}, $recurrence['RDATE']);
}
return $recurrence;
}
/********* iTip message detection *********/
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->ical_message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->ical_message->mime_parts as $part) {
if (self::part_is_vcalendar($part, $this->ical_message)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ical_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part) {
$this->ical_parts = array($itip_part);
}
}
/**
* Getter for the parsed iCal objects attached to the current email message
*
* @return object libvcalendar parser instance with the parsed objects
*/
public function get_mail_ical_objects()
{
// create parser and load ical objects
if (!$this->mail_ical_parser) {
$this->mail_ical_parser = $this->get_ical();
foreach ($this->ical_parts as $mime_id) {
$part = $this->ical_message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET;
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
// check if the parsed object is an instance of a recurring event/task
array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
// stop on the part that has an iTip method specified
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
$this->mail_ical_parser->mime_id = $mime_id;
// store the message's sender address for comparisons
$from = rcube_mime::decode_address_list($this->ical_message->headers->from, 1, true, null, true);
$this->mail_ical_parser->sender = !empty($from) ? $from[1] : '';
if (!empty($this->mail_ical_parser->sender)) {
foreach ($this->mail_ical_parser->objects as $i => $object) {
$this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
$this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
}
}
break;
}
}
}
return $this->mail_ical_parser;
}
/**
* Read the given mime message from IMAP and parse ical data
*
* @param string Mailbox name
* @param string Message UID
* @param string Message part ID and object index (e.g. '1.2:0')
* @param string Object type filter (optional)
*
* @return array Hash array with the parsed iCal
*/
public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
{
$charset = RCUBE_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_folder($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $imap->get_message_part($uid, $mime_id);
$headers = $imap->get_message_headers($uid);
$parser = $this->get_ical();
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$objects = $parser->import($part, $charset);
}
}
// successfully parsed events/tasks?
if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
if ($parser->method)
$object['_method'] = $parser->method;
// store the message's sender address for comparisons
$from = rcube_mime::decode_address_list($headers->from, 1, true, null, true);
$object['_sender'] = !empty($from) ? $from[1] : '';
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
// check if this is an instance of a recurring event/task
self::identify_recurrence_instance($object);
return $object;
}
return null;
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @param rcube_message Message object
*
* @return boolean True if part is of type vcard
*/
public static function part_is_vcalendar($part, $message = null)
{
// First check if the message is "valid" (i.e. not multipart/report)
if ($message) {
$level = explode('.', $part->mime_id);
while (array_pop($level) !== null) {
$parent = $message->mime_parts[join('.', $level) ?: 0];
if ($parent->mimetype == 'multipart/report') {
return false;
}
}
}
return (
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
// Apple sends files as application/x-any (!?)
($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
);
}
/**
* Single occourrences of recurring events are identified by their RECURRENCE-ID property
* in iCal which is represented as 'recurrence_date' in our internal data structure.
*
* Check if such a property exists and derive the '_instance' identifier and '_savemode'
* attributes which are used in the storage backend to identify the nested exception item.
*/
public static function identify_recurrence_instance(&$object)
{
// for savemode=all, remove recurrence instance identifiers
if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
unset($object['_instance'], $object['recurrence_date']);
}
// set instance and 'savemode' according to recurrence-id
else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
$object['_instance'] = self::recurrence_instance_identifier($object);
$object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
}
else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) {
if (strlen($object['_instance']) > 4) {
$object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
}
else {
$object['recurrence_date'] = clone $object['start'];
}
}
}
/**
* Return a date() format string to render identifiers for recurrence instances
*
* @param array Hash array with event properties
* @return string Format string
*/
public static function recurrence_id_format($event)
{
return $event['allday'] ? 'Ymd' : 'Ymd\THis';
}
/**
* Return the identifer for the given instance of a recurring event
*
* @param array Hash array with event properties
* @param bool All-day flag from the main event
*
* @return mixed Format string or null if identifier cannot be generated
*/
public static function recurrence_instance_identifier($event, $allday = null)
{
$instance_date = $event['recurrence_date'] ?: $event['start'];
if ($instance_date && is_a($instance_date, 'DateTime')) {
// According to RFC5545 (3.8.4.4) RECURRENCE-ID format should
// be date/date-time depending on the main event type, not the exception
if ($allday === null) {
$allday = $event['allday'];
}
return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis');
}
}
/********* Attendee handling functions *********/
/**
* Handler for attendee group expansion requests
*/
public function expand_attendee_group()
{
$id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$result = array('id' => $id, 'members' => array());
$maxnum = 500;
// iterate over all autocomplete address books (we don't know the source of the group)
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
foreach ($abook->list_groups($data['name'], 1) as $group) {
// this is the matching group to expand
if (in_array($data['email'], (array)$group['email'])) {
$abook->set_pagesize($maxnum);
$abook->set_group($group['ID']);
// get all members
$res = $abook->list_records($this->rc->config->get('contactlist_fields'));
// handle errors (e.g. sizelimit, timelimit)
if ($abook->get_error()) {
$result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
$res = false;
}
// check for maximum number of members (we don't wanna bloat the UI too much)
else if ($res->count > $maxnum) {
$result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
$res = false;
}
while ($res && ($member = $res->iterate())) {
$emails = (array)$abook->get_col_values('email', $member, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$result['members'][] = array(
'email' => $email,
'name' => rcube_addressbook::compose_list_name($member),
);
}
}
break 2;
}
}
}
}
$this->rc->output->command('plugin.expand_attendee_callback', $result);
}
/**
* Merge attendees of the old and new event version
* with keeping current user and his delegatees status
*
* @param array &$new New object data
* @param array $old Old object data
* @param bool $status New status of the current user
*/
public function merge_attendees(&$new, $old, $status = null)
{
if (empty($status)) {
$emails = $this->get_user_emails();
$delegates = array();
$attendees = array();
// keep attendee status of the current user
foreach ((array) $new['attendees'] as $i => $attendee) {
if (empty($attendee['email'])) {
continue;
}
$attendees[] = $email = strtolower($attendee['email']);
if (in_array($email, $emails)) {
foreach ($old['attendees'] as $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$new['attendees'][$i] = $_attendee;
if ($_attendee['status'] == 'DELEGATED' && ($email = $_attendee['delegated-to'])) {
$delegates[] = strtolower($email);
}
break;
}
}
}
}
// make sure delegated attendee is not lost
foreach ($delegates as $delegatee) {
if (!in_array($delegatee, $attendees)) {
foreach ((array) $old['attendees'] as $attendee) {
if ($attendee['email'] && ($email = strtolower($attendee['email'])) && $email == $delegatee) {
$new['attendees'][] = $attendee;
break;
}
}
}
}
}
// We also make sure that status of any attendee
// is not overriden by NEEDS-ACTION if it was already set
// which could happen if you work with shared events
foreach ((array) $new['attendees'] as $i => $attendee) {
if ($attendee['email'] && $attendee['status'] == 'NEEDS-ACTION') {
foreach ($old['attendees'] as $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$new['attendees'][$i]['status'] = $_attendee['status'];
unset($new['attendees'][$i]['rsvp']);
break;
}
}
}
}
}
/********* Static utility functions *********/
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public static function to_rrule($recurrence, $allday = false)
{
- if (is_string($recurrence))
+ if (is_string($recurrence)) {
return $recurrence;
+ }
$rrule = '';
foreach ((array)$recurrence as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
// convert to UTC according to RFC 5545
if (is_a($val, 'DateTime')) {
- if (!$allday && !$val->_dateonly) {
+ if (!$allday && empty($val->_dateonly)) {
$until = clone $val;
$until->setTimezone(new DateTimeZone('UTC'));
$val = $until->format('Ymd\THis\Z');
}
else {
$val = $val->format('Ymd');
}
}
break;
case 'RDATE':
case 'EXDATE':
foreach ((array)$val as $i => $ex) {
- if (is_a($ex, 'DateTime'))
+ if (is_a($ex, 'DateTime')) {
$val[$i] = $ex->format('Ymd\THis');
+ }
}
$val = join(',', (array)$val);
break;
case 'EXCEPTIONS':
continue 2;
}
- if (strlen($val))
+ if (strlen($val)) {
$rrule .= $k . '=' . $val . ';';
+ }
}
return rtrim($rrule, ';');
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
public static function to_php_date_format($from)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return strtr(strtr($from, array(
'YYYY' => 'Y',
'YY' => 'y',
'yyyy' => 'Y',
'yy' => 'y',
'MMMM' => 'F',
'MMM' => 'M',
'MM' => 'm',
'M' => 'n',
'dddd' => 'l',
'ddd' => 'D',
'DD' => 'd',
'D' => 'j',
'HH' => '**',
'hh' => '%%',
'H' => 'G',
'h' => 'g',
'mm' => 'i',
'ss' => 's',
'TT' => 'A',
'tt' => 'a',
'T' => 'A',
't' => 'a',
'u' => 'c',
)), array(
'**' => 'H',
'%%' => 'h',
));
}
/**
* Convert from PHP date() format to fullcalendar (MomentJS) format string
*/
public static function from_php_date_format($from)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return strtr($from, array(
'y' => 'YY',
'Y' => 'YYYY',
'M' => 'MMM',
'F' => 'MMMM',
'm' => 'MM',
'n' => 'M',
'j' => 'D',
'd' => 'DD',
'D' => 'ddd',
'l' => 'dddd',
'H' => 'HH',
'h' => 'hh',
'G' => 'H',
'g' => 'h',
'i' => 'mm',
's' => 'ss',
'c' => '',
));
}
}
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 164d1841..3dda65d4 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -1,1468 +1,1532 @@
<?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 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',
'schedule-status' => 'SCHEDULE-STATUS',
'schedule-agent' => 'SCHEDULE-AGENT',
'sent-by' => 'SENT-BY',
);
private $organizer_keymap = array(
'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 = 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->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 ($object['recurrence_date']) {
+ if (!empty($object['recurrence_date'])) {
$exceptions[] = $object;
}
- else if (!$seen[$object['uid']]++) {
+ else if (empty($seen[$object['uid']])) {
+ $seen[$object['uid']] = true;
$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]++) {
+ 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 = 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}) {
+ 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 = array('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';
else if ($value == 'CANCELLED')
$event['cancelled'] = true;
else if ($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 = is_array($event['recurrence']) ? $event['recurrence'] : array();
+ $params = !empty($event['recurrence']) && is_array($event['recurrence']) ? $event['recurrence'] : array();
// parse recurrence rule attributes
foreach ($prop->getParts() as $k => $v) {
$params[strtoupper($k)] = is_array($v) ? implode(',', $v) : $v;
}
- if ($params['UNTIL'])
+ if (!empty($params['UNTIL'])) {
$params['UNTIL'] = date_create($params['UNTIL']);
- if (!$params['INTERVAL'])
+ }
+ 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));
- $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], $exdates);
+ 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));
- $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], $rdates);
+ 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':
- $event['categories'] = array_merge((array)$event['categories'], $prop->getParts());
+ if (!empty($event['categories'])) {
+ $event['categories'] = array_merge((array) $event['categories'], $prop->getParts());
+ }
+ else {
+ $event['categories'] = $prop->getParts();
+ }
break;
case 'CLASS':
case 'X-CALENDARSERVER-ACCESS':
$event['sensitivity'] = strtolower($value);
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
- if ($value == 'OOF')
+ if ($value == 'OOF') {
$event['free_busy'] = 'outofoffice';
- else if (in_array($value, array('FREE', 'BUSY', 'TENTATIVE')))
+ }
+ else if (in_array($value, array('FREE', 'BUSY', 'TENTATIVE'))) {
$event['free_busy'] = strtolower($value);
+ }
break;
case 'ATTENDEE':
case 'ORGANIZER':
$params = array('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'];
}
}
else if ($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;
}
else if (strlen($value) && strtoupper($params['VALUE']) == 'BINARY') {
$attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name', 'X-APPLE-FILENAME' => 'name'));
$attachment['data'] = $value;
$attachment['size'] = strlen($value);
$event['attachments'][] = $attachment;
}
break;
default:
if (substr($prop->name, 0, 2) == 'X-')
$event['x-custom'][] = array($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'] = false;
-
- // check for all-day dates
- if ($event['start']->_dateonly) {
- $event['allday'] = true;
- }
+ $event['allday'] = !empty($event['start']->_dateonly);
// events may lack the DTEND property, set it to DTSTART (RFC5545 3.6.1)
if (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) {
$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();
}
else if ($param->name == 'RELATED') {
$alarm['related'] = $param->getValue();
}
}
if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) {
$trigger = $values[2];
}
- if (!$alarm['trigger']) {
+ 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')
+ 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') {
- if ($trigger && !$event['alarms']) // store first alarm in legacy property
+ // store first alarm in legacy property
+ if ($trigger && empty($event['alarms'])) {
$event['alarms'] = $trigger . ':' . $action;
+ }
- if ($alarm['trigger'])
+ if (!empty($alarm['trigger'])) {
$event['valarms'][] = $alarm;
+ }
}
}
// assign current timezone to event start/end
- if ($event['start'] instanceof DateTime) {
+ if (!empty($event['start']) && $event['start'] instanceof DateTime) {
$this->_apply_timezone($event['start']);
}
else {
unset($event['start']);
}
- if ($event['end'] instanceof DateTime) {
+ if (!empty($event['end']) && $event['end'] instanceof DateTime) {
$this->_apply_timezone($event['end']);
}
else {
unset($event['end']);
}
// some iTip CANCEL messages only contain the start date
- if (!$event['end'] && $event['start'] && $this->method == 'CANCEL') {
+ 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'][] = array('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
*/
private function _apply_timezone(&$date)
{
if (empty($this->timezone)) {
return;
}
// For date-only we'll keep the date and time intact
if ($date->_dateonly) {
$dt = new 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 = array('_type' => 'freebusy', 'periods' => array());
$seen = array();
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 = array('DTSTART' => 'start', 'DTEND' => 'end', 'CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed');
+ $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', '', $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 ($seen[$value.':'.$fbtype]++)
+ 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.
list($busyStart, $busyEnd) = explode('/', $period);
$busyStart = DateTimeParser::parse($busyStart);
$busyEnd = 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'] = $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 ? array() : null;
}
else if ($prop instanceof VObject\Property\iCalendar\DateTime) {
if (count($prop->getDateTimes()) > 1) {
$dt = array();
$dateonly = !$prop->hasTime();
foreach ($prop->getDateTimes() as $item) {
$item->_dateonly = $dateonly;
$dt[] = $item;
}
}
else {
$dt = $prop->getDateTime();
if (!$prop->hasTime()) {
$dt->_dateonly = true;
}
}
}
else if ($prop instanceof VObject\Property\iCalendar\Period) {
$dt = array();
foreach ($prop->getParts() as $val) {
try {
list($start, $end) = explode('/', $val);
$start = DateTimeParser::parseDateTime($start);
// This is a duration value.
if ($end[0] === 'P') {
$dur = DateTimeParser::parseDuration($end);
$end = clone $start;
$end->add($dur);
}
else {
$end = DateTimeParser::parseDateTime($end);
}
$dt[] = array($start, $end);
}
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 object VObject\Document parent node to create property for
* @param string Property name
* @param object DateTime
* @param boolean Set as UTC date
* @param boolean Set as VALUE=DATE property
*/
public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null, $set_type = false)
{
if ($utc) {
$dt->setTimeZone(new \DateTimeZone('UTC'));
$is_utc = true;
}
else {
$is_utc = ($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z'));
}
- $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly;
+ $is_dateonly = $dateonly === null ? !empty($dt->_dateonly) : (bool) $dateonly;
$vdt = $cal->createProperty($name, $dt, null, $is_dateonly ? 'DATE' : 'DATE-TIME');
if ($is_dateonly) {
$vdt['VALUE'] = 'DATE';
}
else if ($set_type) {
$vdt['VALUE'] = 'DATE-TIME';
}
// register timezone for VTIMEZONE block
if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) {
$ts = $dt->format('U');
- if (is_array($this->vtimezones[$tzname])) {
+ 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] = 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 $name => $value) {
$params[strtoupper($name)] = strval($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 = 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;
}
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';
+ $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 ($event['allday'] && !empty($event['end'])) {
+ if (!empty($event['allday']) && !empty($event['end'])) {
$event['end'] = clone $event['end'];
$event['end']->add(new \DateInterval('P1D'));
$event['end']->_dateonly = true;
}
- if (!empty($event['created']))
+ if (!empty($event['created'])) {
$ve->add($this->datetime_prop($cal, 'CREATED', $event['created'], true));
- if (!empty($event['changed']))
+ }
+ 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, (bool)$event['allday']));
- if (!empty($event['end']))
- $ve->add($this->datetime_prop($cal, 'DTEND', $event['end'], false, (bool)$event['allday']));
- if (!empty($event['due']))
- $ve->add($this->datetime_prop($cal, 'DUE', $event['due'], false));
+ }
+ 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 && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) {
- $recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $event['recurrence_date'], false, (bool)$event['allday']);
- if ($event['thisandfuture'])
+ if (!$recurrence_id && !empty($event['recurrence_date']) && $event['recurrence_date'] instanceof DateTime) {
+ $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);
}
$ve->add('SUMMARY', $event['title']);
- if ($event['location'])
+ if (!empty($event['location'])) {
$ve->add($this->is_apple() ? new vobject_location_property($cal, 'LOCATION', $event['location']) : $cal->create('LOCATION', $event['location']));
- if ($event['description'])
+ }
+ if (!empty($event['description'])) {
$ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
+ }
- if (isset($event['sequence']))
+ if (isset($event['sequence'])) {
$ve->add('SEQUENCE', $event['sequence']);
+ }
- if ($event['recurrence'] && !$recurrence_id) {
+ 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 ($event['recurrence']['FREQ']) {
- $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], (bool)$event['allday']));
+ 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 DateTime) {
$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 ($event['categories']) {
+ 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 ($event['priority'])
- $ve->add('PRIORITY', $event['priority']);
+ if (!empty($event['priority'])) {
+ $ve->add('PRIORITY', $event['priority']);
+ }
- if ($event['cancelled'])
+ if (!empty($event['cancelled'])) {
$ve->add('STATUS', 'CANCELLED');
- else if ($event['free_busy'] == 'tentative')
+ }
+ else if (!empty($event['free_busy']) && $event['free_busy'] == 'tentative') {
$ve->add('STATUS', 'TENTATIVE');
- else if ($event['complete'] == 100)
+ }
+ else if (!empty($event['complete']) && $event['complete'] == 100) {
$ve->add('STATUS', 'COMPLETED');
- else if (!empty($event['status']))
+ }
+ 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 and BusyCal required the COMPLETED date to be set in order to consider a task complete
- if ($event['status'] == 'COMPLETED' || $event['complete'] == 100) {
- $ve->add($this->datetime_prop($cal, 'COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
+ 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 ($event['valarms']) {
+ if (!empty($event['valarms'])) {
foreach ($event['valarms'] as $alarm) {
$va = $cal->createComponent('VALARM');
$va->action = $alarm['action'];
if ($alarm['trigger'] instanceof DateTime) {
$va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true));
}
else {
$alarm_props = array();
- if (strtoupper($alarm['related']) == 'END') {
+ if (!empty($alarm['related']) && strtoupper($alarm['related']) == 'END') {
$alarm_props['RELATED'] = 'END';
}
$va->add('TRIGGER', $alarm['trigger'], $alarm_props);
}
- if ($alarm['action'] == 'EMAIL') {
- foreach ((array)$alarm['attendees'] as $attendee) {
- $va->add('ATTENDEE', 'mailto:' . $attendee);
+ if (!empty($alarm['action']) && $alarm['action'] == 'EMAIL') {
+ if (!empty($alarm['attendees'])) {
+ foreach ((array) $alarm['attendees'] as $attendee) {
+ $va->add('ATTENDEE', 'mailto:' . $attendee);
+ }
}
}
- if ($alarm['description']) {
- $va->add('DESCRIPTION', $alarm['description'] ?: $event['title']);
+ if (!empty($alarm['description'])) {
+ $va->add('DESCRIPTION', $alarm['description']);
}
- if ($alarm['summary']) {
+ if (!empty($alarm['summary'])) {
$va->add('SUMMARY', $alarm['summary']);
}
- if ($alarm['duration']) {
+ if (!empty($alarm['duration'])) {
$va->add('DURATION', $alarm['duration']);
- $va->add('REPEAT', intval($alarm['repeat']));
+ $va->add('REPEAT', !empty($alarm['repeat']) ? intval($alarm['repeat']) : 0);
}
- if ($alarm['uri']) {
+ if (!empty($alarm['uri'])) {
$va->add('ATTACH', $alarm['uri'], array('VALUE' => 'URI'));
}
$ve->add($va);
}
}
// legacy support
- else if ($event['alarms']) {
+ else if (!empty($event['alarms'])) {
$va = $cal->createComponent('VALARM');
list($trigger, $va->action) = explode(':', $event['alarms']);
$val = libcalendaring::parse_alarm_value($trigger);
- if ($val[3])
+ if (!empty($val[3])) {
$va->add('TRIGGER', $val[3]);
- else if ($val[0] instanceof DateTime)
+ }
+ else if ($val[0] instanceof DateTime) {
$va->add($this->datetime_prop($cal, 'TRIGGER', $val[0], true, null, true));
+ }
$ve->add($va);
}
// Find SCHEDULE-AGENT
- foreach ((array)$event['x-custom'] as $prop) {
- if ($prop[0] === 'SCHEDULE-AGENT') {
- $schedule_agent = $prop[1];
+ if (!empty($event['x-custom'])) {
+ foreach ((array) $event['x-custom'] as $prop) {
+ if ($prop[0] === 'SCHEDULE-AGENT') {
+ $schedule_agent = $prop[1];
+ }
}
}
- 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;
+ if (!empty($event['attendees'])) {
+ 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;
+ }
- $mailto = $attendee['email'];
- $attendee = array_filter(self::map_keys($attendee, $this->attendee_keymap));
+ $mailto = $attendee['email'];
+ $attendee = array_filter(self::map_keys($attendee, $this->attendee_keymap));
- if ($schedule_agent !== null && !isset($attendee['SCHEDULE-AGENT'])) {
- $attendee['SCHEDULE-AGENT'] = $schedule_agent;
- }
+ if (isset($schedule_agent) && !isset($attendee['SCHEDULE-AGENT'])) {
+ $attendee['SCHEDULE-AGENT'] = $schedule_agent;
+ }
- $ve->add('ATTENDEE', 'mailto:' . $mailto, $attendee);
+ $ve->add('ATTENDEE', 'mailto:' . $mailto, $attendee);
+ }
}
}
- if ($event['organizer']) {
+ if (!empty($event['organizer'])) {
$organizer = array_filter(self::map_keys($event['organizer'], $this->organizer_keymap));
- if ($schedule_agent !== null && !isset($organizer['SCHEDULE-AGENT'])) {
+ if (isset($schedule_agent) && !isset($organizer['SCHEDULE-AGENT'])) {
$organizer['SCHEDULE-AGENT'] = $schedule_agent;
}
$ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], $organizer);
}
- foreach ((array)$event['url'] as $url) {
- if (!empty($url)) {
- $ve->add('URL', $url);
+ 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'], array('RELTYPE' => 'PARENT'));
}
- if ($event['comment'])
+ 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 $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',
$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);
+ if (!empty($event['links'])) {
+ 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]);
+ 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 (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) {
+ if (!empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
- $exdate = $ex['recurrence_date'] ?: $ex['start'];
- $recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $exdate, false, (bool)$event['allday']);
- if ($ex['thisandfuture'])
+ $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');
+ }
$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
* @param VObject\Component\VCalendar Optional VCalendar component
*
* @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, $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) {
return false;
}
}
else if (is_a($tzid, '\\DateTimeZone')) {
$tz = $tzid;
}
- if (!is_a($tz, '\\DateTimeZone')) {
+ if (empty($tz) || !is_a($tz, '\\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;
foreach ($transitions as $i => $trans) {
$cmp = null;
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;
}
if ($cmp) {
$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;
}
/*** 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) {
+ if (empty($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\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 = array(
// vCard
'N',
'ADR',
'ORG',
'GENDER',
'LOCATION',
// iCalendar
'REQUEST-STATUS',
);
}
diff --git a/plugins/libcalendaring/tests/libcalendaring.php b/plugins/libcalendaring/tests/libcalendaring.php
index 311d25e2..e93e0b32 100644
--- a/plugins/libcalendaring/tests/libcalendaring.php
+++ b/plugins/libcalendaring/tests/libcalendaring.php
@@ -1,184 +1,184 @@
<?php
/**
* libcalendaring plugin's utility functions tests
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 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/>.
*/
-class libcalendaring_test extends PHPUnit_Framework_TestCase
+class libcalendaring_test extends PHPUnit\Framework\TestCase
{
function setUp()
{
require_once __DIR__ . '/../libcalendaring.php';
}
/**
* libcalendaring::parse_alarm_value()
*/
function test_parse_alarm_value()
{
$alarm = libcalendaring::parse_alarm_value('-15M');
$this->assertEquals('15', $alarm[0]);
$this->assertEquals('-M', $alarm[1]);
$this->assertEquals('-PT15M', $alarm[3]);
$alarm = libcalendaring::parse_alarm_value('-PT5H');
$this->assertEquals('5', $alarm[0]);
$this->assertEquals('-H', $alarm[1]);
$alarm = libcalendaring::parse_alarm_value('P0DT1H0M0S');
$this->assertEquals('1', $alarm[0]);
$this->assertEquals('+H', $alarm[1]);
// FIXME: this should return something like (1140 + 120 + 30)M
$alarm = libcalendaring::parse_alarm_value('-P1DT2H30M');
// $this->assertEquals('1590', $alarm[0]);
// $this->assertEquals('-M', $alarm[1]);
$alarm = libcalendaring::parse_alarm_value('@1420722000');
$this->assertInstanceOf('DateTime', $alarm[0]);
}
/**
* libcalendaring::get_next_alarm()
*/
function test_get_next_alarm()
{
// alarm 10 minutes before event
$date = date('Ymd', strtotime('today + 2 days'));
$event = array(
'start' => new DateTime($date . 'T160000Z'),
'end' => new DateTime($date . 'T200000Z'),
'valarms' => array(
array(
'trigger' => '-PT10M',
'action' => 'DISPLAY',
),
),
);
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals($event['valarms'][0]['action'], $alarm['action']);
$this->assertEquals(strtotime($date . 'T155000Z'), $alarm['time']);
// alarm 1 hour after event start
$event['valarms'] = array(
array(
'trigger' => '+PT1H',
),
);
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals('DISPLAY', $alarm['action']);
$this->assertEquals(strtotime($date . 'T170000Z'), $alarm['time']);
// alarm 1 hour before event end
$event['valarms'] = array(
array(
'trigger' => '-PT1H',
'related' => 'END',
),
);
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals('DISPLAY', $alarm['action']);
$this->assertEquals(strtotime($date . 'T190000Z'), $alarm['time']);
// alarm 1 hour after event end
$event['valarms'] = array(
array(
'trigger' => 'PT1H',
'related' => 'END',
),
);
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals('DISPLAY', $alarm['action']);
$this->assertEquals(strtotime($date . 'T210000Z'), $alarm['time']);
// ignore past alarms
$event['start'] = new DateTime('today 22:00:00');
$event['end'] = new DateTime('today 23:00:00');
$event['valarms'] = array(
array(
'trigger' => '-P2D',
'action' => 'EMAIL',
),
array(
'trigger' => '-PT30M',
'action' => 'DISPLAY',
),
);
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals('DISPLAY', $alarm['action']);
$this->assertEquals(strtotime('today 21:30:00'), $alarm['time']);
// absolute alarm date/time
$event['valarms'] = array(
array('trigger' => new DateTime('today 20:00:00'))
);
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals($event['valarms'][0]['trigger']->format('U'), $alarm['time']);
// no alarms for cancelled events
$event['status'] = 'CANCELLED';
$alarm = libcalendaring::get_next_alarm($event);
$this->assertEquals(null, $alarm);
}
/**
* libcalendaring::part_is_vcalendar()
*/
function test_part_is_vcalendar()
{
$part = new StdClass;
$part->mimetype = 'text/plain';
$part->filename = 'event.ics';
$this->assertFalse(libcalendaring::part_is_vcalendar($part));
$part->mimetype = 'text/calendar';
$this->assertTrue(libcalendaring::part_is_vcalendar($part));
$part->mimetype = 'text/x-vcalendar';
$this->assertTrue(libcalendaring::part_is_vcalendar($part));
$part->mimetype = 'application/ics';
$this->assertTrue(libcalendaring::part_is_vcalendar($part));
$part->mimetype = 'application/x-any';
$this->assertTrue(libcalendaring::part_is_vcalendar($part));
}
/**
* libcalendaring::to_rrule()
*/
function test_to_rrule()
{
$rrule = array(
'FREQ' => 'MONTHLY',
'BYDAY' => '2WE',
'INTERVAL' => 2,
'UNTIL' => new DateTime('2025-05-01 18:00:00 CEST'),
);
$s = libcalendaring::to_rrule($rrule);
$this->assertRegExp('/FREQ='.$rrule['FREQ'].'/', $s, "Recurrence Frequence");
$this->assertRegExp('/INTERVAL='.$rrule['INTERVAL'].'/', $s, "Recurrence Interval");
$this->assertRegExp('/BYDAY='.$rrule['BYDAY'].'/', $s, "Recurrence BYDAY");
$this->assertRegExp('/UNTIL=20250501T160000Z/', $s, "Recurrence End date (in UTC)");
}
}
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index 29f73520..5e2503ed 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -1,609 +1,610 @@
<?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 libvcalendar_test extends PHPUnit_Framework_TestCase
+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->assertInstanceOf('DateTime', $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
*/
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(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->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");
$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('DateTime', $exception['start'], "Exception start");
// categories, class
$this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories");
$this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential");
// 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)");
$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->assertEquals('', $event['valarms'][0]['related'], "First alarm related property");
+ $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('DateTime', $event['valarms'][2]['trigger'], "Absolute trigger date/time");
// 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");
$this->assertContains('TRIGGER;VALUE=DATE-TIME:20130812', $ics, "Date-Time trigger");
}
/**
* @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]);
$this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][1]);
}
/**
* @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");
$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
*/
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");
$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(array($completed));
$this->assertRegExp('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property");
}
/**
* 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('America/Montreal'));
$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('TZOFFSETFROM:-0400', $ics, "TZOFFSETFROM with negative offset (Bug T428)");
$this->assertContains('TZOFFSETTO:-0500', $ics, "TZOFFSETTO with negative offset (Bug T428)");
$this->assertContains('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
$this->assertContains('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
$this->assertSame(2, substr_count($ics, 'DTSTAMP'), "Duplicate DTSTAMP (T1148)");
$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('/: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;RELATED=END:-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];
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'] = 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;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date");
$this->assertContains('RECURRENCE-ID;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: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();
$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->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'));
+ $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 = end($vtz->select('STANDARD'));
+ $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 = 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());
// Making sure VTIMEZOONE contains at least one STANDARD/DAYLIGHT component
// when there's only one transition in specified time period (T5626)
$vtz = libvcalendar::get_vtimezone('Europe/Istanbul', strtotime('2019-10-04T15:00:00'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
$dst = $vtz->select('DAYLIGHT');
$std = $vtz->select('STANDARD');
$this->assertEmpty($dst);
$this->assertCount(1, $std);
$std = end($std);
$this->assertEquals('STANDARD', $std->name);
$this->assertEquals('20181009T150000', $std->DTSTART);
$this->assertEquals('+0300', $std->TZOFFSETFROM);
$this->assertEquals('+0300', $std->TZOFFSETTO);
$this->assertEquals('+03', $std->TZNAME);
}
function get_attachment_data($id, $event)
{
return $this->attachment_data;
}
}
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 8ad0838b..db120aa2 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -1,378 +1,378 @@
<?php
/**
* Kolab core library
*
* Plugin to setup a basic environment for the interaction with a Kolab server.
* Other Kolab-related plugins will depend on it and can use the library classes
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-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/>.
*/
class libkolab extends rcube_plugin
{
static $http_requests = array();
static $bonnie_api = false;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
// load local config
$this->load_config();
// extend include path to load bundled lib classes
$include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
set_include_path($include_path);
$this->add_hook('storage_init', array($this, 'storage_init'));
$this->add_hook('storage_connect', array($this, 'storage_connect'));
$this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
// For Chwala
$this->add_hook('folder_mod', array('kolab_storage', 'folder_mod'));
$rcmail = rcube::get_instance();
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
rcube::raise_error($e, true);
kolab_format::$timezone = new DateTimeZone('GMT');
}
$this->add_texts('localization/', false);
- if ($rcmail->output->type == 'html') {
+ if (!empty($rcmail->output->type) && $rcmail->output->type == 'html') {
$rcmail->output->add_handler('libkolab.folder_search_form', array($this, 'folder_search_form'));
$this->include_stylesheet($this->local_skin_path() . '/libkolab.css');
}
// embed scripts and templates for email message audit trail
if ($rcmail->task == 'mail' && self::get_bonnie_api()) {
if ($rcmail->output->type == 'html') {
$this->add_hook('render_page', array($this, 'bonnie_render_page'));
$this->include_script('libkolab.js');
// add 'Show history' item to message menu
$this->api->add_content(html::tag('li', array('role' => 'menuitem'),
$this->api->output->button(array(
'command' => 'kolab-mail-history',
'label' => 'libkolab.showhistory',
'type' => 'link',
'classact' => 'icon history active',
'class' => 'icon history disabled',
'innerclass' => 'icon history',
))),
'messagemenu');
}
$this->register_action('plugin.message-changelog', array($this, 'message_changelog'));
}
}
/**
* Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
*/
function storage_init($p)
{
$p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID');
return $p;
}
/**
* Hook into IMAP connection to replace client identity
*/
function storage_connect($p)
{
$client_name = 'Roundcube/Kolab';
if (empty($p['ident'])) {
$p['ident'] = array(
'name' => $client_name,
'version' => RCUBE_VERSION,
/*
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
*/
);
}
else {
$p['ident']['name'] = $client_name;
}
return $p;
}
/**
* Getter for a singleton instance of the Bonnie API
*
* @return mixed kolab_bonnie_api instance if configured, false otherwise
*/
public static function get_bonnie_api()
{
// get configuration for the Bonnie API
if (!self::$bonnie_api && ($bonnie_config = rcube::get_instance()->config->get('kolab_bonnie_api', false))) {
self::$bonnie_api = new kolab_bonnie_api($bonnie_config);
}
return self::$bonnie_api;
}
/**
* Hook to append the message history dialog template to the mail view
*/
function bonnie_render_page($p)
{
if (($p['template'] === 'mail' || $p['template'] === 'message') && !$p['kolab-audittrail']) {
// append a template for the audit trail dialog
$this->api->output->add_footer(
html::div(array('id' => 'mailmessagehistory', 'class' => 'uidialog', 'aria-hidden' => 'true', 'style' => 'display:none'),
self::object_changelog_table(array('class' => 'records-table changelog-table'))
)
);
$this->api->output->set_env('kolab_audit_trail', true);
$p['kolab-audittrail'] = true;
}
return $p;
}
/**
* Handler for message audit trail changelog requests
*/
public function message_changelog()
{
if (!self::$bonnie_api) {
return false;
}
$rcmail = rcube::get_instance();
$msguid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST, true);
$mailbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$result = $msguid && $mailbox ? self::$bonnie_api->changelog('mail', null, $mailbox, $msguid) : null;
if (is_array($result)) {
if (is_array($result['changes'])) {
$dtformat = $rcmail->config->get('date_format') . ' ' . $rcmail->config->get('time_format');
array_walk($result['changes'], function(&$change) use ($dtformat, $rcmail) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
}
$this->api->output->command('plugin.message_render_changelog', $result['changes']);
}
else {
$this->api->output->command('plugin.message_render_changelog', false);
}
$this->api->output->send();
}
/**
* Wrapper function to load and initalize the HTTP_Request2 Object
*
* @param string|Net_Url2 Request URL
* @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
* @param array Configuration for this Request instance, that will be merged
* with default configuration
*
* @return HTTP_Request2 Request object
*/
public static function http_request($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
// force CURL adapter, this allows to handle correctly
// compressed responses with SplObserver registered (kolab_files) (#4507)
$http_config['adapter'] = 'HTTP_Request2_Adapter_Curl';
$key = md5(serialize($http_config));
if (!($request = self::$http_requests[$key])) {
// load HTTP_Request2
require_once 'HTTP/Request2.php';
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
self::$http_requests[$key] = $request;
}
// cleanup
try {
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
return $request;
}
/**
* Table oultine for object changelog display
*/
public static function object_changelog_table($attrib = array())
{
$rcube = rcube::get_instance();
$attrib += array('domain' => 'libkolab');
$table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
$table->add_header('diff', '');
$table->add_header('revision', $rcube->gettext('revision', $attrib['domain']));
$table->add_header('date', $rcube->gettext('date', $attrib['domain']));
$table->add_header('user', $rcube->gettext('user', $attrib['domain']));
$table->add_header('operation', $rcube->gettext('operation', $attrib['domain']));
$table->add_header('actions', ' ');
$rcube->output->add_label(
'libkolab.showrevision',
'libkolab.actionreceive',
'libkolab.actionappend',
'libkolab.actionmove',
'libkolab.actiondelete',
'libkolab.actionread',
'libkolab.actionflagset',
'libkolab.actionflagclear',
'libkolab.objectchangelog',
'libkolab.objectchangelognotavailable',
'close'
);
return $table->show($attrib);
}
/**
* Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
*/
public static function html_diff($from, $to, $is_html = null)
{
// auto-detect text/html format
if ($is_html === null) {
$from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '</'.$m[1].'>') > 0);
$to_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '</'.$m[1].'>') > 0);
$is_html = $from_html || $to_html;
// ensure both parts are of the same format
if ($is_html && !$from_html) {
$converter = new rcube_text2html($from, false, array('wrap' => true));
$from = $converter->get_html();
}
if ($is_html && !$to_html) {
$converter = new rcube_text2html($to, false, array('wrap' => true));
$to = $converter->get_html();
}
}
// compute diff from HTML
if ($is_html) {
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php';
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php';
include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php';
// replace data: urls with a transparent image to avoid memory problems
$from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from);
$to = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to);
$diff = new Caxy\HtmlDiff\HtmlDiff($from, $to);
$diffhtml = $diff->build();
// remove empty inserts (from tables)
return preg_replace('!<ins class="diff\w+">\s*</ins>!Uims', '', $diffhtml);
}
else {
include_once __dir__ . '/vendor/finediff.php';
$diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
return $diff->renderDiffToHTML();
}
}
/**
* Return a date() format string to render identifiers for recurrence instances
*
* @param array Hash array with event properties
* @return string Format string
*/
public static function recurrence_id_format($event)
{
return $event['allday'] ? 'Ymd' : 'Ymd\THis';
}
/**
* Returns HTML code for folder search widget
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function folder_search_form($attrib)
{
$rcmail = rcube::get_instance();
$attrib += array(
'gui-object' => false,
'wrapper' => true,
'form-name' => 'foldersearchform',
'command' => 'non-extsing-command',
'reset-command' => 'non-existing-command',
);
if ($attrib['label-domain'] && !strpos($attrib['buttontitle'], '.')) {
$attrib['buttontitle'] = $attrib['label-domain'] . '.' . $attrib['buttontitle'];
}
if ($attrib['buttontitle']) {
$attrib['placeholder'] = $rcmail->gettext($attrib['buttontitle']);
}
return $rcmail->output->search_form($attrib);
}
}
diff --git a/plugins/libkolab/tests/kolab_date_recurrence.php b/plugins/libkolab/tests/kolab_date_recurrence.php
index ad0f3216..ed3e3bd3 100644
--- a/plugins/libkolab/tests/kolab_date_recurrence.php
+++ b/plugins/libkolab/tests/kolab_date_recurrence.php
@@ -1,270 +1,270 @@
<?php
/**
* kolab_date_recurrence tests
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2017, 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 kolab_date_recurrence_test extends PHPUnit_Framework_TestCase
+class kolab_date_recurrence_test extends PHPUnit\Framework\TestCase
{
function setUp()
{
$rcube = rcmail::get_instance();
$rcube->plugins->load_plugin('libkolab', true, true);
}
/**
* kolab_date_recurrence::first_occurrence()
*
* @dataProvider data_first_occurrence
*/
function test_first_occurrence($recurrence_data, $start, $expected)
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$start = new DateTime($start);
if (!empty($recurrence_data['UNTIL'])) {
$recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
}
$event = array('start' => $start, 'recurrence' => $recurrence_data);
$object = kolab_format::factory('event', 3.0);
$object->set($event);
$recurrence = new kolab_date_recurrence($object);
$first = $recurrence->first_occurrence();
$this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
}
/**
* Data for test_first_occurrence()
*/
function data_first_occurrence()
{
// TODO: BYYEARDAY, BYWEEKNO, BYSETPOS, WKST
return array(
// non-recurring
array(
array(), // recurrence data
'2017-08-31 11:00:00', // start date
'2017-08-31 11:00:00', // expected result
),
// daily
array(
array('FREQ' => 'DAILY', 'INTERVAL' => '1'), // recurrence data
'2017-08-31 11:00:00', // start date
'2017-08-31 11:00:00', // expected result
),
// TODO: this one is not supported by the Calendar UI
array(
array('FREQ' => 'DAILY', 'INTERVAL' => '1', 'BYMONTH' => 1),
'2017-08-31 11:00:00',
'2018-01-01 11:00:00',
),
// weekly
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE'),
'2017-08-31 11:00:00', // Thursday
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'TH'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'FR'),
'2017-08-31 11:00:00', // Thursday
'2017-09-01 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '2'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '3', 'BYDAY' => 'WE'),
'2017-08-31 11:00:00', // Thursday
'2017-09-20 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'COUNT' => 1),
'2017-08-31 11:00:00', // Thursday
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'UNTIL' => '2017-09-01'),
'2017-08-31 11:00:00', // Thursday
'',
),
// monthly
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
'2017-08-31 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '1WE'),
'2017-08-16 11:00:00',
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '-1WE'),
'2017-08-16 11:00:00',
'2017-08-30 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2', 'BYMONTHDAY' => '8'),
'2017-08-31 11:00:00',
'2017-09-08 11:00:00', // ??????
),
// yearly
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1'),
'2017-08-16 12:00:00',
'2017-08-16 12:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8'),
'2017-08-16 12:00:00',
'2017-08-16 12:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-12-25 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-08-28 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1', 'BYDAY' => '1MO'),
'2017-08-16 11:00:00',
'2018-01-01 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1,9', 'BYDAY' => '1MO'),
'2017-08-16 11:00:00',
'2017-09-04 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2'),
'2017-08-16 11:00:00',
'2017-08-16 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYMONTH' => '8'),
'2017-08-16 11:00:00',
'2017-08-16 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-12-25 11:00:00',
),
// on dates (FIXME: do we really expect the first occurrence to be on the start date?)
array(
array('RDATE' => array (new DateTime('2017-08-10 11:00:00 Europe/Warsaw'))),
'2017-08-01 11:00:00',
'2017-08-01 11:00:00',
),
);
}
/**
* kolab_date_recurrence::first_occurrence() for all-day events
*
* @dataProvider data_first_occurrence
*/
function test_first_occurrence_allday($recurrence_data, $start, $expected)
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$start = new DateTime($start);
if (!empty($recurrence_data['UNTIL'])) {
$recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
}
$event = array('start' => $start, 'recurrence' => $recurrence_data, 'allday' => true);
$object = kolab_format::factory('event', 3.0);
$object->set($event);
$recurrence = new kolab_date_recurrence($object);
$first = $recurrence->first_occurrence();
$this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
}
/**
* kolab_date_recurrence::next_instance()
*/
function test_next_instance()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
date_default_timezone_set('America/New_York');
$start = new DateTime('2017-08-31 11:00:00', new DateTimeZone('Europe/Berlin'));
$event = array(
'start' => $start,
'recurrence' => array('FREQ' => 'WEEKLY', 'INTERVAL' => '1'),
'allday' => true,
);
$object = kolab_format::factory('event', 3.0);
$object->set($event);
$recurrence = new kolab_date_recurrence($object);
$next = $recurrence->next_instance();
$this->assertEquals($start->format('2017-09-07 H:i:s'), $next['start']->format('Y-m-d H:i:s'), 'Same time');
$this->assertEquals($start->getTimezone()->getName(), $next['start']->getTimezone()->getName(), 'Same timezone');
$this->assertSame($next['start']->_dateonly, true, '_dateonly flag');
}
}
diff --git a/plugins/libkolab/tests/kolab_storage_config.php b/plugins/libkolab/tests/kolab_storage_config.php
index 50a894f2..d0c0ba3f 100644
--- a/plugins/libkolab/tests/kolab_storage_config.php
+++ b/plugins/libkolab/tests/kolab_storage_config.php
@@ -1,238 +1,238 @@
<?php
-class kolab_storage_config_test extends PHPUnit_Framework_TestCase
+class kolab_storage_config_test extends PHPUnit\Framework\TestCase
{
private $params_personal = array(
'folder' => 'Archive',
'uid' => '9',
'message-id' => '<1225270@example.org>',
'date' => 'Mon, 20 Apr 2015 15:30:30 UTC',
'subject' => 'Archived',
);
private $url_personal = 'imap:///user/$user/Archive/9?message-id=%3C1225270%40example.org%3E&date=Mon%2C+20+Apr+2015+15%3A30%3A30+UTC&subject=Archived';
private $params_shared = array(
'folder' => 'Shared Folders/shared/Collected',
'uid' => '4',
'message-id' => '<5270122@example.org>',
'date' => 'Mon, 20 Apr 2015 16:33:03 +0200',
'subject' => 'Shared',
);
private $url_shared = 'imap:///shared/Collected/4?message-id=%3C5270122%40example.org%3E&date=Mon%2C+20+Apr+2015+16%3A33%3A03+%2B0200&subject=Shared';
private $params_other = array(
'folder' => 'Other Users/lucy.white/Mailings',
'uid' => '378',
'message-id' => '<22448899@example.org>',
'date' => 'Tue, 14 Apr 2015 14:14:30 +0200',
'subject' => 'Happy Holidays',
);
private $url_other = 'imap:///user/lucy.white%40example.org/Mailings/378?message-id=%3C22448899%40example.org%3E&date=Tue%2C+14+Apr+2015+14%3A14%3A30+%2B0200&subject=Happy+Holidays';
public static function setUpBeforeClass()
{
$rcube = rcmail::get_instance();
$rcube->plugins->load_plugin('libkolab', true, true);
if (!kolab_format::supports(3)) {
return;
}
if ($rcube->config->get('tests_username')) {
$authenticated = $rcube->login(
$rcube->config->get('tests_username'),
$rcube->config->get('tests_password'),
$rcube->config->get('default_host'),
false
);
if (!$authenticated) {
throw new Exception('IMAP login failed for user ' . $rcube->config->get('tests_username'));
}
// check for defult groupware folders and clear them
$imap = $rcube->get_storage();
$folders = $imap->list_folders('', '*');
foreach (array('Configuration') as $folder) {
if (in_array($folder, $folders)) {
if (!$imap->clear_folder($folder)) {
throw new Exception("Failed to clear folder '$folder'");
}
}
else {
throw new Exception("Default folder '$folder' doesn't exits in test user account");
}
}
}
else {
throw new Exception('Missing test account username/password in config-test.inc.php');
}
kolab_storage::setup();
}
function test_001_build_member_url()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$rcube = rcube::get_instance();
$email = $rcube->get_user_email();
$personal = str_replace('$user', urlencode($email), $this->url_personal);
// personal namespace
$url = kolab_storage_config::build_member_url($this->params_personal);
$this->assertEquals($personal, $url);
// shared namespace
$url = kolab_storage_config::build_member_url($this->params_shared);
$this->assertEquals($this->url_shared, $url);
// other users namespace
$url = kolab_storage_config::build_member_url($this->params_other);
$this->assertEquals($this->url_other, $url);
}
function test_002_parse_member_url()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$rcube = rcube::get_instance();
$email = $rcube->get_user_email();
$personal = str_replace('$user', urlencode($email), $this->url_personal);
// personal namespace
$params = kolab_storage_config::parse_member_url($personal);
$this->assertEquals($this->params_personal['uid'], $params['uid']);
$this->assertEquals($this->params_personal['folder'], $params['folder']);
$this->assertEquals($this->params_personal['subject'], $params['params']['subject']);
$this->assertEquals($this->params_personal['message-id'], $params['params']['message-id']);
// shared namespace
$params = kolab_storage_config::parse_member_url($this->url_shared);
$this->assertEquals($this->params_shared['uid'], $params['uid']);
$this->assertEquals($this->params_shared['folder'], $params['folder']);
// other users namespace
$params = kolab_storage_config::parse_member_url($this->url_other);
$this->assertEquals($this->params_other['uid'], $params['uid']);
$this->assertEquals($this->params_other['folder'], $params['folder']);
}
function test_003_build_parse_member_url()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
// personal namespace
$params = $this->params_personal;
$params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params));
$this->assertEquals($params['uid'], $params_['uid']);
$this->assertEquals($params['folder'], $params_['folder']);
// shared namespace
$params = $this->params_shared;
$params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params));
$this->assertEquals($params['uid'], $params_['uid']);
$this->assertEquals($params['folder'], $params_['folder']);
// other users namespace
$params = $this->params_other;
$params_ = kolab_storage_config::parse_member_url(kolab_storage_config::build_member_url($params));
$this->assertEquals($params['uid'], $params_['uid']);
$this->assertEquals($params['folder'], $params_['folder']);
}
/**
* Test relation/tag objects creation
* These objects will be used by following tests
*/
function test_save()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$config = kolab_storage_config::get_instance();
$tags = array(
array(
'category' => 'tag',
'name' => 'test1',
),
array(
'category' => 'tag',
'name' => 'test2',
),
array(
'category' => 'tag',
'name' => 'test3',
),
array(
'category' => 'tag',
'name' => 'test4',
),
);
foreach ($tags as $tag) {
$result = $config->save($tag, 'relation');
$this->assertTrue(!empty($result));
$this->assertTrue(!empty($tag['uid']));
}
}
/**
* Tests "race condition" in tags handling (T133)
*/
function test_T133()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$config = kolab_storage_config::get_instance();
// get tags
$tags = $config->get_tags();
$this->assertCount(4, $tags);
// create a tag
$tag = array(
'category' => 'tag',
'name' => 'new',
);
$result = $config->save($tag, 'relation');
$this->assertTrue(!empty($result));
// get tags again, make sure it contains the new tag
$tags = $config->get_tags();
$this->assertCount(5, $tags);
// update a tag
$tag['name'] = 'new-tag';
$result = $config->save($tag, 'relation');
$this->assertTrue(!empty($result));
// get tags again, make sure it contains the new tag
$tags = $config->get_tags();
$this->assertCount(5, $tags);
$this->assertSame('new-tag', $tags[4]['name']);
// remove a tag
$result = $config->delete($tag['uid']);
$this->assertTrue(!empty($result));
// get tags again, make sure it contains the new tag
$tags = $config->get_tags();
$this->assertCount(4, $tags);
foreach ($tags as $_tag) {
$this->assertTrue($_tag['uid'] != $tag['uid']);
}
}
}
diff --git a/plugins/libkolab/tests/kolab_storage_folder.php b/plugins/libkolab/tests/kolab_storage_folder.php
index 273ece26..c242b979 100644
--- a/plugins/libkolab/tests/kolab_storage_folder.php
+++ b/plugins/libkolab/tests/kolab_storage_folder.php
@@ -1,255 +1,255 @@
<?php
/**
* libkolab/kolab_storage_folder class tests
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 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/>.
*/
-class kolab_storage_folder_test extends PHPUnit_Framework_TestCase
+class kolab_storage_folder_test extends PHPUnit\Framework\TestCase
{
public static function setUpBeforeClass()
{
// load libkolab plugin
$rcmail = rcmail::get_instance();
$rcmail->plugins->load_plugin('libkolab', true, true);
if (!kolab_format::supports(3)) {
return;
}
if ($rcmail->config->get('tests_username')) {
$authenticated = $rcmail->login(
$rcmail->config->get('tests_username'),
$rcmail->config->get('tests_password'),
$rcmail->config->get('default_host'),
false
);
if (!$authenticated) {
throw new Exception('IMAP login failed for user ' . $rcmail->config->get('tests_username'));
}
// check for defult groupware folders and clear them
$imap = $rcmail->get_storage();
$folders = $imap->list_folders('', '*');
foreach (array('Calendar','Contacts','Files','Tasks','Notes') as $folder) {
if (in_array($folder, $folders)) {
if (!$imap->clear_folder($folder)) {
throw new Exception("Failed to clear folder '$folder'");
}
}
else {
throw new Exception("Default folder '$folder' doesn't exits in test user account");
}
}
}
else {
throw new Exception('Missing test account username/password in config-test.inc.php');
}
kolab_storage::setup();
}
function test_001_folder_type_check()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$folder = new kolab_storage_folder('Calendar', 'event', 'event.default');
$this->assertTrue($folder->valid);
$this->assertEquals($folder->get_error(), 0);
$folder = new kolab_storage_folder('Calendar', 'event', 'mail');
$this->assertFalse($folder->valid);
$this->assertEquals($folder->get_error(), kolab_storage::ERROR_INVALID_FOLDER);
$folder = new kolab_storage_folder('INBOX');
$this->assertFalse($folder->valid);
$this->assertEquals($folder->get_error(), kolab_storage::ERROR_INVALID_FOLDER);
}
function test_002_get_owner()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$rcmail = rcmail::get_instance();
$folder = new kolab_storage_folder('Calendar', 'event', 'event');
$this->assertEquals($folder->get_owner(), $rcmail->config->get('tests_username'));
$domain = preg_replace('/^.+@/', '@', $rcmail->config->get('tests_username'));
$shared_ns = kolab_storage::namespace_root('shared');
$folder = new kolab_storage_folder($shared_ns . 'A-shared-folder', 'event', 'event');
$this->assertEquals($folder->get_owner(true), 'anonymous' . $domain);
$other_ns = kolab_storage::namespace_root('other');
$folder = new kolab_storage_folder($other_ns . 'major.tom/Calendar', 'event', 'event');
$this->assertEquals($folder->get_owner(true), 'major.tom' . $domain);
}
function test_003_get_resource_uri()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$rcmail = rcmail::get_instance();
$foldername = 'Calendar';
$uri = parse_url($rcmail->config->get('default_host'));
$hostname = $uri['host'];
$folder = new kolab_storage_folder($foldername, 'event', 'event.default');
$this->assertEquals($folder->get_resource_uri(), sprintf('imap://%s@%s/%s',
urlencode($rcmail->config->get('tests_username')),
$hostname,
$foldername
));
}
function test_004_get_uid()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$rcmail = rcmail::get_instance();
$folder = new kolab_storage_folder('Doesnt-Exist', 'event', 'event');
// generate UID from folder name if IMAP operations fail
$uid1 = $folder->get_uid();
$this->assertEquals($folder->get_uid(), $uid1);
$this->assertEquals($folder->get_error(), kolab_storage::ERROR_IMAP_CONN);
}
function test_005_subscribe()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$folder = new kolab_storage_folder('Contacts', 'contact');
$this->assertTrue($folder->subscribe(true));
$this->assertTrue($folder->is_subscribed());
$this->assertTrue($folder->subscribe(false));
$this->assertFalse($folder->is_subscribed());
$folder->subscribe(true);
}
function test_006_activate()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$folder = new kolab_storage_folder('Calendar', 'event');
$this->assertTrue($folder->activate(true));
$this->assertTrue($folder->is_active());
$this->assertTrue($folder->activate(false));
$this->assertFalse($folder->is_active());
}
function test_010_write_contacts()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$folder = new kolab_storage_folder('Contacts', 'contact');
$saved = $folder->save(null, 'contact');
$this->assertFalse($saved);
$contact = array(
'name' => 'FN',
'surname' => 'Last',
'firstname' => 'First',
'email' => array(
array('type' => 'home', 'address' => 'first.last@example.org'),
),
'organization' => 'Company A.G.'
);
$saved = $folder->save($contact, 'contact');
$this->assertTrue((bool)$saved);
}
/**
* @depends test_010_write_contacts
*/
function test_011_list_contacts()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$folder = new kolab_storage_folder('Contacts', 'contact');
$this->assertEquals($folder->count(), 1);
}
function test_T491_get_uid()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$rcmail = rcmail::get_instance();
$imap = $rcmail->get_storage();
$db = $rcmail->get_dbh();
// clear cache
//$imap->clear_cache('mailboxes.metadata', true);
// get folder UID
$folder = new kolab_storage_folder('Calendar', 'event', 'event');
$uid = $folder->get_uid();
// now get folder uniqueid annotations
$annotations = array(
'cyrus' => kolab_storage::UID_KEY_CYRUS,
'shared' => kolab_storage::UID_KEY_SHARED,
'private' => '/private/vendor/kolab/uniqueid',
);
foreach ($annotations as $key => $annotation) {
$meta = $imap->get_metadata('Calendar', $annotation);
$annotations[$key] = $meta['Calendar'][$annotation];
}
// compare results
if ($annotations['shared']) {
$this->assertSame($annotations['shared'], $uid);
}
else if ($annotations['cyrus']) {
$this->assertSame($annotations['cyrus'], $uid);
}
else {
// never use private namespace
$this->assertTrue($annotations['private'] != $uid);
}
// @TODO: check if the cache contains valid entries, not so simple with memcache
// as the cache key name is quite internal to the rcube_imap class.
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Mar 19, 8:44 AM (20 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
456365
Default Alt Text
(202 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment