Page MenuHomePhorge

No OneTemporary

Size
202 KB
Referenced Files
None
Subscribers
None
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() . '&nbsp;' . $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', '&nbsp;');
$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

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)

Event Timeline