Page MenuHomePhorge

No OneTemporary

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index ddaa4273..092a2247 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,2453 +1,2470 @@
<?php
/**
* Calendar plugin for Roundcube webmail
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2012, 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 calendar extends rcube_plugin
{
const FREEBUSY_UNKNOWN = 0;
const FREEBUSY_FREE = 1;
const FREEBUSY_BUSY = 2;
const FREEBUSY_TENTATIVE = 3;
const FREEBUSY_OOF = 4;
const SESSION_KEY = 'calendar_temp';
public $task = '?(?!logout).*';
public $rc;
public $lib;
public $driver;
public $home; // declare public to be used in other classes
public $urlbase;
public $timezone;
public $timezone_offset;
public $gmt_offset;
public $ical;
public $ui;
public $defaults = array(
'calendar_default_view' => "agendaWeek",
'calendar_timeslots' => 2,
'calendar_work_start' => 6,
'calendar_work_end' => 18,
'calendar_agenda_range' => 60,
'calendar_agenda_sections' => 'smart',
'calendar_event_coloring' => 0,
'calendar_time_indicator' => true,
'calendar_allow_invite_shared' => false,
);
private $ics_parts = array();
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('calendar', 'calendar');
// load calendar configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
$this->timezone = $this->lib->timezone;
$this->gmt_offset = $this->lib->gmt_offset;
$this->dst_active = $this->lib->dst_active;
$this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
require($this->home . '/lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
// catch iTIP confirmation requests that don're require a valid session
if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
$this->add_hook('startup', array($this, 'itip_attend_response'));
}
else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
$this->add_hook('startup', array($this, 'ical_feed_export'));
}
else {
// default startup routine
$this->add_hook('startup', array($this, 'startup'));
}
}
/**
* Startup hook
*/
public function startup($args)
{
// the calendar module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true))
return;
// load Calendar user interface
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->ui->init();
// settings are required in (almost) every GUI step
if ($args['action'] != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
if ($args['action'] != 'upload') {
$this->load_driver();
}
// register calendar actions
$this->register_action('index', array($this, 'calendar_view'));
$this->register_action('event', array($this, 'event_action'));
$this->register_action('calendar', array($this, 'calendar_action'));
$this->register_action('load_events', array($this, 'load_events'));
$this->register_action('export_events', array($this, 'export_events'));
$this->register_action('import_events', array($this, 'import_events'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('freebusy-status', array($this, 'freebusy_status'));
$this->register_action('freebusy-times', array($this, 'freebusy_times'));
$this->register_action('randomdata', array($this, 'generate_randomdata'));
$this->register_action('print', array($this,'print_view'));
$this->register_action('mailimportevent', array($this, 'mail_import_event'));
$this->register_action('mailtoevent', array($this, 'mail_message2event'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('check-recent', array($this, 'check_recent'));
$this->add_hook('refresh', array($this, 'refresh'));
// remove undo information...
if ($undo = $_SESSION['calendar_event_undo']) {
// ...after timeout
$undo_time = $this->rc->config->get('undo_timeout', 0);
if ($undo['ts'] < time() - $undo_time) {
$this->rc->session->remove('calendar_event_undo');
// @TODO: do EXPUNGE on kolab objects?
}
}
}
else if ($args['task'] == 'settings') {
// add hooks for Calendar settings
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
}
else if ($args['task'] == 'mail') {
// hooks to catch event invitations on incoming mails
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'calendar-create-from-mail',
'label' => 'calendar.createfrommail',
'type' => 'link',
'classact' => 'icon calendarlink active',
'class' => 'icon calendarlink',
'innerclass' => 'icon calendar',
))),
'messagemenu');
$this->api->output->add_label('calendar.createfrommail');
}
}
else if ($args['task'] == 'addressbook') {
if ($this->rc->config->get('calendar_contact_birthdays')) {
$this->add_hook('contact_update', array($this, 'contact_update'));
$this->add_hook('contact_create', array($this, 'contact_update'));
}
}
// add hooks to display alarms
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('calendar_driver', 'database');
$driver_class = $driver_name . '_driver';
require_once($this->home . '/drivers/calendar_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('libkolab');
default:
$this->driver = new $driver_class($this);
break;
}
if ($this->driver->undelete)
$this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once($this->home . '/lib/calendar_itip.php');
$plugin = $this->rc->plugins->exec_hook('calendar_load_itip',
array('identity' => null));
$this->itip = new calendar_itip($this, $plugin['identity']);
}
return $this->itip;
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
$this->ical = libcalendaring::get_ical();
}
return $this->ical;
}
/**
* Get properties of the calendar this user has specified as default
*/
public function get_default_calendar($writeable = false)
{
$default_id = $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars(false, true);
$calendar = $calendars[$default_id] ?: null;
if (!$calendar || ($writeable && $calendar['readonly'])) {
foreach ($calendars as $cal) {
if ($cal['default']) {
$calendar = $cal;
break;
}
if (!$writeable || !$cal['readonly']) {
$first = $cal;
}
}
}
return $calendar ?: $first;
}
/**
* Render the main calendar view from skin template
*/
function calendar_view()
{
$this->rc->output->set_pagetitle($this->gettext('calendar'));
// Add CSS stylesheets to the page header
$this->ui->addCSS();
// Add JS files to the page header
$this->ui->addJS();
$this->ui->init_templates();
$this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning');
// initialize attendees autocompletion
rcube_autocomplete_init();
$this->rc->output->set_env('timezone', $this->timezone->getName());
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$this->rc->output->set_env('view', $view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
$this->rc->output->send("calendar.calendar");
}
/**
* Handler for preferences_sections_list hook.
* Adds Calendar settings sections into preferences sections list.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_sections_list($p)
{
$p['list']['calendar'] = array(
'id' => 'calendar', 'section' => $this->gettext('calendar'),
);
return $p;
}
/**
* Handler for preferences_list hook.
* Adds options blocks into Calendar settings sections in Preferences.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_list($p)
{
if ($p['section'] != 'calendar') {
return $p;
}
$no_override = array_flip((array)$this->rc->config->get('dont_override'));
$p['blocks']['view']['name'] = $this->gettext('mainoptions');
if (!isset($no_override['calendar_default_view'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_default_view';
$select = new html_select(array('name' => '_default_view', 'id' => $field_id));
$select->add($this->gettext('day'), "agendaDay");
$select->add($this->gettext('week'), "agendaWeek");
$select->add($this->gettext('month'), "month");
$select->add($this->gettext('agenda'), "table");
$p['blocks']['view']['options']['default_view'] = array(
'title' => html::label($field_id, Q($this->gettext('default_view'))),
'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
);
}
if (!isset($no_override['calendar_timeslots'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_timeslot';
$choices = array('1', '2', '3', '4', '6');
$select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['timeslots'] = array(
'title' => html::label($field_id, Q($this->gettext('timeslots'))),
'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))),
);
}
if (!isset($no_override['calendar_first_day'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_firstday';
$select = new html_select(array('name' => '_first_day', 'id' => $field_id));
$select->add(rcube_label('sunday'), '0');
$select->add(rcube_label('monday'), '1');
$select->add(rcube_label('tuesday'), '2');
$select->add(rcube_label('wednesday'), '3');
$select->add(rcube_label('thursday'), '4');
$select->add(rcube_label('friday'), '5');
$select->add(rcube_label('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, Q($this->gettext('first_day'))),
'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
);
}
if (!isset($no_override['calendar_first_hour'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
$select_hours = new html_select();
for ($h = 0; $h < 24; $h++)
$select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
$field_id = 'rcmfd_firsthour';
$p['blocks']['view']['options']['first_hour'] = array(
'title' => html::label($field_id, Q($this->gettext('first_hour'))),
'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
);
}
if (!isset($no_override['calendar_work_start'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_workstart';
$p['blocks']['view']['options']['workinghours'] = array(
'title' => html::label($field_id, Q($this->gettext('workinghours'))),
'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
);
}
if (!isset($no_override['calendar_event_coloring'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_coloring';
$select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
$select_colors->add($this->gettext('coloringmode0'), 0);
$select_colors->add($this->gettext('coloringmode1'), 1);
$select_colors->add($this->gettext('coloringmode2'), 2);
$select_colors->add($this->gettext('coloringmode3'), 3);
$p['blocks']['view']['options']['eventcolors'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
);
}
// loading driver is expensive, don't do it if not needed
$this->load_driver();
if (!isset($no_override['calendar_default_alarm_type'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_alarm';
$select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type)
$select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
$p['blocks']['view']['options']['alarmtype'] = array(
'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
);
}
if (!isset($no_override['calendar_default_alarm_offset'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_alarm';
$input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
$select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$p['blocks']['view']['options']['alarmoffset'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
);
}
if (!isset($no_override['calendar_default_calendar'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
// default calendar selection
$field_id = 'rcmfd_default_calendar';
$select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
foreach ((array)$this->driver->list_calendars(false, true) as $id => $prop) {
$select_cal->add($prop['name'], strval($id));
if ($prop['default'])
$default_calendar = $id;
}
$p['blocks']['view']['options']['defaultcalendar'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),
);
}
// category definitions
if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) {
$p['blocks']['categories']['name'] = $this->gettext('categories');
if (!$p['current']) {
$p['blocks']['categories']['content'] = true;
return $p;
}
$categories = (array) $this->driver->list_categories();
$categories_list = '';
foreach ($categories as $name => $color) {
$key = md5($name);
$field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
$category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
$category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable));
$category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
$hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
$categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
}
$p['blocks']['categories']['options']['category_' . $name] = array(
'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
);
$field_id = 'rcmfd_new_category';
$new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
$add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
$p['blocks']['categories']['options']['categories'] = array(
'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
);
$this->rc->output->add_script('function rcube_calendar_add_category(){
var name = $("#rcmfd_new_category").val();
if (name.length) {
var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
$("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
color.miniColors({ colorValues:(rcmail.env.mscolors || []) });
$("#rcmfd_new_category").val("");
}
}');
$this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){
if (event.which == 13) {
rcube_calendar_add_category();
event.preventDefault();
}
});
', 'docready');
// include color picker
$this->include_script('lib/js/jquery.miniColors.min.js');
$this->include_stylesheet($this->local_skin_path() . '/jquery.miniColors.css');
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$this->rc->output->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready');
}
// virtual birthdays calendar
if (!isset($no_override['calendar_contact_birthdays'])) {
$p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar');
if (!$p['current']) {
$p['blocks']['birthdays']['content'] = true;
return $p;
}
$field_id = 'rcmfd_contact_birthdays';
$input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)'));
$p['blocks']['birthdays']['options']['contact_birthdays'] = array(
'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')),
'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0),
);
$input_attrib = array(
'class' => 'calendar_birthday_props',
'disabled' => !$this->rc->config->get('calendar_contact_birthdays'),
);
$sources = array();
$checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib);
foreach ($this->rc->get_address_sources(false, true) as $source) {
$active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : '';
$sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . '&nbsp;' . rcube::Q($source['realname'] ?: $source['name']));
}
$p['blocks']['birthdays']['options']['birthday_adressbooks'] = array(
'title' => rcube::Q($this->gettext('birthdayscalendarsources')),
'content' => join(html::br(), $sources),
);
$field_id = 'rcmfd_birthdays_alarm';
$select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib);
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type) {
$select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
}
$input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib);
$select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib);
foreach (array('-M','-H','-D') as $trigger)
$select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D'));
$p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array(
'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))),
'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . '&nbsp;' . $select_offset->show($preset[1]),
);
}
return $p;
}
/**
* Handler for preferences_save hook.
* Executed on Calendar settings form submit.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_save($p)
{
if ($p['section'] == 'calendar') {
$this->load_driver();
// compose default alarm preset value
$alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST);
$default_alarm = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1];
$birthdays_alarm_offset = get_input_value('_birthdays_alarm_offset', RCUBE_INPUT_POST);
$birthdays_alarm_value = $birthdays_alarm_offset[0] . intval(get_input_value('_birthdays_alarm_value', RCUBE_INPUT_POST)) . $birthdays_alarm_offset[1];
$p['prefs'] = array(
'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
'calendar_timeslots' => intval(get_input_value('_timeslots', RCUBE_INPUT_POST)),
'calendar_first_day' => intval(get_input_value('_first_day', RCUBE_INPUT_POST)),
'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST)),
'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST)),
'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST)),
'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST)),
'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST),
'calendar_default_alarm_offset' => $default_alarm,
'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST),
'calendar_date_format' => null, // clear previously saved values
'calendar_time_format' => null,
'calendar_contact_birthdays' => get_input_value('_contact_birthdays', RCUBE_INPUT_POST) ? true : false,
'calendar_birthday_adressbooks' => (array)get_input_value('_birthday_adressbooks', RCUBE_INPUT_POST),
'calendar_birthdays_alarm_type' => get_input_value('_birthdays_alarm_type', RCUBE_INPUT_POST),
'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,
);
// categories
if (!$this->driver->nocategories) {
$old_categories = $new_categories = array();
foreach ($this->driver->list_categories() as $name => $color) {
$old_categories[md5($name)] = $name;
}
$categories = (array) get_input_value('_categories', RCUBE_INPUT_POST);
$colors = (array) get_input_value('_colors', RCUBE_INPUT_POST);
foreach ($categories as $key => $name) {
$color = preg_replace('/^#/', '', strval($colors[$key]));
// rename categories in existing events -> driver's job
if ($oldname = $old_categories[$key]) {
$this->driver->replace_category($oldname, $name, $color);
unset($old_categories[$key]);
}
else
$this->driver->add_category($name, $color);
$new_categories[$name] = $color;
}
// these old categories have been removed, alter events accordingly -> driver's job
foreach ((array)$old_categories[$key] as $key => $name) {
$this->driver->remove_category($name);
}
$p['prefs']['calendar_categories'] = $new_categories;
}
}
return $p;
}
/**
* Dispatcher for calendar actions initiated by the client
*/
function calendar_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$cal = get_input_value('c', RCUBE_INPUT_GPC);
$success = $reload = false;
if (isset($cal['showalarms']))
$cal['showalarms'] = intval($cal['showalarms']);
switch ($action) {
case "form-new":
case "form-edit":
echo $this->ui->calendar_editform($action, $cal);
exit;
case "new":
$success = $this->driver->create_calendar($cal);
$reload = true;
break;
case "edit":
$success = $this->driver->edit_calendar($cal);
$reload = true;
break;
case "remove":
if ($success = $this->driver->remove_calendar($cal))
$this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
break;
case "subscribe":
if (!$this->driver->subscribe_calendar($cal))
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else {
$error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
$this->rc->output->command('plugin.unlock_saving');
if ($success && $reload)
$this->rc->output->command('plugin.reload_view');
}
/**
* Dispatcher for event actions initiated by the client
*/
function event_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$event = get_input_value('e', RCUBE_INPUT_POST, true);
$success = $reload = $got_msg = false;
// don't notify if modifying a recurring instance (really?)
if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify'])
unset($event['_notify']);
// read old event data in order to find changes
if (($event['_notify'] || $event['decline']) && $action != 'new')
$old = $this->driver->get_event($event);
switch ($action) {
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
$this->prepare_event($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$this->cleanup_event($event);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
break;
case "edit":
$this->prepare_event($event, $action);
if ($success = $this->driver->edit_event($event))
$this->cleanup_event($event);
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->prepare_event($event, $action);
$success = $this->driver->resize_event($event);
$reload = $event['_savemode'] ? 2 : 1;
break;
case "move":
$this->prepare_event($event, $action);
$success = $this->driver->move_event($event);
$reload = $success && $event['_savemode'] ? 2 : 1;
break;
case "remove":
// remove previous deletes
$undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
$this->rc->session->remove('calendar_event_undo');
// search for event if only UID is given
if (!isset($event['calendar']) && $event['uid']) {
if (!($event = $this->driver->get_event($event, true))) {
break;
}
$undo_time = 0;
}
$success = $this->driver->remove_event($event, $undo_time < 1);
$reload = (!$success || $event['_savemode']) ? 2 : 1;
if ($undo_time > 0 && $success) {
$_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
// display message with Undo link.
$msg = html::span(null, $this->gettext('successremoval'))
. ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
$this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
$got_msg = true;
}
else if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
$got_msg = true;
}
// send iTIP reply that participant has declined the event
if ($success && $event['decline']) {
$emails = $this->get_user_emails();
foreach ($old['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = $attendee;
else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$old['attendees'][$i]['status'] = 'DECLINED';
$reply_sender = $attendee['email'];
}
}
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "undo":
// Restore deleted event
$event = $_SESSION['calendar_event_undo']['data'];
if ($event)
$success = $this->driver->restore_event($event);
if ($success) {
$this->rc->session->remove('calendar_event_undo');
$this->rc->output->show_message('calendar.successrestore', 'confirmation');
$got_msg = true;
$reload = 2;
}
break;
case "rsvp-status":
$action = 'rsvp';
$status = $event['fallback'];
$latest = false;
$html = html::div('rsvp-status', $status != 'CANCELLED' ? $this->gettext('acceptinvitation') : '');
if (is_numeric($event['changed']))
$event['changed'] = new DateTime('@'.$event['changed']);
$this->load_driver();
if ($existing = $this->driver->get_event($event, true, false, true)) {
$latest = ($event['sequence'] && $existing['sequence'] == $event['sequence']) || (!$event['sequence'] && $existing['changed'] && $existing['changed'] >= $event['changed']);
$emails = $this->get_user_emails();
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$status = $attendee['status'];
break;
}
}
}
else {
// get a list of writeable calendars
$calendars = $this->driver->list_calendars(false, true);
$calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto', 'is_escaped' => true));
$numcals = 0;
foreach ($calendars as $calendar) {
if (!$calendar['readonly']) {
$calendar_select->add($calendar['name'], $calendar['id']);
$numcals++;
}
}
if ($numcals <= 1)
$calendar_select = null;
}
if ($status == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) {
$html = html::div('rsvp-status', $this->gettext('notanattendee'));
$action = 'import';
}
else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) {
$html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
if ($existing['sequence'] > $event['sequence'] || (!$event['sequence'] && $existing['changed'] && $existing['changed'] > $event['changed'])) {
$action = ''; // nothing to do here, outdated invitation
}
}
$default_calendar = $calendar_select ? $this->get_default_calendar(true) : null;
$this->rc->output->command('plugin.update_event_rsvp_status', array(
'uid' => $event['uid'],
'id' => asciiwords($event['uid'], true),
'saved' => $existing ? true : false,
'latest' => $latest,
'status' => $status,
'action' => $action,
'html' => $html,
'select' => $calendar_select ? html::span('calendar-select', $this->gettext('saveincalendar') . '&nbsp;' . $calendar_select->show($this->rc->config->get('calendar_default_calendar', $default_calendar['id']))) : '',
));
return;
case "rsvp":
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$event = $ev;
if ($success = $this->driver->edit_event($event)) {
$status = get_input_value('status', RCUBE_INPUT_GPC);
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
$itip = $this->load_itip();
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "dismiss":
$event['ids'] = explode(',', $event['id']);
$plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event);
$success = $plugin['success'];
foreach ($event['ids'] as $id) {
if (strpos($id, 'cal:') === 0)
$success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
}
break;
}
// show confirmation/error message
if (!$got_msg) {
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
// send out notifications
if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
$sent = $this->notify_attendees($event, $old, $action);
if ($sent > 0)
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
else if ($sent < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
// update event object on the client or trigger a complete refretch if too complicated
if ($reload) {
$args = array('source' => $event['calendar']);
if ($reload > 1)
$args['refetch'] = true;
else if ($success && $action != 'remove')
$args['update'] = $this->_client_event($this->driver->get_event($event));
$this->rc->output->command('plugin.refresh_calendar', $args);
}
}
/**
* Handler for load-requests from fullcalendar
* This will return pure JSON formatted output
*/
function load_events()
{
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GET),
get_input_value('end', RCUBE_INPUT_GET),
($query = get_input_value('q', RCUBE_INPUT_GET)),
get_input_value('source', RCUBE_INPUT_GET)
);
echo $this->encode($events, !empty($query));
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active calendars and sync them to the client
*/
public function refresh($attr)
{
// refresh the entire calendar every 10th time to also sync deleted events
if (rand(0,10) == 10) {
$this->rc->output->command('plugin.refresh_calendar', array('refetch' => true));
return;
}
foreach ($this->driver->list_calendars(true) as $cal) {
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GET),
get_input_value('end', RCUBE_INPUT_GET),
get_input_value('q', RCUBE_INPUT_GET),
$cal['id'],
1,
$attr['last']
);
foreach ($events as $event) {
$this->rc->output->command('plugin.refresh_calendar',
array('source' => $cal['id'], 'update' => $this->_client_event($event)));
}
}
}
/**
* Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
* This will check for pending notifications and pass them to the client
*/
public function pending_alarms($p)
{
$this->load_driver();
$time = $p['time'] ?: time();
if ($alarms = $this->driver->pending_alarms($time)) {
foreach ($alarms as $alarm) {
$alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
$p['alarms'][] = $alarm;
}
}
// get alarms for birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') {
$cache = $this->rc->get_cache('calendar.birthdayalarms', 'db');
foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) {
$alarm = libcalendaring::get_next_alarm($e);
// overwrite alarm time with snooze value (or null if dismissed)
if ($dismissed = $cache->get($e['id']))
$alarm['time'] = $dismissed['notifyat'];
// add to list if alarm is set
if ($alarm && $alarm['time'] && $alarm['time'] <= $time) {
$e['id'] = 'cal:bday:' . $e['id'];
$e['notifyat'] = $alarm['time'];
$p['alarms'][] = $e;
}
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by libcalendaring
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'cal:bday:') === 0) {
$p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']);
}
else if (strpos($id, 'cal:') === 0) {
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);
}
}
return $p;
}
/**
* Handler for check-recent requests which are accidentally sent to calendar taks
*/
function check_recent()
{
// NOP
$this->rc->output->send();
}
/**
* Hook triggered when a contact is saved
*/
function contact_update($p)
{
// clear birthdays calendar cache
if (!empty($p['record']['birthday'])) {
$cache = $this->rc->get_cache('calendar.birthdays', 'db');
$cache->remove();
}
}
/**
*
*/
function import_events()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
@set_time_limit(0);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
// extract zip file
if ($_FILES['_data']['type'] == 'application/zip') {
$count = 0;
if (class_exists('ZipArchive', false)) {
$zip = new ZipArchive();
if ($zip->open($_FILES['_data']['tmp_name'])) {
$randname = uniqid('zip-' . session_id(), true);
$tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;
mkdir($tmpdir, 0700);
// extract each ical file from the archive and import it
for ($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
if (preg_match('/\.ics$/i', $filename)) {
$tmpfile = $tmpdir . '/' . basename($filename);
if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) {
$count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors);
unlink($tmpfile);
}
}
}
rmdir($tmpdir);
$zip->close();
}
else {
$errors = 1;
$msg = 'Failed to open zip file.';
}
}
else {
$errors = 1;
$msg = 'Zip files are not supported for import.';
}
}
else {
// attempt to import teh uploaded file directly
$count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors);
}
if ($count) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
$this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
}
else if (!$errors) {
$this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
$this->rc->output->command('plugin.import_success', array('source' => $calendar));
}
else {
$this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
}
}
else {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('plugin.import_error', array('message' => $msg));
}
$this->rc->output->send('iframe');
}
/**
* Helper function to parse and import a single .ics file
*/
private function import_from_file($filepath, $calendar, $rangestart, &$errors)
{
$user_email = $this->rc->user->get_username();
$ical = $this->get_ical();
$errors = !$ical->fopen($filepath);
$count = $i = 0;
foreach ($ical as $event) {
// keep the browser connection alive on long import jobs
if (++$i > 100 && $i % 100 == 0) {
echo "<!-- -->";
ob_flush();
}
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['_owner'] = $user_email;
$event['calendar'] = $calendar;
if ($this->driver->new_event($event)) {
$count++;
}
else {
$errors++;
}
}
return $count;
}
/**
* Construct the ics file for exporting events to iCalendar format;
*/
function export_events($terminate = true)
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!isset($start))
$start = 'today -1 year';
if (!is_numeric($start))
$start = strtotime($start . ' 00:00:00');
if (!$end)
$end = 'today +10 years';
if (!is_numeric($end))
$end = strtotime($end . ' 23:59:59');
$attachments = get_input_value('attachments', RCUBE_INPUT_GET);
$calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
if (empty($calname)) $calname = $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0);
}
else
$events = array();
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
$this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
if ($terminate)
exit;
}
/**
* Handler for iCal feed requests
*/
function ical_feed_export()
{
// process HTTP auth info
if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
$_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
$auth = $this->rc->plugins->exec_hook('authenticate', array(
'host' => $this->rc->autoselect_host(),
'user' => trim($_SERVER['PHP_AUTH_USER']),
'pass' => $_SERVER['PHP_AUTH_PW'],
'cookiecheck' => true,
'valid' => true,
));
if ($auth['valid'] && !$auth['abort'])
$this->rc->login($auth['user'], $auth['pass'], $auth['host']);
}
// require HTTP auth
if (empty($_SESSION['user_id'])) {
header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
// decode calendar feed hash
$format = 'ics';
$calhash = get_input_value('_cal', RCUBE_INPUT_GET);
if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
$format = strtolower($m[1]);
$calhash = preg_replace($suff_regex, '', $calhash);
}
if (!strpos($calhash, ':'))
$calhash = base64_decode($calhash);
list($user, $_GET['source']) = explode(':', $calhash, 2);
// sanity check user
if ($this->rc->user->get_username() == $user) {
$this->load_driver();
$this->export_events(false);
}
else {
header('HTTP/1.0 404 Not Found');
}
// don't save session data
session_destroy();
exit;
}
/**
*
*/
function load_settings()
{
$this->lib->load_settings();
$this->defaults += $this->lib->defaults;
$settings = array();
// configuration
$settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
$settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
$settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
$settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
$settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
$settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
$settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
$settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
$settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
foreach ($this->rc->user->list_identities() as $rec) {
if (!$identity)
$identity = $rec;
$identity['emails'][] = $rec['email'];
$settings['identities'][$rec['identity_id']] = $rec['email'];
}
$identity['emails'][] = $this->rc->user->get_username();
$settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])));
}
return $settings;
}
/**
* Encode events as JSON
*
* @param array Events as array
* @param boolean Add CSS class names according to calendar and categories
* @return string JSON encoded events
*/
function encode($events, $addcss = false)
{
$json = array();
foreach ($events as $event) {
$json[] = $this->_client_event($event, $addcss);
}
return json_encode($json);
}
/**
* Convert an event object to be used on the client
*/
private function _client_event($event, $addcss = false)
{
// compose a human readable strings for alarms_text and recurrence_text
if ($event['alarms'])
$event['alarms_text'] = libcalendaring::alarms_text($event['alarms']);
if ($event['recurrence']) {
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
if ($event['recurrence']['UNTIL'])
$event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c');
unset($event['recurrence']['EXCEPTIONS']);
}
foreach ((array)$event['attachments'] as $k => $attachment) {
$event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
// check for organizer in attendees list
$organizer = null;
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
if ($organizer === null && !empty($event['organizer'])) {
$organizer = $event['organizer'];
$organizer['role'] = 'ORGANIZER';
if (!is_array($event['attendees']))
$event['attendees'] = array();
array_unshift($event['attendees'], $organizer);
}
// mapping url => vurl because of the fullcalendar client script
$event['vurl'] = $event['url'];
unset($event['url']);
return array(
'_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'),
'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
// 'changed' might be empty for event recurrences (Bug #2185)
'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
'title' => strval($event['title']),
'description' => strval($event['description']),
'location' => strval($event['location']),
'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true),
'allDay' => ($event['allday'] == 1),
) + $event;
}
/**
* Render localized text describing the recurrence rule of an event
*/
private function _recurrence_text($rrule)
{
+ // derive missing FREQ and INTERVAL from RDATE list
+ if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
+ $first = $rrule['RDATE'][0];
+ $second = $rrule['RDATE'][1];
+ if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
+ $diff = $first->diff($second);
+ foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
+ if ($diff->$k != 0) {
+ $rrule['FREQ'] = $freq;
+ $rrule['INTERVAL'] = $diff->$k;
+ break;
+ }
+ }
+ }
+ $rrule['UNTIL'] = end($rrule['RDATE']);
+ }
+
// TODO: finish this
$freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
$details = '';
switch ($rrule['FREQ']) {
case 'DAILY':
$freq .= $this->gettext('days');
break;
case 'WEEKLY':
$freq .= $this->gettext('weeks');
break;
case 'MONTHLY':
$freq .= $this->gettext('months');
break;
case 'YEARLY':
$freq .= $this->gettext('years');
break;
}
if ($rrule['INTERVAL'] <= 1)
$freq = $this->gettext(strtolower($rrule['FREQ']));
if ($rrule['COUNT'])
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
else if ($rrule['UNTIL'])
$until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
else
$until = $this->gettext('forever');
return rtrim($freq . $details . ', ' . $until);
}
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
/**
* TEMPORARY: generate random event data for testing
* Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500
*/
public function generate_randomdata()
{
$num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
$cats = array_keys($this->driver->list_categories());
$cals = $this->driver->list_calendars(true);
$count = 0;
while ($count++ < $num) {
$start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300;
$duration = round(rand(30, 360) / 30) * 30 * 60;
$allday = rand(0,20) > 18;
$alarm = rand(-30,12) * 5;
$fb = rand(0,2);
if (date('G', $start) > 23)
$start -= 3600;
if ($allday) {
$start = strtotime(date('Y-m-d 00:00:00', $start));
$duration = 86399;
}
$title = '';
$len = rand(2, 12);
$words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
// $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
for ($i = 0; $i < $len; $i++)
$title .= $words[rand(0,count($words)-1)] . " ";
$this->driver->new_event(array(
'uid' => $this->generate_uid(),
'start' => new DateTime('@'.$start),
'end' => new DateTime('@'.($start + $duration)),
'allday' => $allday,
'title' => rtrim($title),
'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
'categories' => $cats[array_rand($cats)],
'calendar' => array_rand($cals),
'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
'priority' => rand(0,9),
));
}
$this->rc->output->redirect('');
}
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
$this->lib->attachment_upload(self::SESSION_KEY, 'cal:');
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
// show loading page
if (!empty($_GET['_preload'])) {
return $this->lib->attachment_loading_page();
}
$event_id = get_input_value('_event', RCUBE_INPUT_GPC);
$calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
$id = get_input_value('_id', RCUBE_INPUT_GPC);
$event = array('id' => $event_id, 'calendar' => $calendar);
$attachment = $this->driver->get_attachment($id, $event);
// show part page
if (!empty($_GET['_frame'])) {
$this->lib->attachment = $attachment;
$this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
$this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
$this->rc->output->send('calendar.attachment');
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $event);
$this->lib->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Prepares new/edited event properties before save
*/
private function prepare_event(&$event, $action)
{
// convert dates into DateTime objects in user's current timezone
$event['start'] = new DateTime($event['start'], $this->timezone);
$event['end'] = new DateTime($event['end'], $this->timezone);
// start/end is all we need for 'move' action (#1480)
if ($action == 'move') {
return;
}
if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL']))
$event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone);
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
}
}
}
}
$event['attachments'] = $attachments;
// check for organizer in attendees
if ($action == 'new' || $action == 'edit') {
if (!$event['attendees'])
$event['attendees'] = array();
$emails = $this->get_user_emails();
$organizer = $owner = false;
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = $i;
if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails))
$owner = $i;
else if (!isset($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = true;
}
// set new organizer identity
if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) {
$event['attendees'][$organizer]['name'] = $identity['name'];
$event['attendees'][$organizer]['email'] = $identity['email'];
}
// set owner as organizer if yet missing
if ($organizer === false && $owner !== false) {
$event['attendees'][$owner]['role'] = 'ORGANIZER';
unset($event['attendees'][$owner]['rsvp']);
}
else if ($organizer === false && $action == 'new' && ($identity = $this->rc->user->get_identity($event['_identity'])) && $identity['email']) {
array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
}
}
// mapping url => vurl because of the fullcalendar client script
$event['url'] = $event['vurl'];
unset($event['vurl']);
}
/**
* Releases some resources after successful event save
*/
private function cleanup_event(&$event)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* Send out an invitation/notification to all event attendees
*/
private function notify_attendees($event, $old, $action = 'edit')
{
if ($action == 'remove') {
$event['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->get_user_emails();
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method);
// list existing attendees from $old event
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0;
foreach ((array)$event['attendees'] as $attendee) {
// skip myself for obvious reasons
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails))
continue;
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
// finally send the message
if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Echo simple free/busy status text for the given user and time range
*/
public function freebusy_status()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
// convert dates into unix timestamps
if (!empty($start) && !is_numeric($start)) {
$dts = new DateTime($start, $this->timezone);
$start = $dts->format('U');
}
if (!empty($end) && !is_numeric($end)) {
$dte = new DateTime($end, $this->timezone);
$end = $dte->format('U');
}
if (!$start) $start = time();
if (!$end) $end = $start + 3600;
$fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
$status = 'UNKNOWN';
// if the backend has free-busy information
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
if (is_array($fblist)) {
$status = 'FREE';
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $end && $to > $start) {
$status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
break;
}
}
}
// let this information be cached for 5min
send_future_expire_header(300);
echo $status;
exit;
}
/**
* Return a list of free/busy time slots within the given period
* Echo data in JSON encoding
*/
public function freebusy_times()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
$interval = intval(get_input_value('interval', RCUBE_INPUT_GPC));
$strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
// convert dates into unix timestamps
if (!empty($start) && !is_numeric($start)) {
$dts = new DateTime($start, $this->timezone);
$start = $dts->format('U');
}
if (!empty($end) && !is_numeric($end)) {
$dte = new DateTime($end, $this->timezone);
$end = $dte->format('U');
}
if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 30;
if (!$interval) $interval = 60; // 1 hour
if (!$dte) {
$dts = new DateTime('@'.$start);
$dts->setTimezone($this->timezone);
}
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
$slots = array();
// build a list from $start till $end with blocks representing the fb-status
for ($s = 0, $t = $start; $t <= $end; $s++) {
$status = self::FREEBUSY_UNKNOWN;
$t_end = $t + $interval * 60;
$dt = new DateTime('@'.$t);
$dt->setTimezone($this->timezone);
// determine attendee's status
if (is_array($fblist)) {
$status = self::FREEBUSY_FREE;
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $t_end && $to > $t) {
$status = isset($type) ? $type : self::FREEBUSY_BUSY;
if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
break;
}
}
}
$slots[$s] = $status;
$times[$s] = intval($dt->format($strformat));
$t = $t_end;
}
$dte = new DateTime('@'.$t_end);
$dte->setTimezone($this->timezone);
// let this information be cached for 5min
send_future_expire_header(300);
echo json_encode(array(
'email' => $email,
'start' => $dts->format('c'),
'end' => $dte->format('c'),
'interval' => $interval,
'slots' => $slots,
'times' => $times,
));
exit;
}
/**
* Handler for printing calendars
*/
public function print_view()
{
$title = $this->gettext('print');
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$view = 'agendaDay';
$this->rc->output->set_env('view',$view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
if ($range = get_input_value('range', RCUBE_INPUT_GPC))
$this->rc->output->set_env('listRange', intval($range));
if (isset($_REQUEST['sections']))
$this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC));
if ($search = get_input_value('search', RCUBE_INPUT_GPC)) {
$this->rc->output->set_env('search', $search);
$title .= ' "' . $search . '"';
}
// Add CSS stylesheets to the page header
$skin_path = $this->local_skin_path();
$this->include_stylesheet($skin_path . '/fullcalendar.css');
$this->include_stylesheet($skin_path . '/print.css');
// Add JS files to the page header
$this->include_script('print.js');
$this->include_script('lib/js/fullcalendar.js');
$this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
$this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
$this->rc->output->set_pagetitle($title);
$this->rc->output->send("calendar.print");
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['calendar.'.$label] = $this->gettext($label);
$texts['calendar.new_event'] = $this->gettext('createfrommail');
$this->ui->init_templates();
$this->ui->calendar_list(); # set env['calendars']
echo $this->api->output->parse('calendar.eventedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n".
"rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
"rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
"rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
/**
* Compare two event objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing event properties
*/
public static function event_diff($a, $b)
{
$diff = array();
$ignore = array('changed' => 1, 'attachments' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
}
// only compare number of attachments
if (count($a['attachments']) != count($b['attachments']))
$diff[] = 'attachments';
return $diff;
}
/**** Event invitation plugin hooks ****/
/**
* Handler for URLs that allow an invitee to respond on his invitation mail
*/
public function itip_attend_response($p)
{
if ($p['action'] == 'attend') {
$this->ui->init();
$this->rc->output->set_env('task', 'calendar'); // override some env vars
$this->rc->output->set_env('refresh_interval', 0);
$this->rc->output->set_pagetitle($this->gettext('calendar'));
$itip = $this->load_itip();
$token = get_input_value('_t', RCUBE_INPUT_GPC);
// read event info stored under the given token
if ($invitation = $itip->get_invitation($token)) {
$this->token = $token;
$this->event = $invitation['event'];
// show message about cancellation
if ($invitation['cancelled']) {
$this->invitestatus = html::div('rsvp-status declined', $this->gettext('eventcancelled'));
}
// save submitted RSVP status
else if (!empty($_POST['rsvp'])) {
$status = null;
foreach (array('accepted','tentative','declined') as $method) {
if ($_POST['rsvp'] == $this->gettext('itip' . $method)) {
$status = $method;
break;
}
}
// send itip reply to organizer
if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
$this->invitestatus = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
}
else
$this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
// if user is logged in...
if ($this->rc->user->ID) {
$this->load_driver();
$invitation = $itip->get_invitation($token);
// save the event to his/her default calendar if not yet present
if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar(true))) {
$invitation['event']['calendar'] = $calendar['id'];
if ($this->driver->new_event($invitation['event']))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
}
}
}
$this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
$this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
if (!$this->invitestatus)
$this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
$this->rc->output->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event['title']);
}
else
$this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
$this->rc->output->send('calendar.itipattend');
}
}
/**
*
*/
public function itip_event_inviteform($attrib)
{
$hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
}
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->message->mime_parts as $part) {
if ($this->is_vcalendar($part)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ics_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part)
$this->ics_parts = array($itip_part);
}
/**
* Add UI element to copy event invitations or updates to the calendar
*/
public function mail_messagebody_html($p)
{
// load iCalendar functions (if necessary)
if (!empty($this->ics_parts)) {
$this->get_ical();
}
$html = '';
foreach ($this->ics_parts as $mime_id) {
$part = $this->message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
$events = $this->ical->import($this->message->get_part_content($mime_id), $charset);
$title = $this->gettext('title');
$date = rcube_utils::anytodatetime($this->message->headers->date);
// successfully parsed events?
if (empty($events))
continue;
// show a box for every event in the file
foreach ($events as $idx => $event) {
// define buttons according to method
if ($this->ical->method == 'REPLY') {
$title = $this->gettext('itipreply');
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('updateattendeestatus'),
));
}
else if ($this->ical->method == 'REQUEST') {
$emails = $this->get_user_emails();
$title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
// add (hidden) buttons and activate them from asyncronous request
foreach (array('accepted','tentative','declined') as $method) {
$rsvp_buttons .= html::tag('input', array(
'type' => 'button',
'class' => "button $method",
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')",
'value' => $this->gettext('itip' . $method),
));
}
$import_button = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
// check my status
$status = 'unknown';
foreach ($event['attendees'] as $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$status = strtoupper($attendee['status']);
break;
}
}
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$changed = is_object($event['changed']) ? $event['changed'] : $date;
$script = json_serialize(array(
'uid' => $event['uid'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => $status,
));
$this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
}
else if ($this->ical->method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
// create buttons to be activated from async request checking existence of this event in local calendars
$button_import = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
$button_remove = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$changed = is_object($event['changed']) ? $event['changed'] : $date;
$script = json_serialize(array(
'uid' => $event['uid'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => 'CANCELLED',
));
$this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
}
else {
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
}
// show event details with buttons
$html .= html::div('calendar-invitebox', $this->ui->event_details_table($event, $title) . $buttons_pre . html::div('rsvp-buttons', $buttons));
// limit listing
if ($idx >= 3)
break;
}
}
// prepend event boxes to message body
if ($html) {
$this->ui->init();
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
}
return $p;
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_event()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$mime_id = get_input_value('_part', RCUBE_INPUT_POST);
$status = get_input_value('_status', RCUBE_INPUT_POST);
$delete = intval(get_input_value('_del', RCUBE_INPUT_POST));
$charset = RCMAIL_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $imap->get_message_part($uid, $mime_id);
if ($part->ctype_parameters['charset'])
$charset = $part->ctype_parameters['charset'];
$headers = $imap->get_message_headers($uid);
}
$events = $this->get_ical()->import($part, $charset);
$error_msg = $this->gettext('errorimportingevent');
$success = false;
// successfully parsed events?
if (!empty($events) && ($event = $events[$index])) {
// find writeable calendar to store event
$cal_id = !empty($_REQUEST['_calendar']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : null;
$calendars = $this->driver->list_calendars(false, true);
$calendar = $calendars[$cal_id] ?: $this->get_default_calendar(true);
// update my attendee status according to submitted method
if (!empty($status)) {
$organizer = null;
$emails = $this->get_user_emails();
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
$reply_sender = $attendee['email'];
}
}
// add attendee with this user's default identity if not listed
if (!$reply_sender) {
$sender_identity = $this->rc->user->get_identity();
$event['attendees'][] = array(
'name' => $sender_identity['name'],
'email' => $sender_identity['email'],
'role' => 'OPT-PARTICIPANT',
'status' => strtoupper($status),
);
}
}
// save to calendar
if ($calendar && !$calendar['readonly']) {
$event['calendar'] = $calendar['id'];
// check for existing event with the same UID
$existing = $this->driver->get_event($event['uid'], true, false, true);
if ($existing) {
// only update attendee status
if ($this->ical->method == 'REPLY') {
// try to identify the attendee using the email sender address
$sender = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
$sender_utf = rcube_idn_to_utf8($sender);
$existing_attendee = -1;
foreach ($existing['attendees'] as $i => $attendee) {
if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
$existing_attendee = $i;
break;
}
}
$event_attendee = null;
foreach ($event['attendees'] as $attendee) {
if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
$event_attendee = $attendee;
break;
}
}
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee;
$success = $this->driver->edit_event($existing);
}
// update the entire attendees block
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
$existing['attendees'][] = $event_attendee;
$success = $this->driver->edit_event($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// delete the event when declined (#1670)
else if ($status == 'declined' && $delete) {
$deleted = $this->driver->remove_event($existing, true);
$success = true;
}
// import the (newer) event
else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
$event['id'] = $existing['id'];
$event['calendar'] = $existing['calendar'];
if ($status == 'declined') // show me as free when declined (#1670)
$event['free_busy'] = 'free';
$success = $this->driver->edit_event($event);
}
else if (!empty($status)) {
$existing['attendees'] = $event['attendees'];
if ($status == 'declined') // show me as free when declined (#1670)
$existing['free_busy'] = 'free';
$success = $this->driver->edit_event($existing);
}
else
$error_msg = $this->gettext('newerversionexists');
}
else if (!$existing && $status != 'declined') {
$success = $this->driver->new_event($event);
}
else if ($status == 'declined')
$error_msg = null;
}
else if ($status == 'declined')
$error_msg = null;
else
$error_msg = $this->gettext('nowritecalendarfound');
}
if ($success) {
$message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : 'importedsuccessfully');
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
$this->rc->output->command('plugin.fetch_event_rsvp_status', array(
'uid' => $event['uid'],
'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => strtoupper($status),
));
$error_msg = null;
}
else if ($error_msg)
$this->rc->output->command('display_message', $error_msg, 'error');
// send iTip reply
if ($this->ical->method == 'REQUEST' && $organizer && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**
* Read email message and return contents for a new event based on that message
*/
public function mail_message2event()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$event = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$event['title'] = trim($message->subject);
$event['description'] = trim($message->first_text_part());
// copy mail attachments to event
if ($message->attachments) {
$eventid = 'cal:';
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $eventid;
$_SESSION[self::SESSION_KEY]['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $eventid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
$attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
$event['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2event_dialog', $event);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @return boolean True if part is of type vcard
*/
private function is_vcalendar($part)
{
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))
);
}
/**
* Get a list of email addresses of the current user (from login and identities)
*/
private function get_user_emails()
{
$emails = array();
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
$emails = array_map('strtolower', $plugin['emails']);
if ($plugin['abort']) {
return $emails;
}
$emails[] = $this->rc->user->get_username();
foreach ($this->rc->user->list_identities() as $identity)
$emails[] = strtolower($identity['email']);
return array_unique($emails);
}
/**
* Build an absolute URL with the given parameters
*/
public function get_url($param = array())
{
$param += array('task' => 'calendar');
$schema = 'http';
$default_port = 80;
if (rcube_https_check()) {
$schema = 'https';
$default_port = 443;
}
$url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
if ($_SERVER['SERVER_PORT'] != $default_port)
$url .= ':' . $_SERVER['SERVER_PORT'];
if (dirname($_SERVER['SCRIPT_NAME']) != '/')
$url .= dirname($_SERVER['SCRIPT_NAME']);
$url .= preg_replace('!^\./!', '/', $this->rc->url($param));
return $url;
}
public function ical_feed_hash($source)
{
return base64_encode($this->rc->user->get_username() . ':' . $source);
}
}
diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php
index 81f0857d..35f884c7 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -1,1673 +1,1705 @@
<?php
/**
* This is a modified copy of Horde/Date/Recurrence.php
* Pull the latest version of this file from the PEAR channel of the Horde
* project at http://pear.horde.org by installing the Horde_Date package.
*/
if (!class_exists('Horde_Date'))
require_once(dirname(__FILE__) . '/Horde_Date.php');
// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
class Horde_Date_Translation
{
function t($arg) { return $arg; }
function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); }
}
/**
* This file contains the Horde_Date_Recurrence class and according constants.
*
* Copyright 2007-2012 Horde LLC (http://www.horde.org/)
*
* See the enclosed file COPYING for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package Date
*/
/**
* The Horde_Date_Recurrence class implements algorithms for calculating
* recurrences of events, including several recurrence types, intervals,
* exceptions, and conversion from and to vCalendar and iCalendar recurrence
* rules.
*
* All methods expecting dates as parameters accept all values that the
* Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
* object, an ISO time string or a hash.
*
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @package Date
*/
class Horde_Date_Recurrence
{
/** No Recurrence **/
const RECUR_NONE = 0;
/** Recurs daily. */
const RECUR_DAILY = 1;
/** Recurs weekly. */
const RECUR_WEEKLY = 2;
/** Recurs monthly on the same date. */
const RECUR_MONTHLY_DATE = 3;
/** Recurs monthly on the same week day. */
const RECUR_MONTHLY_WEEKDAY = 4;
/** Recurs yearly on the same date. */
const RECUR_YEARLY_DATE = 5;
/** Recurs yearly on the same day of the year. */
const RECUR_YEARLY_DAY = 6;
/** Recurs yearly on the same week day. */
const RECUR_YEARLY_WEEKDAY = 7;
/**
* The start time of the event.
*
* @var Horde_Date
*/
public $start;
/**
* The end date of the recurrence interval.
*
* @var Horde_Date
*/
public $recurEnd = null;
/**
* The number of recurrences.
*
* @var integer
*/
public $recurCount = null;
/**
* The type of recurrence this event follows. RECUR_* constant.
*
* @var integer
*/
public $recurType = self::RECUR_NONE;
/**
* The length of time between recurrences. The time unit depends on the
* recurrence type.
*
* @var integer
*/
public $recurInterval = 1;
/**
* Any additional recurrence data.
*
* @var integer
*/
public $recurData = null;
/**
* BYDAY recurrence number
*
* @var integer
*/
public $recurNthDay = null;
/**
* BYMONTH recurrence data
*
* @var array
*/
public $recurMonths = array();
+ /**
+ * RDATE recurrence values
+ *
+ * @var array
+ */
+ public $rdates = array();
+
/**
* All the exceptions from recurrence for this event.
*
* @var array
*/
public $exceptions = array();
/**
* All the dates this recurrence has been marked as completed.
*
* @var array
*/
public $completions = array();
/**
* Constructor.
*
* @param Horde_Date $start Start of the recurring event.
*/
public function __construct($start)
{
$this->start = new Horde_Date($start);
}
/**
* Resets the class properties.
*/
public function reset()
{
$this->recurEnd = null;
$this->recurCount = null;
$this->recurType = self::RECUR_NONE;
$this->recurInterval = 1;
$this->recurData = null;
$this->exceptions = array();
$this->completions = array();
}
/**
* Checks if this event recurs on a given day of the week.
*
* @param integer $dayMask A mask consisting of Horde_Date::MASK_*
* constants specifying the day(s) to check.
*
* @return boolean True if this event recurs on the given day(s).
*/
public function recurOnDay($dayMask)
{
return ($this->recurData & $dayMask);
}
/**
* Specifies the days this event recurs on.
*
* @param integer $dayMask A mask consisting of Horde_Date::MASK_*
* constants specifying the day(s) to recur on.
*/
public function setRecurOnDay($dayMask)
{
$this->recurData = $dayMask;
}
/**
*
* @param integer $nthDay The nth weekday of month to repeat events on
*/
public function setRecurNthWeekday($nth)
{
$this->recurNthDay = (int)$nth;
}
/**
*
* @return integer The nth weekday of month to repeat events.
*/
public function getRecurNthWeekday()
{
return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
}
/**
* Specifies the months for yearly (weekday) recurrence
*
* @param array $months List of months (integers) this event recurs on.
*/
function setRecurByMonth($months)
{
$this->recurMonths = (array)$months;
}
/**
* Returns a list of months this yearly event recurs on
*
* @return array List of months (integers) this event recurs on.
*/
function getRecurByMonth()
{
return $this->recurMonths;
}
/**
* Returns the days this event recurs on.
*
* @return integer A mask consisting of Horde_Date::MASK_* constants
* specifying the day(s) this event recurs on.
*/
public function getRecurOnDays()
{
return $this->recurData;
}
/**
* Returns whether this event has a specific recurrence type.
*
* @param integer $recurrence RECUR_* constant of the
* recurrence type to check for.
*
* @return boolean True if the event has the specified recurrence type.
*/
public function hasRecurType($recurrence)
{
return ($recurrence == $this->recurType);
}
/**
* Sets a recurrence type for this event.
*
* @param integer $recurrence A RECUR_* constant.
*/
public function setRecurType($recurrence)
{
$this->recurType = $recurrence;
}
/**
* Returns recurrence type of this event.
*
* @return integer A RECUR_* constant.
*/
public function getRecurType()
{
return $this->recurType;
}
/**
* Returns a description of this event's recurring type.
*
* @return string Human readable recurring type.
*/
public function getRecurName()
{
switch ($this->getRecurType()) {
case self::RECUR_NONE: return Horde_Date_Translation::t("No recurrence");
case self::RECUR_DAILY: return Horde_Date_Translation::t("Daily");
case self::RECUR_WEEKLY: return Horde_Date_Translation::t("Weekly");
case self::RECUR_MONTHLY_DATE:
case self::RECUR_MONTHLY_WEEKDAY: return Horde_Date_Translation::t("Monthly");
case self::RECUR_YEARLY_DATE:
case self::RECUR_YEARLY_DAY:
case self::RECUR_YEARLY_WEEKDAY: return Horde_Date_Translation::t("Yearly");
}
}
/**
* Sets the length of time between recurrences of this event.
*
* @param integer $interval The time between recurrences.
*/
public function setRecurInterval($interval)
{
if ($interval > 0) {
$this->recurInterval = $interval;
}
}
/**
* Retrieves the length of time between recurrences of this event.
*
* @return integer The number of seconds between recurrences.
*/
public function getRecurInterval()
{
return $this->recurInterval;
}
/**
* Sets the number of recurrences of this event.
*
* @param integer $count The number of recurrences.
*/
public function setRecurCount($count)
{
if ($count > 0) {
$this->recurCount = (int)$count;
// Recurrence counts and end dates are mutually exclusive.
$this->recurEnd = null;
} else {
$this->recurCount = null;
}
}
/**
* Retrieves the number of recurrences of this event.
*
* @return integer The number recurrences.
*/
public function getRecurCount()
{
return $this->recurCount;
}
/**
* Returns whether this event has a recurrence with a fixed count.
*
* @return boolean True if this recurrence has a fixed count.
*/
public function hasRecurCount()
{
return isset($this->recurCount);
}
/**
* Sets the start date of the recurrence interval.
*
* @param Horde_Date $start The recurrence start.
*/
public function setRecurStart($start)
{
$this->start = clone $start;
}
/**
* Retrieves the start date of the recurrence interval.
*
* @return Horde_Date The recurrence start.
*/
public function getRecurStart()
{
return $this->start;
}
/**
* Sets the end date of the recurrence interval.
*
* @param Horde_Date $end The recurrence end.
*/
public function setRecurEnd($end)
{
if (!empty($end)) {
// Recurrence counts and end dates are mutually exclusive.
$this->recurCount = null;
$this->recurEnd = clone $end;
} else {
$this->recurEnd = $end;
}
}
/**
* Retrieves the end date of the recurrence interval.
*
* @return Horde_Date The recurrence end.
*/
public function getRecurEnd()
{
return $this->recurEnd;
}
/**
* Returns whether this event has a recurrence end.
*
* @return boolean True if this recurrence ends.
*/
public function hasRecurEnd()
{
return isset($this->recurEnd) && isset($this->recurEnd->year) &&
$this->recurEnd->year != 9999;
}
/**
* Finds the next recurrence of this event that's after $afterDate.
*
* @param Horde_Date|string $after Return events after this date.
*
* @return Horde_Date|boolean The date of the next recurrence or false
* if the event does not recur after
* $afterDate.
*/
public function nextRecurrence($after)
{
if (!($after instanceof Horde_Date)) {
$after = new Horde_Date($after);
} else {
$after = clone($after);
}
// Make sure $after and $this->start are in the same TZ
$after->setTimezone($this->start->timezone);
if ($this->start->compareDateTime($after) >= 0) {
return clone $this->start;
}
- if ($this->recurInterval == 0) {
+ if ($this->recurInterval == 0 && empty($this->rdates)) {
return false;
}
switch ($this->getRecurType()) {
case self::RECUR_DAILY:
$diff = $this->start->diff($after);
$recur = ceil($diff / $this->recurInterval);
if ($this->recurCount && $recur >= $this->recurCount) {
return false;
}
$recur *= $this->recurInterval;
$next = $this->start->add(array('day' => $recur));
if ((!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) &&
$next->compareDateTime($after) >= 0) {
return $next;
}
break;
case self::RECUR_WEEKLY:
if (empty($this->recurData)) {
return false;
}
$start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'),
$this->start->year);
$start_week->timezone = $this->start->timezone;
$start_week->hour = $this->start->hour;
$start_week->min = $this->start->min;
$start_week->sec = $this->start->sec;
// Make sure we are not at the ISO-8601 first week of year while
// still in month 12...OR in the ISO-8601 last week of year while
// in month 1 and adjust the year accordingly.
$week = $after->format('W');
if ($week == 1 && $after->month == 12) {
$theYear = $after->year + 1;
} elseif ($week >= 52 && $after->month == 1) {
$theYear = $after->year - 1;
} else {
$theYear = $after->year;
}
$after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear);
$after_week->timezone = $this->start->timezone;
$after_week_end = clone $after_week;
$after_week_end->mday += 7;
$diff = $start_week->diff($after_week);
$interval = $this->recurInterval * 7;
$repeats = floor($diff / $interval);
if ($diff % $interval < 7) {
$recur = $diff;
} else {
/**
* If the after_week is not in the first week interval the
* search needs to skip ahead a complete interval. The way it is
* calculated here means that an event that occurs every second
* week on Monday and Wednesday with the event actually starting
* on Tuesday or Wednesday will only have one incidence in the
* first week.
*/
$recur = $interval * ($repeats + 1);
}
if ($this->hasRecurCount()) {
$recurrences = 0;
/**
* Correct the number of recurrences by the number of events
* that lay between the start of the start week and the
* recurrence start.
*/
$next = clone $start_week;
while ($next->compareDateTime($this->start) < 0) {
if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
$recurrences--;
}
++$next->mday;
}
if ($repeats > 0) {
$weekdays = $this->recurData;
$total_recurrences_per_week = 0;
while ($weekdays > 0) {
if ($weekdays % 2) {
$total_recurrences_per_week++;
}
$weekdays = ($weekdays - ($weekdays % 2)) / 2;
}
$recurrences += $total_recurrences_per_week * $repeats;
}
}
$next = clone $start_week;
$next->mday += $recur;
while ($next->compareDateTime($after) < 0 &&
$next->compareDateTime($after_week_end) < 0) {
if ($this->hasRecurCount()
&& $next->compareDateTime($after) < 0
&& $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
$recurrences++;
}
++$next->mday;
}
if ($this->hasRecurCount() &&
$recurrences >= $this->recurCount) {
return false;
}
if (!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) {
if ($next->compareDateTime($after_week_end) >= 0) {
return $this->nextRecurrence($after_week_end);
}
while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
$next->compareDateTime($after_week_end) < 0) {
++$next->mday;
}
if (!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) {
if ($next->compareDateTime($after_week_end) >= 0) {
return $this->nextRecurrence($after_week_end);
} else {
return $next;
}
}
}
break;
case self::RECUR_MONTHLY_DATE:
$start = clone $this->start;
if ($after->compareDateTime($start) < 0) {
$after = clone $start;
} else {
$after = clone $after;
}
// If we're starting past this month's recurrence of the event,
// look in the next month on the day the event recurs.
if ($after->mday > $start->mday) {
++$after->month;
$after->mday = $start->mday;
}
// Adjust $start to be the first match.
$offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
if ($this->recurCount &&
($offset / $this->recurInterval) >= $this->recurCount) {
return false;
}
$start->month += $offset;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
// Bail if we've gone past the end of recurrence.
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($start) < 0) {
return false;
}
if ($start->isValid()) {
return $start;
}
// If the interval is 12, and the date isn't valid, then we
// need to see if February 29th is an option. If not, then the
// event will _never_ recur, and we need to stop checking to
// avoid an infinite loop.
if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
return false;
}
// Add the recurrence interval.
$start->month += $this->recurInterval;
} while (true);
break;
case self::RECUR_MONTHLY_WEEKDAY:
// Start with the start date of the event.
$estart = clone $this->start;
// What day of the week, and week of the month, do we recur on?
if (isset($this->recurNthDay)) {
$nth = $this->recurNthDay;
$weekday = log($this->recurData, 2);
} else {
$nth = ceil($this->start->mday / 7);
$weekday = $estart->dayOfWeek();
}
// Adjust $estart to be the first candidate.
$offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
// Adjust our working date until it's after $after.
$estart->month += $offset - $this->recurInterval;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
$estart->month += $this->recurInterval;
$next = clone $estart;
$next->setNthWeekday($weekday, $nth);
if ($next->compareDateTime($after) < 0) {
// We haven't made it past $after yet, try again.
continue;
}
if ($this->hasRecurEnd() &&
$next->compareDateTime($this->recurEnd) > 0) {
// We've gone past the end of recurrence; we can give up
// now.
return false;
}
// We have a candidate to return.
break;
} while (true);
return $next;
case self::RECUR_YEARLY_DATE:
// Start with the start date of the event.
$estart = clone $this->start;
$after = clone $after;
if ($after->month > $estart->month ||
($after->month == $estart->month && $after->mday > $estart->mday)) {
++$after->year;
$after->month = $estart->month;
$after->mday = $estart->mday;
}
// Seperate case here for February 29th
if ($estart->month == 2 && $estart->mday == 29) {
while (!Horde_Date_Utils::isLeapYear($after->year)) {
++$after->year;
}
}
// Adjust $estart to be the first candidate.
$offset = $after->year - $estart->year;
if ($offset > 0) {
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
$estart->year += $offset;
}
// We've gone past the end of recurrence; give up.
if ($this->recurCount &&
$offset >= $this->recurCount) {
return false;
}
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($estart) < 0) {
return false;
}
return $estart;
case self::RECUR_YEARLY_DAY:
// Check count first.
$dayofyear = $this->start->dayOfYear();
$count = ($after->year - $this->start->year) / $this->recurInterval + 1;
if ($this->recurCount &&
($count > $this->recurCount ||
($count == $this->recurCount &&
$after->dayOfYear() > $dayofyear))) {
return false;
}
// Start with a rough interval.
$estart = clone $this->start;
$estart->year += floor($count - 1) * $this->recurInterval;
// Now add the difference to the required day of year.
$estart->mday += $dayofyear - $estart->dayOfYear();
// Add an interval if the estimation was wrong.
if ($estart->compareDate($after) < 0) {
$estart->year += $this->recurInterval;
$estart->mday += $dayofyear - $estart->dayOfYear();
}
// We've gone past the end of recurrence; give up.
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($estart) < 0) {
return false;
}
return $estart;
case self::RECUR_YEARLY_WEEKDAY:
// Start with the start date of the event.
$estart = clone $this->start;
// What day of the week, and week of the month, do we recur on?
if (isset($this->recurNthDay)) {
$nth = $this->recurNthDay;
$weekday = log($this->recurData, 2);
} else {
$nth = ceil($this->start->mday / 7);
$weekday = $estart->dayOfWeek();
}
// Adjust $estart to be the first candidate.
$offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
// Adjust our working date until it's after $after.
$estart->year += $offset - $this->recurInterval;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
$estart->year += $this->recurInterval;
$next = clone $estart;
$next->setNthWeekday($weekday, $nth);
if ($next->compareDateTime($after) < 0) {
// We haven't made it past $after yet, try again.
continue;
}
if ($this->hasRecurEnd() &&
$next->compareDateTime($this->recurEnd) > 0) {
// We've gone past the end of recurrence; we can give up
// now.
return false;
}
// We have a candidate to return.
break;
} while (true);
return $next;
}
+ // fall-back to RDATE properties
+ if (!empty($this->rdates)) {
+ $next = clone $this->start;
+ foreach ($this->rdates as $rdate) {
+ $next->year = $rdate->year;
+ $next->month = $rdate->month;
+ $next->mday = $rdate->mday;
+ if ($next->compareDateTime($after) > 0) {
+ return $next;
+ }
+ }
+ }
+
// We didn't find anything, the recurType was bad, or something else
// went wrong - return false.
return false;
}
/**
* Returns whether this event has any date that matches the recurrence
* rules and is not an exception.
*
* @return boolean True if an active recurrence exists.
*/
public function hasActiveRecurrence()
{
if (!$this->hasRecurEnd()) {
return true;
}
$next = $this->nextRecurrence(new Horde_Date($this->start));
while (is_object($next)) {
if (!$this->hasException($next->year, $next->month, $next->mday) &&
!$this->hasCompletion($next->year, $next->month, $next->mday)) {
return true;
}
$next = $this->nextRecurrence($next->add(array('day' => 1)));
}
return false;
}
/**
* Returns the next active recurrence.
*
* @param Horde_Date $afterDate Return events after this date.
*
* @return Horde_Date|boolean The date of the next active
* recurrence or false if the event
* has no active recurrence after
* $afterDate.
*/
public function nextActiveRecurrence($afterDate)
{
$next = $this->nextRecurrence($afterDate);
while (is_object($next)) {
if (!$this->hasException($next->year, $next->month, $next->mday) &&
!$this->hasCompletion($next->year, $next->month, $next->mday)) {
return $next;
}
$next->mday++;
$next = $this->nextRecurrence($next);
}
return false;
}
+ /**
+ * Adds an absolute recurrence date.
+ *
+ * @param integer $year The year of the instance.
+ * @param integer $month The month of the instance.
+ * @param integer $mday The day of the month of the instance.
+ */
+ public function addRDate($year, $month, $mday)
+ {
+ $this->rdates[] = new Horde_Date($year, $month, $mday);
+ }
+
/**
* Adds an exception to a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the exception.
*/
public function addException($year, $month, $mday)
{
$this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
}
/**
* Deletes an exception from a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the exception.
*/
public function deleteException($year, $month, $mday)
{
$key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
if ($key !== false) {
unset($this->exceptions[$key]);
}
}
/**
* Checks if an exception exists for a given reccurence of an event.
*
* @param integer $year The year of the reucrance.
* @param integer $month The month of the reucrance.
* @param integer $mday The day of the month of the reucrance.
*
* @return boolean True if an exception exists for the given date.
*/
public function hasException($year, $month, $mday)
{
return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
$this->getExceptions());
}
/**
* Retrieves all the exceptions for this event.
*
* @return array Array containing the dates of all the exceptions in
* YYYYMMDD form.
*/
public function getExceptions()
{
return $this->exceptions;
}
/**
* Adds a completion to a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the completion.
*/
public function addCompletion($year, $month, $mday)
{
$this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
}
/**
* Deletes a completion from a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the completion.
*/
public function deleteCompletion($year, $month, $mday)
{
$key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
if ($key !== false) {
unset($this->completions[$key]);
}
}
/**
* Checks if a completion exists for a given reccurence of an event.
*
* @param integer $year The year of the reucrance.
* @param integer $month The month of the recurrance.
* @param integer $mday The day of the month of the recurrance.
*
* @return boolean True if a completion exists for the given date.
*/
public function hasCompletion($year, $month, $mday)
{
return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
$this->getCompletions());
}
/**
* Retrieves all the completions for this event.
*
* @return array Array containing the dates of all the completions in
* YYYYMMDD form.
*/
public function getCompletions()
{
return $this->completions;
}
/**
* Parses a vCalendar 1.0 recurrence rule.
*
* @link http://www.imc.org/pdi/vcal-10.txt
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param string $rrule A vCalendar 1.0 conform RRULE value.
*/
public function fromRRule10($rrule)
{
$this->reset();
if (!$rrule) {
return;
}
if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
// No recurrence data - event does not recur.
$this->setRecurType(self::RECUR_NONE);
}
// Always default the recurInterval to 1.
$this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
$remainder = trim($matches[3]);
switch ($matches[1]) {
case 'D':
$this->setRecurType(self::RECUR_DAILY);
break;
case 'W':
$this->setRecurType(self::RECUR_WEEKLY);
if (!empty($remainder)) {
$mask = 0;
while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
$day = trim($matches[0]);
$remainder = substr($remainder, strlen($matches[0]));
$mask |= $maskdays[$day];
}
$this->setRecurOnDay($mask);
} else {
// Recur on the day of the week of the original recurrence.
$maskdays = array(
Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY,
);
$this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
}
break;
case 'MP':
$this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
break;
case 'MD':
$this->setRecurType(self::RECUR_MONTHLY_DATE);
break;
case 'YM':
$this->setRecurType(self::RECUR_YEARLY_DATE);
break;
case 'YD':
$this->setRecurType(self::RECUR_YEARLY_DAY);
break;
}
// We don't support modifiers at the moment, strip them.
while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
$remainder = substr($remainder, 1);
}
if (!empty($remainder)) {
if (strpos($remainder, '#') === 0) {
$this->setRecurCount(substr($remainder, 1));
} else {
list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
$this->setRecurEnd(new Horde_Date(array('year' => $year,
'month' => $month,
'mday' => $mday,
'hour' => 23,
'min' => 59,
'sec' => 59)));
}
}
}
/**
* Creates a vCalendar 1.0 recurrence rule.
*
* @link http://www.imc.org/pdi/vcal-10.txt
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param Horde_Icalendar $calendar A Horde_Icalendar object instance.
*
* @return string A vCalendar 1.0 conform RRULE value.
*/
public function toRRule10($calendar)
{
switch ($this->recurType) {
case self::RECUR_NONE:
return '';
case self::RECUR_DAILY:
$rrule = 'D' . $this->recurInterval;
break;
case self::RECUR_WEEKLY:
$rrule = 'W' . $this->recurInterval;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
for ($i = 0; $i <= 7; ++$i) {
if ($this->recurOnDay(pow(2, $i))) {
$rrule .= ' ' . $vcaldays[$i];
}
}
break;
case self::RECUR_MONTHLY_DATE:
$rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
break;
case self::RECUR_MONTHLY_WEEKDAY:
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
break;
case self::RECUR_YEARLY_DATE:
$rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
break;
case self::RECUR_YEARLY_DAY:
$rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
break;
default:
return '';
}
if ($this->hasRecurEnd()) {
$recurEnd = clone $this->recurEnd;
return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
}
return $rrule . ' #' . (int)$this->getRecurCount();
}
/**
* Parses an iCalendar 2.0 recurrence rule.
*
* @link http://rfc.net/rfc2445.html#s4.3.10
* @link http://rfc.net/rfc2445.html#s4.8.5
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param string $rrule An iCalendar 2.0 conform RRULE value.
*/
public function fromRRule20($rrule)
{
$this->reset();
// Parse the recurrence rule into keys and values.
$rdata = array();
$parts = explode(';', $rrule);
foreach ($parts as $part) {
list($key, $value) = explode('=', $part, 2);
$rdata[strtoupper($key)] = $value;
}
if (isset($rdata['FREQ'])) {
// Always default the recurInterval to 1.
$this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
$maskdays = array(
'SU' => Horde_Date::MASK_SUNDAY,
'MO' => Horde_Date::MASK_MONDAY,
'TU' => Horde_Date::MASK_TUESDAY,
'WE' => Horde_Date::MASK_WEDNESDAY,
'TH' => Horde_Date::MASK_THURSDAY,
'FR' => Horde_Date::MASK_FRIDAY,
'SA' => Horde_Date::MASK_SATURDAY,
);
switch (strtoupper($rdata['FREQ'])) {
case 'DAILY':
$this->setRecurType(self::RECUR_DAILY);
break;
case 'WEEKLY':
$this->setRecurType(self::RECUR_WEEKLY);
if (isset($rdata['BYDAY'])) {
$days = explode(',', $rdata['BYDAY']);
$mask = 0;
foreach ($days as $day) {
$mask |= $maskdays[$day];
}
$this->setRecurOnDay($mask);
} else {
// Recur on the day of the week of the original
// recurrence.
$maskdays = array(
Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
$this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
}
break;
case 'MONTHLY':
if (isset($rdata['BYDAY'])) {
$this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
$this->setRecurOnDay($maskdays[$m[2]]);
$this->setRecurNthWeekday($m[1]);
}
} else {
$this->setRecurType(self::RECUR_MONTHLY_DATE);
}
break;
case 'YEARLY':
if (isset($rdata['BYYEARDAY'])) {
$this->setRecurType(self::RECUR_YEARLY_DAY);
} elseif (isset($rdata['BYDAY'])) {
$this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
$this->setRecurOnDay($maskdays[$m[2]]);
$this->setRecurNthWeekday($m[1]);
}
if ($rdata['BYMONTH']) {
$months = explode(',', $rdata['BYMONTH']);
$this->setRecurByMonth($months);
}
} else {
$this->setRecurType(self::RECUR_YEARLY_DATE);
}
break;
}
if (isset($rdata['UNTIL'])) {
list($year, $month, $mday) = sscanf($rdata['UNTIL'],
'%04d%02d%02d');
$this->setRecurEnd(new Horde_Date(array('year' => $year,
'month' => $month,
'mday' => $mday,
'hour' => 23,
'min' => 59,
'sec' => 59)));
}
if (isset($rdata['COUNT'])) {
$this->setRecurCount($rdata['COUNT']);
}
} else {
// No recurrence data - event does not recur.
$this->setRecurType(self::RECUR_NONE);
}
}
/**
* Creates an iCalendar 2.0 recurrence rule.
*
* @link http://rfc.net/rfc2445.html#s4.3.10
* @link http://rfc.net/rfc2445.html#s4.8.5
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param Horde_Icalendar $calendar A Horde_Icalendar object instance.
*
* @return string An iCalendar 2.0 conform RRULE value.
*/
public function toRRule20($calendar)
{
switch ($this->recurType) {
case self::RECUR_NONE:
return '';
case self::RECUR_DAILY:
$rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval;
break;
case self::RECUR_WEEKLY:
$rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
for ($i = $flag = 0; $i <= 7; ++$i) {
if ($this->recurOnDay(pow(2, $i))) {
if ($flag) {
$rrule .= ',';
}
$rrule .= $vcaldays[$i];
$flag = true;
}
}
break;
case self::RECUR_MONTHLY_DATE:
$rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
break;
case self::RECUR_MONTHLY_WEEKDAY:
if (isset($this->recurNthDay)) {
$nth_weekday = $this->recurNthDay;
$day_of_week = log($this->recurData, 2);
} else {
$day_of_week = $this->start->dayOfWeek();
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
}
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
. ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
break;
case self::RECUR_YEARLY_DATE:
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
break;
case self::RECUR_YEARLY_DAY:
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
. ';BYYEARDAY=' . $this->start->dayOfYear();
break;
case self::RECUR_YEARLY_WEEKDAY:
if (isset($this->recurNthDay)) {
$nth_weekday = $this->recurNthDay;
$day_of_week = log($this->recurData, 2);
} else {
$day_of_week = $this->start->dayOfWeek();
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
}
$months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
. ';BYDAY='
. $nth_weekday
. $vcaldays[$day_of_week]
. ';BYMONTH=' . $this->start->month;
break;
}
if ($this->hasRecurEnd()) {
$recurEnd = clone $this->recurEnd;
$rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
}
if ($count = $this->getRecurCount()) {
$rrule .= ';COUNT=' . $count;
}
return $rrule;
}
/**
* Parses the recurrence data from a hash.
*
* @param array $hash The hash to convert.
*
* @return boolean True if the hash seemed valid, false otherwise.
*/
public function fromHash($hash)
{
$this->reset();
if (!isset($hash['interval']) || !isset($hash['cycle'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->setRecurInterval((int)$hash['interval']);
$month2number = array(
'january' => 1,
'february' => 2,
'march' => 3,
'april' => 4,
'may' => 5,
'june' => 6,
'july' => 7,
'august' => 8,
'september' => 9,
'october' => 10,
'november' => 11,
'december' => 12,
);
$parse_day = false;
$set_daymask = false;
$update_month = false;
$update_daynumber = false;
$update_weekday = false;
$nth_weekday = -1;
switch ($hash['cycle']) {
case 'daily':
$this->setRecurType(self::RECUR_DAILY);
break;
case 'weekly':
$this->setRecurType(self::RECUR_WEEKLY);
$parse_day = true;
$set_daymask = true;
break;
case 'monthly':
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
switch ($hash['type']) {
case 'daynumber':
$this->setRecurType(self::RECUR_MONTHLY_DATE);
$update_daynumber = true;
break;
case 'weekday':
$this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
$this->setRecurNthWeekday($hash['daynumber']);
$parse_day = true;
$set_daymask = true;
break;
}
break;
case 'yearly':
if (!isset($hash['type'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
switch ($hash['type']) {
case 'monthday':
$this->setRecurType(self::RECUR_YEARLY_DATE);
$update_month = true;
$update_daynumber = true;
break;
case 'yearday':
if (!isset($hash['month'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->setRecurType(self::RECUR_YEARLY_DAY);
// Start counting days in January.
$hash['month'] = 'january';
$update_month = true;
$update_daynumber = true;
break;
case 'weekday':
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
$this->setRecurNthWeekday($hash['daynumber']);
$parse_day = true;
$set_daymask = true;
if ($hash['month'] && isset($month2number[$hash['month']])) {
$this->setRecurByMonth($month2number[$hash['month']]);
}
break;
}
}
if (isset($hash['range-type']) && isset($hash['range'])) {
switch ($hash['range-type']) {
case 'number':
$this->setRecurCount((int)$hash['range']);
break;
case 'date':
$recur_end = new Horde_Date($hash['range']);
$recur_end->hour = 23;
$recur_end->min = 59;
$recur_end->sec = 59;
$this->setRecurEnd($recur_end);
break;
}
}
// Need to parse <day>?
$last_found_day = -1;
if ($parse_day) {
if (!isset($hash['day'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$mask = 0;
$bits = array(
'monday' => Horde_Date::MASK_MONDAY,
'tuesday' => Horde_Date::MASK_TUESDAY,
'wednesday' => Horde_Date::MASK_WEDNESDAY,
'thursday' => Horde_Date::MASK_THURSDAY,
'friday' => Horde_Date::MASK_FRIDAY,
'saturday' => Horde_Date::MASK_SATURDAY,
'sunday' => Horde_Date::MASK_SUNDAY,
);
$days = array(
'monday' => Horde_Date::DATE_MONDAY,
'tuesday' => Horde_Date::DATE_TUESDAY,
'wednesday' => Horde_Date::DATE_WEDNESDAY,
'thursday' => Horde_Date::DATE_THURSDAY,
'friday' => Horde_Date::DATE_FRIDAY,
'saturday' => Horde_Date::DATE_SATURDAY,
'sunday' => Horde_Date::DATE_SUNDAY,
);
foreach ($hash['day'] as $day) {
// Validity check.
if (empty($day) || !isset($bits[$day])) {
continue;
}
$mask |= $bits[$day];
$last_found_day = $days[$day];
}
if ($set_daymask) {
$this->setRecurOnDay($mask);
}
}
if ($update_month || $update_daynumber || $update_weekday) {
if ($update_month) {
if (isset($month2number[$hash['month']])) {
$this->start->month = $month2number[$hash['month']];
}
}
if ($update_daynumber) {
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->start->mday = $hash['daynumber'];
}
if ($update_weekday) {
$this->setNthWeekday($nth_weekday);
}
}
// Exceptions.
if (isset($hash['exceptions'])) {
$this->exceptions = $hash['exceptions'];
}
if (isset($hash['completions'])) {
$this->completions = $hash['completions'];
}
return true;
}
/**
* Export this object into a hash.
*
* @return array The recurrence hash.
*/
public function toHash()
{
if ($this->getRecurType() == self::RECUR_NONE) {
return array();
}
$day2number = array(
0 => 'sunday',
1 => 'monday',
2 => 'tuesday',
3 => 'wednesday',
4 => 'thursday',
5 => 'friday',
6 => 'saturday'
);
$month2number = array(
1 => 'january',
2 => 'february',
3 => 'march',
4 => 'april',
5 => 'may',
6 => 'june',
7 => 'july',
8 => 'august',
9 => 'september',
10 => 'october',
11 => 'november',
12 => 'december'
);
$hash = array('interval' => $this->getRecurInterval());
$start = $this->getRecurStart();
switch ($this->getRecurType()) {
case self::RECUR_DAILY:
$hash['cycle'] = 'daily';
break;
case self::RECUR_WEEKLY:
$hash['cycle'] = 'weekly';
$bits = array(
'monday' => Horde_Date::MASK_MONDAY,
'tuesday' => Horde_Date::MASK_TUESDAY,
'wednesday' => Horde_Date::MASK_WEDNESDAY,
'thursday' => Horde_Date::MASK_THURSDAY,
'friday' => Horde_Date::MASK_FRIDAY,
'saturday' => Horde_Date::MASK_SATURDAY,
'sunday' => Horde_Date::MASK_SUNDAY,
);
$days = array();
foreach ($bits as $name => $bit) {
if ($this->recurOnDay($bit)) {
$days[] = $name;
}
}
$hash['day'] = $days;
break;
case self::RECUR_MONTHLY_DATE:
$hash['cycle'] = 'monthly';
$hash['type'] = 'daynumber';
$hash['daynumber'] = $start->mday;
break;
case self::RECUR_MONTHLY_WEEKDAY:
$hash['cycle'] = 'monthly';
$hash['type'] = 'weekday';
$hash['daynumber'] = $start->weekOfMonth();
$hash['day'] = array ($day2number[$start->dayOfWeek()]);
break;
case self::RECUR_YEARLY_DATE:
$hash['cycle'] = 'yearly';
$hash['type'] = 'monthday';
$hash['daynumber'] = $start->mday;
$hash['month'] = $month2number[$start->month];
break;
case self::RECUR_YEARLY_DAY:
$hash['cycle'] = 'yearly';
$hash['type'] = 'yearday';
$hash['daynumber'] = $start->dayOfYear();
break;
case self::RECUR_YEARLY_WEEKDAY:
$hash['cycle'] = 'yearly';
$hash['type'] = 'weekday';
$hash['daynumber'] = $start->weekOfMonth();
$hash['day'] = array ($day2number[$start->dayOfWeek()]);
$hash['month'] = $month2number[$start->month];
}
if ($this->hasRecurCount()) {
$hash['range-type'] = 'number';
$hash['range'] = $this->getRecurCount();
} elseif ($this->hasRecurEnd()) {
$date = $this->getRecurEnd();
$hash['range-type'] = 'date';
$hash['range'] = $date->datestamp();
} else {
$hash['range-type'] = 'none';
$hash['range'] = '';
}
// Recurrence exceptions
$hash['exceptions'] = $this->exceptions;
$hash['completions'] = $this->completions;
return $hash;
}
/**
* Returns a simple object suitable for json transport representing this
* object.
*
* Possible properties are:
* - t: type
* - i: interval
* - e: end date
* - c: count
* - d: data
* - co: completions
* - ex: exceptions
*
* @return object A simple object.
*/
public function toJson()
{
$json = new stdClass;
$json->t = $this->recurType;
$json->i = $this->recurInterval;
if ($this->hasRecurEnd()) {
$json->e = $this->recurEnd->toJson();
}
if ($this->recurCount) {
$json->c = $this->recurCount;
}
if ($this->recurData) {
$json->d = $this->recurData;
}
if ($this->completions) {
$json->co = $this->completions;
}
if ($this->exceptions) {
$json->ex = $this->exceptions;
}
return $json;
}
}
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index d4a36412..5c218523 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -1,115 +1,119 @@
<?php
/**
* Recurrence computation class for the Calendar plugin
*
* Uitility class to compute instances of recurring events.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @package @package_name@
*
* Copyright (C) 2012, 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 calendar_recurrence
{
private $cal;
private $event;
private $next;
private $engine;
private $duration;
private $hour = 0;
/**
* Default constructor
*
* @param object calendar The calendar plugin instance
* @param array The event object to operate on
*/
function __construct($cal, $event)
{
// use Horde classes to compute recurring instances
// TODO: replace with something that has less than 6'000 lines of code
require_once(__DIR__ . '/Horde_Date_Recurrence.php');
$this->cal = $cal;
$this->event = $event;
$this->next = new Horde_Date($event['start'], $cal->timezone->getName());
$this->hour = $this->next->hour;
if (is_object($event['start']) && is_object($event['end']))
$this->duration = $event['start']->diff($event['end']);
$this->engine = new Horde_Date_Recurrence($event['start']);
$this->engine->fromRRule20(libcalendaring::to_rrule($event['recurrence']));
if (is_array($event['recurrence']['EXDATE'])) {
foreach ($event['recurrence']['EXDATE'] as $exdate)
$this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
}
+ if (is_array($event['recurrence']['RDATE'])) {
+ foreach ($event['recurrence']['RDATE'] as $rdate)
+ $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+ }
}
/**
* Get date/time of the next occurence of this event
*
* @return mixed DateTime object or False if recurrence ended
*/
public function next_start()
{
$time = false;
$after = clone $this->next;
$after->mday = $after->mday + 1;
if ($this->next && ($next = $this->engine->nextActiveRecurrence($after))) {
if (!$next->after($this->next)) {
// avoid endless loops if recurrence computation fails
return false;
}
if ($this->event['allday']) {
$next->hour = $this->hour; # fix time for all-day events
$next->min = 0;
}
$time = $next->toDateTime();
$this->next = $next;
}
return $time;
}
/**
* Get the next recurring instance of this event
*
* @return mixed Array with event properties or False if recurrence ended
*/
public function next_instance()
{
if ($next_start = $this->next_start()) {
$next_end = clone $next_start;
$next_end->add($this->duration);
$next = $this->event;
$next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
$next['end'] = $next_end;
unset($next['_formatobj']);
return $next;
}
return false;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 22, 4:39 PM (1 d, 2 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80114
Default Alt Text
(155 KB)

Event Timeline