Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index e68e9b95..88aae693 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,3313 +1,3313 @@
<?php
/**
* Calendar plugin for Roundcube webmail
*
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class 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 $resources_dir;
public $home; // declare public to be used in other classes
public $urlbase;
public $timezone;
public $timezone_offset;
public $gmt_offset;
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,
'calendar_itip_send_option' => 3,
'calendar_itip_after_action' => 0,
);
private $ical;
private $itip;
private $driver;
/**
* 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'));
}
$this->add_hook('user_delete', array($this, 'user_delete'));
}
/**
* 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'] || $args['action'] == 'preview')) {
$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('count', array($this, 'count_events'));
$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('mailimportitip', array($this, 'mail_import_itip'));
$this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
$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->register_action('itip-status', array($this, 'event_itip_status'));
$this->register_action('itip-remove', array($this, 'event_itip_remove'));
$this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
$this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
$this->register_action('resources-list', array($this, 'resources_list'));
$this->register_action('resources-owner', array($this, 'resources_owner'));
$this->register_action('resources-calendar', array($this, 'resources_calendar'));
$this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
$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('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');
}
$this->add_hook('messages_list', array($this, 'mail_messages_list'));
$this->add_hook('message_compose', array($this, 'mail_message_compose'));
}
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');
$this->driver = new $driver_class($this);
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');
$this->itip = new calendar_itip($this);
if ($this->rc->config->get('kolab_invitation_calendars'))
$this->itip->set_rsvp_actions(array('accepted','tentative','declined','delegated','needs-action'));
}
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($sensitivity = null)
{
$default_id = $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_WRITEABLE);
$calendar = $calendars[$default_id] ?: null;
if (!$calendar || $sensitivity) {
foreach ($calendars as $cal) {
if ($sensitivity && $cal['subtype'] == $sensitivity) {
$calendar = $cal;
break;
}
if ($cal['default'] && $cal['editable']) {
$calendar = $cal;
}
if ($cal['editable']) {
$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','close');
$this->rc->output->add_label('libcalendaring.itipaccepted','libcalendaring.itiptentative','libcalendaring.itipdeclined','libcalendaring.itipdelegated','libcalendaring.expandattendeegroup','libcalendaring.expandattendeegroupnodata');
// initialize attendees autocompletion
$this->rc->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('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
$this->rc->output->set_env('mscolors', jqueryui::get_color_values());
$this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer'))));
$view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$this->rc->output->set_env('view', $view);
if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC))
$this->rc->output->set_env('date', $date);
if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC))
$this->rc->output->set_env('itip_events', $this->itip_events($msgref));
$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, rcube::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, rcube::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($this->gettext('sunday'), '0');
$select->add($this->gettext('monday'), '1');
$select->add($this->gettext('tuesday'), '2');
$select->add($this->gettext('wednesday'), '3');
$select->add($this->gettext('thursday'), '4');
$select->add($this->gettext('friday'), '5');
$select->add($this->gettext('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, rcube::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, rcube::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, rcube::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', rcube::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']) || !isset($no_override['calendar_default_alarm_offset'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$alarm_type = $alarm_offset = '';
if (!isset($no_override['calendar_default_alarm_type'])) {
$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($this->rc->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
}
$alarm_type = $select_type->show($this->rc->config->get('calendar_default_alarm_type', ''));
}
if (!isset($no_override['calendar_default_alarm_offset'])) {
$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($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);
}
$preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$alarm_offset = $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]);
}
$p['blocks']['view']['options']['alarmtype'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('defaultalarmtype'))),
'content' => $alarm_type . ' ' . $alarm_offset,
);
}
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(calendar_driver::FILTER_PERSONAL) 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', rcube::Q($this->gettext('defaultcalendar'))),
'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),
);
}
$p['blocks']['itip']['name'] = $this->gettext('itipoptions');
// Invitations handling
if (!isset($no_override['calendar_itip_after_action'])) {
if (!$p['current']) {
$p['blocks']['itip']['content'] = true;
return $p;
}
$field_id = 'rcmfd_after_action';
$select = new html_select(array('name' => '_after_action', 'id' => $field_id,
'onchange' => "\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()"));
$select->add($this->gettext('afternothing'), '');
$select->add($this->gettext('aftertrash'), 1);
$select->add($this->gettext('afterdelete'), 2);
$select->add($this->gettext('afterflagdeleted'), 3);
$select->add($this->gettext('aftermoveto'), 4);
$val = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
if ($val !== null && $val !== '' && !is_int($val)) {
$folder = $val;
$val = 4;
}
$folders = $this->rc->folder_selector(array(
'id' => $field_id . '_select',
'name' => '_after_action_folder',
'maxlength' => 30,
'folder_filter' => 'mail',
'folder_rights' => 'w',
'style' => $val !== 4 ? 'display:none' : '',
));
$p['blocks']['itip']['options']['after_action'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('afteraction'))),
'content' => $select->show($val) . $folders->show($folder),
);
}
// 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');
// load miniColors js/css files
jqueryui::miniColors();
}
// 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($this->rc->gettext(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($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_alarm_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 = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST);
$alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST);
$default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1];
$birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST);
$birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST);
$birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1];
$p['prefs'] = array(
'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST),
'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)),
'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)),
'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)),
'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)),
'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)),
'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)),
'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST),
'calendar_default_alarm_offset' => $default_alarm,
'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST),
'calendar_date_format' => null, // clear previously saved values
'calendar_time_format' => null,
'calendar_contact_birthdays' => rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST) ? true : false,
'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST),
'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST),
'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,
'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)),
);
if ($p['prefs']['calendar_itip_after_action'] == 4) {
$p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true);
}
// 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) rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST);
$colors = (array) rcube_utils::get_input_value('_colors', rcube_utils::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 = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$cal = rcube_utils::get_input_value('c', rcube_utils::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 "delete":
if ($success = $this->driver->delete_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;
case "search":
$results = array();
$color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
$query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
foreach ((array) $this->driver->search_calendars($query, $source) as $id => $prop) {
$editname = $prop['editname'];
unset($prop['editname']); // force full name to be displayed
$prop['active'] = false;
// let the UI generate HTML and CSS representation for this calendar
$html = $this->ui->calendar_list_item($id, $prop, $jsenv);
$cal = $jsenv[$id];
$cal['editname'] = $editname;
$cal['html'] = $html;
if (!empty($prop['color']))
$cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);
$results[] = $cal;
}
// report more results available
if ($this->driver->search_more_results)
$this->rc->output->show_message('autocompletemore', 'info');
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
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 = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
$success = $reload = $got_msg = false;
// force notify if hidden + active
if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
$event['_notify'] = 1;
// read old event data in order to find changes
if (($event['_notify'] || $event['_decline']) && $action != 'new') {
$old = $this->driver->get_event($event);
// load main event if savemode is 'all' or if deleting 'future' events
if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline'])) && $old['recurrence_id']) {
$old['id'] = $old['recurrence_id'];
$old = $this->driver->get_event($old);
}
}
switch ($action) {
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
$this->write_preprocess($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$event['_savemode'] = 'all';
$this->cleanup_event($event);
$this->event_save_success($event, null, $action, true);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
break;
case "edit":
$this->write_preprocess($event, $action);
if ($success = $this->driver->edit_event($event)) {
$this->cleanup_event($event);
$this->event_save_success($event, $old, $action, $success);
}
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->write_preprocess($event, $action);
if ($success = $this->driver->resize_event($event)) {
$this->event_save_success($event, $old, $action, $success);
}
$reload = $event['_savemode'] ? 2 : 1;
break;
case "move":
$this->write_preprocess($event, $action);
if ($success = $this->driver->move_event($event)) {
$this->event_save_success($event, $old, $action, $success);
}
$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, calendar_driver::FILTER_WRITEABLE))) {
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'))",
rcmail_output::JS_OBJECT_NAME, rcmail_output::JS_OBJECT_NAME)), $this->gettext('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 cancellation for the main event
if ($event['_savemode'] == 'all') {
unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);
}
// send an update for the main event's recurrence rule instead of a cancellation message
else if ($event['_savemode'] == 'future' && $success !== false && $success !== true) {
$event['_savemode'] = 'all'; // force event_save_success() to load master event
$action = 'edit';
$success = 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'];
}
}
if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) {
$old['thisandfuture'] = true;
}
$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');
}
else if ($success) {
$this->event_save_success($event, $old, $action, $success);
}
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":
$itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
$status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);
$attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);
$reply_comment = $event['comment'];
$this->write_preprocess($event, 'edit');
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$ev['free_busy'] = $event['free_busy'];
$ev['_savemode'] = $event['_savemode'];
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $event['to']) {
$itip = $this->load_itip();
if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) {
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
$noreply = false;
}
}
$event = $ev;
// compose a list of attendees affected by this change
$updated_attendees = array_filter(array_map(function($j) use ($event) {
return $event['attendees'][$j];
}, $attendees));
if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) {
$noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
$reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
$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)) {
$reply_sender = $attendee['email'];
}
}
if (!$noreply) {
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
$event['comment'] = $reply_comment;
$event['thisandfuture'] = $event['_savemode'] == 'future';
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');
}
// refresh all calendars
if ($event['calendar'] != $ev['calendar']) {
$this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true));
$reload = 0;
}
}
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;
case "changelog":
$data = $this->driver->get_event_changelog($event);
if (is_array($data) && !empty($data)) {
$lib = $this->lib;
$dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($data, function(&$change) use ($lib, $dtformat) {
if ($change['date']) {
$dt = $lib->adjust_timezone($change['date']);
if ($dt instanceof DateTime)
$change['date'] = $this->rc->format_date($dt, $dtformat, false);
}
});
$this->rc->output->command('plugin.render_event_changelog', $data);
}
else {
$this->rc->output->command('plugin.render_event_changelog', false);
}
$got_msg = true;
$reload = false;
break;
case "diff":
$data = $this->driver->get_event_diff($event, $event['rev1'], $event['rev2']);
if (is_array($data)) {
// convert some properties, similar to self::_client_event()
$lib = $this->lib;
array_walk($data['changes'], function(&$change, $i) use ($event, $lib) {
// convert date cols
foreach (array('start','end','created','changed') as $col) {
if ($change['property'] == $col) {
$change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
$change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
}
}
// create textual representation for alarms and recurrence
if ($change['property'] == 'alarms') {
if (is_array($change['old']))
$change['old_'] = libcalendaring::alarm_text($change['old']);
if (is_array($change['new']))
$change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'recurrence') {
if (is_array($change['old']))
$change['old_'] = $lib->recurrence_text($change['old']);
if (is_array($change['new']))
$change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'attachments') {
if (is_array($change['old']))
$change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
if (is_array($change['new']))
$change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
}
// compute a nice diff of description texts
if ($change['property'] == 'description') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
}
});
$this->rc->output->command('plugin.event_show_diff', $data);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
$got_msg = true;
$reload = false;
break;
case "show":
if ($event = $this->driver->get_event_revison($event, $event['rev'])) {
$this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
}
else {
$this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
}
$got_msg = true;
$reload = false;
break;
case "restore":
if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
$_event = $this->driver->get_event($event);
$reload = $_event['recurrence'] ? 2 : 1;
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation');
$this->rc->output->command('plugin.close_history_dialog');
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
$reload = 0;
}
$got_msg = true;
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');
}
// 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), true);
$this->rc->output->command('plugin.refresh_calendar', $args);
}
}
/**
* Helper method sending iTip notifications after successful event updates
*/
private function event_save_success(&$event, $old, $action, $success)
{
// $success is a new event ID
if ($success !== true) {
// send update notification on the main event
if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) {
$master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']), 0, true);
unset($master['_instance'], $master['recurrence_date']);
$sent = $this->notify_attendees($master, null, $action, $event['_comment'], false);
if ($sent < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
$event['attendees'] = $master['attendees']; // this tricks us into the next if clause
}
// delete old reference if saved as new
if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') {
$old = null;
}
$event['id'] = $success;
$event['_savemode'] = 'all';
}
// send out notifications
if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) {
$_savemode = $event['_savemode'];
// send notification for the main event when savemode is 'all'
if ($action != 'remove' && $_savemode == 'all' && ($event['recurrence_id'] || $old['recurrence_id'] || ($old && $old['id'] != $event['id']))) {
$event['id'] = $event['recurrence_id'] ?: ($old['recurrence_id'] ?: $old['id']);
$event = $this->driver->get_event($event, 0, true);
unset($event['_instance'], $event['recurrence_date']);
}
else {
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true);
}
$event['_savemode'] = $_savemode;
if ($old) {
$old['thisandfuture'] = $_savemode == 'future';
}
// 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, $event['_comment']);
if ($sent > 0)
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
else if ($sent < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
}
/**
* Handler for load-requests from fullcalendar
* This will return pure JSON formatted output
*/
function load_events()
{
$events = $this->driver->load_events(
rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
rcube_utils::get_input_value('end', rcube_utils::INPUT_GET),
($query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET)),
rcube_utils::get_input_value('source', rcube_utils::INPUT_GET)
);
echo $this->encode($events, !empty($query));
exit;
}
/**
* Handler for requests fetching event counts for calendars
*/
public function count_events()
{
// don't update session on these requests (avoiding race conditions)
$this->rc->session->nowrite = true;
$start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
if (!$start) {
$start = new DateTime('today 00:00:00', $this->timezone);
$start = $start->format('U');
}
$counts = $this->driver->count_events(
rcube_utils::get_input_value('source', rcube_utils::INPUT_GET),
$start,
rcube_utils::get_input_value('end', rcube_utils::INPUT_GET)
);
$this->rc->output->command('plugin.update_counts', array('counts' => $counts));
}
/**
* Load event data from an iTip message attachment
*/
public function itip_events($msgref)
{
$path = explode('/', $msgref);
$msg = array_pop($path);
$mbox = join('/', $path);
list($uid, $mime_id) = explode('#', $msg);
$events = array();
if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
$partstat = 'NEEDS-ACTION';
/*
$user_emails = $this->lib->get_user_emails();
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails)) {
$partstat = $attendee['status'];
break;
}
}
*/
$event['id'] = $event['uid'];
$event['temporary'] = true;
$event['readonly'] = true;
$event['calendar'] = '--invitation--itip';
$event['className'] = 'fc-invitation-' . strtolower($partstat);
$event['_mbox'] = $mbox;
$event['_uid'] = $uid;
$event['_part'] = $mime_id;
$events[] = $this->_client_event($event, true);
// add recurring instances
if (!empty($event['recurrence'])) {
foreach ($this->driver->get_recurring_events($event, $event['start']) as $recurring) {
$recurring['temporary'] = true;
$recurring['readonly'] = true;
$recurring['calendar'] = '--invitation--itip';
$events[] = $this->_client_event($recurring, true);
}
}
}
return $events;
}
/**
* 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;
}
$counts = array();
foreach ($this->driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) {
$events = $this->driver->load_events(
rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC),
rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC),
rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
$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)));
}
// refresh count for this calendar
if ($cal['counts']) {
$today = new DateTime('today 00:00:00', $this->timezone);
$counts += $this->driver->count_events($cal['id'], $today->format('U'));
}
}
if (!empty($counts)) {
$this->rc->output->command('plugin.update_counts', array('counts' => $counts));
}
}
/**
* 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'])) {
$this->rc->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 = rcube_utils::get_input_value('calendar', rcube_utils::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 = $this->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = $this->gettext('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 = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
$end = rcube_utils::get_input_value('end', rcube_utils::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');
$event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
$attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET);
$calid = $filename = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
$calendars = $this->driver->list_calendars();
$events = array();
if ($calendars[$calid]) {
$filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
if (!empty($event_id)) {
if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id), 0, true)) {
if ($event['recurrence_id']) {
$event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']), 0, true);
}
$events = array($event);
$filename = asciiwords($event['title']);
if (empty($filename))
$filename = 'event';
}
}
else {
$events = $this->driver->load_events($start, $end, null, $calid, 0);
if (empty($filename))
$filename = $calid;
}
}
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$filename.'.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()
{
$session_exists = !empty($_SESSION['user_id']);
// 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 = rcube_utils::get_input_value('_cal', rcube_utils::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
if (!$session_exists)
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']);
$settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false);
$settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
foreach ($this->rc->user->list_emails() 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['valarms']) {
$event['alarms_text'] = libcalendaring::alarms_text($event['valarms']);
$event['valarms'] = libcalendaring::to_client_alarms($event['valarms']);
}
if ($event['recurrence']) {
$event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
$event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
unset($event['recurrence_date']);
}
foreach ((array)$event['attachments'] as $k => $attachment) {
$event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
// convert link URIs references into structs
if (array_key_exists('links', $event)) {
foreach ((array)$event['links'] as $i => $link) {
if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) {
$event['links'][$i] = $msgref;
}
}
}
// check for organizer in attendees list
$organizer = null;
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
$event['attendees'][$i]['noreply'] = true;
}
else {
unset($event['attendees'][$i]['noreply']);
}
}
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);
}
// Convert HTML description into plain text
if ($this->is_html($event)) {
$h2t = new rcube_html2text($event['description'], false, true, 0);
$event['description'] = trim($h2t->get_text());
}
// 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,
'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->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) .
rtrim(' ' . $event['className']),
'allDay' => ($event['allday'] == 1),
) + $event;
}
/**
* 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&_date=2014-08-01&_dev=120
*/
public function generate_randomdata()
{
@set_time_limit(0);
$num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
$date = $_REQUEST['_date'] ?: 'now';
$dev = $_REQUEST['_dev'] ?: 30;
$cats = array_keys($this->driver->list_categories());
$cals = $this->driver->list_calendars(calendar_driver::FILTER_ACTIVE);
$count = 0;
while ($count++ < $num) {
$spread = intval($dev) * 86400; // days
$refdate = strtotime($date);
$start = round(($refdate + rand(-$spread, $spread)) / 600) * 600;
$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 = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC);
$calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC);
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
$event = array('id' => $event_id, 'calendar' => $calendar, 'rev' => $rev);
$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;
}
/**
* Determine whether the given event description is HTML formatted
*/
private function is_html($event)
{
// check for opening and closing <html> or <body> tags
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $event['description'], $m) && strpos($event['description'], '</'.$m[1].'>') > 0);
}
/**
* Prepares new/edited event properties before save
*/
private function write_preprocess(&$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);
$event['allday'] = (bool)$event['allday'];
// start/end is all we need for 'move' action (#1480)
if ($action == 'move') {
return;
}
// convert the submitted recurrence settings
if (is_array($event['recurrence'])) {
$event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);
}
// convert the submitted alarm values
if ($event['valarms']) {
$event['valarms'] = libcalendaring::from_client_alarms($event['valarms']);
}
$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;
// convert link references into simple URIs
if (array_key_exists('links', $event)) {
$event['links'] = array_map(function($link) {
return is_array($link) ? $link['uri'] : strval($link);
}, (array)$event['links']);
}
// 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;
if (!isset($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = true;
else if (is_string($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
}
// 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']);
}
}
// mapping url => vurl because of the fullcalendar client script
if (array_key_exists('vurl', $event)) {
$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', $comment = null, $rsvp = null)
{
if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) {
$event['cancelled'] = true;
$is_cancelled = true;
}
if ($rsvp === null)
$rsvp = !$old || $event['sequence'] > $old['sequence'];
$itip = $this->load_itip();
$emails = $this->get_user_emails();
$itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
// add comment to the iTip attachment
$event['comment'] = $comment;
// set a valid recurrence-id if this is a recurrence instance
libcalendaring::identify_recurrence_instance($event);
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method, $rsvp);
// 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; $current = array();
foreach ((array)$event['attendees'] as $attendee) {
$current[] = strtolower($attendee['email']);
// skip myself for obvious reasons
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails))
continue;
// skip if notification is disabled for this attendee
if ($attendee['noreply'] && $itip_notify & 2)
continue;
// skip if this attendee has delegated and set RSVP=FALSE
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false)
continue;
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$is_rsvp = $is_new || $event['sequence'] > $old['sequence'];
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
$event['comment'] = $comment;
// finally send the message
if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
$sent++;
else
$sent = -100;
}
// TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if ($attendee['role'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
continue;
$vevent = $old;
$vevent['cancelled'] = $is_cancelled;
$vevent['attendees'] = array($attendee);
$vevent['comment'] = $comment;
if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Echo simple free/busy status text for the given user and time range
*/
public function freebusy_status()
{
$email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
$start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
$end = rcube_utils::get_input_value('end', rcube_utils::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
$this->rc->output->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 = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
$start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
$end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
$interval = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC));
$strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
// convert dates into unix timestamps
if (!empty($start) && !is_numeric($start)) {
$dts = rcube_utils::anytodatetime($start, $this->timezone);
$start = $dts ? $dts->format('U') : null;
}
if (!empty($end) && !is_numeric($end)) {
$dte = rcube_utils::anytodatetime($end, $this->timezone);
$end = $dte ? $dte->format('U') : null;
}
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;
// check for possible all-day times
if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') {
// shift into the user's timezone for sane matching
$from -= $this->gmt_offset;
$to -= $this->gmt_offset;
}
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
$this->rc->output->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 = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$view = 'agendaDay';
$this->rc->output->set_env('view',$view);
if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC))
$this->rc->output->set_env('date', $date);
if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC))
$this->rc->output->set_env('listRange', intval($range));
if (isset($_REQUEST['sections']))
$this->rc->output->set_env('listSections', rcube_utils::get_input_value('sections', rcube_utils::INPUT_GPC));
if ($search = rcube_utils::get_input_value('search', rcube_utils::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] && $key[0] != '_' && $a[$key] != $b[$key])
$diff[] = $key;
}
// only compare number of attachments
if (count($a['attachments']) != count($b['attachments']))
$diff[] = 'attachments';
return $diff;
}
/**
* Update attendee properties on the given event object
*
* @param array The event object to be altered
* @param array List of hash arrays each represeting an updated/added attendee
*/
public static function merge_attendee_data(&$event, $attendees, $removed = null)
{
if (!empty($attendees) && !is_array($attendees[0])) {
$attendees = array($attendees);
}
foreach ($attendees as $attendee) {
$found = false;
foreach ($event['attendees'] as $i => $candidate) {
if ($candidate['email'] == $attendee['email']) {
$event['attendees'][$i] = $attendee;
$found = true;
break;
}
}
if (!$found) {
$event['attendees'][] = $attendee;
}
}
// filter out removed attendees
if (!empty($removed)) {
$event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) {
return !in_array($attendee['email'], $removed);
});
}
}
/**** Resource management functions ****/
/**
* Getter for the configured implementation of the resource directory interface
*/
private function resources_directory()
{
if (is_object($this->resources_dir)) {
return $this->resources_dir;
}
if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
$driver_class = 'resources_driver_' . $driver_name;
require_once($this->home . '/drivers/resources_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
$this->resources_dir = new $driver_class($this);
}
return $this->resources_dir;
}
/**
* Handler for resoruce autocompletion requests
*/
public function resources_autocomplete()
{
$search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
$sid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
$maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
$results = array();
if ($directory = $this->resources_directory()) {
foreach ($directory->load_resources($search, $maxnum) as $rec) {
$results[] = array(
'name' => $rec['name'],
'email' => $rec['email'],
'type' => $rec['_type'],
);
}
}
$this->rc->output->command('ksearch_query_results', $results, $search, $sid);
$this->rc->output->send();
}
/**
* Handler for load-requests for resource data
*/
function resources_list()
{
$data = array();
if ($directory = $this->resources_directory()) {
foreach ($directory->load_resources() as $rec) {
$data[] = $rec;
}
}
$this->rc->output->command('plugin.resource_data', $data);
$this->rc->output->send();
}
/**
* Handler for requests loading resource owner information
*/
function resources_owner()
{
if ($directory = $this->resources_directory()) {
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$data = $directory->get_resource_owner($id);
}
$this->rc->output->command('plugin.resource_owner', $data);
$this->rc->output->send();
}
/**
* Deliver event data for a resource's calendar
*/
function resources_calendar()
{
$events = array();
if ($directory = $this->resources_directory()) {
$events = $directory->get_resource_calendar(
rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
rcube_utils::get_input_value('end', rcube_utils::INPUT_GET));
}
echo $this->encode($events);
exit;
}
/**** Event invitation plugin hooks ****/
/**
* Handler for calendar/itip-status requests
*/
function event_itip_status()
{
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
// find local copy of the referenced event
$this->load_driver();
$existing = $this->driver->get_event($data, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL);
$itip = $this->load_itip();
$response = $itip->get_itip_status($data, $existing);
// get a list of writeable calendars to save new events to
if (!$existing && !$data['nosave'] && $response['action'] == 'rsvp' || $response['action'] == 'import') {
$calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
$calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true));
$calendar_select->add('--', '');
$numcals = 0;
foreach ($calendars as $calendar) {
if ($calendar['editable']) {
$calendar_select->add($calendar['name'], $calendar['id']);
$numcals++;
}
}
if ($numcals <= 1)
$calendar_select = null;
}
if ($calendar_select) {
$default_calendar = $this->get_default_calendar($data['sensitivity']);
$response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . '&nbsp;' .
$calendar_select->show($default_calendar['id']));
}
else if ($data['nosave']) {
$response['select'] = html::tag('input', array('type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => ''));
}
// render small agenda view for the respective day
if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') {
$event_start = rcube_utils::anytodatetime($data['date']);
$day_start = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone);
$day_end = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone);
// get events on that day from the user's personal calendars
$calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
$events = $this->driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars));
usort($events, function($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; });
$before = $after = array();
foreach ($events as $event) {
// TODO: skip events with free_busy == 'free' ?
if ($event['uid'] == $data['uid'] || $event['end'] < $day_start || $event['start'] > $day_end)
continue;
else if ($event['start'] < $event_start)
$before[] = $this->mail_agenda_event_row($event);
else
$after[] = $this->mail_agenda_event_row($event);
}
$response['append'] = array(
'selector' => '.calendar-agenda-preview',
'replacements' => array(
'%before%' => !empty($before) ? join("\n", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')),
'%after%' => !empty($after) ? join("\n", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')),
),
);
}
$this->rc->output->command('plugin.update_itip_object_status', $response);
}
/**
* Handler for calendar/itip-remove requests
*/
function event_itip_remove()
{
$success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
$instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
$savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
// search for event if only UID is given
if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $instance), calendar_driver::FILTER_WRITEABLE)) {
$event['_savemode'] = $savemode;
$success = $this->driver->remove_event($event, true);
}
if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
}
else {
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
}
/**
* 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 = rcube_utils::get_input_value('_t', rcube_utils::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', $itip->gettext('eventcancelled'));
}
// save submitted RSVP status
else if (!empty($_POST['rsvp'])) {
$status = null;
foreach (array('accepted','tentative','declined') as $method) {
if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) {
$status = $method;
break;
}
}
// send itip reply to organizer
$invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
$this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->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($invitation['event']['sensitivity']))) {
$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->itip->set_rsvp_actions(array('accepted','tentative','declined'));
$this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
}
$this->rc->output->set_pagetitle($itip->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();
}
/**
*
*/
private function mail_agenda_event_row($event, $class = '')
{
$time = $event['allday'] ? $this->gettext('all-day') :
$this->rc->format_date($event['start'], $this->rc->config->get('time_format')) . ' - ' .
$this->rc->format_date($event['end'], $this->rc->config->get('time_format'));
return html::div(rtrim('event-row ' . $class),
html::span('event-date', $time) .
html::span('event-title', rcube::Q($event['title']))
);
}
/**
*
*/
public function mail_messages_list($p)
{
if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) {
foreach ($p['messages'] as $header) {
$part = new StdClass;
$part->mimetype = $header->ctype;
if (libcalendaring::part_is_vcalendar($part)) {
$header->list_flags['attachmentClass'] = 'ical';
}
else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) {
// TODO: fetch bodystructure and search for ical parts. Maybe too expensive?
if (!empty($header->structure) && is_array($header->structure->parts)) {
foreach ($header->structure->parts as $part) {
if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) {
$header->list_flags['attachmentClass'] = 'ical';
break;
}
}
}
}
}
}
}
/**
* 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->lib->ical_parts)) {
$this->get_ical();
$this->load_itip();
}
$html = '';
$has_events = false;
$ical_objects = $this->lib->get_mail_ical_objects();
// show a box for every event in the file
foreach ($ical_objects as $idx => $event) {
if ($event['_type'] != 'event') // skip non-event objects (#2928)
continue;
$has_events = true;
// get prepared inline UI for this event object
if ($ical_objects->method) {
$append = '';
// prepare a small agenda preview to be filled with actual event data on async request
if ($ical_objects->method == 'REQUEST') {
$append = html::div('calendar-agenda-preview',
html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' .
html::span('date', $this->rc->format_date($event['start'], $this->rc->config->get('date_format')))
) . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%');
}
$html .= html::div('calendar-invitebox',
$this->itip->mail_itip_inline_ui(
$event,
$ical_objects->method,
$ical_objects->mime_id . ':' . $idx,
'calendar',
rcube_utils::anytodatetime($ical_objects->message_date),
$this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $event['start']->format('U')
) . $append
);
}
// 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');
}
// add "Save to calendar" button into attachment menu
if ($has_events) {
$this->add_button(array(
'id' => 'attachmentsavecal',
'name' => 'attachmentsavecal',
'type' => 'link',
'wrapper' => 'li',
'command' => 'attachment-save-calendar',
'class' => 'icon calendarlink',
'classact' => 'icon calendarlink active',
'innerclass' => 'icon calendar',
'label' => 'calendar.savetocalendar',
), 'attachmentmenu');
}
return $p;
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_itip()
{
$itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
$noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
$instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
$savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
$error_msg = $this->gettext('errorimportingevent');
$success = false;
$delegate = null;
if ($status == 'delegated') {
$delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
$delegate = reset($delegates);
if (empty($delegate) || empty($delegate['mailto'])) {
$this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
return;
}
}
// successfully parsed events?
if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
// forward iTip request to delegatee
if ($delegate) {
$rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST));
$itip = $this->load_itip();
if ($itip->delegate_to($event, $delegate, $rsvpme ? true : false)) {
$this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
}
else {
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
// the delegator is set to non-participant, thus save as non-blocking
$event['free_busy'] = 'free';
}
// find writeable calendar to store event
$cal_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
$dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST');
$calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
$calendar = $calendars[$cal_id];
// select default calendar except user explicitly selected 'none'
if (!$calendar && !$dontsave)
$calendar = $this->get_default_calendar($event['sensitivity']);
$metadata = array(
'uid' => $event['uid'],
'_instance' => $event['_instance'],
'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => strtoupper($status),
'method' => $event['_method'],
'task' => 'calendar',
);
// 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);
if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED')))
$event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
$reply_sender = $attendee['email'];
$event_attendee = $attendee;
}
}
// add attendee with this user's default identity if not listed
if (!$reply_sender) {
$sender_identity = $this->rc->user->list_emails(true);
$event['attendees'][] = array(
'name' => $sender_identity['name'],
'email' => $sender_identity['email'],
'role' => 'OPT-PARTICIPANT',
'status' => strtoupper($status),
);
$metadata['attendee'] = $sender_identity['email'];
}
}
// save to calendar
if ($calendar && $calendar['editable']) {
// check for existing event with the same UID
$existing = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL);
if ($existing) {
// forward savemode for correct updates of recurring events
$existing['_savemode'] = $savemode ?: $event['_savemode'];
// only update attendee status
if ($event['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
$existing_attendee = -1;
$existing_attendee_emails = array();
foreach ($existing['attendees'] as $i => $attendee) {
$existing_attendee_emails[] = $attendee['email'];
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
$existing_attendee = $i;
}
}
$event_attendee = null;
$update_attendees = array();
foreach ($event['attendees'] as $attendee) {
if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
$event_attendee = $attendee;
$update_attendees[] = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee['status'] != 'DELEGATED') {
break;
}
}
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) &&
(stripos($attendee['delegated-from'], $event['_sender']) !== false ||
stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) {
$update_attendees[] = $attendee;
if (!in_array($attendee['email'], $existing_attendee_emails)) {
$existing['attendees'][] = $attendee;
}
}
}
// if delegatee has declined, set delegator's RSVP=True
if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] == $event_attendee['delegated-from']) {
$existing['attendees'][$i]['rsvp'] = true;
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->update_attendees($existing, $update_attendees);
}
// update the entire attendees block
else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
$existing['attendees'][] = $event_attendee;
$success = $this->driver->update_attendees($existing, $update_attendees);
}
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'];
// preserve my participant status for regular updates
if (empty($status)) {
$emails = $this->get_user_emails();
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
foreach ($existing['attendees'] as $j => $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$event['attendees'][$i] = $existing['attendees'][$j];
break;
}
}
}
}
}
// set status=CANCELLED on CANCEL messages
if ($event['_method'] == 'CANCEL')
$event['status'] = 'CANCELLED';
// show me as free when declined (#1670)
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT')
$event['free_busy'] = 'free';
$success = $this->driver->edit_event($event);
}
else if (!empty($status)) {
$existing['attendees'] = $event['attendees'];
if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // 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' || $this->rc->config->get('kolab_invitation_calendars'))) {
if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
$event['free_busy'] = 'free';
}
// if the RSVP reply only refers to a single instance:
// store unmodified master event with current instance as exception
if (!empty($instance) && !empty($savemode) && $savemode != 'all') {
$master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
if ($master['recurrence'] && !$master['_instance']) {
// compute recurring events until this instance's date
if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
$recurrence_date->setTime(23,59,59);
foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
if ($recurring['_instance'] == $instance) {
// copy attendees block with my partstat to exception
$recurring['attendees'] = $event['attendees'];
$master['recurrence']['EXCEPTIONS'][] = $recurring;
$event = $recurring; // set reference for iTip reply
break;
}
}
$master['calendar'] = $event['calendar'] = $calendar['id'];
$success = $this->driver->new_event($master);
}
else {
$master = null;
}
}
else {
$master = null;
}
}
// save to the selected/default calendar
if (!$master) {
$event['calendar'] = $calendar['id'];
$success = $this->driver->new_event($event);
}
}
else if ($status == 'declined')
$error_msg = null;
}
else if ($status == 'declined' || $dontsave)
$error_msg = null;
else
$error_msg = $this->gettext('nowritecalendarfound');
}
if ($success) {
$message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
}
if ($success || $dontsave) {
$metadata['calendar'] = $event['calendar'];
$metadata['nosave'] = $dontsave;
$metadata['rsvp'] = intval($metadata['rsvp']);
$metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
}
else if ($error_msg) {
$this->rc->output->command('display_message', $error_msg, 'error');
}
// send iTip reply
if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
$event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
$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();
}
/**
* Handler for calendar/itip-remove requests
*/
function mail_itip_decline_reply()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') {
$event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
foreach ($event['attendees'] as $_attendee) {
if ($_attendee['role'] != 'ORGANIZER') {
$attendee = $_attendee;
break;
}
}
$itip = $this->load_itip();
if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel'))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
else {
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
/**
* Handler for calendar/itip-delegate requests
*/
function mail_itip_delegate()
{
// forward request to mail_import_itip() with the right status
$_POST['_status'] = $_REQUEST['_status'] = 'delegated';
$this->mail_import_itip();
}
/**
* Import the full payload from a mail message attachment
*/
public function mail_import_attachment()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$charset = RCUBE_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
- $imap->set_mailbox($mbox);
+ $imap->set_folder($mbox);
if ($uid && $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);
if ($part) {
$events = $this->get_ical()->import($part, $charset);
}
}
$success = $existing = 0;
if (!empty($events)) {
// find writeable calendar to store event
$cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null;
$calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
foreach ($events as $event) {
// save to calendar
$calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']);
if ($calendar && $calendar['editable'] && $event['_type'] == 'event') {
$event['calendar'] = $calendar['id'];
if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) {
$success += (bool)$this->driver->new_event($event);
}
else {
$existing++;
}
}
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array(
'name' => 'importsuccess',
'vars' => array('nr' => $success),
)), 'confirmation');
}
else if ($existing) {
$this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
}
else {
$this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
}
}
/**
* Read email message and return contents for a new event based on that message
*/
public function mail_message2event()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$event = array();
// establish imap connection
$imap = $this->rc->get_storage();
- $imap->set_mailbox($mbox);
+ $imap->set_folder($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$event['title'] = trim($message->subject);
$event['description'] = trim($message->first_text_part());
$this->load_driver();
// add a reference to the email message
if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
$event['links'] = array($msgref);
}
// copy mail attachments to event
else 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();
}
/**
* Handler for the 'message_compose' plugin hook. This will check for
* a compose parameter 'calendar_event' and create an attachment with the
* referenced event in iCal format
*/
public function mail_message_compose($args)
{
// set the submitted event ID as attachment
if (!empty($args['param']['calendar_event'])) {
$this->load_driver();
list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) {
$filename = asciiwords($event['title']);
if (empty($filename))
$filename = 'event';
// save ics to a temp file and register as attachment
$tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body')));
$args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar');
$args['param']['subject'] = $event['title'];
}
}
return $args;
}
/**
* Get a list of email addresses of the current user (from login and identities)
*/
public function get_user_emails()
{
return $this->lib->get_user_emails();
}
/**
* Build an absolute URL with the given parameters
*/
public function get_url($param = array())
{
$param += array('task' => 'calendar');
return $this->rc->url($param, true, true);
}
public function ical_feed_hash($source)
{
return base64_encode($this->rc->user->get_username() . ':' . $source);
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
// delete itipinvitations entries related to this user
$db = $this->rc->get_dbh();
$table_itipinvitations = $db->table_name('itipinvitations', true);
$db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID);
$this->load_driver();
return $this->driver->user_delete($args);
}
/**
* Magic getter for public access to protected members
*/
public function __get($name)
{
switch ($name) {
case 'ical':
return $this->get_ical();
case 'itip':
return $this->load_itip();
case 'driver':
$this->load_driver();
return $this->driver;
}
return null;
}
}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 9c9405ce..a3fc7679 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1,1664 +1,1664 @@
<?php
/**
* Library providing common functions for calendaring plugins
*
* Provides utility functions for calendar-related modules such as
* - alarms display and dismissal
* - attachment handling
* - recurrence computation and UI elements
* - ical parsing and exporting
* - itip scheduling protocol
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class libcalendaring extends rcube_plugin
{
public $rc;
public $timezone;
public $gmt_offset;
public $dst_active;
public $timezone_offset;
public $ical_parts = array();
public $ical_message;
public $defaults = array(
'calendar_date_format' => "yyyy-MM-dd",
'calendar_date_short' => "M-d",
'calendar_date_long' => "MMM d yyyy",
'calendar_date_agenda' => "ddd MM-dd",
'calendar_time_format' => "HH:mm",
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'calendar_date_format_sets' => array(
'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
),
);
private static $instance;
private static $email_regex = '/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/';
private $mail_ical_parser;
/**
* Singleton getter to allow direct access from other plugins
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new libcalendaring(rcube::get_instance()->plugins);
self::$instance->init_instance();
}
return self::$instance;
}
/**
* Initializes class properties
*/
public function init_instance()
{
$this->rc = rcube::get_instance();
// set user's timezone
try {
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
$this->timezone = new DateTimeZone('GMT');
}
$now = new DateTime('now', $this->timezone);
$this->gmt_offset = $now->getOffset();
$this->dst_active = $now->format('I');
$this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
$this->add_texts('localization/', false);
}
/**
* Required plugin startup method
*/
public function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
$this->init_instance();
// include client scripts and styles
if ($this->rc->output) {
// add hook to display alarms
$this->add_hook('refresh', array($this, 'refresh'));
$this->register_action('plugin.alarms', array($this, 'alarms_action'));
$this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
}
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
if ($this->rc->output && $this->rc->output->type == 'html') {
$this->rc->output->set_env('libcal_settings', $this->load_settings());
$this->include_script('libcalendaring.js');
$this->include_stylesheet($this->local_skin_path() . '/libcal.css');
}
if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
}
}
}
/**
* Load iCalendar functions
*/
public static function get_ical()
{
$self = self::get_instance();
require_once __DIR__ . '/libvcalendar.php';
return new libvcalendar();
}
/**
* Load iTip functions
*/
public static function get_itip($domain = 'libcalendaring')
{
$self = self::get_instance();
require_once __DIR__ . '/lib/libcalendaring_itip.php';
return new libcalendaring_itip($self, $domain);
}
/**
* Load recurrence computation engine
*/
public static function get_recurrence()
{
$self = self::get_instance();
require_once __DIR__ . '/lib/libcalendaring_recurrence.php';
return new libcalendaring_recurrence($self);
}
/**
* Shift dates into user's current timezone
*
* @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
* @return object DateTime object in user's timezone
*/
public function adjust_timezone($dt, $dateonly = false)
{
if (is_numeric($dt))
$dt = new DateTime('@'.$dt);
else if (is_string($dt))
$dt = rcube_utils::anytodatetime($dt);
if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
$dt->setTimezone($this->timezone);
}
return $dt;
}
/**
*
*/
public function load_settings()
{
$this->date_format_defaults();
$settings = array();
// configuration
$settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
$settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
$settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['timezone'] = $this->timezone_offset;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
$this->rc->gettext('sunday'), $this->rc->gettext('monday'),
$this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'),
$this->rc->gettext('thursday'), $this->rc->gettext('friday'),
$this->rc->gettext('saturday')
);
$settings['days_short'] = array(
$this->rc->gettext('sun'), $this->rc->gettext('mon'),
$this->rc->gettext('tue'), $this->rc->gettext('wed'),
$this->rc->gettext('thu'), $this->rc->gettext('fri'),
$this->rc->gettext('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
// define list of file types which can be displayed inline
// same as in program/steps/mail/show.inc
$settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes');
return $settings;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private function date_format_defaults()
{
static $defaults = array();
// nothing to be done
if (isset($defaults['date_format']))
return;
$defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
$defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
// override defaults
if ($defaults['date_format'])
$this->defaults['calendar_date_format'] = $defaults['date_format'];
if ($defaults['time_format'])
$this->defaults['calendar_time_format'] = $defaults['time_format'];
// derive format variants from basic date format
$format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
$this->defaults['calendar_date_long'] = $format_set[0];
$this->defaults['calendar_date_short'] = $format_set[1];
$this->defaults['calendar_date_agenda'] = $format_set[2];
}
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$fromto = '--';
// handle task objects
if ($event['_type'] == 'task' && is_object($event['due'])) {
$date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
$fromto = $this->rc->format_date($event['due'], $date_format, false);
// add timezone information
if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
// abort if no valid event dates are given
if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
return $fromto;
}
$duration = $event['start']->diff($event['end'])->format('s');
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = $this->rc->format_date($event['start'], $date_format);
if (($todate = $this->rc->format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
$fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
' - ' . $this->rc->format_date($event['end'], $time_format);
}
else {
$fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
' - ' . $this->rc->format_date($event['end'], $date_format) . ' ' . $this->rc->format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
/**
* Render HTML form for alarm configuration
*/
public function alarm_select($attrib, $alarm_types, $absolute_time = true)
{
unset($attrib['name']);
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id']));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
$select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related'));
$object_type = $attrib['_type'] ?: 'event';
$select_type->add($this->gettext('none'), '');
foreach ($alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
$select_offset->add($this->gettext('trigger0'), '0');
if ($absolute_time)
$select_offset->add($this->gettext('trigger@'), '@');
$select_related->add($this->gettext('relatedstart'), 'start');
$select_related->add($this->gettext('relatedend' . $object_type), 'end');
// pre-set with default values from user settings
$preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$hidden = array('style' => 'display:none');
$html = html::span('edit-alarm-set',
$select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
$input_value->show($preset[0]) . ' ' .
$select_offset->show($preset[1]) . ' ' .
$select_related->show() . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return $html;
}
/**
* Get a list of email addresses of the given user (from login and identities)
*
* @param string User Email (default to current user)
* @return array Email addresses related to the user
*/
public function get_user_emails($user = null)
{
static $_emails = array();
if (empty($user)) {
$user = $this->rc->user->get_username();
}
// return cached result
if (is_array($_emails[$user])) {
return $_emails[$user];
}
$emails = array($user);
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
$emails = array_map('strtolower', $plugin['emails']);
// add all emails from the current user's identities
if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
foreach ($this->rc->user->list_emails() as $identity) {
$emails[] = strtolower($identity['email']);
}
}
$_emails[$user] = array_unique($emails);
return $_emails[$user];
}
/**
* Set the given participant status to the attendee matching the current user's identities
*
* @param array Hash array with event struct
* @param string The PARTSTAT value to set
* @return mixed Email address of the updated attendee or False if none matching found
*/
public function set_partstat(&$event, $status, $recursive = true)
{
$success = false;
$emails = $this->get_user_emails();
foreach ((array)$event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
$success = $attendee['email'];
}
}
// apply partstat update to each existing exception
if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false);
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
return $success;
}
/********* Alarms handling *********/
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public static function parse_alarm_value($val)
{
if ($val[0] == '@') {
return array(new DateTime($val));
}
else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
if ($m[1] == '')
$m[1] = '+';
foreach ($m2 as $seg) {
$prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
if ($seg[1] > 0) { // ignore zero values
// convert seconds to minutes
if ($seg[2] == 'S') {
$seg[2] = 'M';
$seg[1] = max(1, round($seg[1]/60));
}
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
}
// return zero value nevertheless
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
return false;
}
/**
* Convert the alarms list items to be processed on the client
*/
public static function to_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'] instanceof DateTime) {
$alarm['trigger'] = '@' . $alarm['trigger']->format('U');
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[2];
}
return $alarm;
}, (array)$valarms);
}
/**
* Process the alarms values submitted by the client
*/
public static function from_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'][0] == '@') {
try {
$alarm['trigger'] = new DateTime($alarm['trigger']);
$alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
}
catch (Exception $e) { /* handle this ? */ }
}
else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[3];
}
return $alarm;
}, (array)$valarms);
}
/**
* Render localized text for alarm settings
*/
public static function alarms_text($alarms)
{
if (is_array($alarms) && is_array($alarms[0])) {
$texts = array();
foreach ($alarms as $alarm) {
if ($text = self::alarm_text($alarm))
$texts[] = $text;
}
return join(', ', $texts);
}
else {
return self::alarm_text($alarms);
}
}
/**
* Render localized text for a single alarm property
*/
public static function alarm_text($alarm)
{
if (is_string($alarm)) {
list($trigger, $action) = explode(':', $alarm);
}
else {
$trigger = $alarm['trigger'];
$action = $alarm['action'];
$related = $alarm['related'];
}
$text = '';
$rcube = rcube::get_instance();
switch ($action) {
case 'EMAIL':
$text = $rcube->gettext('libcalendaring.alarmemail');
break;
case 'DISPLAY':
$text = $rcube->gettext('libcalendaring.alarmdisplay');
break;
case 'AUDIO':
$text = $rcube->gettext('libcalendaring.alarmaudio');
break;
}
if ($trigger instanceof DateTime) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($trigger))
));
}
else if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($m[1]))
));
}
else if ($val = self::parse_alarm_value($trigger)) {
$r = strtoupper($related ?: 'start') == 'END' ? 'end' : '';
// TODO: for all-day events say 'on date of event at XX' ?
if ($val[0] == 0) {
$text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r);
}
else {
$label = 'libcalendaring.trigger' . $r . $val[1];
$text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label);
}
}
else {
return false;
}
return $text;
}
/**
* Get the next alarm (time & action) for the given event
*
* @param array Record data
* @return array Hash array with alarm time/type or null if no alarms are configured
*/
public static function get_next_alarm($rec, $type = 'event')
{
if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
return null;
if ($type == 'task') {
$timezone = self::get_instance()->timezone;
if ($rec['startdate'])
$rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
if ($rec['date'])
$rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
}
if (!$rec['end'])
$rec['end'] = $rec['start'];
// support legacy format
if (!$rec['valarms']) {
list($trigger, $action) = explode(':', $rec['alarms'], 2);
if ($alarm = self::parse_alarm_value($trigger)) {
$rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
}
}
$expires = new DateTime('now - 12 hours');
$alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
// handle multiple alarms
$notify_at = null;
foreach ($rec['valarms'] as $alarm) {
$notify_time = null;
if ($alarm['trigger'] instanceof DateTime) {
$notify_time = $alarm['trigger'];
}
else if (is_string($alarm['trigger'])) {
$refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start'];
// abort if no reference date is available to compute notification time
if (!is_a($refdate, 'DateTime'))
continue;
// TODO: for all-day events, take start @ 00:00 as reference date ?
try {
$interval = new DateInterval(trim($alarm['trigger'], '+-'));
$interval->invert = $alarm['trigger'][0] == '-';
$notify_time = clone $refdate;
$notify_time->add($interval);
}
catch (Exception $e) {
rcube::raise_error($e, true);
continue;
}
}
if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
$notify_at = $notify_time;
$action = $alarm['action'];
$alarm_prop = $alarm;
// generate a unique alarm ID if multiple alarms are set
if (count($rec['valarms']) > 1) {
$alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
}
}
}
return !$notify_at ? null : array(
'time' => $notify_at->format('U'),
'action' => $action ? strtoupper($action) : 'DISPLAY',
'id' => $alarm_id,
'prop' => $alarm_prop,
);
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
public function refresh($attr)
{
// collect pending alarms from all providers (e.g. calendar, tasks)
$plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
'time' => time(),
'alarms' => array(),
));
if (!$plugin['abort'] && !empty($plugin['alarms'])) {
// make sure texts and env vars are available on client
$this->add_texts('localization/', true);
$this->rc->output->add_label('close');
$this->rc->output->set_env('snooze_select', $this->snooze_select());
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
}
}
/**
* Handler for alarm dismiss/snooze requests
*/
public function alarms_action()
{
// $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$data['ids'] = explode(',', $data['id']);
$plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
if ($plugin['success'])
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private function _alarms_output($alarms)
{
$out = array();
foreach ($alarms as $alarm) {
$out[] = array(
'id' => $alarm['id'],
'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '',
'allDay' => ($alarm['allday'] == 1)?true:false,
'title' => $alarm['title'],
'location' => $alarm['location'],
'calendar' => $alarm['calendar'],
);
}
return $out;
}
/**
* Render a dropdown menu to choose snooze time
*/
private function snooze_select($attrib = array())
{
$steps = array(
5 => 'repeatinmin',
10 => 'repeatinmin',
15 => 'repeatinmin',
20 => 'repeatinmin',
30 => 'repeatinmin',
60 => 'repeatinhr',
120 => 'repeatinhrs',
1440 => 'repeattomorrow',
10080 => 'repeatinweek',
);
$items = array();
foreach ($steps as $n => $label) {
$items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
$this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
}
return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
}
/********* Recurrence rules handling ********/
/**
* Render localized text describing the recurrence rule of an event
*/
public 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];
$third = $rrule['RDATE'][2];
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;
// verify interval with next item
if (is_a($third, 'DateTime')) {
$diff2 = $second->diff($third);
if ($diff2->$k != $diff->$k) {
unset($rrule['INTERVAL']);
}
}
break;
}
}
}
if (!$rrule['INTERVAL']) {
$rrule['FREQ'] = 'RDATE';
}
$rrule['UNTIL'] = end($rrule['RDATE']);
}
$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') . ' ' . $this->rc->format_date($rrule['UNTIL'], self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
}
else {
$until = $this->gettext('forever');
}
$except = '';
if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
$format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$exdates = array_map(
function($dt) use ($format) { return rcmail::get_instance()->format_date($dt, $format); },
array_slice($rrule['EXDATE'], 0, 10)
);
$except = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates);
}
return rtrim($freq . $details . ', ' . $until . $except);
}
/**
* Generate the form for recurrence settings
*/
public function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
$select->add($this->gettext('never'), '');
$select->add($this->gettext('daily'), 'DAILY');
$select->add($this->gettext('weekly'), 'WEEKLY');
$select->add($this->gettext('monthly'), 'MONTHLY');
$select->add($this->gettext('yearly'), 'YEARLY');
$select->add($this->gettext('rdate'), 'RDATE');
$html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show('');
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days')));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks')));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'),
$checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
$this->gettext($daymap[$d])
) . ' ';
}
$html .= html::div($attrib, html::label(null, $this->gettext('bydays')) . $weekdays);
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months')));
$checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery')));
$table->add(null, $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, $table->show());
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years')));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
// day rule selection
$html .= html::div($attrib, html::label(null, $this->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
$html = html::div('line first',
html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
$this->gettext('forever'))
);
$forntimes = $this->gettext(array(
'name' => 'forntimes',
'vars' => array('nr' => '%s'))
);
$html .= html::div('line',
$radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' .
sprintf($forntimes, $select->show(1))
);
$html .= html::div('line',
$radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' .
$this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
);
$html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html);
break;
case 'rdate':
$ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
$input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10"));
$button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
$html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->gettext('first'),
$this->gettext('second'),
$this->gettext('third'),
$this->gettext('fourth'),
$this->gettext('last')
),
array(1, 2, 3, 4, -1));
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
return $select_prefix->show() . '&nbsp;' . $select_wday->show();
}
/**
* Convert the recurrence settings to be processed on the client
*/
public function to_client_recurrence($recurrence, $allday = false)
{
if ($recurrence['UNTIL'])
$recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
// format RDATE values
if (is_array($recurrence['RDATE'])) {
$libcal = $this;
$recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
}, $recurrence['RDATE']);
}
unset($recurrence['EXCEPTIONS']);
return $recurrence;
}
/**
* Process the alarms values submitted by the client
*/
public function from_client_recurrence($recurrence, $start = null)
{
if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
$recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
}
if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
$tz = $this->timezone;
$recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
$dt = new DateTime($rdate, $tz);
if (is_a($start, 'DateTime'))
$dt->setTime($start->format('G'), $start->format('i'));
return $dt;
}
catch (Exception $e) {
return null;
}
}, $recurrence['RDATE']);
}
return $recurrence;
}
/********* Attachments handling *********/
/**
* Handler for attachment uploads
*/
public function attachment_upload($session_key, $id_prefix = '')
{
// Upload progress update
if (!empty($_GET['_progress'])) {
$this->rc->upload_progress();
}
$recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
$_SESSION[$session_key] = array();
$_SESSION[$session_key]['id'] = $recid;
$_SESSION[$session_key]['attachments'] = array();
}
// clear all stored output properties (like scripts and env vars)
$this->rc->output->reset();
if (is_array($_FILES['_attachments']['tmp_name'])) {
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_attachments']['error'][$i];
if (!$err) {
$attachment = array(
'path' => $filepath,
'size' => $_FILES['_attachments']['size'][$i],
'name' => $_FILES['_attachments']['name'][$i],
'mimetype' => rcube_mime::file_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
'group' => $recid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
}
if (!$err && $attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$_SESSION[$session_key]['attachments'][$id] = $attachment;
if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => $this->rc->gettext('delete')
));
}
else {
$button = rcube::Q($this->rc->gettext('delete'));
}
$content = html::a(array(
'href' => "#delete",
'class' => 'delete',
'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
), $button);
$content .= rcube::Q($attachment['name']);
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true), $uploadid);
}
else { // upload failed
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else if ($attachment['error']) {
$msg = $attachment['error'];
}
else {
$msg = $this->rc->gettext('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size'))
$msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
'size' => $this->rc->show_bytes(parse_bytes($maxsize)))));
else
$msg = $this->rc->gettext('fileuploaderror');
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
$this->rc->output->send('iframe');
}
/**
* Deliver an event/task attachment to the client
* (similar as in Roundcube core program/steps/mail/get.inc)
*/
public function attachment_get($attachment)
{
ob_end_clean();
if ($attachment && $attachment['body']) {
// allow post-processing of the attachment body
$part = new rcube_message_part;
$part->filename = $attachment['name'];
$part->size = $attachment['size'];
$part->mimetype = $attachment['mimetype'];
$plugin = $this->rc->plugins->exec_hook('message_part_get', array(
'body' => $attachment['body'],
'mimetype' => strtolower($attachment['mimetype']),
'download' => !empty($_GET['_download']),
'part' => $part,
));
if ($plugin['abort'])
exit;
$mimetype = $plugin['mimetype'];
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$browser = $this->rc->output->browser;
// send download headers
if ($plugin['download']) {
header("Content-Type: application/octet-stream");
if ($browser->ie)
header("Content-Type: application/force-download");
}
else if ($ctype_primary == 'text') {
header("Content-Type: text/$ctype_secondary");
}
else {
header("Content-Type: $mimetype");
header("Content-Transfer-Encoding: binary");
}
// display page, @TODO: support text/plain (and maybe some other text formats)
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$OUTPUT = new rcmail_html_page();
// @TODO: use washtml on $body
$OUTPUT->write($plugin['body']);
}
else {
// don't kill the connection if download takes more than 30 sec.
@set_time_limit(0);
$filename = $attachment['name'];
$filename = preg_replace('[\r\n]', '', $filename);
if ($browser->ie && $browser->ver < 7)
$filename = rawurlencode(abbreviate_string($filename, 55));
else if ($browser->ie)
$filename = rawurlencode($filename);
else
$filename = addcslashes($filename, '"');
$disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
header("Content-Disposition: $disposition; filename=\"$filename\"");
echo $plugin['body'];
}
exit;
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Show "loading..." page in attachment iframe
*/
public function attachment_loading_page()
{
$url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
$message = $this->rc->gettext('loadingdata');
header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
print "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url='.rcube::Q($url).'">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset='.RCUBE_CHARSET.'">' . "\n"
. "</head>\n<body>\n$message\n</body>\n</html>";
exit;
}
/**
* Template object for attachment display frame
*/
public function attachment_frame($attrib = array())
{
$mimetype = strtolower($this->attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
$this->rc->output->add_gui_object('attachmentframe', $attrib['id']);
return html::iframe($attrib);
}
/**
*
*/
public function attachment_header($attrib = array())
{
$rcmail = rcmail::get_instance();
$dl_link = strtolower($attrib['downloadlink']) == 'true';
$dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET);
$table = new html_table(array('cols' => $dl_link ? 3 : 2));
if (!empty($this->attachment['name'])) {
$table->add('title', rcube::Q($this->rc->gettext('filename')));
$table->add('header', rcube::Q($this->attachment['name']));
if ($dl_link) {
$table->add('download-link', html::a($dl_url, rcube::Q($this->rc->gettext('download'))));
}
}
if (!empty($this->attachment['mimetype'])) {
$table->add('title', rcube::Q($this->rc->gettext('type')));
$table->add('header', rcube::Q($this->attachment['mimetype']));
}
if (!empty($this->attachment['size'])) {
$table->add('title', rcube::Q($this->rc->gettext('filesize')));
$table->add('header', rcube::Q($this->rc->show_bytes($this->attachment['size'])));
}
$this->rc->output->set_env('attachment_download_url', $dl_url);
return $table->show($attrib);
}
/********* iTip message detection *********/
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->ical_message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->ical_message->mime_parts as $part) {
if (self::part_is_vcalendar($part)) {
if ($part->ctype_parameters['method'])
$itip_part = $part->mime_id;
else
$this->ical_parts[] = $part->mime_id;
}
}
// priorize part with method parameter
if ($itip_part) {
$this->ical_parts = array($itip_part);
}
}
/**
* Getter for the parsed iCal objects attached to the current email message
*
* @return object libvcalendar parser instance with the parsed objects
*/
public function get_mail_ical_objects()
{
// create parser and load ical objects
if (!$this->mail_ical_parser) {
$this->mail_ical_parser = $this->get_ical();
foreach ($this->ical_parts as $mime_id) {
$part = $this->ical_message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET;
$this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
// check if the parsed object is an instance of a recurring event/task
array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
// stop on the part that has an iTip method specified
if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
$this->mail_ical_parser->message_date = $this->ical_message->headers->date;
$this->mail_ical_parser->mime_id = $mime_id;
// store the message's sender address for comparisons
$this->mail_ical_parser->sender = preg_match(self::$email_regex, $this->ical_message->headers->from, $m) ? $m[1] : '';
if (!empty($this->mail_ical_parser->sender)) {
foreach ($this->mail_ical_parser->objects as $i => $object) {
$this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
$this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
}
}
break;
}
}
}
return $this->mail_ical_parser;
}
/**
* Read the given mime message from IMAP and parse ical data
*
* @param string Mailbox name
* @param string Message UID
* @param string Message part ID and object index (e.g. '1.2:0')
* @param string Object type filter (optional)
*
* @return array Hash array with the parsed iCal
*/
public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
{
$charset = RCUBE_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
- $imap->set_mailbox($mbox);
+ $imap->set_folder($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $imap->get_message_part($uid, $mime_id);
$headers = $imap->get_message_headers($uid);
$parser = $this->get_ical();
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$objects = $parser->import($part, $charset);
}
}
// successfully parsed events/tasks?
if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
if ($parser->method)
$object['_method'] = $parser->method;
// store the message's sender address for comparisons
$object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
$object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
// check if this is an instance of a recurring event/task
self::identify_recurrence_instance($object);
return $object;
}
return null;
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @return boolean True if part is of type vcard
*/
public static function part_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))
);
}
/**
* Single occourrences of recurring events are identified by their RECURRENCE-ID property
* in iCal which is represented as 'recurrence_date' in our internal data structure.
*
* Check if such a property exists and derive the '_instance' identifier and '_savemode'
* attributes which are used in the storage backend to identify the nested exception item.
*/
public static function identify_recurrence_instance(&$object)
{
// for savemode=all, remove recurrence instance identifiers
if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
unset($object['_instance'], $object['recurrence_date']);
}
// set instance and 'savemode' according to recurrence-id
else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
$object['_instance'] = self::recurrence_instance_identifier($object);
$object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
}
else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) {
if (strlen($object['_instance']) > 4) {
$object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
}
else {
$object['recurrence_date'] = clone $object['start'];
}
}
}
/**
* Return a date() format string to render identifiers for recurrence instances
*
* @param array Hash array with event properties
* @return string Format string
*/
public static function recurrence_id_format($event)
{
return $event['allday'] ? 'Ymd' : 'Ymd\THis';
}
/**
* Return the identifer for the given instance of a recurring event
*
* @param array Hash array with event properties
* @return mixed Format string or null if identifier cannot be generated
*/
public static function recurrence_instance_identifier($event)
{
$instance_date = $event['recurrence_date'] ?: $event['start'];
if ($instance_date && is_a($instance_date, 'DateTime')) {
$recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
return $instance_date->format($recurrence_id_format);
}
return null;
}
/********* Attendee handling functions *********/
/**
* Handler for attendee group expansion requests
*/
public function expand_attendee_group()
{
$id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
$result = array('id' => $id, 'members' => array());
$maxnum = 500;
// iterate over all autocomplete address books (we don't know the source of the group)
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
foreach ($abook->list_groups($data['name'], 1) as $group) {
// this is the matching group to expand
if (in_array($data['email'], (array)$group['email'])) {
$abook->set_pagesize($maxnum);
$abook->set_group($group['ID']);
// get all members
$res = $abook->list_records($this->rc->config->get('contactlist_fields'));
// handle errors (e.g. sizelimit, timelimit)
if ($abook->get_error()) {
$result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
$res = false;
}
// check for maximum number of members (we don't wanna bloat the UI too much)
else if ($res->count > $maxnum) {
$result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
$res = false;
}
while ($res && ($member = $res->iterate())) {
$emails = (array)$abook->get_col_values('email', $member, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$result['members'][] = array(
'email' => $email,
'name' => rcube_addressbook::compose_list_name($member),
);
}
}
break 2;
}
}
}
}
$this->rc->output->command('plugin.expand_attendee_callback', $result);
}
/********* Static utility functions *********/
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public static function to_rrule($recurrence, $allday = false)
{
if (is_string($recurrence))
return $recurrence;
$rrule = '';
foreach ((array)$recurrence as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
// convert to UTC according to RFC 5545
if (is_a($val, 'DateTime')) {
if (!$allday && !$val->_dateonly) {
$until = clone $val;
$until->setTimezone(new DateTimeZone('UTC'));
$val = $until->format('Ymd\THis\Z');
}
else {
$val = $val->format('Ymd');
}
}
break;
case 'RDATE':
case 'EXDATE':
foreach ((array)$val as $i => $ex) {
if (is_a($ex, 'DateTime'))
$val[$i] = $ex->format('Ymd\THis');
}
$val = join(',', (array)$val);
break;
case 'EXCEPTIONS':
continue 2;
}
if (strlen($val))
$rrule .= $k . '=' . $val . ';';
}
return rtrim($rrule, ';');
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
public static function to_php_date_format($from)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return strtr(strtr($from, array(
'yyyy' => 'Y',
'yy' => 'y',
'MMMM' => 'F',
'MMM' => 'M',
'MM' => 'm',
'M' => 'n',
'dddd' => 'l',
'ddd' => 'D',
'dd' => 'd',
'd' => 'j',
'HH' => '**',
'hh' => '%%',
'H' => 'G',
'h' => 'g',
'mm' => 'i',
'ss' => 's',
'TT' => 'A',
'tt' => 'a',
'T' => 'A',
't' => 'a',
'u' => 'c',
)), array(
'**' => 'H',
'%%' => 'h',
));
}
/**
* Convert from PHP date() format to fullcalendar format string
*/
public static function from_php_date_format($from)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return strtr($from, array(
'y' => 'yy',
'Y' => 'yyyy',
'M' => 'MMM',
'F' => 'MMMM',
'm' => 'MM',
'n' => 'M',
'j' => 'd',
'd' => 'dd',
'D' => 'ddd',
'l' => 'dddd',
'H' => 'HH',
'h' => 'hh',
'G' => 'H',
'g' => 'h',
'i' => 'mm',
's' => 'ss',
'A' => 'TT',
'a' => 'tt',
'c' => 'u',
));
}
}
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 2fb81c71..e0c167f7 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1,2277 +1,2277 @@
<?php
/**
* Tasks plugin for Roundcube webmail
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* 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 tasklist extends rcube_plugin
{
const FILTER_MASK_TODAY = 1;
const FILTER_MASK_TOMORROW = 2;
const FILTER_MASK_WEEK = 4;
const FILTER_MASK_LATER = 8;
const FILTER_MASK_NODATE = 16;
const FILTER_MASK_OVERDUE = 32;
const FILTER_MASK_FLAGGED = 64;
const FILTER_MASK_COMPLETE = 128;
const FILTER_MASK_ASSIGNED = 256;
const FILTER_MASK_MYTASKS = 512;
const SESSION_KEY = 'tasklist_temp';
public static $filter_masks = array(
'today' => self::FILTER_MASK_TODAY,
'tomorrow' => self::FILTER_MASK_TOMORROW,
'week' => self::FILTER_MASK_WEEK,
'later' => self::FILTER_MASK_LATER,
'nodate' => self::FILTER_MASK_NODATE,
'overdue' => self::FILTER_MASK_OVERDUE,
'flagged' => self::FILTER_MASK_FLAGGED,
'complete' => self::FILTER_MASK_COMPLETE,
'assigned' => self::FILTER_MASK_ASSIGNED,
'mytasks' => self::FILTER_MASK_MYTASKS,
);
public $task = '?(?!login|logout).*';
public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order');
public $rc;
public $lib;
public $driver;
public $timezone;
public $ui;
public $home; // declare public to be used in other classes
private $collapsed_tasks = array();
private $message_tasks = array();
private $itip;
private $ical;
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->require_plugin('jqueryui');
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('tasks', 'tasklist');
// load plugin configuration
$this->load_config();
$this->timezone = $this->lib->timezone;
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('user_delete', array($this, 'user_delete'));
}
/**
* Startup hook
*/
public function startup($args)
{
// the tasks module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('tasklist_disabled', false) || !$this->rc->config->get('tasklist_enabled', true))
return;
// load localizations
$this->add_texts('localization/', $args['task'] == 'tasks' && (!$args['action'] || $args['action'] == 'print'));
$this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle'))); // add label for task title
if ($args['task'] == 'tasks' && $args['action'] != 'save-pref') {
$this->load_driver();
// register calendar actions
$this->register_action('index', array($this, 'tasklist_view'));
$this->register_action('task', array($this, 'task_action'));
$this->register_action('tasklist', array($this, 'tasklist_action'));
$this->register_action('counts', array($this, 'fetch_counts'));
$this->register_action('fetch', array($this, 'fetch_tasks'));
$this->register_action('print', array($this, 'print_tasks'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('mailimportitip', array($this, 'mail_import_itip'));
$this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
$this->register_action('itip-status', array($this, 'task_itip_status'));
$this->register_action('itip-remove', array($this, 'task_itip_remove'));
$this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
$this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
$this->add_hook('refresh', array($this, 'refresh'));
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
}
else if ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
if ($this->rc->config->get('tasklist_mail_embed', true)) {
$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' => 'tasklist-create-from-mail',
'label' => 'tasklist.createfrommail',
'type' => 'link',
'classact' => 'icon taskaddlink active',
'class' => 'icon taskaddlink',
'innerclass' => 'icon taskadd',
))),
'messagemenu');
$this->api->output->add_label('tasklist.createfrommail');
}
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->load_ui();
$this->ui->init();
}
// add hooks for alarms handling
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
*
*/
private function load_ui()
{
if (!$this->ui) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
}
}
/**
* 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('tasklist_driver', 'database');
$driver_class = 'tasklist_' . $driver_name . '_driver';
require_once($this->home . '/drivers/tasklist_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;
}
$this->rc->output->set_env('tasklist_driver', $driver_name);
}
/**
* Dispatcher for task-related actions initiated by the client
*/
public function task_action()
{
$filter = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
$oldrec = $rec;
$success = $refresh = $got_msg = false;
// force notify if hidden + active
$itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
if ($itip_send_option === 1 && empty($rec['_reportpartstat']))
$rec['_notify'] = 1;
switch ($action) {
case 'new':
$oldrec = null;
$rec = $this->prepare_task($rec);
$rec['uid'] = $this->generate_uid();
$temp_id = $rec['tempid'];
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
$this->cleanup_task($rec);
}
break;
case 'complete':
$complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST));
if (!($rec = $this->driver->get_task($rec))) {
break;
}
$oldrec = $rec;
$rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION');
// sent itip notifications if enabled (no user interaction here)
if (($itip_send_option & 1)) {
if ($this->is_attendee($rec)) {
$rec['_reportpartstat'] = $rec['status'];
}
else if ($this->is_organizer($rec)) {
$rec['_notify'] = 1;
}
}
case 'edit':
$oldrec = $this->driver->get_task($rec);
$rec = $this->prepare_task($rec);
$clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
if ($success = $this->driver->edit_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
$this->cleanup_task($rec);
// add clone from recurring task
if ($clone && $this->driver->create_task($clone)) {
$refresh[] = $this->driver->get_task($clone);
$this->driver->clear_alarms($rec['id']);
}
// move all childs if list assignment was changed
if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) {
foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']);
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
$refresh[] = $r;
}
}
}
}
}
break;
case 'move':
foreach ((array)$rec['id'] as $id) {
$r = $rec;
$r['id'] = $id;
if ($this->driver->move_task($r)) {
$new_task = $this->driver->get_task($r);
$new_task['tempid'] = $id;
$refresh[] = $new_task;
$success = true;
// move all childs, too
foreach ($this->driver->get_childs(array('id' => $id, 'list' => $rec['_fromlist']), true) as $cid) {
$child = $rec;
$child['id'] = $cid;
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
$r['tempid'] = $cid;
$refresh[] = $r;
}
}
}
}
}
break;
case 'delete':
$mode = intval(rcube_utils::get_input_value('mode', rcube_utils::INPUT_POST));
$oldrec = $this->driver->get_task($rec);
if ($success = $this->driver->delete_task($rec, false)) {
// delete/modify all childs
foreach ($this->driver->get_childs($rec, $mode) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list']);
if ($mode == 1) { // delete all childs
if ($this->driver->delete_task($child, false)) {
if ($this->driver->undelete)
$_SESSION['tasklist_undelete'][$rec['id']][] = $cid;
}
else
$success = false;
}
else {
$child['parent_id'] = strval($oldrec['parent_id']);
$this->driver->edit_task($child);
}
}
// update parent task to adjust list of children
if (!empty($oldrec['parent_id'])) {
$refresh[] = $this->driver->get_task(array('id' => $oldrec['parent_id'], 'list' => $rec['list']));
}
}
if (!$success)
$this->rc->output->command('plugin.reload_data');
break;
case 'undelete':
if ($success = $this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) {
if ($this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
}
}
}
break;
case 'collapse':
foreach (explode(',', $rec['id']) as $rec_id) {
if (intval(rcube_utils::get_input_value('collapsed', rcube_utils::INPUT_GPC))) {
$this->collapsed_tasks[] = $rec_id;
}
else {
$i = array_search($rec_id, $this->collapsed_tasks);
if ($i !== false)
unset($this->collapsed_tasks[$i]);
}
}
$this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));
return; // avoid further actions
case 'rsvp':
$status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
$noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action';
$task = $this->driver->get_task($rec);
$task['attendees'] = $rec['attendees'];
$task['_type'] = 'task';
// send invitation to delegatee + add it as attendee
if ($status == 'delegated' && $rec['to']) {
$itip = $this->load_itip();
if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) {
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
$refresh[] = $task;
$noreply = false;
}
}
$rec = $task;
if ($success = $this->driver->edit_task($rec)) {
if (!$noreply) {
// let the reply clause further down send the iTip message
$rec['_reportpartstat'] = $status;
}
}
break;
case 'changelog':
$data = $this->driver->get_task_changelog($rec);
if (is_array($data) && !empty($data)) {
$lib = $this->lib;
$dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($data, function(&$change) use ($lib, $dtformat) {
if ($change['date']) {
$dt = $lib->adjust_timezone($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $this->rc->format_date($dt, $dtformat, false);
}
}
});
$this->rc->output->command('plugin.task_render_changelog', $data);
}
else {
$this->rc->output->command('plugin.task_render_changelog', false);
}
$got_msg = true;
break;
case 'diff':
$data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']);
if (is_array($data)) {
// convert some properties, similar to self::_client_event()
$lib = $this->lib;
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
$time_format = $this->rc->config->get('time_format', 'H:i');
array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) {
// convert date cols
if (in_array($change['property'], array('date','start','created','changed'))) {
if (!empty($change['old'])) {
$dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format;
$change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat);
}
if (!empty($change['new'])) {
$dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format;
$change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat);
}
}
// create textual representation for alarms and recurrence
if ($change['property'] == 'alarms') {
if (is_array($change['old']))
$change['old_'] = libcalendaring::alarm_text($change['old']);
if (is_array($change['new']))
$change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'recurrence') {
if (is_array($change['old']))
$change['old_'] = $lib->recurrence_text($change['old']);
if (is_array($change['new']))
$change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'complete') {
$change['old_'] = intval($change['old']) . '%';
$change['new_'] = intval($change['new']) . '%';
}
if ($change['property'] == 'attachments') {
if (is_array($change['old']))
$change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
if (is_array($change['new'])) {
$change['new'] = array_merge((array)$change['old'], $change['new']);
$change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
}
}
// resolve parent_id to the refered task title for display
if ($change['property'] == 'parent_id') {
$change['property'] = 'parent-title';
if (!empty($change['old']) && ($old_parent = $this->driver->get_task(array('id' => $change['old'], 'list' => $rec['list'])))) {
$change['old_'] = $old_parent['title'];
}
if (!empty($change['new']) && ($new_parent = $this->driver->get_task(array('id' => $change['new'], 'list' => $rec['list'])))) {
$change['new_'] = $new_parent['title'];
}
}
// compute a nice diff of description texts
if ($change['property'] == 'description') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
}
});
$this->rc->output->command('plugin.task_show_diff', $data);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
$got_msg = true;
break;
case 'show':
if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) {
$this->encode_task($rec);
$rec['readonly'] = 1;
$this->rc->output->command('plugin.task_show_revision', $rec);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
}
$got_msg = true;
break;
case 'restore':
if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) {
$refresh = $this->driver->get_task($rec);
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation');
$this->rc->output->command('plugin.close_history_dialog');
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
}
$got_msg = true;
break;
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
else if (!$got_msg) {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
// send out notifications
if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) {
// make sure we have the complete record
$task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);
// only notify if data really changed (TODO: do diff check on client already)
if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) {
$sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
if ($sent > 0)
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
else if ($sent < 0)
$this->rc->output->show_message('tasklist.errornotifying', 'error');
}
}
else if ($success && $rec['_reportpartstat'] && $rec['_reportpartstat'] != 'NEEDS-ACTION') {
// get the full record after update
$task = $this->driver->get_task($rec);
// send iTip REPLY with the updated partstat
if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
$sender = $task['attendees'][$idx];
$status = strtolower($sender['status']);
if (!empty($_POST['comment']))
$task['comment'] = rcube_utils::get_input_value('comment', rcube_utils::INPUT_POST);
$itip = $this->load_itip();
$itip->set_sender_email($sender['email']);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
if ($refresh['id']) {
$this->encode_task($refresh);
}
else if (is_array($refresh)) {
foreach ($refresh as $i => $r)
$this->encode_task($refresh[$i]);
}
$this->rc->output->command('plugin.update_task', $refresh);
}
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined','delegated'));
$this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
}
return $this->itip;
}
/**
* repares new/edited task properties before save
*/
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
if ($rec['raw']) {
foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
$locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
$normwords[] = $word;
$datewords[] = $word;
}
foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
$locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
$normwords[] = $month;
$datewords[] = $month;
}
foreach (array('on','this','next','at') as $word) {
$fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
$fillwords[] = $word;
}
$raw = trim($rec['raw']);
$date_str = '';
// translate localized keywords
$raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
$raw = preg_replace($locwords, $normwords, $raw);
// find date pattern
$date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
if (preg_match($date_pattern, $raw, $m)) {
$date_str .= $m[1] . $m[2] . $m[3];
$raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
// add year to date string
if ($m[1] && !$m[3])
$date_str .= date('Y');
}
// find time pattern
$time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
if (preg_match($time_pattern, $raw, $m)) {
$has_time = true;
$date_str .= ($date_str ? ' ' : 'today ') . $m[1];
$raw = preg_replace($time_pattern, '', $raw);
}
// yes, raw input matched a (valid) date
if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
$rec['date'] = $date->format('Y-m-d');
if ($has_time)
$rec['time'] = $date->format('H:i');
$rec['title'] = $raw;
}
else
$rec['title'] = $rec['raw'];
}
// normalize input from client
if (isset($rec['complete'])) {
$rec['complete'] = floatval($rec['complete']);
if ($rec['complete'] > 1)
$rec['complete'] /= 100;
}
if (isset($rec['flagged']))
$rec['flagged'] = intval($rec['flagged']);
// fix for garbage input
if ($rec['description'] == 'null')
$rec['description'] = '';
foreach ($rec as $key => $val) {
if ($val === 'null')
$rec[$key] = null;
}
if (!empty($rec['date'])) {
$this->normalize_dates($rec, 'date', 'time');
}
if (!empty($rec['startdate'])) {
$this->normalize_dates($rec, 'startdate', 'starttime');
}
// convert tags to array, filter out empty entries
if (isset($rec['tags']) && !is_array($rec['tags'])) {
$rec['tags'] = array_filter((array)$rec['tags']);
}
// convert the submitted alarm values
if ($rec['valarms']) {
$valarms = array();
foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) {
// alarms can only work with a date (either task start, due or absolute alarm date)
if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate'])
$valarms[] = $alarm;
}
$rec['valarms'] = $valarms;
}
// convert the submitted recurrence settings
if (is_array($rec['recurrence'])) {
$refdate = null;
if (!empty($rec['date'])) {
$refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
}
else if (!empty($rec['startdate'])) {
$refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
}
if ($refdate) {
$rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate);
// translate count into an absolute end date.
// why? because when shifting completed tasks to the next recurrence,
// the initial start date to count from gets lost.
if ($rec['recurrence']['COUNT']) {
$engine = libcalendaring::get_recurrence();
$engine->init($rec['recurrence'], $refdate);
if ($until = $engine->end()) {
$rec['recurrence']['UNTIL'] = $until;
unset($rec['recurrence']['COUNT']);
}
}
}
else { // recurrence requires a reference date
$rec['recurrence'] = '';
}
}
$attachments = array();
$taskid = $rec['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
unset($attachments[$id]['abort'], $attachments[$id]['group']);
}
}
}
}
$rec['attachments'] = $attachments;
// convert link references into simple URIs
if (array_key_exists('links', $rec)) {
$rec['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$rec['links']);
}
// convert invalid data
if (isset($rec['attendees']) && !is_array($rec['attendees']))
$rec['attendees'] = array();
foreach ((array)$rec['attendees'] as $i => $attendee) {
if (is_string($attendee['rsvp'])) {
$rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
}
}
// copy the task status to my attendee partstat
if (!empty($rec['_reportpartstat'])) {
if (($idx = $this->is_attendee($rec)) !== false) {
if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
$rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
else
unset($rec['_reportpartstat']);
}
}
// set organizer from identity selector
if ((isset($rec['_identity']) || (!empty($rec['attendees']) && empty($rec['organizer']))) &&
($identity = $this->rc->user->get_identity($rec['_identity']))) {
$rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
}
if (is_numeric($rec['id']) && $rec['id'] < 0)
unset($rec['id']);
return $rec;
}
/**
* Utility method to convert a tasks date/time values into a normalized format
*/
private function normalize_dates(&$rec, $date_key, $time_key)
{
try {
// parse date from user format (#2801)
$date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d');
$date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone);
// fall back to default strtotime logic
if (empty($date)) {
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
}
$rec[$date_key] = $date->format('Y-m-d');
if (!empty($rec[$time_key]))
$rec[$time_key] = $date->format('H:i');
return true;
}
catch (Exception $e) {
$rec[$date_key] = $rec[$time_key] = null;
}
return false;
}
/**
* Releases some resources after successful save
*/
private function cleanup_task(&$rec)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* When flagging a recurring task as complete,
* clone it and shift dates to the next occurrence
*/
private function handle_recurrence(&$rec, $old)
{
$clone = null;
if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) {
$engine = libcalendaring::get_recurrence();
$rrule = $rec['recurrence'];
$updates = array();
// compute the next occurrence of date attributes
foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) {
if (empty($rec[$date_key]))
continue;
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
$engine->init($rrule, $date);
if ($next = $engine->next()) {
$updates[$date_key] = $next->format('Y-m-d');
if (!empty($rec[$time_key]))
$updates[$time_key] = $next->format('H:i');
}
}
// shift absolute alarm dates
if (!empty($updates) && is_array($rec['valarms'])) {
$updates['valarms'] = array();
unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited
foreach ($rec['valarms'] as $i => $alarm) {
if ($alarm['trigger'] instanceof DateTime) {
$engine->init($rrule, $alarm['trigger']);
if ($next = $engine->next()) {
$alarm['trigger'] = $next;
}
}
$updates['valarms'][$i] = $alarm;
}
}
if (!empty($updates)) {
// clone task to save a completed copy
$clone = $rec;
$clone['uid'] = $this->generate_uid();
$clone['parent_id'] = $rec['id'];
unset($clone['id'], $clone['recurrence'], $clone['attachments']);
// update the task but unset completed flag
$rec = array_merge($rec, $updates);
$rec['complete'] = $old['complete'];
$rec['status'] = $old['status'];
}
}
return $clone;
}
/**
* Send out an invitation/notification to all task attendees
*/
private function notify_attendees($task, $old, $action = 'edit', $comment = null)
{
if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) {
$task['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->lib->get_user_emails();
$itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3);
// add comment to the iTip attachment
$task['comment'] = $comment;
// needed to generate VTODO instead of VEVENT entry
$task['_type'] = 'task';
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'delete' ? 'CANCEL' : 'REQUEST';
$object = $this->to_libcal($task);
$message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']);
// list existing attendees from the $old task
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0; $current = array();
foreach ((array)$task['attendees'] as $attendee) {
$current[] = strtolower($attendee['email']);
// skip myself for obvious reasons
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) {
continue;
}
// skip if notification is disabled for this attendee
if ($attendee['noreply'] && $itip_notify & 2) {
continue;
}
// skip if this attendee has delegated and set RSVP=FALSE
if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) {
continue;
}
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$is_rsvp = $is_new || $task['sequence'] > $old['sequence'];
$bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody');
$subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty'));
// finally send the message
if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
$sent++;
else
$sent = -100;
}
// send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) {
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
continue;
}
$vtodo = $this->to_libcal($old);
$vtodo['cancelled'] = $is_cancelled;
$vtodo['attendees'] = array($attendee);
$vtodo['comment'] = $comment;
if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Compare two task objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing task properties
*/
public static function task_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;
}
/**
* Dispatcher for tasklist actions initiated by the client
*/
public function tasklist_action()
{
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true);
$success = false;
if (isset($list['showalarms']))
$list['showalarms'] = intval($list['showalarms']);
switch ($action) {
case 'form-new':
case 'form-edit':
echo $this->ui->tasklist_editform($action, $list);
exit;
case 'new':
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
if (!$list['_reload']) {
$this->load_ui();
$list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
$list += (array)$jsenv[$insert_id];
}
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
break;
case 'edit':
if ($newid = $this->driver->edit_list($list)) {
$list['oldid'] = $list['id'];
$list['id'] = $newid;
$this->rc->output->command('plugin.update_tasklist', $list);
$success = true;
}
break;
case 'subscribe':
$success = $this->driver->subscribe_list($list);
break;
case 'delete':
if (($success = $this->driver->delete_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
case 'search':
$this->load_ui();
$results = array();
$query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
foreach ((array)$this->driver->search_lists($query, $source) as $id => $prop) {
$editname = $prop['editname'];
unset($prop['editname']); // force full name to be displayed
$prop['active'] = false;
// let the UI generate HTML and CSS representation for this calendar
$html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
$prop += (array)$jsenv[$id];
$prop['editname'] = $editname;
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($this->driver->search_more_results) {
$this->rc->output->show_message('autocompletemore', 'info');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
$this->rc->output->command('plugin.unlock_saving');
}
/**
* Get counts for active tasks divided into different selectors
*/
public function fetch_counts()
{
if (isset($_REQUEST['lists'])) {
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
}
else {
foreach ($this->driver->get_lists() as $list) {
if ($list['active'])
$lists[] = $list['id'];
}
}
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
/**
* Adjust the cached counts after changing a task
*/
public function update_counts($oldrec, $newrec)
{
// rebuild counts until this function is finally implemented
$this->fetch_counts();
// $this->rc->output->command('plugin.update_counts', $counts);
}
/**
*
*/
public function fetch_tasks()
{
$f = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
$filter = array('mask' => $f, 'search' => $search);
/*
// convert magic date filters into a real date range
switch ($f) {
case self::FILTER_MASK_TODAY:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $filter['to'] = $today->format('Y-m-d');
break;
case self::FILTER_MASK_TOMORROW:
$tomorrow = new DateTime('now + 1 day', $this->timezone);
$filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
break;
case self::FILTER_MASK_OVERDUE:
$yesterday = new DateTime('yesterday', $this->timezone);
$filter['to'] = $yesterday->format('Y-m-d');
break;
case self::FILTER_MASK_WEEK:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $today->format('Y-m-d');
$weekend = new DateTime('now + 7 days', $this->timezone);
$filter['to'] = $weekend->format('Y-m-d');
break;
case self::FILTER_MASK_LATER:
$date = new DateTime('now + 8 days', $this->timezone);
$filter['from'] = $date->format('Y-m-d');
break;
}
*/
$data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f);
$this->rc->output->command('plugin.data_ready', array(
'filter' => $f,
'lists' => $lists,
'search' => $search,
'data' => $data,
'tags' => $this->driver->get_tags(),
));
}
/**
* Handler for printing calendars
*/
public function print_tasks()
{
// Add CSS stylesheets to the page header
$skin_path = $this->local_skin_path();
$this->include_stylesheet($skin_path . '/print.css');
$this->include_script('tasklist.js');
$this->rc->output->add_handlers(array(
'plugin.tasklist_print' => array($this, 'print_tasks_list'),
));
$this->rc->output->set_pagetitle($this->gettext('print'));
$this->rc->output->send('tasklist.print');
}
/**
* Handler for printing calendars
*/
public function print_tasks_list($attrib)
{
$f = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
$search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
$filter = array('mask' => $f, 'search' => $search);
$data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f);
// we'll build the tasks table in javascript on page load
// where we have sorting methods, etc.
$this->rc->output->set_env('tasks', $data);
return $this->ui->tasks_resultview($attrib);
}
/**
* Prepare and sort the given task records to be sent to the client
*/
private function tasks_data($records, $f)
{
$data = $this->task_tree = $this->task_titles = array();
foreach ($records as $rec) {
if ($rec['parent_id']) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
$this->encode_task($rec);
// apply filter; don't trust the driver on this :-)
if ((!$f && !$this->driver->is_complete($rec)) || ($rec['mask'] & $f))
$data[] = $rec;
}
// assign hierarchy level indicators for later sorting
array_walk($data, array($this, 'task_walk_tree'));
return $data;
}
/**
* Prepare the given task record before sending it to the client
*/
private function encode_task(&$rec)
{
$rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']);
if (is_object($rec['created'])) {
$rec['created_'] = $this->rc->format_date($rec['created']);
$rec['created'] = $rec['created']->format('U');
}
if (is_object($rec['changed'])) {
$rec['changed_'] = $this->rc->format_date($rec['changed']);
$rec['changed'] = $rec['changed']->format('U');
}
else {
$rec['changed'] = null;
}
if ($rec['date']) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['datetime'] = intval($date->format('U'));
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
$rec['_hasdate'] = 1;
}
catch (Exception $e) {
$rec['date'] = $rec['datetime'] = null;
}
}
else {
$rec['date'] = $rec['datetime'] = null;
$rec['_hasdate'] = 0;
}
if ($rec['startdate']) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdatetime'] = intval($date->format('U'));
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
}
catch (Exception $e) {
$rec['startdate'] = $rec['startdatetime'] = null;
}
}
if ($rec['valarms']) {
$rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']);
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
}
if ($rec['recurrence']) {
$rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
}
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
// convert link URIs references into structs
if (array_key_exists('links', $rec)) {
foreach ((array)$rec['links'] as $i => $link) {
if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) {
$rec['links'][$i] = $msgref;
}
}
}
// Convert HTML description into plain text
if ($this->is_html($rec)) {
$h2t = new rcube_html2text($rec['description'], false, true, 0);
$rec['description'] = $h2t->get_text();
}
if (!is_array($rec['tags']))
$rec['tags'] = (array)$rec['tags'];
sort($rec['tags'], SORT_LOCALE_STRING);
if (in_array($rec['id'], $this->collapsed_tasks))
$rec['collapsed'] = true;
if (empty($rec['parent_id']))
$rec['parent_id'] = null;
$this->task_titles[$rec['id']] = $rec['title'];
}
/**
* Determine whether the given task description is HTML formatted
*/
private function is_html($task)
{
// check for opening and closing <html> or <body> tags
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m) && strpos($task['description'], '</'.$m[1].'>') > 0);
}
/**
* Callback function for array_walk over all tasks.
* Sets tree depth and parent titles
*/
private function task_walk_tree(&$rec)
{
$rec['_depth'] = 0;
$parent_titles = array();
$parent_id = $this->task_tree[$rec['id']];
while ($parent_id) {
$rec['_depth']++;
array_unshift($parent_titles, $this->task_titles[$parent_id]);
$parent_id = $this->task_tree[$parent_id];
}
if (count($parent_titles)) {
$rec['parent_title'] = join(' » ', array_filter($parent_titles));
}
}
/**
* Compute the filter mask of the given task
*
* @param array Hash array with Task record properties
* @return int Filter mask
*/
public function filter_mask($rec)
{
static $today, $tomorrow, $weeklimit;
if (!$today) {
$today_date = new DateTime('now', $this->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$week_date = new DateTime('now + 7 days', $this->timezone);
$weeklimit = $week_date->format('Y-m-d');
}
$mask = 0;
$start = $rec['startdate'] ?: '1900-00-00';
$duedate = $rec['date'] ?: '3000-00-00';
if ($rec['flagged'])
$mask |= self::FILTER_MASK_FLAGGED;
if ($this->driver->is_complete($rec))
$mask |= self::FILTER_MASK_COMPLETE;
if (empty($rec['date']))
$mask |= self::FILTER_MASK_NODATE;
else if ($rec['date'] < $today)
$mask |= self::FILTER_MASK_OVERDUE;
if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
$mask |= self::FILTER_MASK_TODAY;
if ($duedate <= $tomorrow || ($rec['startdate'] && $start <= $tomorrow))
$mask |= self::FILTER_MASK_TOMORROW;
if (($start > $tomorrow && $start <= $weeklimit) || ($duedate > $tomorrow && $duedate <= $weeklimit))
$mask |= self::FILTER_MASK_WEEK;
else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
$mask |= self::FILTER_MASK_LATER;
// add masks for assigned tasks
if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false)
$mask |= self::FILTER_MASK_ASSIGNED;
else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false)
$mask |= self::FILTER_MASK_MYTASKS;
return $mask;
}
/**
* Determine whether the current user is an attendee of the given task
*/
public function is_attendee($task)
{
$emails = $this->lib->get_user_emails();
foreach ((array)$task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
return $i;
}
}
return false;
}
/**
* Determine whether the current user is the organizer of the given task
*/
public function is_organizer($task)
{
$emails = $this->lib->get_user_emails();
return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function tasklist_view()
{
$this->ui->init();
$this->ui->init_templates();
// set autocompletion env
$this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
$this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'close', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['tasklist.'.$label] = $this->gettext($label);
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
// collect env variables
$env = array(
'tasklists' => array(),
'tasklist_settings' => $this->ui->load_settings(),
);
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
$script_add = '';
foreach ($this->ui->get_gui_objects() as $obj => $id) {
$script_add .= rcmail_output::JS_OBJECT_NAME . ".gui_object('$obj', '$id');\n";
}
echo html::tag('script', array('type' => 'text/javascript'),
rcmail_output::JS_OBJECT_NAME . ".set_env(" . json_encode($env) . ");\n".
rcmail_output::JS_OBJECT_NAME . ".add_label(" . json_encode($texts) . ");\n".
$script_add
);
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active lists and sync them to the client
*/
public function refresh($attr)
{
// refresh the entire list every 10th time to also sync deleted items
if (rand(0,10) == 10) {
$this->rc->output->command('plugin.reload_data');
return;
}
$filter = array(
'since' => $attr['last'],
'search' => rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
'mask' => intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)) & self::FILTER_MASK_COMPLETE,
);
$lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);;
$updates = $this->driver->list_tasks($filter, $lists);
if (!empty($updates)) {
$this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates, 255), true);
// update counts
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
}
/**
* 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();
if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
foreach ($alarms as $alarm) {
// encode alarm object to suit the expectations of the calendaring code
if ($alarm['date'])
$alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone);
$alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task:
$alarm['allday'] = empty($alarm['time']) ? 1 : 0;
$p['alarms'][] = $alarm;
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by the calendar module
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'task:') === 0)
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']);
}
return $p;
}
/******* Attachment handling *******/
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
$this->lib->attachment_upload(self::SESSION_KEY);
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
// show loading page
if (!empty($_GET['_preload'])) {
return $this->lib->attachment_loading_page();
}
$task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
$task = array('id' => $task, 'list' => $list, 'rev' => $rev);
$attachment = $this->driver->get_attachment($id, $task);
// 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('tasklist.attachment');
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
$this->lib->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/******* Email related function *******/
public function mail_message2task()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$task = array();
// establish imap connection
$imap = $this->rc->get_storage();
- $imap->set_mailbox($mbox);
+ $imap->set_folder($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$task['title'] = trim($message->subject);
$task['description'] = trim($message->first_text_part());
$task['id'] = -$uid;
$this->load_driver();
// add a reference to the email message
if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
$task['links'] = array($msgref);
}
// copy mail attachments to task
else if ($message->attachments && $this->driver->attachments) {
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $task['id'];
$_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' => $task['id'],
);
$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'
$task['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2taskdialog', $task);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/**
* Add UI element to copy task invitations or updates to the tasklist
*/
public function mail_messagebody_html($p)
{
// load iCalendar functions (if necessary)
if (!empty($this->lib->ical_parts)) {
$this->get_ical();
$this->load_itip();
}
$html = '';
$has_tasks = false;
$ical_objects = $this->lib->get_mail_ical_objects();
// show a box for every task in the file
foreach ($ical_objects as $idx => $task) {
if ($task['_type'] != 'task') {
continue;
}
$has_tasks = true;
// get prepared inline UI for this event object
if ($ical_objects->method) {
$html .= html::div('tasklist-invitebox',
$this->itip->mail_itip_inline_ui(
$task,
$ical_objects->method,
$ical_objects->mime_id . ':' . $idx,
'tasks',
rcube_utils::anytodatetime($ical_objects->message_date)
)
);
}
// limit listing
if ($idx >= 3) {
break;
}
}
// list linked tasks
$links = array();
foreach ($this->message_tasks as $task) {
$checkbox = new html_checkbox(array(
'name' => 'completed',
'class' => 'complete',
'title' => $this->gettext('complete'),
'data-list' => $task['list'],
));
$complete = $this->driver->is_complete($task);
$links[] = html::tag('li', 'messagetaskref' . ($complete ? ' complete' : ''),
$checkbox->show($complete ? $task['id'] : null, array('value' => $task['id'])) . ' ' .
html::a(array(
'href' => $this->rc->url(array(
'task' => 'tasks',
'list' => $task['list'],
'id' => $task['id'],
)),
'class' => 'messagetasklink',
'rel' => $task['id'] . '@' . $task['list'],
'target' => '_blank',
), Q($task['title']))
);
}
if (count($links)) {
$html .= html::div('messagetasklinks', html::tag('ul', 'tasklist', join("\n", $links)));
}
// prepend iTip/relation boxes to message body
if ($html) {
$this->load_ui();
$this->ui->init();
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
}
// add "Save to tasks" button into attachment menu
if ($has_tasks) {
$this->add_button(array(
'id' => 'attachmentsavetask',
'name' => 'attachmentsavetask',
'type' => 'link',
'wrapper' => 'li',
'command' => 'attachment-save-task',
'class' => 'icon tasklistlink',
'classact' => 'icon tasklistlink active',
'innerclass' => 'icon taskadd',
'label' => 'tasklist.savetotasklist',
), 'attachmentmenu');
}
return $p;
}
/**
* Lookup backend storage and find notes associated with the given message
*/
public function mail_message_load($p)
{
if (!$p['object']->headers->others['x-kolab-type']) {
$this->load_driver();
$this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder);
// sort message tasks by completeness and due date
$driver = $this->driver;
array_walk($this->message_tasks, array($this, 'encode_task'));
usort($this->message_tasks, function($a, $b) use ($driver) {
$a_complete = intval($driver->is_complete($a));
$b_complete = intval($driver->is_complete($b));
$d = $a_complete - $b_complete;
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
});
}
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
$this->ical = libcalendaring::get_ical();
}
return $this->ical;
}
/**
* Get properties of the tasklist this user has specified as default
*/
public function get_default_tasklist($sensitivity = null)
{
$lists = $this->driver->get_lists();
$list = null;
foreach ($lists as $l) {
if ($sensitivity && $l['subtype'] == $sensitivity) {
$list = $l;
break;
}
if ($l['default']) {
$list = $l;
}
if ($l['editable']) {
$first = $l;
}
}
return $list ?: $first;
}
/**
* Import the full payload from a mail message attachment
*/
public function mail_import_attachment()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$charset = RCUBE_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
- $imap->set_mailbox($mbox);
+ $imap->set_folder($mbox);
if ($uid && $mime_id) {
$part = $imap->get_message_part($uid, $mime_id);
// $headers = $imap->get_message_headers($uid);
if ($part->ctype_parameters['charset']) {
$charset = $part->ctype_parameters['charset'];
}
if ($part) {
$tasks = $this->get_ical()->import($part, $charset);
}
}
$success = $existing = 0;
if (!empty($tasks)) {
// find writeable tasklist to store task
$cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
$lists = $this->driver->get_lists();
foreach ($tasks as $task) {
// save to tasklist
$list = $lists[$cal_id] ?: $this->get_default_tasklist($task['sensitivity']);
if ($list && $list['editable'] && $task['_type'] == 'task') {
$task = $this->from_ical($task);
$task['list'] = $list['id'];
if (!$this->driver->get_task($task['uid'])) {
$success += (bool) $this->driver->create_task($task);
}
else {
$existing++;
}
}
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array(
'name' => 'importsuccess',
'vars' => array('nr' => $success),
)), 'confirmation');
}
else if ($existing) {
$this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
}
else {
$this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error');
}
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_itip()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action';
$error_msg = $this->gettext('errorimportingtask');
$success = false;
$delegate = null;
if ($status == 'delegated') {
$delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
$delegate = reset($delegates);
if (empty($delegate) || empty($delegate['mailto'])) {
$this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
return;
}
}
// successfully parsed tasks?
if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) {
$task = $this->from_ical($task);
// forward iTip request to delegatee
if ($delegate) {
$rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST));
$itip = $this->load_itip();
if ($itip->delegate_to($task, $delegate, $rsvpme ? true : false)) {
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
}
else {
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
// find writeable list to store the task
$list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
$lists = $this->driver->get_lists();
$list = $lists[$list_id];
$dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST');
// select default list except user explicitly selected 'none'
if (!$list && !$dontsave) {
$list = $this->get_default_tasklist($task['sensitivity']);
}
$metadata = array(
'uid' => $task['uid'],
'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0,
'sequence' => intval($task['sequence']),
'fallback' => strtoupper($status),
'method' => $task['_method'],
'task' => 'tasks',
);
// update my attendee status according to submitted method
if (!empty($status)) {
$organizer = $task['organizer'];
$emails = $this->lib->get_user_emails();
foreach ($task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
$reply_sender = $attendee['email'];
$task['attendees'][$i]['status'] = strtoupper($status);
if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) {
$task['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
}
}
}
// add attendee with this user's default identity if not listed
if (!$reply_sender) {
$sender_identity = $this->rc->user->list_emails(true);
$task['attendees'][] = array(
'name' => $sender_identity['name'],
'email' => $sender_identity['email'],
'role' => 'OPT-PARTICIPANT',
'status' => strtoupper($status),
);
$metadata['attendee'] = $sender_identity['email'];
}
}
// save to tasklist
if ($list && $list['editable']) {
$task['list'] = $list['id'];
// check for existing task with the same UID
$existing = $this->driver->get_task($task['uid']);
if ($existing) {
// only update attendee status
if ($task['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
$existing_attendee = -1;
$existing_attendee_emails = array();
foreach ($existing['attendees'] as $i => $attendee) {
$existing_attendee_emails[] = $attendee['email'];
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
$existing_attendee = $i;
}
}
$task_attendee = null;
foreach ($task['attendees'] as $attendee) {
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
$task_attendee = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee['status'] != 'DELEGATED') {
break;
}
}
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) &&
(stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) &&
(!in_array($attendee['email'], $existing_attendee_emails))) {
$existing['attendees'][] = $attendee;
}
}
// if delegatee has declined, set delegator's RSVP=True
if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) {
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] == $task_attendee['delegated-from']) {
$existing['attendees'][$i]['rsvp'] = true;
break;
}
}
}
// found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $task_attendee) {
$existing['attendees'][$existing_attendee] = $task_attendee;
$success = $this->driver->edit_task($existing);
}
// update the entire attendees block
else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) {
$existing['attendees'][] = $task_attendee;
$success = $this->driver->edit_task($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// delete the task when declined
else if ($status == 'declined' && $delete) {
$deleted = $this->driver->delete_task($existing, true);
$success = true;
}
// import the (newer) task
else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) {
$task['id'] = $existing['id'];
$task['list'] = $existing['list'];
// preserve my participant status for regular updates
if (empty($status)) {
$emails = $this->lib->get_user_emails();
foreach ($task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
foreach ($existing['attendees'] as $j => $_attendee) {
if ($attendee['email'] == $_attendee['email']) {
$task['attendees'][$i] = $existing['attendees'][$j];
break;
}
}
}
}
}
// set status=CANCELLED on CANCEL messages
if ($task['_method'] == 'CANCEL') {
$task['status'] = 'CANCELLED';
}
// show me as free when declined (#1670)
if ($status == 'declined' || $task['status'] == 'CANCELLED') {
$task['free_busy'] = 'free';
}
$success = $this->driver->edit_task($task);
}
else if (!empty($status)) {
$existing['attendees'] = $task['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' || $this->rc->config->get('kolab_invitation_tasklists'))) {
$success = $this->driver->create_task($task);
}
else if ($status == 'declined') {
$error_msg = null;
}
}
else if ($status == 'declined' || $dontsave) {
$error_msg = null;
}
else {
$error_msg = $this->gettext('nowritetasklistfound');
}
}
if ($success || $dontsave) {
if ($success) {
$message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
}
$metadata['rsvp'] = intval($metadata['rsvp']);
$metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
}
else if ($error_msg) {
$this->rc->output->command('display_message', $error_msg, 'error');
}
// send iTip reply
if ($task['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
$task['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**** Task invitation plugin hooks ****/
/**
* Handler for task/itip-delegate requests
*/
function mail_itip_delegate()
{
// forward request to mail_import_itip() with the right status
$_POST['_status'] = $_REQUEST['_status'] = 'delegated';
$this->mail_import_itip();
}
/**
* Handler for task/itip-status requests
*/
public function task_itip_status()
{
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
// find local copy of the referenced task
$existing = $this->driver->get_task($data);
$itip = $this->load_itip();
$response = $itip->get_itip_status($data, $existing);
// get a list of writeable lists to save new tasks to
if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
$lists = $this->driver->get_lists();
$select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true));
$select->add('--', '');
foreach ($lists as $list) {
if ($list['editable']) {
$select->add($list['name'], $list['id']);
}
}
}
if ($select) {
$default_list = $this->get_default_tasklist($data['sensitivity']);
$response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . '&nbsp;' .
$select->show($default_list['id']));
}
$this->rc->output->command('plugin.update_itip_object_status', $response);
}
/**
* Handler for task/itip-remove requests
*/
public function task_itip_remove()
{
$success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
// search for event if only UID is given
if ($task = $this->driver->get_task($uid)) {
$success = $this->driver->delete_task($task, true);
}
if ($success) {
$this->rc->output->show_message('tasklist.successremoval', 'confirmation');
}
else {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
}
/******* Utility functions *******/
/**
* 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));
}
/**
* Map task properties for ical exprort using libcalendaring
*/
public function to_libcal($task)
{
$object = $task;
$object['_type'] = 'task';
$object['categories'] = (array)$task['tags'];
// convert to datetime objects
if (!empty($task['date'])) {
$object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone);
if (empty($task['time']))
$object['due']->_dateonly = true;
unset($object['date']);
}
if (!empty($task['startdate'])) {
$object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone);
if (empty($task['starttime']))
$object['start']->_dateonly = true;
unset($object['startdate']);
}
$object['complete'] = $task['complete'] * 100;
if ($task['complete'] == 1.0 && empty($task['complete'])) {
$object['status'] = 'COMPLETED';
}
if ($task['flagged']) {
$object['priority'] = 1;
}
else if (!$task['priority']) {
$object['priority'] = 0;
}
return $object;
}
/**
* Convert task properties from ical parser to the internal format
*/
public function from_ical($vtodo)
{
$task = $vtodo;
$task['tags'] = array_filter((array)$vtodo['categories']);
$task['flagged'] = $vtodo['priority'] == 1;
$task['complete'] = floatval($vtodo['complete'] / 100);
// convert from DateTime to internal date format
if (is_a($vtodo['due'], 'DateTime')) {
$due = $this->lib->adjust_timezone($vtodo['due']);
$task['date'] = $due->format('Y-m-d');
if (!$vtodo['due']->_dateonly)
$task['time'] = $due->format('H:i');
}
// convert from DateTime to internal date format
if (is_a($vtodo['start'], 'DateTime')) {
$start = $this->lib->adjust_timezone($vtodo['start']);
$task['startdate'] = $start->format('Y-m-d');
if (!$vtodo['start']->_dateonly)
$task['starttime'] = $start->format('H:i');
}
if (is_a($vtodo['dtstamp'], 'DateTime')) {
$task['changed'] = $vtodo['dtstamp'];
}
unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']);
return $task;
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
$this->load_driver();
return $this->driver->user_delete($args);
}
/**
* Magic getter for public access to protected members
*/
public function __get($name)
{
switch ($name) {
case 'ical':
return $this->get_ical();
case 'itip':
return $this->load_itip();
case 'driver':
$this->load_driver();
return $this->driver;
}
return null;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 2:23 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196919
Default Alt Text
(288 KB)

Event Timeline