Page MenuHomePhorge

No OneTemporary

Size
291 KB
Referenced Files
None
Subscribers
None
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 5f27d74b..e70a4ca7 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,3291 +1,3292 @@
<?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($writeable = false, $confidential = false)
{
$default_id = $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
$calendar = $calendars[$default_id] ?: null;
if (!$calendar || $confidential || ($writeable && !$calendar['editable'])) {
foreach ($calendars as $cal) {
if ($confidential && $cal['subtype'] == 'confidential') {
$calendar = $cal;
break;
}
if ($cal['default']) {
$calendar = $cal;
if (!$confidential)
break;
}
if (!$writeable || $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
rcube_autocomplete_init();
$this->rc->output->set_env('timezone', $this->timezone->getName());
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('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, Q($this->gettext('default_view'))),
'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
);
}
if (!isset($no_override['calendar_timeslots'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_timeslot';
$choices = array('1', '2', '3', '4', '6');
$select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['timeslots'] = array(
'title' => html::label($field_id, Q($this->gettext('timeslots'))),
'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))),
);
}
if (!isset($no_override['calendar_first_day'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_firstday';
$select = new html_select(array('name' => '_first_day', 'id' => $field_id));
$select->add(rcube_label('sunday'), '0');
$select->add(rcube_label('monday'), '1');
$select->add(rcube_label('tuesday'), '2');
$select->add(rcube_label('wednesday'), '3');
$select->add(rcube_label('thursday'), '4');
$select->add(rcube_label('friday'), '5');
$select->add(rcube_label('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, Q($this->gettext('first_day'))),
'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
);
}
if (!isset($no_override['calendar_first_hour'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
$select_hours = new html_select();
for ($h = 0; $h < 24; $h++)
$select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
$field_id = 'rcmfd_firsthour';
$p['blocks']['view']['options']['first_hour'] = array(
'title' => html::label($field_id, Q($this->gettext('first_hour'))),
'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
);
}
if (!isset($no_override['calendar_work_start'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_workstart';
$p['blocks']['view']['options']['workinghours'] = array(
'title' => html::label($field_id, Q($this->gettext('workinghours'))),
'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
);
}
if (!isset($no_override['calendar_event_coloring'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_coloring';
$select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
$select_colors->add($this->gettext('coloringmode0'), 0);
$select_colors->add($this->gettext('coloringmode1'), 1);
$select_colors->add($this->gettext('coloringmode2'), 2);
$select_colors->add($this->gettext('coloringmode3'), 3);
$p['blocks']['view']['options']['eventcolors'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
);
}
// loading driver is expensive, don't do it if not needed
$this->load_driver();
if (!isset($no_override['calendar_default_alarm_type'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_alarm';
$select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type)
$select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
$p['blocks']['view']['options']['alarmtype'] = array(
'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
);
}
if (!isset($no_override['calendar_default_alarm_offset'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
$field_id = 'rcmfd_alarm';
$input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
$select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$p['blocks']['view']['options']['alarmoffset'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
);
}
if (!isset($no_override['calendar_default_calendar'])) {
if (!$p['current']) {
$p['blocks']['view']['content'] = true;
return $p;
}
// default calendar selection
$field_id = 'rcmfd_default_calendar';
$select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
foreach ((array)$this->driver->list_calendars(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', 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, 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(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
}
$input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib);
$select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib);
foreach (array('-M','-H','-D') as $trigger)
$select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
$preset = libcalendaring::parse_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'))",
JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
$this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
$got_msg = true;
}
else if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
$got_msg = true;
}
// send 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;
array_walk($data, function(&$change) use ($lib) {
if ($change['date']) {
$dt = $lib->adjust_timezone($change['date']);
if ($dt instanceof DateTime)
$change['date'] = $dt->format('c');
}
});
$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('eventdiffnotavailable'), '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('eventnotfound'), 'error');
}
$got_msg = true;
$reload = false;
break;
case "restore":
if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
}
else {
$this->rc->output->command('display_message', 'Not implemented yet', 'error');
$got_msg = true;
}
$reload = false;
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']);
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'])) {
rcube_upload_progress();
}
@set_time_limit(0);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$calendar = 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 = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('plugin.import_error', array('message' => $msg));
}
$this->rc->output->send('iframe');
}
/**
* Helper function to parse and import a single .ics file
*/
private function import_from_file($filepath, $calendar, $rangestart, &$errors)
{
$user_email = $this->rc->user->get_username();
$ical = $this->get_ical();
$errors = !$ical->fopen($filepath);
$count = $i = 0;
foreach ($ical as $event) {
// keep the browser connection alive on long import jobs
if (++$i > 100 && $i % 100 == 0) {
echo "<!-- -->";
ob_flush();
}
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['_owner'] = $user_email;
$event['calendar'] = $calendar;
if ($this->driver->new_event($event)) {
$count++;
}
else {
$errors++;
}
}
return $count;
}
/**
* Construct the ics file for exporting events to iCalendar format;
*/
function export_events($terminate = true)
{
$start = 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);
}
// 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);
+ $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;
}
/**
* 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)
{
if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) {
$event['cancelled'] = true;
$is_cancelled = true;
}
$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, !$old || $event['sequence'] > $old['sequence']);
// 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
send_future_expire_header(300);
echo $status;
exit;
}
/**
* Return a list of free/busy time slots within the given period
* Echo data in JSON encoding
*/
public function freebusy_times()
{
$email = 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
send_future_expire_header(300);
echo json_encode(array(
'email' => $email,
'start' => $dts->format('c'),
'end' => $dte->format('c'),
'interval' => $interval,
'slots' => $slots,
'times' => $times,
));
exit;
}
/**
* Handler for printing calendars
*/
public function print_view()
{
$title = $this->gettext('print');
$view = 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(true, $data['sensitivity'] == 'confidential');
$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(true, $invitation['event']['sensitivity'] == 'confidential'))) {
$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', 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(true, $event['sensitivity'] == 'confidential');
$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 = RCMAIL_CHARSET;
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($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(true, $event['sensitivity'] == 'confidential');
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);
$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/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 8671b108..c1cace90 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,2413 +1,2497 @@
<?php
/**
* Kolab driver for the Calendar plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@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 kolab_driver extends calendar_driver
{
const INVITATIONS_CALENDAR_PENDING = '--invitation--pending';
const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $alarm_types = array('DISPLAY','AUDIO');
public $categoriesimmutable = true;
private $rc;
private $cal;
private $calendars;
private $has_writeable = false;
private $freebusy_trigger = false;
private $bonnie_api = false;
/**
* Default constructor
*/
public function __construct($cal)
{
$cal->require_plugin('libkolab');
// load helper classes *after* libkolab has been loaded (#3248)
require_once(dirname(__FILE__) . '/kolab_calendar.php');
require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php');
$this->cal = $cal;
$this->rc = $cal->rc;
$this->_read_calendars();
$this->cal->register_action('push-freebusy', array($this, 'push_freebusy'));
$this->cal->register_action('calendar-acl', array($this, 'calendar_acl'));
$this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
if (kolab_storage::$version == '2.0') {
$this->alarm_types = array('DISPLAY');
$this->alarm_absolute = false;
}
// get configuration for the Bonnie API
if ($bonnie_config = $this->cal->rc->config->get('kolab_bonnie_api', false))
$this->bonnie_api = new kolab_bonnie_api($bonnie_config);
// calendar uses fully encoded identifiers
kolab_storage::$encode_ids = true;
}
/**
* Read available calendars from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type, sorted by namespace/name
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
$this->calendars = array();
foreach ($folders as $folder) {
if ($folder instanceof kolab_storage_folder_user) {
$calendar = new kolab_user_calendar($folder->name, $this->cal);
$calendar->subscriptions = count($folder->children) > 0;
}
else {
$calendar = new kolab_calendar($folder->name, $this->cal);
}
if ($calendar->ready) {
$this->calendars[$calendar->id] = $calendar;
if ($calendar->editable)
$this->has_writeable = true;
}
}
return $this->calendars;
}
/**
* Get a list of available calendars from this source
*
* @param integer $filter Bitmask defining filter criterias
* @param object $tree Reference to hierarchical folder tree object
*
* @return array List of calendars
*/
public function list_calendars($filter = 0, &$tree = null)
{
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
unset($this->calendars);
$this->_read_calendars();
}
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$folders = $this->filter_calendars($filter);
$calendars = array();
// include virtual folders for a full folder tree
if (!is_null($tree))
$folders = kolab_storage::folder_hierarchy($folders, $tree);
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
$listname = $cal->get_foldername();
$imap_path = explode($delim, $cal->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->calendars[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->calendars[$parent_id]) {
$parent_id = kolab_storage::folder_id($cal->get_parent());
}
// turn a kolab_storage_folder object into a kolab_calendar
if ($cal instanceof kolab_storage_folder) {
$cal = new kolab_calendar($cal->name, $this->cal);
$this->calendars[$cal->id] = $cal;
}
// special handling for user or virtual folders
if ($cal instanceof kolab_storage_folder_user) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => kolab_storage::object_name($fullname),
'listname' => $listname,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'active' => $cal->is_active(),
'title' => $cal->get_owner(),
'owner' => $cal->get_owner(),
'history' => false,
'virtual' => false,
'editable' => false,
'group' => 'other',
'class' => 'user',
'removable' => true,
);
}
else if ($cal->virtual) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
'virtual' => true,
'editable' => false,
'group' => $cal->get_namespace(),
'class' => 'folder',
);
}
else {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'editable' => $cal->editable,
'rights' => $cal->rights,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => $cal->get_namespace(),
'default' => $cal->default,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => true, // TODO: determine if that folder indeed has child folders
'parent' => $parent_id,
'subtype' => $cal->subtype,
'caldavurl' => $cal->get_caldav_url(),
'removable' => !$cal->default,
);
}
if ($cal->subscriptions) {
$calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
}
}
// list virtual calendars showing invitations
if ($this->rc->config->get('kolab_invitation_calendars')) {
foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
$cal = new kolab_invitation_calendar($id, $this->cal);
$this->calendars[$cal->id] = $cal;
if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
$calendars[$id] = array(
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_name(),
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'editable' => $cal->editable,
'rights' => $cal->rights,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => false,
);
if ($id == self::INVITATIONS_CALENDAR_PENDING) {
$calendars[$id]['counts'] = true;
}
if (is_object($tree)) {
$tree->children[] = $cal;
}
}
}
}
// append the virtual birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays', false)) {
$id = self::BIRTHDAY_CALENDAR_ID;
$prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs
if (!($filter & self::FILTER_ACTIVE) || $prefs[$id]['active']) {
$calendars[$id] = array(
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
'listname' => $this->cal->gettext('birthdays'),
'color' => $prefs[$id]['color'] ?: '87CEFA',
'active' => (bool)$prefs[$id]['active'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'group' => 'x-birthdays',
'editable' => false,
'default' => false,
'children' => false,
'history' => false,
);
}
}
return $calendars;
}
/**
* Get list of calendars according to specified filters
*
* @param integer Bitmask defining restrictions. See FILTER_* constants for possible values.
*
* @return array List of calendars
*/
protected function filter_calendars($filter)
{
$calendars = array();
$plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array(
'list' => $this->calendars,
'calendars' => $calendars,
'filter' => $filter,
'editable' => ($filter & self::FILTER_WRITEABLE),
'insert' => ($filter & self::FILTER_INSERTABLE),
'active' => ($filter & self::FILTER_ACTIVE),
'personal' => ($filter & self::FILTER_PERSONAL),
));
if ($plugin['abort']) {
return $plugin['calendars'];
}
foreach ($this->calendars as $cal) {
if (!$cal->ready) {
continue;
}
if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) {
continue;
}
if (($filter & self::FILTER_INSERTABLE) && !$cal->insert) {
continue;
}
if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) {
continue;
}
if (($filter & self::FILTER_PERSONAL) && $cal->get_namespace() != 'personal') {
continue;
}
$calendars[$cal->id] = $cal;
}
return $calendars;
}
/**
* Get the kolab_calendar instance for the given calendar ID
*
* @param string Calendar identifier (encoded imap folder name)
* @return object kolab_calendar Object nor null if calendar doesn't exist
*/
public function get_calendar($id)
{
// create calendar object if necesary
if (!$this->calendars[$id] && in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
$this->calendars[$id] = new kolab_invitation_calendar($id, $this->cal);
}
else if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
$calendar = kolab_calendar::factory($id, $this->cal);
if ($calendar->ready)
$this->calendars[$calendar->id] = $calendar;
}
return $this->calendars[$id];
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$prop['type'] = 'event';
$prop['active'] = true;
$prop['subscribed'] = true;
$folder = kolab_storage::folder_update($prop);
if ($folder === false) {
$this->last_error = $this->cal->gettext(kolab_storage::$last_error);
return false;
}
// create ID
$id = kolab_storage::folder_id($folder);
// save color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return $id;
}
/**
* Update properties of an existing calendar
*
* @see calendar_driver::edit_calendar()
*/
public function edit_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$id = $cal->update($prop);
}
else {
$id = $prop['id'];
}
// fallback to local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']);
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID)
$prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
else if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if (!empty($prefs['kolab_calendars'][$id]))
$this->rc->user->save_prefs($prefs);
return true;
}
/**
* Set active/subscribed state of a calendar
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) {
$ret = false;
if (isset($prop['permanent']))
$ret |= $cal->storage->subscribe(intval($prop['permanent']));
if (isset($prop['active']))
$ret |= $cal->storage->activate(intval($prop['active']));
// apply to child folders, too
if ($prop['recursive']) {
foreach ((array)kolab_storage::list_folders($cal->storage->name, '*', 'event') as $subfolder) {
if (isset($prop['permanent']))
($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
if (isset($prop['active']))
($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
}
}
return $ret;
}
else {
// save state in local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
$prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active'];
$this->rc->user->save_prefs($prefs);
return true;
}
return false;
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::delete_calendar()
*/
public function delete_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$folder = $cal->get_realname();
// TODO: unsubscribe if no admin rights
if (kolab_storage::folder_delete($folder)) {
// remove color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
$this->rc->user->save_prefs($prefs);
return true;
}
else
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* Search for shared or otherwise not listed calendars the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of calendars
*/
public function search_calendars($query, $source)
{
if (!kolab_storage::setup())
return array();
$this->calendars = array();
$this->search_more_results = false;
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
$calendar = new kolab_calendar($folder->name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
}
}
// find other user's virtual calendars
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit, $count) as $user) {
$calendar = new kolab_user_calendar($user, $this->cal);
$this->calendars[$calendar->id] = $calendar;
// search for calendar folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
$cal = new kolab_calendar($foldername, $this->cal);
$this->calendars[$cal->id] = $cal;
$calendar->subscriptions = true;
}
}
if ($count > $limit) {
$this->search_more_results = true;
}
}
// don't list the birthday calendar
$this->rc->config->set('calendar_contact_birthdays', false);
$this->rc->config->set('kolab_invitation_calendars', false);
return $this->list_calendars();
}
/**
* Fetch a single event
*
* @see calendar_driver::get_event()
* @return array Hash array with event properties, false if not found
*/
public function get_event($event, $scope = 0, $full = false)
{
if (is_array($event)) {
$id = $event['id'] ?: $event['uid'];
$cal = $event['calendar'];
// we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
if (!$event['id'] && $event['_instance']) {
$id .= '-' . $event['_instance'];
}
}
else {
$id = $event;
}
if ($cal) {
if ($storage = $this->get_calendar($cal)) {
$result = $storage->get_event($id);
return self::to_rcube_event($result);
}
// get event from the address books birthday calendar
else if ($cal == self::BIRTHDAY_CALENDAR_ID) {
return $this->get_birthday_event($id);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->filter_calendars($scope) as $calendar) {
if ($result = $calendar->get_event($id)) {
return self::to_rcube_event($result);
}
}
}
return false;
}
/**
* Add a single event to the database
*
* @see calendar_driver::new_event()
*/
public function new_event($event)
{
if (!$this->validate($event))
return false;
$event = self::from_rcube_event($event);
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
if ($storage = $this->get_calendar($cid)) {
// if this is a recurrence instance, append as exception to an already existing object for this UID
if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
self::add_exception($master, $event);
$success = $storage->update_event($master);
}
else {
$success = $storage->insert_event($event);
}
if ($success && $this->freebusy_trigger) {
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
$this->freebusy_trigger = false; // disable after first execution (#2355)
}
return $success;
}
return false;
}
/**
* Update an event entry with the given data
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function edit_event($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
}
/**
* Extended event editing with possible changes to the argument
*
* @param array Hash array with event properties
* @param string New participant status
* @param array List of hash arrays with updated attendees
* @return boolean True on success, False on error
*/
public function edit_rsvp(&$event, $status, $attendees)
{
$update_event = $event;
// apply changes to master (and all exceptions)
if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
if ($storage = $this->get_calendar($event['calendar'])) {
$update_event = $storage->get_event($event['recurrence_id']);
$update_event['_savemode'] = $event['_savemode'];
$update_event['id'] = $update_event['uid'];
unset($update_event['recurrence_id']);
calendar::merge_attendee_data($update_event, $attendees);
}
}
if ($ret = $this->update_attendees($update_event, $attendees)) {
// replace with master event (for iTip reply)
$event = self::to_rcube_event($update_event);
// re-assign to the according (virtual) calendar
if ($this->rc->config->get('kolab_invitation_calendars')) {
if (strtoupper($status) == 'DECLINED')
$event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
else if (strtoupper($status) == 'NEEDS-ACTION')
$event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
else if ($event['_folder_id'])
$event['calendar'] = $event['_folder_id'];
}
}
return $ret;
}
/**
* Update the participant status for the given attendees
*
* @see calendar_driver::update_attendees()
*/
public function update_attendees(&$event, $attendees)
{
// for this-and-future updates, merge the updated attendees onto all exceptions in range
if (($event['_savemode'] == 'future' && $event['recurrence_id']) || (!empty($event['recurrence']) && !$event['recurrence_id'])) {
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// load master event
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
// apply attendee update to each existing exception
if ($master['recurrence'] && !empty($master['recurrence']['EXCEPTIONS'])) {
$saved = false;
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// merge the new event properties onto future exceptions
if ($exception['_instance'] >= strval($event['_instance'])) {
calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
}
// update a specific instance
if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
$saved = true;
}
}
// add the given event as new exception
if (!$saved && $event['id'] != $master['id']) {
$event['thisandfuture'] = true;
$master['recurrence']['EXCEPTIONS'][] = $event;
}
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
return $this->update_event($master);
}
}
// just update the given event (instance)
return $this->update_event($event);
}
/**
* Move a single event
*
* @see calendar_driver::move_event()
* @return boolean True on success, False on error
*/
public function move_event($event)
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
self::clear_attandee_noreply($ev);
return $this->update_event($event + $ev);
}
return false;
}
/**
* Resize a single event
*
* @see calendar_driver::resize_event()
* @return boolean True on success, False on error
*/
public function resize_event($event)
{
if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
self::clear_attandee_noreply($ev);
return $this->update_event($event + $ev);
}
return false;
}
/**
* Remove a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return boolean True on success, False on error
*/
public function remove_event($event, $force = true)
{
$ret = true;
$success = false;
$savemode = $event['_savemode'];
$decline = $event['_decline'];
if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $savemode;
$savemode = 'all';
$master = $event;
$this->rc->session->remove('calendar_restore_event_data');
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id'] || $event['isexception']) {
$master = $storage->get_event($event['uid']);
$savemode = $event['_savemode'] ?: ($event['_instance'] || $event['isexception'] ? 'current' : 'all');
// force 'current' mode for single occurrences stored as exception
if (!$event['recurrence'] && !$event['recurrence_id'] && $event['isexception'])
$savemode = 'current';
}
// removing an exception instance
if (($event['recurrence_id'] || $event['isexception']) && is_array($master['exceptions'])) {
foreach ($master['exceptions'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
unset($master['exceptions'][$i]);
// set event date back to the actual occurrence
if ($exception['recurrence_date'])
$event['start'] = $exception['recurrence_date'];
}
}
if (is_array($master['recurrence'])) {
$master['recurrence']['EXCEPTIONS'] = &$master['exceptions'];
}
}
switch ($savemode) {
case 'current':
$_SESSION['calendar_restore_event_data'] = $master;
// removing the first instance => just move to next occurence
if ($master['recurrence'] && $event['_instance'] == libcalendaring::recurrence_instance_identifier($master)) {
$recurring = reset($storage->get_recurring_events($event, $event['start'], null, $event['id'].'-1'));
// no future instances found: delete the master event (bug #1677)
if (!$recurring['start']) {
$success = $storage->delete_event($master, $force);
break;
}
$master['start'] = $recurring['start'];
$master['end'] = $recurring['end'];
if ($master['recurrence']['COUNT'])
$master['recurrence']['COUNT']--;
}
// remove the matching RDATE entry
else if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
unset($master['recurrence']['RDATE'][$j]);
break;
}
}
}
else { // add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
}
$success = $storage->update_event($master);
break;
case 'future':
$master['_instance'] = libcalendaring::recurrence_instance_identifier($master);
if ($master['_instance'] != $event['_instance']) {
$_SESSION['calendar_restore_event_data'] = $master;
// set until-date on master event
$master['recurrence']['UNTIL'] = clone $event['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']);
// if all future instances are deleted, remove recurrence rule entirely (bug #1677)
if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
$master['recurrence'] = array();
}
// remove matching RDATE entries
else if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
$master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
break;
}
}
}
$success = $storage->update_event($master);
$ret = $master['uid'];
break;
}
default: // 'all' is default
// removing the master event with loose exceptions (not recurring though)
if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
// make the first exception the new master
$newmaster = array_shift($master['exceptions']);
$newmaster['exceptions'] = $master['exceptions'];
$newmaster['_attachments'] = $master['_attachments'];
$newmaster['_mailbox'] = $master['_mailbox'];
$newmaster['_msguid'] = $master['_msguid'];
$success = $storage->update_event($newmaster);
}
else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
// don't delete but set PARTSTAT=DECLINED
if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
$success = $storage->update_event($master);
}
}
if (!$success)
$success = $storage->delete_event($master, $force);
break;
}
}
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success ? $ret : false;
}
/**
* Restore a single deleted event
*
* @param array Hash array with event properties:
* id: Event identifier
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
if ($storage = $this->get_calendar($event['calendar'])) {
if (!empty($_SESSION['calendar_restore_event_data']))
$success = $storage->update_event($_SESSION['calendar_restore_event_data']);
else
$success = $storage->restore_event($event);
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
return false;
}
/**
* Wrapper to update an event object depending on the given savemode
*/
private function update_event($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
return false;
$old = $fromcalendar->get_event($event['id']);
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
return false;
}
$fromcalendar = $storage;
}
}
else
$fromcalendar = $storage;
$success = false;
$savemode = 'all';
$attachments = array();
$old = $master = $storage->get_event($event['id']);
if (!$old || !$old['start']) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load event object to update: id=" . $event['id']),
true, false);
return false;
}
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) {
$master = $storage->get_event($old['uid']);
$savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all');
// this-and-future on the first instance equals to 'all'
if ($savemode == 'future' && $master['start'] && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master))
$savemode = 'all';
// force 'current' mode for single occurrences stored as exception
else if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception'])
$savemode = 'current';
}
// check if update affects scheduling and update attendee status accordingly
$reschedule = $this->check_scheduling($event, $old, true);
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
if (isset($event['recurrence']['EXCEPTIONS']))
$with_exceptions = true; // exceptions already provided (e.g. from iCal import)
else if ($old['recurrence']['EXCEPTIONS'])
$event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
else if ($old['exceptions'])
$event['exceptions'] = $old['exceptions'];
// remove some internal properties which should not be saved
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
$event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']);
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['_copyfrom'] = $master['_msguid'];
$event['_mailbox'] = $master['_mailbox'];
$event['uid'] = $this->cal->generate_uid();
unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
// copy attachment metadata to new event
$event = self::from_rcube_event($event, $master);
self::clear_attandee_noreply($event);
if ($success = $storage->insert_event($event))
$success = $event['uid'];
break;
case 'future':
// create a new recurring event
$event['_copyfrom'] = $master['_msguid'];
$event['_mailbox'] = $master['_mailbox'];
$event['uid'] = $this->cal->generate_uid();
unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
// copy attachment metadata to new event
$event = self::from_rcube_event($event, $master);
// remove recurrence exceptions on re-scheduling
if ($reschedule) {
unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
}
else if (is_array($event['recurrence']['EXCEPTIONS'])) {
// only keep relevant exceptions
$event['recurrence']['EXCEPTIONS'] = array_filter($event['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
return $exception['start'] > $event['start'];
});
if (is_array($event['recurrence']['EXDATE'])) {
$event['recurrence']['EXDATE'] = array_filter($event['recurrence']['EXDATE'], function($exdate) use ($event) {
return $exdate > $event['start'];
});
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
// compute remaining occurrences
if ($event['recurrence']['COUNT']) {
if (!$old['_count'])
$old['_count'] = $this->get_recurrence_count($master, $old['start']);
$event['recurrence']['COUNT'] -= intval($old['_count']);
}
// remove fixed weekday when date changed
if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) {
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
unset($event['recurrence']['BYMONTH']);
}
// set until-date on master event
$master['recurrence']['UNTIL'] = clone $old['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']);
// remove all exceptions after $event['start']
if (is_array($master['recurrence']['EXCEPTIONS'])) {
$master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
return $exception['start'] < $event['start'];
});
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
}
if (is_array($master['recurrence']['EXDATE'])) {
$master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function($exdate) use ($event) {
return $exdate < $event['start'];
});
}
// save new event
if ($success = $storage->insert_event($event)) {
$success = $event['uid'];
// update master event (no rescheduling!)
self::clear_attandee_noreply($master);
$storage->update_event($master);
}
break;
case 'current':
// recurring instances shall not store recurrence rules and attachments
$event['recurrence'] = array();
$event['thisandfuture'] = $savemode == 'future';
unset($event['attachments'], $event['id']);
// increment sequence of this instance if scheduling is affected
if ($reschedule) {
$event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
}
else if (!isset($event['sequence'])) {
$event['sequence'] = $old['sequence'] ?: $master['sequence'];
}
// save properties to a recurrence exception instance
if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
$success = $storage->update_event($master, $old['id']);
break;
}
}
$add_exception = true;
// adjust matching RDATE entry if dates changed
if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $old_date) {
$master['recurrence']['RDATE'][$j] = $event['start'];
sort($master['recurrence']['RDATE']);
$add_exception = false;
break;
}
}
}
// save as new exception to master event
if ($add_exception) {
self::add_exception($master, $event, $old);
}
$success = $storage->update_event($master);
break;
default: // 'all' is default
$event['id'] = $master['uid'];
$event['uid'] = $master['uid'];
// use start date from master but try to be smart on time or duration changes
$old_start_date = $old['start']->format('Y-m-d');
$old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
$old_duration = $old['end']->format('U') - $old['start']->format('U');
$new_start_date = $event['start']->format('Y-m-d');
$new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
$new_duration = $event['end']->format('U') - $event['start']->format('U');
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
$date_shift = $old['start']->diff($event['start']);
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start']->add($date_shift);
$event['end'] = clone $event['start'];
$event['end']->add(new DateInterval('PT'.$new_duration.'S'));
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
if ($old_start_date != $new_start_date) {
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
unset($event['recurrence']['BYMONTH']);
}
}
// dates did not change, use the ones from master
else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
$event['start'] = $master['start'];
$event['end'] = $master['end'];
}
// when saving an instance in 'all' mode, copy recurrence exceptions over
if ($old['recurrence_id']) {
$event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
}
else if ($master['_instance']) {
$event['_instance'] = $master['_instance'];
$event['recurrence_date'] = $master['recurrence_date'];
}
// TODO: forward changes to exceptions (which do not yet have differing values stored)
if (is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
// determine added and removed attendees
$old_attendees = $current_attendees = $added_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
foreach ((array)$event['attendees'] as $attendee) {
$current_attendees[] = $attendee['email'];
if (!in_array($attendee['email'], $old_attendees)) {
$added_attendees[] = $attendee;
}
}
$removed_attendees = array_diff($old_attendees, $current_attendees);
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
}
// adjust recurrence-id when start changed and therefore the entire recurrence chain changes
if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
$recurrence_id_format = libcalendaring::recurrence_id_format($event);
foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
$recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
if (is_a($recurrence_id, 'DateTime')) {
$recurrence_id->add($date_shift);
$event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
$event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
}
}
}
// set link to top-level exceptions
$event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
}
// unset _dateonly flags in (cached) date objects
unset($event['start']->_dateonly, $event['end']->_dateonly);
$success = $storage->update_event($event) ? $event['id'] : false; // return master UID
break;
}
if ($success && $this->freebusy_trigger)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Determine whether the current change affects scheduling and reset attendee status accordingly
*/
public function check_scheduling(&$event, $old, $update = true)
{
// skip this check when importing iCal/iTip events
if (isset($event['sequence']) || !empty($event['_method'])) {
return false;
}
// iterate through the list of properties considered 'significant' for scheduling
$kolab_event = $old['_formatobj'] ?: new kolab_format_event();
$reschedule = $kolab_event->check_rescheduling($event, $old);
// reset all attendee status to needs-action (#4360)
if ($update && $reschedule && is_array($event['attendees'])) {
$is_organizer = false;
$emails = $this->cal->get_user_emails();
$attendees = $event['attendees'];
foreach ($attendees as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$is_organizer = true;
}
else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
$attendees[$i]['status'] = 'NEEDS-ACTION';
$attendees[$i]['rsvp'] = true;
}
}
// update attendees only if I'm the organizer
if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
$event['attendees'] = $attendees;
}
}
return $reschedule;
}
/**
* Apply the given changes to already existing exceptions
*/
protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
{
$saved = false;
$existing = null;
// determine added and removed attendees
$added_attendees = $removed_attendees = array();
if ($savemode == 'future') {
$old_attendees = $current_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
foreach ((array)$event['attendees'] as $attendee) {
$current_attendees[] = $attendee['email'];
if (!in_array($attendee['email'], $old_attendees)) {
$added_attendees[] = $attendee;
}
}
$removed_attendees = array_diff($old_attendees, $current_attendees);
}
foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
// update a specific instance
if ($exception['_instance'] == $old['_instance']) {
$existing = $i;
// check savemode against existing exception mode.
// if matches, we can update this existing exception
if ((bool)$exception['thisandfuture'] === ($savemode == 'future')) {
$event['_instance'] = $old['_instance'];
$event['thisandfuture'] = $old['thisandfuture'];
$event['recurrence_date'] = $old['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$saved = true;
}
}
// merge the new event properties onto future exceptions
if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
unset($event['thisandfuture']);
self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, array('attendees'));
if (!empty($added_attendees) || !empty($removed_attendees)) {
calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
}
}
}
/*
// we could not update the existing exception due to savemode mismatch...
if (!$saved && $existing !== null && $master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture']) {
// ... try to move the existing this-and-future exception to the next occurrence
foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
// our old this-and-future exception is obsolete
if ($candidate['thisandfuture']) {
unset($master['recurrence']['EXCEPTIONS'][$existing]);
$saved = true;
break;
}
// this occurrence doesn't yet have an exception
else if (!$candidate['isexception']) {
$event['_instance'] = $candidate['_instance'];
$event['recurrence_date'] = $candidate['recurrence_date'];
$master['recurrence']['EXCEPTIONS'][$i] = $event;
$saved = true;
break;
}
}
}
*/
// set link to top-level exceptions
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
// returning false here will add a new exception
return $saved;
}
/**
* Add or update the given event as an exception to $master
*/
public static function add_exception(&$master, $event, $old = null)
{
if ($old) {
$event['_instance'] = $old['_instance'];
if (!$event['recurrence_date'])
$event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
}
else if (!$event['recurrence_date']) {
$event['recurrence_date'] = $event['start'];
}
if (!$event['_instance'] && is_a($event['recurrence_date'], 'DateTime')) {
$event['_instance'] = libcalendaring::recurrence_instance_identifier($event);
}
if (!is_array($master['exceptions']) && is_array($master['recurrence']['EXCEPTIONS'])) {
$master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
}
$existing = false;
foreach ((array)$master['exceptions'] as $i => $exception) {
if ($exception['_instance'] == $event['_instance']) {
$master['exceptions'][$i] = $event;
$existing = true;
}
}
if (!$existing) {
$master['exceptions'][] = $event;
}
return true;
}
/**
* Remove the noreply flags from attendees
*/
public static function clear_attandee_noreply(&$event)
{
foreach ((array)$event['attendees'] as $i => $attendee) {
unset($event['attendees'][$i]['noreply']);
}
}
/**
* Merge certain properties from the overlay event to the base event object
*
* @param array The event object to be altered
* @param array The overlay event object to be merged over $event
* @param array List of properties not allowed to be overwritten
*/
public static function merge_exception_data(&$event, $overlay, $blacklist = null)
{
$forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
if (is_array($blacklist))
$forbidden = array_merge($forbidden, $blacklist);
// compute date offset from the exception
if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) {
$date_offset = $overlay['recurrence_date']->diff($overlay['start']);
}
foreach ($overlay as $prop => $value) {
if ($prop == 'start' || $prop == 'end') {
if (is_object($event[$prop]) && $event[$prop] instanceof DateTime) {
// set date value if overlay is an exception of the current instance
if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
$event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
}
// apply date offset
else if ($date_offset) {
$event[$prop]->add($date_offset);
}
// adjust time of the recurring event instance
$event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
}
}
else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
$event[$prop] = $value;
}
else if ($prop[0] != '_' && !in_array($prop, $forbidden))
$event[$prop] = $value;
}
}
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Include virtual events (optional)
* @param integer Only list events modified since this time (unix timestamp)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
else if (!$calendars)
$calendars = array_keys($this->calendars);
$query = array();
if ($modifiedsince)
$query[] = array('changed', '>=', $modifiedsince);
$events = $categories = array();
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
$categories += $storage->categories;
}
}
// add events from the address books birthday calendar
if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
$events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
array_walk($events, 'kolab_driver::to_rcube_event');
return $events;
}
/**
* Get number of events in the given calendar
*
* @param mixed List of calendar IDs to count events (either as array or comma-separated string)
* @param integer Date range start (unix timestamp)
* @param integer Date range end (unix timestamp)
* @return array Hash array with counts grouped by calendar ID
*/
public function count_events($calendars, $start, $end = null)
{
$counts = array();
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
else if (!$calendars)
$calendars = array_keys($this->calendars);
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$counts[$cid] = $storage->count_events($start, $end);
}
}
return $counts;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return array();
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$time = $slot + $interval;
$candidates = array();
$query = array(array('tags', '=', 'x-has-alarms'));
foreach ($this->calendars as $cid => $calendar) {
// skip calendars with alarms disabled
if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
continue;
foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($e);
if ($alarm && $alarm['time'] && $alarm['time'] >= $last && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $e['title'],
'location' => $e['location'],
'start' => $e['start'],
'end' => $e['end'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query("SELECT *"
. " FROM " . $this->rc->db->table_name('kolab_alarms', true)
. " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
. " AND `user_id` = ?",
$this->rc->user->ID
);
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['alarm_id']] = $e;
}
}
$alarms = array();
foreach ($candidates as $id => $alarm) {
// skip dismissed alarms
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
if ($notifyat <= $time)
$alarms[] = $alarm;
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($alarm_id, $snooze = 0)
{
$alarms_table = $this->rc->db->table_name('kolab_alarms', true);
// delete old alarm entry
$this->rc->db->query("DELETE FROM $alarms_table"
. " WHERE `alarm_id` = ? AND `user_id` = ?",
$alarm_id,
$this->rc->user->ID
);
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query("INSERT INTO $alarms_table"
. " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
. " VALUES (?, ?, ?, ?)",
$alarm_id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments from the given event
*/
public function list_attachments($event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
$event = $storage->get_event($event['id']);
return $event['attachments'];
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!($storage = $this->get_calendar($event['calendar'])))
return false;
- $event = $storage->get_event($event['id']);
+ // get old revision of event
+ if ($event['rev']) {
+ $event = $this->get_event_revison($event, $event['rev'], true);
+ }
+ else {
+ $event = $storage->get_event($event['id']);
+ }
if ($event && !empty($event['_attachments'])) {
foreach ($event['_attachments'] as $att) {
if ($att['id'] == $id) {
return $att;
}
}
}
return null;
}
/**
* Get attachment body
* @see calendar_driver::get_attachment_body()
*/
public function get_attachment_body($id, $event)
{
if (!($cal = $this->get_calendar($event['calendar'])))
return false;
+ // get old revision of event
+ if ($event['rev']) {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $cid = substr($id, 4);
+
+ // call Bonnie API and get the raw mime message
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+ if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['rev'], $mailbox, $msguid)) {
+ // parse the message and find the part with the matching content-id
+ $message = rcube_mime::parse_message($msg_raw);
+ foreach ((array)$message->parts as $part) {
+ if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
+ return $part->body;
+ }
+ }
+ }
+
+ return false;
+ }
+
return $cal->get_attachment_body($id, $event);
}
/**
* Build a struct representing the given message reference
*
* @see calendar_driver::get_message_reference()
*/
public function get_message_reference($uri_or_headers, $folder = null)
{
if (is_object($uri_or_headers)) {
$uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
}
if (is_string($uri_or_headers)) {
return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
}
return false;
}
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
// FIXME: complete list with categories saved in config objects (KEP:12)
return $this->rc->config->get('calendar_categories', $this->default_categories);
}
/**
* Create instances of a recurring event
*
* @param array Hash array with event properties
* @param object DateTime Start date of the recurrence window
* @param object DateTime End date of the recurrence window
* @return array List of recurring event instances
*/
public function get_recurring_events($event, $start, $end = null)
{
// load the given event data into a libkolabxml container
if (!$event['_formatobj']) {
$event_xml = new kolab_format_event();
$event_xml->set($event);
$event['_formatobj'] = $event_xml;
}
$this->_read_calendars();
$storage = reset($this->calendars);
return $storage->get_recurring_events($event, $start, $end);
}
/**
*
*/
private function get_recurrence_count($event, $dtstart)
{
// use libkolab to compute recurring events
if (class_exists('kolabcalendaring') && $event['_formatobj']) {
$recurrence = new kolab_date_recurrence($event['_formatobj']);
}
else {
// fallback to local recurrence implementation
require_once($this->cal->home . '/lib/calendar_recurrence.php');
$recurrence = new calendar_recurrence($this->cal, $event);
}
$count = 0;
while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
$count++;
}
return $count;
}
/**
* Fetch free/busy information from a person within the given range
*/
public function get_freebusy_list($email, $start, $end)
{
if (empty($email)/* || $end < time()*/)
return false;
// map vcalendar fbtypes to internal values
$fbtypemap = array(
'FREE' => calendar::FREEBUSY_FREE,
'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE,
'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF,
'OOF' => calendar::FREEBUSY_OOF);
// ask kolab server first
try {
$request_config = array(
'store_body' => true,
'follow_redirects' => true,
);
$request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config);
$response = $request->send();
// authentication required
if ($response->getStatus() == 401) {
$request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
$response = $request->send();
}
if ($response->getStatus() == 200)
$fbdata = $response->getBody();
unset($request, $response);
}
catch (Exception $e) {
PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage());
}
// get free-busy url from contacts
if (!$fbdata) {
$fburl = null;
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
$abook = $this->rc->get_address_book($book);
if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) {
while ($contact = $result->iterate()) {
if ($fburl = $contact['freebusyurl']) {
$fbdata = @file_get_contents($fburl);
break;
}
}
}
if ($fbdata)
break;
}
}
// parse free-busy information using Horde classes
if ($fbdata) {
$ical = $this->cal->get_ical();
$ical->import($fbdata);
if ($fb = $ical->freebusy) {
$result = array();
foreach ($fb['periods'] as $tuple) {
list($from, $to, $type) = $tuple;
$result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
}
// we take 'dummy' free-busy lists as "unknown"
if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy'))
return false;
// set period from $start till the begin of the free-busy information as 'unknown'
if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
}
// pad period till $end with status 'unknown'
if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
$result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
}
return $result;
}
}
return false;
}
/**
* Handler to push folder triggers when sent from client.
* Used to push free-busy changes asynchronously after updating an event
*/
public function push_freebusy()
{
// make shure triggering completes
set_time_limit(0);
ignore_user_abort(true);
$cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
if (!($cal = $this->get_calendar($cal)))
return false;
// trigger updates on folder
$trigger = $cal->storage->trigger();
if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
true, false);
}
exit;
}
/**
* Convert from driver format to external caledar app data
*/
public static function to_rcube_event(&$record)
{
if (!is_array($record))
return $record;
$record['id'] = $record['uid'];
if ($record['_instance']) {
$record['id'] .= '-' . $record['_instance'];
if (!$record['recurrence_id'] && !empty($record['recurrence']))
$record['recurrence_id'] = $record['uid'];
}
// all-day events go from 12:00 - 13:00
if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = clone $record['start'];
$record['end']->add(new DateInterval('PT1H'));
}
// translate internal '_attachments' to external 'attachments' list
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
unset($attachment['path'], $attachment['content']);
$attachments[] = $attachment;
}
}
$record['attachments'] = $attachments;
}
if (!empty($record['attendees'])) {
foreach ((array)$record['attendees'] as $i => $attendee) {
if (is_array($attendee['delegated-from'])) {
$record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
}
if (is_array($attendee['delegated-to'])) {
$record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
}
}
}
// Roundcube only supports one category assignment
if (is_array($record['categories']))
$record['categories'] = $record['categories'][0];
// the cancelled flag transltes into status=CANCELLED
if ($record['cancelled'])
$record['status'] = 'CANCELLED';
// The web client only supports DISPLAY type of alarms
if (!empty($record['alarms']))
$record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
// remove empty recurrence array
if (empty($record['recurrence']))
unset($record['recurrence']);
// clean up exception data
if (is_array($record['recurrence']['EXCEPTIONS'])) {
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
});
}
unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
$record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']);
return $record;
}
/**
*
*/
public static function from_rcube_event($event, $old = array())
{
// in kolab_storage attachments are indexed by content-id
if (is_array($event['attachments']) || !empty($event['deleted_attachments'])) {
$event['_attachments'] = array();
foreach ($event['attachments'] as $attachment) {
$key = null;
// Roundcube ID has nothing to do with the storage ID, remove it
if ($attachment['content'] || $attachment['path']) {
unset($attachment['id']);
}
else {
foreach ((array)$old['_attachments'] as $cid => $oldatt) {
if ($attachment['id'] == $oldatt['id'])
$key = $cid;
}
}
// flagged for deletion => set to false
if ($attachment['_deleted'] || in_array($attachment['id'], (array)$event['deleted_attachments'])) {
$event['_attachments'][$key] = false;
}
// replace existing entry
else if ($key) {
$event['_attachments'][$key] = $attachment;
}
// append as new attachment
else {
$event['_attachments'][] = $attachment;
}
}
$event['_attachments'] = array_merge((array)$old['_attachments'], $event['_attachments']);
// attachments flagged for deletion => set to false
foreach ($event['_attachments'] as $key => $attachment) {
if ($attachment['_deleted'] || in_array($attachment['id'], (array)$event['deleted_attachments'])) {
$event['_attachments'][$key] = false;
}
}
}
return $event;
}
/**
* Set CSS class according to the event's attendde partstat
*/
public static function add_partstat_class($event, $partstats, $user = null)
{
// set classes according to PARTSTAT
if (is_array($event['attendees'])) {
$user_emails = libcalendaring::get_instance()->get_user_emails($user);
$partstat = 'UNKNOWN';
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails)) {
$partstat = $attendee['status'];
break;
}
}
if (in_array($partstat, $partstats)) {
$event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
}
}
return $event;
}
/**
* Provide a list of revisions for the given event
*
* @param array $event Hash array with event properties
*
* @return array List of changes, each as a hash array
* @see calendar_driver::get_event_changelog()
*/
public function get_event_changelog($event)
{
if (empty($this->bonnie_api)) {
return false;
}
- list($uid, $mailbox) = $this->_resolve_event_identity($event);
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
- $result = $this->bonnie_api->changelog('event', $uid, $mailbox);
+ $result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
return $result['changes'];
}
return false;
}
/**
* Get a list of property changes beteen two revisions of an event
*
* @param array $event Hash array with event properties
* @param mixed $rev1 Old Revision
* @param mixed $rev2 New Revision
*
* @return array List of property changes, each as a hash array
* @see calendar_driver::get_event_diff()
*/
public function get_event_diff($event, $rev1, $rev2)
{
if (empty($this->bonnie_api)) {
return false;
}
- list($uid, $mailbox) = $this->_resolve_event_identity($event);
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
// call Bonnie API
- $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox);
+ $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
$keymap = array(
'dtstart' => 'start',
'dtend' => 'end',
'dstamp' => 'changed',
'summary' => 'title',
'alarm' => 'alarms',
'attendee' => 'attendees',
'attach' => 'attachments',
'rrule' => 'recurrence',
'transparency' => 'free_busy',
'classification' => 'sensitivity',
'lastmodified-date' => 'changed',
);
$prop_keymaps = array(
'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
'attendees' => array('partstat' => 'status'),
);
$special_changes = array();
// map kolab event properties to keys the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// translate free_busy values
if ($change['property'] == 'free_busy') {
$change['old'] = $old['old'] ? 'free' : 'busy';
$change['new'] = $old['new'] ? 'free' : 'busy';
}
// map alarms trigger value
if ($change['property'] == 'alarms') {
if (is_array($change['old']) && is_array($change['old']['trigger']))
$change['old']['trigger'] = $change['old']['trigger']['value'];
if (is_array($change['new']) && is_array($change['new']['trigger']))
$change['new']['trigger'] = $change['new']['trigger']['value'];
}
// make all property keys uppercase
if ($change['property'] == 'recurrence') {
$special_changes['recurrence'] = $i;
foreach (array('old','new') as $m) {
if (is_array($change[$m])) {
$props = array();
foreach ($change[$m] as $k => $v)
$props[strtoupper($k)] = $v;
$change[$m] = $props;
}
}
}
// map property keys names
if (is_array($prop_keymaps[$change['property']])) {
foreach ($prop_keymaps[$change['property']] as $k => $dest) {
if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
$change['old'][$dest] = $change['old'][$k];
unset($change['old'][$k]);
}
if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
$change['new'][$dest] = $change['new'][$k];
unset($change['new'][$k]);
}
}
}
if ($change['property'] == 'exdate') {
$special_changes['exdate'] = $i;
}
else if ($change['property'] == 'rdate') {
$special_changes['rdate'] = $i;
}
});
// merge some recurrence changes
foreach (array('exdate','rdate') as $prop) {
if (array_key_exists($prop, $special_changes)) {
$exdate = $result['changes'][$special_changes[$prop]];
if (array_key_exists('recurrence', $special_changes)) {
$recurrence = &$result['changes'][$special_changes['recurrence']];
}
else {
$i = count($result['changes']);
$result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
$recurrence = &$result['changes'][$i]['recurrence'];
}
$key = strtoupper($prop);
$recurrence['old'][$key] = $exdate['old'];
$recurrence['new'][$key] = $exdate['new'];
unset($result['changes'][$special_changes[$prop]]);
}
}
return $result;
}
return false;
}
/**
* Return full data of a specific revision of an event
*
* @param array Hash array with event properties
* @param mixed $rev Revision number
*
* @return array Event object as hash array
* @see calendar_driver::get_event_revison()
*/
- public function get_event_revison($event, $rev)
+ public function get_event_revison($event, $rev, $internal = false)
{
if (empty($this->bonnie_api)) {
return false;
}
$calid = $event['calendar'];
- list($uid, $mailbox) = $this->_resolve_event_identity($event);
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
// call Bonnie API
- $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox);
+ $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('event');
$format->load($result['xml']);
$event = $format->to_array();
+ $format->get_attachments($event, true);
+
+ // TODO: get the right instance from a recurring event
if ($format->is_valid()) {
$event['calendar'] = $calid;
$event['rev'] = $result['rev'];
- return self::to_rcube_event($event);
+ return $internal ? $event : self::to_rcube_event($event);
}
}
return false;
}
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+ $calendar = $this->get_calendar($event['calendar']);
+ $success = false;
+
+ if ($calendar && $calendar->storage && $calendar->editable) {
+ if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) {
+ $imap = $this->rc->get_storage();
+
+ // insert $raw_msg as new message
+ if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) {
+ $success = true;
+
+ // delete old revision from imap and cache
+ $imap->delete_message($msguid, $calendar->storage->name);
+ $calendar->storage->cache->set($msguid, false);
+ }
+ }
+ }
+
+ return $success;
+ }
+
/**
* Helper method to resolved the given event identifier into uid and folder
*
- * @return array (uid,folder) tuple
+ * @return array (uid,folder,msguid) tuple
*/
private function _resolve_event_identity($event)
{
- $mailbox = null;
+ $mailbox = $msguid = null;
if (is_array($event)) {
- $uid = $event['id'] ?: $event['uid'];
+ $uid = $event['uid'] ?: $event['id'];
if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
$mailbox = $cal->get_mailbox_id();
+
+ // get event object from storage in order to get the real object uid an msguid
+ if ($ev = $cal->get_event($event['id'])) {
+ $msguid = $ev['_msguid'];
+ $uid = $ev['uid'];
+ }
}
}
else {
$uid = $event;
+
+ // get event object from storage in order to get the real object uid an msguid
+ if ($ev = $this->get_event($event)) {
+ $mailbox = $ev['_mailbox'];
+ $msguid = $ev['_msguid'];
+ $uid = $ev['uid'];
+ }
}
- return array($uid, $mailbox);
+ return array($uid, $mailbox, $msguid);
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
// show default dialog for birthday calendar
if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID)
unset($formfields['showalarms']);
return parent::calendar_form($action, $calendar, $formfields);
}
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
}
else {
$path_imap = '';
}
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
// Disable folder name input
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
$formfields['name']['value'] = kolab_storage::object_name($folder)
. $input_name->show($folder);
}
// calendar name (default field)
$form['props']['fieldsets']['location'] = array(
'name' => $this->rc->gettext('location'),
'content' => array(
'name' => $formfields['name']
),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = kolab_storage::folder_selector('event', array('name' => 'parent', 'id' => 'calendar-parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'id' => 'calendar-parent',
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// calendar color (default field)
$form['props']['fieldsets']['settings'] = array(
'name' => $this->rc->gettext('settings'),
'content' => array(
'color' => $formfields['color'],
'showalarms' => $formfields['showalarms'],
),
);
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->cal->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)),
'width' => '100%',
'height' => 350,
'border' => 0,
'style' => 'border:0'),
''),
);
}
$this->form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$this->form_html .= $hiddenfield->show() . "\n";
}
}
// Create form output
foreach ($form as $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = $this->get_form_part($fieldset);
if ($subcontent) {
$content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
}
}
}
else {
$content = $this->get_form_part($tab);
}
if ($content) {
$this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
// Parse form template for skin-dependent stuff
$this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html'));
return $this->rc->output->parse('calendar.kolabform', false, false);
}
/**
* Handler for template object
*/
public function calendar_form_html()
{
return $this->form_html;
}
/**
* Helper function used in calendar_form_content(). Creates a part of the form.
*/
private function get_form_part($form)
{
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', html::label($colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $form['content'];
}
return $content;
}
/**
* Handler to render ACL form for a calendar folder
*/
public function calendar_acl()
{
$this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form'));
$this->rc->output->send('calendar.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function calendar_acl_form()
{
$calid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
if ($calid && ($cal = $this->get_calendar($calid))) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
// Allow plugins to modify the form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('calendar_form_kolab',
array('form' => $form, 'options' => $options, 'name' => $folder));
}
if (!$plugin['form']['sharing']['content'])
$plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights'));
return $plugin['form']['sharing']['content'];
}
/**
* Handler for user_delete plugin hook
*/
public function user_delete($args)
{
$db = $this->rc->get_dbh();
foreach (array('kolab_alarms', 'itipinvitations') as $table) {
$db->query("DELETE FROM " . $this->rc->db->table_name($table, true)
. " WHERE `user_id` = ?", $args['user']->ID);
}
}
}
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 4229b1a1..c137fb8b 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1,2327 +1,2327 @@
/**
* Roundcube Calendar plugin styles for skin "Larry"
*
* Copyright (c) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
body.calendarmain {
overflow: hidden;
}
body.calendarmain #mainscreen {
left: 0;
}
/* overrides for tablets and mobile phones */
@media screen and (max-device-width: 1024px){
body.calendarmain {
overflow: visible;
}
body.calendarmain #mainscreen {
min-width: 1000px !important;
min-height: 520px !important;
}
body.calendarmain #header {
min-width: 1020px !important;
}
}
#calendarsidebar {
position: absolute;
top: 0;
left: 10px;
bottom: 0;
width: 250px;
}
#datepicker {
position: absolute;
top: 40px;
left: 0;
width: 100%;
min-height: 190px;
}
#datepicker .ui-datepicker {
width: 100% !important;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
}
#datepicker .ui-datepicker td a {
padding: 5px 4px;
font-size: 12px;
}
#datepicker td.ui-datepicker-activerange {
border-color: #69a2b6;
}
#datepicker .ui-datepicker-activerange a {
color: #185d7a;
background: #d9f1fb;
background: -moz-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9f1fb), color-stop(100%,#c5e3ee));
background: -o-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: -ms-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0);
}
#datepicker .ui-datepicker-days-cell-over a.ui-state-default {
color: #fff;
border-color: #2fa0c0;
background: rgba(73,180,210,0.6);
text-shadow: 0px 1px 1px #666;
filter: none;
}
#datepicker .ui-datepicker-activerange a.ui-state-active {
color: #fff;
background: #00acd4;
background: -moz-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00acd4), color-stop(100%,#008fc7));
background: -o-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: -ms-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: linear-gradient(top, #00acd4 0%, #008fc7 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00acd4', endColorstr='#008fc7', GradientType=0);
}
#datepicker td.ui-datepicker-week-col {
cursor: pointer;
}
#datepicker .ui-datepicker-title {
margin: 2px 2.3em 3px 2.3em;
}
#datepicker .ui-datepicker .ui-datepicker-prev,
#datepicker .ui-datepicker .ui-datepicker-next {
top: 4px;
}
#calsidebarsplitter {
position: absolute;
left: 264px;
width: 6px;
top: 40px !important;
bottom: 0;
background: url(images/toggle.gif) -1px 48% no-repeat transparent;
}
div.sidebarclosed {
background-position: -8px 48% !important;
cursor: pointer;
}
#calsidebarsplitter:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 0;
left: 276px;
right: 0;
bottom: 0;
}
.calendarmain #message.statusbar {
border: 1px solid #c3c3c3;
border-bottom-color: #ababab;
}
#timezonedisplay {
position: absolute;
bottom: 5px;
right: 12px;
font-size: 0.85em;
color: #666;
}
#print {
width: 680px;
}
pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#calendars {
position: absolute;
top: 276px;
left: 0;
bottom: 0;
right: 0;
}
#calendars .boxtitle {
position: relative;
}
#calendars .boxtitle a.iconbutton.search {
position: absolute;
top: 8px;
right: 8px;
width: 16px;
cursor: pointer;
background-position: -2px -317px;
}
#calendars .listsearchbox {
display: none;
}
#calendars .listsearchbox.expanded {
display: block;
}
#calendars .scroller {
top: 34px;
}
#calendars .listsearchbox.expanded + .scroller {
top: 68px;
}
#calendars .treelist li {
margin: 0;
position: relative;
}
#calendars .treelist li div.folder,
#calendars .treelist li div.calendar {
position: relative;
height: 28px;
}
#calendars .treelist li div.virtual {
height: 22px;
}
#calendars .treelist li span.calname {
display: block;
padding: 0px 18px 2px 2px;
position: absolute;
top: 7px;
left: 38px;
right: 45px;
cursor: default;
background: url(images/calendars.png) right 20px no-repeat;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #004458;
}
.quickview-active #calendars .treelist div input,
.quickview-active #calendars .treelist div .calname {
opacity: 0.35;
}
.quickview-active #calendars .treelist div.focusview .calname {
opacity: 1.0;
background-image: none;
}
#calendars .treelist li div.virtual > span.calname {
color: #aaa;
top: 4px;
left: 20px;
}
#calendars .treelist li.x-birthdays span.calname,
#calendars .treelist li.x-invitations span.calname {
font-style: italic;
}
#calendars .treelist.flat li span.calname {
left: 24px;
right: 42px;
}
#calendars .treelist li span.handle {
display: inline-block;
position: absolute;
top: 8px;
right: 6px;
padding: 0;
width: 10px;
height: 10px;
border-radius: 7px;
font-size: 0.8em;
border: 1px solid rgba(0, 0, 0, 0.5);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
#calendars .treelist div span.actions {
display: inline-block;
position: absolute;
top: 2px;
right: 22px;
padding: 5px 20px 0 6px;
/* min-width: 40px; */
height: 19px;
text-align: right;
z-index: 4;
}
#calendars .treelist div:hover span.actions {
top: 1px;
right: 21px;
border: 1px solid #c6c6c6;
border-radius: 4px;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9', endColorstr='#e6e6e6', GradientType=0);
}
#calendars .treelist li a.subscribed {
display: inline-block;
position: absolute;
top: 5px;
right: 3px;
height: 16px;
width: 16px;
padding: 0;
background: url(images/calendars.png) -100px 0 no-repeat;
overflow: hidden;
text-indent: -5000px;
cursor: pointer;
}
#calendars .treelist div:hover a.subscribed,
#calendars .treelist div a.subscribed:focus {
background-position: 0 -110px;
}
#calendars .treelist div.subscribed a.subscribed,
#calendars .treelist div.subscribed a.subscribed:focus {
background-position: -16px -110px;
}
#calendars .treelist div.subscribed.partial a.subscribed,
#calendars .treelist div.subscribed.partial a.subscribed:focus {
background-position: -16px -148px;
}
#calendars .treelist div a.remove:focus,
#calendars .treelist div a.quickview:focus,
#calendars .treelist div a.subscribed:focus {
border-radius: 3px;
outline: 2px solid rgba(30,150,192, 0.5);
}
#calendars .treelist div a.remove,
#calendars .treelist div a.quickview {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
padding: 0;
background: url(images/calendars.png) -100px 0 no-repeat;
overflow: hidden;
text-indent: -5000px;
cursor: pointer;
}
#calendars .treelist div a.quickview:focus,
#calendars .treelist div:hover a.quickview {
background-position: 0 -128px;
background-color: transparent !important;
}
#calendars .treelist div.focusview a.quickview {
background-position: -16px -128px;
}
#calendars .treelist div a.remove:focus,
#calendars .treelist div:hover a.remove {
background-position: -16px -168px;
background-color: transparent !important;
}
#calendars .searchresults .treelist div a.remove {
display: none;
}
#calendars .treelist li input {
position: absolute;
top: 5px;
left: 18px;
}
#calendars .treelist li div.treetoggle {
top: 8px;
}
#calendars .treelist li.virtual div.treetoggle {
top: 6px;
}
#calendars .treelist.flat li input {
left: 4px;
}
#calendars .treelist ul li div.folder,
#calendars .treelist ul li div.calendar {
margin-left: 16px;
}
#calendars .treelist ul ul li div.folder,
#calendars .treelist ul ul li div.calendar {
margin-left: 32px;
}
#calendars .treelist ul ul ul li div.folder,
#calendars .treelist ul ul ul li div.calendar {
margin-left: 48px;
}
#calendars .treelist li.selected > div.calendar {
background-color: #c7e3ef;
}
#calendars .treelist li.selected > span.calname {
font-weight: bold;
}
#calendars .treelist div.readonly span.calname {
background-position: right -20px;
}
#calendars .treelist li.user > div > span.calname {
background-position: right -38px;
}
/*
#calendars .treelist div.user.readonly span.calname {
background-position: right -56px;
}
#calendars .treelist div.shared span.calname {
background-position: right -74px;
}
#calendars .treelist div.shared.readonly span.calname {
background-position: right -92px;
}
*/
#calendars .treelist .calendar .count {
position: absolute;
display: inline-block;
top: 5px;
right: 68px;
min-width: 1.3em;
padding: 2px 4px;
background: #005d76;
background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
background: linear-gradient(to bottom, #005d76 0%, #004558 100%);
-webkit-box-shadow: inset 0 1px 1px 0 #002635;
box-shadow: inset 0 1px 1px 0 #002635;
border-radius: 10px;
color: #fff;
text-align: center;
font-style: normal;
font-weight: bold;
text-shadow: none;
z-index: 3;
}
#calendars .searchresults {
background: #b0ccd7;
margin-top: 8px;
}
#calendars .searchresults .boxtitle {
background: none;
padding: 2px 8px 2px 8px;
}
#calendars .searchresults .listing li {
background-color: #c7e3ef;
}
#calfeedurl,
#caldavurl {
width: 98%;
background: #fbfbfb;
padding: 4px;
margin-bottom: 1em;
resize: none;
}
#agendalist {
width: 100%;
margin: 0 auto;
margin-top: 60px;
border: 1px solid #C1DAD7;
display: none;
}
#agendalist table {
width: 100%;
}
#agendalist td,
#agendalist th {
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
background: #fff;
padding: 6px 6px 6px 12px;
}
#agendalist tr {
vertical-align: top;
}
#agendalist th {
font-weight: bold;
}
#calendartoolbar {
position: absolute;
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
}
#calendartoolbar a.button {
background-image: url(images/toolbar.png);
padding-left: 0;
padding-right: 0;
min-width: 50px;
max-width: 60px;
}
#calendartoolbar a.button.addevent {
background-position: center 1px;
max-width: 70px;
}
#calendartoolbar a.button.export {
background-position: center -40px;
}
#calendartoolbar a.button.import {
background-position: center -440px;
}
#calendartoolbar a.button.print {
background-position: center -80px;
}
body.calendarmain #quicksearchbar {
z-index: 20;
}
body.calendarmain #searchmenulink {
width: 15px;
}
.calendarmain div.uidialog {
display: none;
}
#user {
position: absolute;
top: 10px;
right: 100px;
left: 100px;
text-align: center;
}
a.morelink {
font-size: 90%;
color: #0069a6;
text-decoration: none;
}
a.morelink:hover {
text-decoration: underline;
}
a.miniColors-trigger {
margin-top: -3px;
}
.calendar.attachmentwin #attachmenttoolbar {
position: relative;
top: -6px;
height: 40px;
}
.calendar.attachmentwin #attachmentcontainer {
position: absolute;
top: 0;
left: 232px;
right: 0;
bottom: 0;
}
.calendar.attachmentwin #attachmentframe {
width: 100%;
height: 100%;
border: 0;
background-color: #fff;
border-radius: 4px;
}
.calendar.attachmentwin #partheader {
position: absolute;
top: 0;
left: 0;
width: 220px;
bottom: 0;
}
.calendar.attachmentwin #partheader table {
table-layout: fixed;
overflow: hidden;
}
.calendar.attachmentwin #partheader table td {
color: #666;
padding: 4px 6px;
text-overflow: ellipsis;
overflow: hidden;
}
.calendar.attachmentwin #partheader table td.header {
font-weight: bold;
}
.calendar.attachmentwin #partheader table td.title {
width: 60px;
padding-right: 0;
}
#edit-attachments {
margin: 0.6em 0;
}
#edit-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 4px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
line-height: 20px;
}
#edit-attachments ul li a.file {
padding: 0;
}
#edit-attachments-form {
margin-top: 1em;
padding-top: 0.8em;
border-top: 2px solid #fafafa;
}
#edit-attachments-form .formbuttons {
margin: 0.5em 0;
}
#eventedit .droptarget {
background-image: url(../../../../skins/larry/images/filedrop.png) !important;
background-position: center bottom !important;
background-repeat: no-repeat !important;
}
#eventedit .droptarget.hover,
#eventedit .droptarget.active {
border-color: #019bc6;
box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
}
#eventedit .droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#event-attachments .attachmentslist li {
float: left;
margin-right: 1em;
}
#event-attachments .attachmentslist li a {
outline: none;
}
#event-panel-attachments.disabled .attachmentslist li a.delete {
visibility: hidden;
}
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.png) right 0 no-repeat;
}
.event-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
outline: none;
}
.event-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
.event-attendees span.accepted {
background-position: right -20px;
}
.event-attendees span.declined {
background-position: right -40px;
}
.event-attendees span.tentative {
background-position: right -60px;
}
.event-attendees span.delegated {
background-position: right -180px;
}
.event-attendees span.organizer {
background-position: right -80px;
}
#all-event-attendees span.attendee {
display: block;
margin-bottom: 0.4em;
padding-bottom: 0.3em;
border-bottom: 1px solid #ddd;
}
.calendarmain .fc-view-table td.fc-list-header,
#attendees-freebusy-table h3.boxtitle,
#schedule-freebusy-times thead th,
.edit-attendees-table thead th
{
color: #69939e;
font-size: 11px;
font-weight: bold;
background: #d6eaf3;
background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3));
background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
border: 0;
border-bottom: 1px solid #ccc;
height: 18px;
line-height: 18px;
padding: 8px 7px 3px 7px;
}
/* jQuery UI overrides */
.calendarmain .eventdialog h1 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
.calendarmain .eventdialog label,
.calendarmain .eventdialog h5.label {
font-weight: normal;
font-size: 1em;
color: #999;
margin: 0 0 0.2em 0;
}
.calendarmain .eventdialog label span.index,
.calendarmain .eventdialog h5.label .index {
vertical-align: inherit;
margin-left: 0.6em;
}
.calendarmain .eventdialog {
margin: 0 -0.2em;
}
.calendarmain .eventdialog.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
.calendarmain .eventdialog.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
.calendarmain .eventdialog.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
.calendarmain .sensitivity-private #event-title {
margin-right: 50px;
}
.calendarmain .sensitivity-confidential #event-title {
margin-right: 60px;
}
.calendarmain .eventdialog div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
.calendarmain .eventdialog div.event-line a.iconbutton {
margin-left: 0.5em;
line-height: 17px;
}
.calendarmain .eventdialog div.event-line span.event-text + label {
margin-left: 2em;
}
.calendarmain .eventdialog #event-rsvp-comment,
.calendarmain .eventdialog #event-created-changed {
margin-top: 0.6em;
}
.eventdialog .event-text-old,
.eventdialog .event-text-new,
.eventdialog .event-text-diff {
padding: 2px;
}
.eventdialog .event-text-diff del,
.eventdialog .event-text-diff ins {
text-decoration: none;
color: inherit;
}
.eventdialog .event-text-old,
.eventdialog .event-text-diff del {
background-color: #fdd;
/* text-decoration: line-through; */
}
.eventdialog .event-text-new,
.eventdialog .event-text-diff ins {
background-color: #dfd;
}
#eventdiff .attachmentslist li a,
#eventdiff .attachmentslist li a:hover {
cursor: default;
text-decoration: none;
}
#eventhistory .loading {
color: #666;
margin: 1em 0;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
#eventhistory .compare-button {
margin: 4px 0;
}
#event-changelog-table tbody td {
padding: 4px 7px;
vertical-align: middle;
}
#event-changelog-table tbody tr:last-child td {
border-bottom: 0;
}
#event-changelog-table tbody tr.undisclosed td.date,
#event-changelog-table tbody tr.undisclosed td.user {
font-style: italic;
}
#event-changelog-table .diff {
width: 4em;
padding: 2px;
}
#event-changelog-table .revision {
- width: 7em;
+ width: 6em;
}
#event-changelog-table .date {
width: 11em;
}
#event-changelog-table .user {
width: auto;
}
#event-changelog-table .operation {
width: 15%;
}
#event-changelog-table .actions {
width: 50px;
text-align: right;
padding: 4px;
}
#event-changelog-table td a.iconbutton.restore,
#event-changelog-table td a.iconbutton.preview {
width: 16px;
margin-right: 2px;
background-image: url(images/calendars.png);
background-position: -1px -147px;
}
#event-changelog-table td a.iconbutton.restore {
background-image: url(images/calendars.png);
background-position: -1px -167px;
}
#event-changelog-table tr.first td a.iconbutton {
opacity: 0.3;
cursor: default;
}
#event-partstat .changersvp {
cursor: pointer;
color: #333;
text-decoration: none;
}
#event-partstat .iconbutton {
visibility: hidden;
}
#event-partstat .changersvp:focus .iconbutton,
#event-partstat:hover .iconbutton {
visibility: visible;
}
#eventedit {
position: relative;
top: -1.5em;
padding: 0.5em 0.1em;
margin: 0 -0.2em;
}
#eventedit input.text,
#eventedit textarea {
width: 97%;
}
#eventtabs {
position: relative;
padding: 0;
border: 0;
border-radius: 0;
}
div.form-section,
.calendarmain .eventdialog div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.6em;
}
#eventtabs .border-after {
padding-bottom: 0.8em;
margin-bottom: 0.8em;
border-bottom: 2px solid #fafafa;
}
.calendarmain .eventdialog label,
#eventedit label,
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
}
.calendarmain .eventdialog #event-url .event-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#event-links .attachmentslist {
display: inline-block;
}
#event-links label,
#edit-event-links label {
float: left;
margin-top: 0.3em;
padding-right: 0.75em;
}
#edit-event-links .event-text {
margin-left: 8em;
min-height: 22px;
}
#edit-event-links .attachmentslist li.message a.messagelink,
#event-links .attachmentslist li.message a.messagelink {
padding: 0 0 0 24px;
}
#edit-event-links .attachmentslist li a.delete {
top: 0;
background-position: -6px -378px;
}
#edit-event-links .attachmentslist li.deleted a.messagelink,
#edit-event-links .attachmentslist li.deleted a.messagelink:hover {
text-decoration: line-through;
}
#eventedit .formtable td.label {
min-width: 6em;
}
td.topalign {
vertical-align: top;
}
#eventedit label.weekday,
#eventedit label.monthday {
min-width: 3em;
}
#eventedit label.month {
min-width: 5em;
}
#eventedit .formtable td {
padding: 0.2em 0;
}
.ui-dialog .event-update-confirm {
padding: 0 0.5em 0.5em 0.5em;
}
.event-dialog-message,
.event-update-confirm .message {
margin-top: 0.5em;
padding: 0.8em;
border: 1px solid #ffdf0e;
background-color: #fef893;
}
.event-dialog-message .message,
.event-update-confirm .message {
margin-bottom: 0.5em;
}
.edit-recurring-warning .savemode {
padding-left: 20px;
}
.event-update-confirm .savemode {
padding-left: 30px;
}
.event-dialog-message span.ui-icon,
.event-update-confirm span.ui-icon {
float: left;
margin: 0 7px 20px 0;
}
.event-dialog-message label,
.event-update-confirm label {
min-width: 3em;
padding-right: 1em;
}
.event-update-confirm a.button {
margin: 0 0.5em 0 0.2em;
min-width: 5em;
text-align: center;
}
.libcal-rsvp-replymode li a {
cursor: default;
}
#event-rsvp,
#edit-attendees-notify {
margin: 0.6em 0 0.3em 0;
padding: 0.5em;
}
#event-rsvp .itip-reply-controls {
margin-top: 0.5em;
}
#event-rsvp .itip-reply-controls label {
color: #333;
}
#event-rsvp .itip-reply-controls textarea {
width: 95%;
}
#eventedit .edit-attendees-table {
width: 100%;
margin-top: 0.5em;
}
#eventedit .edit-attendees-table th.role,
#eventedit .edit-attendees-table td.role {
width: 9em;
}
#eventedit .edit-attendees-table th.availability,
#eventedit .edit-attendees-table td.availability,
#eventedit .edit-attendees-table th.confirmstate,
#eventedit .edit-attendees-table td.confirmstate {
width: 4em;
}
#eventedit .edit-attendees-table th.options,
#eventedit .edit-attendees-table td.options {
width: 16px;
padding: 2px 4px;
}
#eventedit .edit-attendees-table th.invite,
#eventedit .edit-attendees-table td.invite {
width: 44px;
padding: 2px;
}
#eventedit .edit-attendees-table th.invite label {
display: inline-block;
position: relative;
top: 4px;
width: 24px;
height: 18px;
min-width: 24px;
padding: 0;
overflow: hidden;
text-indent: -5000px;
white-space: nowrap;
background: url(images/sendinvitation.png) 1px 0 no-repeat;
}
#eventedit .edit-attendees-table tbody tr:last-child td {
border-bottom: 0;
}
#eventedit .edit-attendees-table th.name,
#eventedit .edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#eventedit .edit-attendees-table td.name select {
width: 100%;
}
#eventedit .edit-attendees-table td.name .attendee-name {
display: block;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 7px 6px;
margin: -5px -7px -6px;
}
#eventedit .edit-attendees-table a.deletelink {
display: inline-block;
width: 17px;
height: 17px;
padding: 0;
overflow: hidden;
text-indent: 1000px;
}
#eventedit .edit-attendees-table a.expandlink {
position: absolute;
top: 4px;
right: 6px;
width: 16px;
height: 16px;
}
#edit-attendees-form,
#edit-resources-form {
position: relative;
margin-top: 15px;
}
#edit-attendees-form .attendees-invitebox {
text-align: right;
margin: 0;
}
#edit-attendees-form .attendees-invitebox label {
padding-right: 3px;
}
#edit-resources-form #edit-resource-find {
position: absolute;
top: 0;
right: 0;
}
#edit-attendees-form #edit-attendee-schedule {
position: absolute;
right: 0;
top: 0;
}
.edit-attendees-table select.edit-attendee-role {
border: 0;
padding: 2px;
background: white;
width: 100%;
}
.availability img.availabilityicon {
margin: 1px;
width: 14px;
height: 14px;
border-radius: 4px;
-moz-border-radius: 4px;
}
.availability img.availabilityicon.loading {
background: url(images/loading_blue.gif) center no-repeat;
}
#schedule-freebusy-times td.unknown,
.availability img.availabilityicon.unknown {
background: #ddd;
}
#schedule-freebusy-times td.free,
.availability img.availabilityicon.free {
background: #abd640;
}
#schedule-freebusy-times td.busy,
.availability img.availabilityicon.busy {
background: #e26569;
}
#schedule-freebusy-times td.tentative,
.availability img.availabilityicon.tentative {
background: #8383fc;
}
#schedule-freebusy-times td.out-of-office,
.availability img.availabilityicon.out-of-office {
background: #fbaa68;
}
#schedule-freebusy-times td.all-busy,
#schedule-freebusy-times td.all-tentative,
#schedule-freebusy-times td.all-out-of-office {
background-image: url(images/freebusy-colors.png);
background-position: top right;
background-repeat: no-repeat;
}
#schedule-freebusy-times td.all-tentative {
background-position: right -40px;
}
#schedule-freebusy-times td.all-out-of-office {
background-position: right -80px;
}
#edit-attendees-legend {
margin-top: 3em;
margin-bottom: 0.5em;
}
#edit-attendees-legend .legend {
margin-right: 2em;
white-space: nowrap;
}
#edit-attendees-legend img.availabilityicon {
vertical-align: middle;
}
.edit-attendees-table tbody td.confirmstate {
overflow: hidden;
white-space: nowrap;
text-indent: -2000%;
}
.edit-attendees-table td.confirmstate span {
display: block;
width: 20px;
background: url(images/attendee-status.png) 5px 0 no-repeat;
}
.edit-attendees-table td.confirmstate span.needs-action {
}
.edit-attendees-table td.confirmstate span.accepted {
background-position: 5px -20px;
}
.edit-attendees-table td.confirmstate span.declined {
background-position: 5px -40px;
}
.edit-attendees-table td.confirmstate span.tentative {
background-position: 5px -60px;
}
.edit-attendees-table td.confirmstate span.delegated {
background-position: 5px -180px;
}
#attendees-freebusy-table {
width: 100%;
table-layout: fixed;
border: 1px solid #bbd3da;
}
#attendees-freebusy-table td.attendees {
width: 18em;
vertical-align: top;
overflow: hidden;
}
#attendees-freebusy-table td.times {
width: auto;
vertical-align: top;
}
#attendees-freebusy-table div.scroll {
position: relative;
overflow: auto;
}
#attendees-freebusy-table h3.boxtitle {
margin: 0;
border-color: #ccc;
}
.attendees-list .attendee {
padding: 4px 4px 4px 1px;
background: url(images/attendee-status.png) 2px -97px no-repeat;
white-space: nowrap;
}
.attendees-list a.attendee-role-toggle {
display: inline-block;
width: 16px;
margin-right: 3px;
cursor: pointer;
}
.attendees-list div.attendee {
border-top: 1px solid #ccc;
}
.attendees-list span.attendee {
padding-left: 20px;
margin-right: 2em;
}
.attendees-list .organizer {
background-position: 3px -77px;
}
.attendees-list .opt-participant {
background-position: 2px -117px;
}
.attendees-list .non-participant {
background-position: 2px -137px;
}
.attendees-list .chair {
background-position: 2px -157px;
}
.attendees-list .loading {
background: url(images/loading_blue.gif) 1px 50% no-repeat;
}
.attendees-list .total {
background: none;
padding-left: 4px;
font-weight: bold;
}
.attendees-list .spacer,
#schedule-freebusy-times tr.spacer td {
background: 0;
font-size: 50%;
}
#schedule-freebusy-times {
border-collapse: collapse;
width: 100%;
}
#schedule-freebusy-times td {
padding: 4px;
border: 1px solid #ccc;
}
#attendees-freebusy-table div.timesheader,
#schedule-freebusy-times tr.times td {
min-width: 30px;
font-size: 9px;
padding: 5px 2px 6px 2px;
text-align: center;
color: #004658;
}
#schedule-freebusy-times tr.times td.allday {
min-width: 60px;
}
#schedule-freebusy-times tr.times td {
cursor: pointer;
}
#schedule-event-time {
position: absolute;
border: 2px solid #333;
background: #777;
background: rgba(60, 60, 60, 0.6);
opacity: 0.5;
border-radius: 4px;
cursor: move;
filter: alpha(opacity=40); /* IE8 */
}
#eventfreebusy .schedule-options {
position: relative;
margin-bottom: 1.5em;
}
#eventfreebusy .schedule-buttons {
position: absolute;
top: 0.5em;
right: 0;
margin-right: 0;
}
#eventfreebusy .schedule-find-buttons {
padding-bottom:0.5em;
}
#eventfreebusy .schedule-find-buttons button {
min-width: 9em;
text-align: center;
}
#eventedit .attendees-commentbox label {
display: block;
}
#eventedit .ui-tabs-panel {
min-height: 24em;
}
#rcmKSearchpane ul li.resource i.icon,
#rcmKSearchpane ul li.collection i.icon {
background-image: url(images/autocomplete.png);
background-position: -1px -2px;
}
#rcmKSearchpane ul li.collection i.icon {
background-position: -1px -26px;
}
a.dropdown-link {
font-size: 11px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
font-size: 10px;
color: #666;
}
.ui-dialog-buttonset a.dropdown-link {
position: relative;
top: 2px;
margin: 0 1em;
color: #333;
}
#calendarsidebar .ui-datepicker-calendar {
table-layout: fixed;
}
.ui-datepicker-calendar .ui-datepicker-week-col {
border: 0;
color: #999;
font-size: 90%;
text-align: right;
padding-right: 6px;
width: 20px;
overflow: hidden;
}
.ui-autocomplete {
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
.ui-autocomplete .ui-menu-item {
white-space: nowrap;
}
* html .ui-autocomplete {
height: 160px;
}
.calendarmain span.spacer {
padding-left: 3em;
}
#agendaoptions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: auto;
z-index: 10;
padding: 4px 5px;
border: 1px solid #c3c3c3;
border-top-color: #ddd;
border-bottom-color: #bbb;
border-radius: 0 0 4px 4px;
background: #ebebeb;
background: -moz-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ebebeb), color-stop(100%,#c6c6c6));
background: -o-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -ms-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
}
#agendaoptions label {
text-shadow: 1px 1px #fff;
padding-right: 0.5em;
}
#calendar-kolabform {
position: relative;
margin: 0 -8px;
min-width: 660px;
min-height: 400px;
}
#calendar-kolabform table td.title {
font-weight: bold;
white-space: nowrap;
color: #666;
padding-right: 10px;
}
#resource-selection {
position: absolute;
top: 0;
left: 8px;
right: 0;
bottom: 0;
}
#resource-selection .scroller {
top: 34px;
}
#resource-dialog-left {
position: absolute;
top: 10px;
left: 0;
width: 380px;
bottom: 10px;
}
#resource-dialog-right {
position: absolute;
top: 10px;
left: 392px;
right: 8px;
bottom: 10px;
}
#resource-info {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48%;
}
#resource-info table {
margin: 8px;
width: 97%;
}
#resource-info thead td {
background: none;
font-weight: bold;
font-size: 14px;
}
#resource-availability {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 49%;
}
#resource-freebusy-calendar {
position: absolute;
top: 33px;
left: -1px;
right: -1px;
bottom: -1px;
}
#resource-freebusy-calendar .fc-content {
top: 0;
}
#resource-freebusy-calendar .fc-content .fc-event-bg {
background: 0;
}
#resource-freebusy-calendar .fc-event.status-busy,
#resource-freebusy-calendar .status-busy .fc-event-skin {
border-color: #e26569;
background-color: #e26569;
}
#resource-freebusy-calendar .fc-event.status-tentative,
#resource-freebusy-calendar .status-tentative .fc-event-skin {
border-color: #8383fc;
background: #8383fc;
}
#resource-freebusy-calendar .fc-event.status-outofoffice,
#resource-freebusy-calendar .status-outofoffice .fc-event-skin {
border-color: #fbaa68;
background: #fbaa68;
}
#resourcequicksearch {
padding: 4px;
background: #c7e3ef;
}
#resourcesearchbox {
width: 100%;
height: 26px;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#resourcequicksearch .iconbutton.searchoptions {
position: absolute;
top: 5px;
left: 6px;
width: 16px;
}
.searchbox .iconbutton.reset {
position: absolute;
top: 4px;
right: 1px;
}
/* fullcalendar style overrides */
.rcube-fc-content {
overflow: hidden;
border: 0;
border-radius: 4px;
box-shadow: 0 0 2px #999;
-o-box-shadow: 0 0 2px #999;
-webkit-box-shadow: 0 0 2px #999;
-moz-box-shadow: 0 0 2px #999;
}
.calendarmain .fc-content {
position: absolute !important;
top: 40px;
left: 0;
right: 0;
bottom: 0;
background: #fff;
}
.calendarmain.quickview-active .fc-content {
background-image: url('images/focusview.png');
background-position: center;
background-repeat: no-repeat;
}
#fish-eye-view .fc-content {
top: 2px;
bottom: 2px;
}
#quickview-calendar {
padding: 8px;
overflow: hidden;
}
.calendarmain .fc-button,
.calendarmain .fc-button.fc-state-default,
.calendarmain .fc-button.fc-state-hover {
background-color: #f5f5f5;
background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
background-position: 0 0;
}
.calendarmain #calendar .fc-button,
.calendarmain #calendar .fc-button.fc-state-default,
.calendarmain #calendar .fc-button.fc-state-hover {
margin: 0 0 0 0;
height: 20px;
line-height: 20px;
color: #505050;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #e6e6e6;
background: #d8d8d8;
background: -moz-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d8d8d8), color-stop(100%,#bababa));
background: -o-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -ms-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: linear-gradient(top, #d8d8d8 0%, #bababa 100%);
box-shadow: 0 1px 1px 0 #999;
-o-box-shadow: 0 1px 1px 0 #999;
-webkit-box-shadow: 0 1px 1px 0 #999;
-moz-box-shadow: 0 1px 1px 0 #999;
text-decoration: none;
}
.calendarmain #calendar .fc-button.fc-state-disabled {
color: #999;
background: #d8d8d8;
}
.calendarmain .fc-button.fc-state-active,
.calendarmain .fc-button.fc-state-down,
.calendarmain #calendar .fc-button.fc-state-active,
.calendarmain #calendar .fc-button.fc-state-down {
color: #333;
background: #bababa;
background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
}
.calendarmain #calendar .fc-header .fc-button {
margin-left: -1px;
margin-right: 0;
}
.calendarmain #calendar .fc-header-left .fc-button {
display: inline-block;
margin: 0;
text-align: center;
font-size: 10px;
color: #555;
min-width: 50px;
max-width: 75px;
height: 13px;
line-height: 1em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: -7px 0 0 0;
padding: 28px 2px 0 2px;
text-shadow: 0px 1px 1px #EEE;
border: 0;
background: url(images/toolbar.png) center 100px no-repeat;
box-shadow: none;
-o-box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
outline: none;
}
.calendarmain #calendar .fc-header-left .fc-button:focus {
color: #fff;
text-shadow: 0px 1px 1px #666;
background-color: rgba(30,150,192, 0.5);
border-radius: 3px;
}
.calendarmain #calendar .fc-header-left .fc-button.fc-state-active {
font-weight: bold;
color: #222;
text-shadow: none;
background-color: transparent;
}
.calendarmain #calendar .fc-header-left .fc-button-agendaDay {
background-position: center -120px;
}
.calendarmain #calendar .fc-header-left .fc-button-agendaDay.fc-state-active {
background-position: center -160px;
}
.calendarmain #calendar .fc-header-left .fc-button-agendaWeek {
background-position: center -200px;
}
.calendarmain #calendar .fc-header-left .fc-button-agendaWeek.fc-state-active {
background-position: center -240px;
}
.calendarmain #calendar .fc-header-left .fc-button-month {
background-position: center -280px;
}
.calendarmain #calendar .fc-header-left .fc-button-month.fc-state-active {
background-position: center -320px;
}
.calendarmain #calendar .fc-header-left .fc-button-table {
background-position: center -360px;
}
.calendarmain #calendar .fc-header-left .fc-button-table.fc-state-active {
background-position: center -400px;
}
.calendarmain #calendar .fc-header-right {
padding-right: 252px;
padding-top: 4px;
}
.calendarmain #calendar .fc-header-title {
padding-top: 5px;
}
.fc-event {
font-size: 1em !important;
}
.fc-event-hori.fc-type-freebusy,
.fc-event-vert.fc-type-freebusy {
opacity: 0.60;
/*
color: #fff !important;
background: rgba(80,80,80,0.85) !important;
background: -moz-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.9) 100%) !important;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(80,80,80,0.85)), color-stop(100%,rgba(48,48,48,0.9))) !important;
background: -webkit-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
background: -o-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
background: -ms-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
background: linear-gradient(to bottom, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
border-color: #444 !important;
cursor: default !important;
*/
-moz-box-shadow: inset 0px 1px 0 0px #888;
-webkit-box-shadow: inset 0px 1px 0 0px #888;
-o-box-shadow: inset 0px 1px 0 0px #888;
box-shadow: inset 0px 1px 0 0px #888;
}
.fc-event-row.fc-type-freebusy td {
color: #999;
}
.fc-event-hori.fc-type-freebusy .fc-event-skin,
.fc-event-hori.fc-type-freebusy .fc-event-inner,
.fc-event-vert.fc-type-freebusy .fc-event-skin,
.fc-event-vert.fc-type-freebusy .fc-event-inner {
/*
background-color: transparent !important;
border-color: #444 !important;
color: #fff !important;
text-shadow: 0 1px 1px #000;
*/
}
.fc-event-hori.fc-type-freebusy .fc-event-title,
.fc-event-vert.fc-type-freebusy .fc-event-title {
position: absolute;
top: -5000px;
}
.fc-event-vert.fc-invitation-needs-action,
.fc-event-hori.fc-invitation-needs-action {
border: 1px dashed #5757c7 !important;
}
.fc-event-vert.fc-invitation-tentative,
.fc-event-hori.fc-invitation-tentative {
border: 1px dashed #eb8900 !important;
}
.fc-event-vert.fc-invitation-declined,
.fc-event-hori.fc-invitation-declined {
border: 1px dashed #c00 !important;
}
.fc-event-vert.fc-event-ns-other.fc-invitation-declined,
.fc-event-hori.fc-event-ns-other.fc-invitation-declined {
opacity: 0.7;
}
.fc-event-ns-other.fc-invitation-declined .fc-event-title {
text-decoration: line-through;
}
.fc-event-vert.fc-invitation-tentative .fc-event-head,
.fc-event-vert.fc-invitation-declined .fc-event-head,
.fc-event-vert.fc-invitation-needs-action .fc-event-head {
/* background-color: transparent !important; */
}
.fc-event-vert.fc-invitation-tentative .fc-event-bg {
background: url(data:image/gif;base64,R0lGODlhCAAIAPABAOuJAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
}
.fc-event-vert.fc-invitation-needs-action .fc-event-bg {
background: url(data:image/gif;base64,R0lGODlhCAAIAPABAFdXx////yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
}
.fc-event-vert.fc-invitation-declined .fc-event-bg {
background: url(data:image/gif;base64,R0lGODlhCAAIAPABAMwAAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
}
.fc-view-table tr.fc-invitation-tentative td,
.fc-view-table tr.fc-invitation-declined td,
.fc-view-table tr.fc-invitation-needs-action td {
color: #888;
}
.fc-view-table tr.fc-invitation-tentative td.fc-event-title,
.fc-view-table tr.fc-invitation-declined td.fc-event-title,
.fc-view-table tr.fc-invitation-needs-action td.fc-event-title {
font-weight: normal;
}
#quickview-calendar .fc-view-table tr.fc-invitation-tentative td,
#quickview-calendar .fc-view-table tr.fc-invitation-declined td,
#quickview-calendar .fc-view-table tr.fc-invitation-needs-action td {
color: #333;
}
.calendarmain .fc-event:focus {
outline: 1px solid rgba(71,135,177, 0.4);
-webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
-moz-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
-o-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
}
.fc-event-title {
font-weight: bold;
}
.cal-event-status-cancelled .fc-event-title {
text-decoration: line-through;
}
.fc-event-hori .fc-event-title {
font-weight: normal;
white-space: nowrap;
}
.fc-event-hori .fc-event-time {
white-space: nowrap;
font-weight: normal !important;
font-size: 10px;
padding-right: 0.6em;
}
.fc-grid .fc-event-time {
font-weight: normal !important;
padding-right: 0.3em;
}
.calendarmain .fc-event-vert .fc-event-inner {
z-index: 0;
}
.fc-event-cateories {
font-style:italic;
}
div.fc-event-location {
font-size: 90%;
}
.fc-more-link {
color: #999;
padding-top: 1px;
cursor: pointer;
}
.fc-agenda-slots td div {
height: 22px;
}
.fc-sat, .fc-sun {
background-color: rgba(198,198,198, 0.08);
}
.calendarmain .fc-state-highlight {
background-color: rgba(233,198,14, 0.12);
}
.fc-widget-header {
background-color: #d6eaf3;
color: #004458;
text-shadow: 0px 1px 1px #fff;
}
.fc-view thead th.fc-widget-header {
padding: 8px 0;
color: #69939e;
}
.fc-day-number {
color: #578da5;
}
.fc-icon-alarms,
.fc-icon-sensitive,
.fc-icon-recurring {
display: inline-block;
width: 11px;
height: 11px;
background: url(images/eventicons.png) 0 0 no-repeat;
margin-left: 3px;
line-height: 10px;
}
.fc-icon-alarms {
background-position: 0 -13px;
}
.fc-icon-sensitive {
background-position: 0 -25px;
}
.fc-list-section .fc-event {
cursor: pointer;
}
.calendarmain .fc-view-table td.fc-list-header {
color: #004458;
font-size: 12px;
}
.calendarmain .fc-view-table tr.fc-event td {
border-color: #ddd;
padding: 4px 7px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.calendarmain .fc-view-table tr.fc-event td.fc-event-handle {
padding: 5px 0 2px 7px;
width: 12px;
}
.calendarmain .fc-view-table .fc-event-handle .fc-event-skin {
margin: 0;
padding: 0;
display: inline-block;
width: 10px;
height: 10px;
font-size: 6px;
border-radius: 8px;
}
.calendarmain .fc-view-table .fc-event-handle .fc-event-inner {
display: inline-block;
width: 10px;
height: 10px;
padding: 0;
margin: -1px;
font-size: 10px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.4);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
.calendarmain .fc-view-table col.fc-event-location {
width: 25%;
}
.fc-view-table table.fc-list-smart {
/* table-layout: auto; */
}
.fc-listappend {
text-align: center;
margin: 1em 0;
}
.fc-listappend .message {
padding: 0.5em;
margin-bottom: 0.5em;
font-size: 150%;
color: #999;
}
.fc-listappend .formlinks a {
font-size: 12px;
padding: 0 0.3em;
}
.fc-event-temp {
opacity: 0.4;
filter: alpha(opacity=40); /* IE8 */
}
/* Settings section */
fieldset #calendarcategories div {
margin-bottom: 0.3em;
}
/* Invitation UI in mail */
.messagelist tbody .attachment span.ical {
display: inline-block;
vertical-align: middle;
height: 18px;
width: 20px;
padding: 0;
background: url(images/ical-attachment.png) 2px 1px no-repeat;
}
ul.toolbarmenu li a.calendarlink span.calendar,
#attachmentmenu li a.calendarlink span.calendar {
background-position: 0px -2197px;
}
div.calendar-invitebox {
min-height: 20px;
margin: 5px 8px;
padding: 3px 6px 6px 34px;
border: 1px solid #ffdf0e;
background: url(images/calendar.png) 6px 5px no-repeat #fef893;
}
div.calendar-invitebox td.ititle {
font-weight: bold;
padding-right: 0.5em;
}
div.calendar-invitebox td {
padding: 2px;
}
div.calendar-invitebox td.label {
color: #666;
padding-right: 1em;
}
div.calendar-invitebox td.sensitivity {
color: #d31400;
font-weight: bold;
}
div.calendar-invitebox td.recurrence-id {
text-transform: uppercase;
font-style: italic;
}
div.calendar-invitebox td em {
font-weight: bold;
}
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .itip-buttons div {
margin-top: 0.5em;
}
#event-rsvp input.button,
div.calendar-invitebox input.button {
font-weight: bold;
margin-right: 0.5em;
}
div.calendar-invitebox input.button.preview {
margin-left: 1em;
margin-right: 0;
}
div.calendar-invitebox .folder-select {
font-weight: 10px;
margin-left: 1em;
white-space: nowrap;
}
div.calendar-invitebox .rsvp-status {
padding-left: 2px;
}
div.calendar-invitebox .rsvp-status.loading {
color: #666;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
div.calendar-invitebox .rsvp-status.hint {
color: #666;
text-shadow: none;
font-style: italic;
}
#event-partstat .changersvp,
div.calendar-invitebox .rsvp-status.declined,
div.calendar-invitebox .rsvp-status.tentative,
div.calendar-invitebox .rsvp-status.accepted,
div.calendar-invitebox .rsvp-status.delegated,
div.calendar-invitebox .rsvp-status.needs-action {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
#event-partstat .changersvp.declined,
div.calendar-invitebox .rsvp-status.declined {
background-position: 2px -40px;
}
#event-partstat .changersvp.tentative,
div.calendar-invitebox .rsvp-status.tentative {
background-position: 2px -60px;
}
#event-partstat .changersvp.delegated,
div.calendar-invitebox .rsvp-status.delegated {
background-position: 2px -180px;
}
#event-partstat .changersvp.needs-action,
div.calendar-invitebox .rsvp-status.needs-action {
background-position: 2px 0;
}
div.calendar-invitebox .calendar-agenda-preview {
display: none;
border-top: 1px solid #dfdfdf;
margin-top: 1em;
padding-top: 0.6em;
}
div.calendar-invitebox .calendar-agenda-preview h3.preview-title {
margin: 0 0 0.5em 0;
font-size: 12px;
color: #333;
}
div.calendar-invitebox .calendar-agenda-preview .event-row {
color: #777;
padding: 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.calendar-invitebox .calendar-agenda-preview .event-row.current {
color: #333;
font-weight: bold;
}
div.calendar-invitebox .calendar-agenda-preview .event-row.no-event {
font-style: italic;
}
div.calendar-invitebox .calendar-agenda-preview .event-date {
display: inline-block;
min-width: 8em;
margin-right: 1em;
white-space: nowrap;
}
/* iTIP attend reply page */
.calendaritipattend .centerbox {
width: 40em;
min-height: 7em;
margin: 80px auto 0 auto;
padding: 10px 10px 10px 90px;
background: url(images/invitation.png) 10px 10px no-repeat #fff;
}
.calendaritipattend #message {
width: 46em;
margin: 0 auto;
padding: 10px;
}
.calendaritipattend .calendar-invitebox {
background: none;
padding-left: 0;
border: 0;
margin: 0 0 2em 0;
}
.calendaritipattend .calendar-invitebox .rsvp-status {
margin-top: 2.5em;
font-size: 110%;
font-weight: bold;
}
.calendaritipattend .calendar-invitebox td.title,
.calendaritipattend .calendar-invitebox td.ititle {
font-size: 120%;
}
.calendaritipattend .itip-reply-controls .noreply-toggle,
.calendaritipattend .itip-reply-controls #noreply-event-rsvp {
display: none;
}
.calendaritipattend .itip-reply-controls a.reply-comment-toggle {
margin-left: 2px;
}
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 625483b7..5041dd38 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -1,699 +1,707 @@
<?php
/**
* Kolab format model class wrapping libkolabxml bindings
*
* Abstract base class for different Kolab groupware objects read from/written
* to the new Kolab 3 format using the PHP bindings of libkolabxml.
*
* @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/>.
*/
abstract class kolab_format
{
public static $timezone;
public /*abstract*/ $CTYPE;
public /*abstract*/ $CTYPEv2;
protected /*abstract*/ $objclass;
protected /*abstract*/ $read_func;
protected /*abstract*/ $write_func;
protected $obj;
protected $data;
protected $xmldata;
protected $xmlobject;
protected $formaterror;
protected $loaded = false;
protected $version = '3.0';
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
const PRODUCT_ID = 'Roundcube-libkolab-1.1';
// mapping table for valid PHP timezones not supported by libkolabxml
// basically the entire list of ftp://ftp.iana.org/tz/data/backward
protected static $timezone_map = array(
'Africa/Asmera' => 'Africa/Asmara',
'Africa/Timbuktu' => 'Africa/Abidjan',
'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
'America/Atka' => 'America/Adak',
'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
'America/Catamarca' => 'America/Argentina/Catamarca',
'America/Coral_Harbour' => 'America/Atikokan',
'America/Cordoba' => 'America/Argentina/Cordoba',
'America/Ensenada' => 'America/Tijuana',
'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
'America/Indianapolis' => 'America/Indiana/Indianapolis',
'America/Jujuy' => 'America/Argentina/Jujuy',
'America/Knox_IN' => 'America/Indiana/Knox',
'America/Louisville' => 'America/Kentucky/Louisville',
'America/Mendoza' => 'America/Argentina/Mendoza',
'America/Porto_Acre' => 'America/Rio_Branco',
'America/Rosario' => 'America/Argentina/Cordoba',
'America/Virgin' => 'America/Port_of_Spain',
'Asia/Ashkhabad' => 'Asia/Ashgabat',
'Asia/Calcutta' => 'Asia/Kolkata',
'Asia/Chungking' => 'Asia/Shanghai',
'Asia/Dacca' => 'Asia/Dhaka',
'Asia/Katmandu' => 'Asia/Kathmandu',
'Asia/Macao' => 'Asia/Macau',
'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
'Asia/Tel_Aviv' => 'Asia/Jerusalem',
'Asia/Thimbu' => 'Asia/Thimphu',
'Asia/Ujung_Pandang' => 'Asia/Makassar',
'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
'Atlantic/Faeroe' => 'Atlantic/Faroe',
'Atlantic/Jan_Mayen' => 'Europe/Oslo',
'Australia/ACT' => 'Australia/Sydney',
'Australia/Canberra' => 'Australia/Sydney',
'Australia/LHI' => 'Australia/Lord_Howe',
'Australia/NSW' => 'Australia/Sydney',
'Australia/North' => 'Australia/Darwin',
'Australia/Queensland' => 'Australia/Brisbane',
'Australia/South' => 'Australia/Adelaide',
'Australia/Tasmania' => 'Australia/Hobart',
'Australia/Victoria' => 'Australia/Melbourne',
'Australia/West' => 'Australia/Perth',
'Australia/Yancowinna' => 'Australia/Broken_Hill',
'Brazil/Acre' => 'America/Rio_Branco',
'Brazil/DeNoronha' => 'America/Noronha',
'Brazil/East' => 'America/Sao_Paulo',
'Brazil/West' => 'America/Manaus',
'Canada/Atlantic' => 'America/Halifax',
'Canada/Central' => 'America/Winnipeg',
'Canada/East-Saskatchewan' => 'America/Regina',
'Canada/Eastern' => 'America/Toronto',
'Canada/Mountain' => 'America/Edmonton',
'Canada/Newfoundland' => 'America/St_Johns',
'Canada/Pacific' => 'America/Vancouver',
'Canada/Saskatchewan' => 'America/Regina',
'Canada/Yukon' => 'America/Whitehorse',
'Chile/Continental' => 'America/Santiago',
'Chile/EasterIsland' => 'Pacific/Easter',
'Cuba' => 'America/Havana',
'Egypt' => 'Africa/Cairo',
'Eire' => 'Europe/Dublin',
'Europe/Belfast' => 'Europe/London',
'Europe/Tiraspol' => 'Europe/Chisinau',
'GB' => 'Europe/London',
'GB-Eire' => 'Europe/London',
'Greenwich' => 'Etc/GMT',
'Hongkong' => 'Asia/Hong_Kong',
'Iceland' => 'Atlantic/Reykjavik',
'Iran' => 'Asia/Tehran',
'Israel' => 'Asia/Jerusalem',
'Jamaica' => 'America/Jamaica',
'Japan' => 'Asia/Tokyo',
'Kwajalein' => 'Pacific/Kwajalein',
'Libya' => 'Africa/Tripoli',
'Mexico/BajaNorte' => 'America/Tijuana',
'Mexico/BajaSur' => 'America/Mazatlan',
'Mexico/General' => 'America/Mexico_City',
'NZ' => 'Pacific/Auckland',
'NZ-CHAT' => 'Pacific/Chatham',
'Navajo' => 'America/Denver',
'PRC' => 'Asia/Shanghai',
'Pacific/Ponape' => 'Pacific/Pohnpei',
'Pacific/Samoa' => 'Pacific/Pago_Pago',
'Pacific/Truk' => 'Pacific/Chuuk',
'Pacific/Yap' => 'Pacific/Chuuk',
'Poland' => 'Europe/Warsaw',
'Portugal' => 'Europe/Lisbon',
'ROC' => 'Asia/Taipei',
'ROK' => 'Asia/Seoul',
'Singapore' => 'Asia/Singapore',
'Turkey' => 'Europe/Istanbul',
'UCT' => 'Etc/UCT',
'US/Alaska' => 'America/Anchorage',
'US/Aleutian' => 'America/Adak',
'US/Arizona' => 'America/Phoenix',
'US/Central' => 'America/Chicago',
'US/East-Indiana' => 'America/Indiana/Indianapolis',
'US/Eastern' => 'America/New_York',
'US/Hawaii' => 'Pacific/Honolulu',
'US/Indiana-Starke' => 'America/Indiana/Knox',
'US/Michigan' => 'America/Detroit',
'US/Mountain' => 'America/Denver',
'US/Pacific' => 'America/Los_Angeles',
'US/Samoa' => 'Pacific/Pago_Pago',
'Universal' => 'Etc/UTC',
'W-SU' => 'Europe/Moscow',
'Zulu' => 'Etc/UTC',
);
/**
* Factory method to instantiate a kolab_format object of the given type and version
*
* @param string Object type to instantiate
* @param float Format version
* @param string Cached xml data to initialize with
* @return object kolab_format
*/
public static function factory($type, $version = '3.0', $xmldata = null)
{
if (!isset(self::$timezone))
self::$timezone = new DateTimeZone('UTC');
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
$type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
return new $classname($xmldata, $version);
return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
}
/**
* Determine support for the given format version
*
* @param float Format version to check
* @return boolean True if supported, False otherwise
*/
public static function supports($version)
{
if ($version == '2.0')
return class_exists('kolabobject');
// default is version 3
return class_exists('kolabformat');
}
/**
* Convert the given date/time value into a cDateTime object
*
* @param mixed Date/Time value either as unix timestamp, date string or PHP DateTime object
* @param DateTimeZone The timezone the date/time is in. Use global default if Null, local time if False
* @param boolean True of the given date has no time component
* @return object The libkolabxml date/time object
*/
public static function get_datetime($datetime, $tz = null, $dateonly = false)
{
// use timezone information from datetime of global setting
if (!$tz && $tz !== false) {
if ($datetime instanceof DateTime)
$tz = $datetime->getTimezone();
if (!$tz)
$tz = self::$timezone;
}
$result = new cDateTime();
try {
// got a unix timestamp (in UTC)
if (is_numeric($datetime)) {
$datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
if ($tz) $datetime->setTimezone($tz);
}
else if (is_string($datetime) && strlen($datetime)) {
$datetime = $tz ? new DateTime($datetime, $tz) : new DateTime($datetime);
}
}
catch (Exception $e) {}
if ($datetime instanceof DateTime) {
$result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
if (!$dateonly)
$result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z'))) {
$result->setUTC(true);
}
else if ($tz !== false) {
$tzid = $tz->getName();
if (array_key_exists($tzid, self::$timezone_map))
$tzid = self::$timezone_map[$tzid];
$result->setTimezone($tzid);
}
}
return $result;
}
/**
* Convert the given cDateTime into a PHP DateTime object
*
* @param object cDateTime The libkolabxml datetime object
* @return object DateTime PHP datetime instance
*/
public static function php_datetime($cdt)
{
if (!is_object($cdt) || !$cdt->isValid())
return null;
$d = new DateTime;
$d->setTimezone(self::$timezone);
try {
if ($tzs = $cdt->timezone()) {
$tz = new DateTimeZone($tzs);
$d->setTimezone($tz);
}
else if ($cdt->isUTC()) {
$d->setTimezone(new DateTimeZone('UTC'));
}
}
catch (Exception $e) { }
$d->setDate($cdt->year(), $cdt->month(), $cdt->day());
if ($cdt->isDateOnly()) {
$d->_dateonly = true;
$d->setTime(12, 0, 0); // set time to noon to avoid timezone troubles
}
else {
$d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
}
return $d;
}
/**
* Convert a libkolabxml vector to a PHP array
*
* @param object vector Object
* @return array Indexed array containing vector elements
*/
public static function vector2array($vec, $max = PHP_INT_MAX)
{
$arr = array();
for ($i=0; $i < $vec->size() && $i < $max; $i++)
$arr[] = $vec->get($i);
return $arr;
}
/**
* Build a libkolabxml vector (string) from a PHP array
*
* @param array Array with vector elements
* @return object vectors
*/
public static function array2vector($arr)
{
$vec = new vectors;
foreach ((array)$arr as $val) {
if (strlen($val))
$vec->push($val);
}
return $vec;
}
/**
* Parse the X-Kolab-Type header from MIME messages and return the object type in short form
*
* @param string X-Kolab-Type header value
* @return string Kolab object type (contact,event,task,note,etc.)
*/
public static function mime2object_type($x_kolab_type)
{
return preg_replace(
array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
array( 'dictionary', 'distribution-list'),
substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
);
}
/**
* Default constructor of all kolab_format_* objects
*/
public function __construct($xmldata = null, $version = null)
{
$this->obj = new $this->objclass;
$this->xmldata = $xmldata;
if ($version)
$this->version = $version;
// use libkolab module if available
if (class_exists('kolabobject'))
$this->xmlobject = new XMLObject();
}
/**
* Check for format errors after calling kolabformat::write*()
*
* @return boolean True if there were errors, False if OK
*/
protected function format_errors()
{
$ret = $log = false;
switch (kolabformat::error()) {
case kolabformat::NoError:
$ret = false;
break;
case kolabformat::Warning:
$ret = false;
$uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
$log = "Warning @ $uid";
break;
default:
$ret = true;
$log = "Error";
}
if ($log && !isset($this->formaterror)) {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "kolabformat $log: " . kolabformat::errorMessage(),
), true);
$this->formaterror = $ret;
}
return $ret;
}
/**
* Save the last generated UID to the object properties.
* Should be called after kolabformat::writeXXXX();
*/
protected function update_uid()
{
// get generated UID
if (!$this->data['uid']) {
if ($this->xmlobject) {
$this->data['uid'] = $this->xmlobject->getSerializedUID();
}
if (empty($this->data['uid'])) {
$this->data['uid'] = kolabformat::getSerializedUID();
}
$this->obj->setUid($this->data['uid']);
}
}
/**
* Initialize libkolabxml object with cached xml data
*/
protected function init()
{
if (!$this->loaded) {
if ($this->xmldata) {
$this->load($this->xmldata);
$this->xmldata = null;
}
$this->loaded = true;
}
}
/**
* Get constant value for libkolab's version parameter
*
* @param float Version value to convert
* @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available
*/
protected function libversion($v = null)
{
if (class_exists('kolabobject')) {
$version = $v ?: $this->version;
if ($version <= '2.0')
return kolabobject::KolabV2;
else
return kolabobject::KolabV3;
}
return false;
}
/**
* Determine the correct libkolab(xml) wrapper function for the given call
* depending on the available PHP modules
*/
protected function libfunc($func)
{
if (is_array($func) || strpos($func, '::'))
return $func;
else if (class_exists('kolabobject'))
return array($this->xmlobject, $func);
else
return 'kolabformat::' . $func;
}
/**
* Direct getter for object properties
*/
public function __get($var)
{
return $this->data[$var];
}
/**
* Load Kolab object data from the given XML block
*
* @param string XML data
* @return boolean True on success, False on failure
*/
public function load($xml)
{
$this->formaterror = null;
$read_func = $this->libfunc($this->read_func);
if (is_array($read_func))
$r = call_user_func($read_func, $xml, $this->libversion());
else
$r = call_user_func($read_func, $xml, false);
if (is_resource($r))
$this->obj = new $this->objclass($r);
else if (is_a($r, $this->objclass))
$this->obj = $r;
$this->loaded = !$this->format_errors();
}
/**
* Write object data to XML format
*
* @param float Format version to write
* @return string XML data
*/
public function write($version = null)
{
$this->formaterror = null;
$this->init();
$write_func = $this->libfunc($this->write_func);
if (is_array($write_func))
$this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID);
else
$this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID);
if (!$this->format_errors())
$this->update_uid();
else
$this->xmldata = null;
return $this->xmldata;
}
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
$this->init();
if (!empty($object['uid']))
$this->obj->setUid($object['uid']);
// set some automatic values if missing
if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
$cdt = $this->obj->created();
$object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', new DateTimeZone('UTC'));
if (!$cdt || !$cdt->isValid())
$this->obj->setCreated(self::get_datetime($object['created']));
}
$object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
$this->obj->setLastModified(self::get_datetime($object['changed']));
// Save custom properties of the given object
if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
$vcustom = new vectorcs;
foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
$vcustom->push(new CustomProperty($cp[0], $cp[1]));
}
$this->obj->setCustomProperties($vcustom);
}
// load custom properties from XML for caching (#2238) if method exists (#3125)
else if (method_exists($this->obj, 'customProperties')) {
$object['x-custom'] = array();
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
}
/**
* Convert the Kolab object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Kolab object data as hash array
*/
public function to_array($data = array())
{
$this->init();
// read object properties into local data object
$object = array(
'uid' => $this->obj->uid(),
'changed' => self::php_datetime($this->obj->lastModified()),
);
// not all container support the created property
if (method_exists($this->obj, 'created')) {
$object['created'] = self::php_datetime($this->obj->created());
}
// read custom properties
if (method_exists($this->obj, 'customProperties')) {
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
// merge with additional data, e.g. attachments from the message
if ($data) {
foreach ($data as $idx => $value) {
if (is_array($value)) {
$object[$idx] = array_merge((array)$object[$idx], $value);
}
else {
$object[$idx] = $value;
}
}
}
return $object;
}
/**
* Object validation method to be implemented by derived classes
*/
abstract public function is_valid();
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
return array();
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
return array();
}
/**
* Utility function to extract object attachment data
*
* @param array Hash array reference to append attachment data into
*/
- public function get_attachments(&$object)
+ public function get_attachments(&$object, $all = false)
{
$this->init();
// handle attachments
$vattach = $this->obj->attachments();
for ($i=0; $i < $vattach->size(); $i++) {
$attach = $vattach->get($i);
// skip cid: attachments which are mime message parts handled by kolab_storage_folder
if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
$name = $attach->label();
$key = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
$content = $attach->data();
$object['_attachments'][$key] = array(
'id' => 'i:'.$i,
'name' => $name,
'mimetype' => $attach->mimetype(),
'size' => strlen($content),
'content' => $content,
);
}
+ else if ($all && substr($attach->uri(), 0, 4) == 'cid:') {
+ $key = $attach->uri();
+ $object['_attachments'][$key] = array(
+ 'id' => $key,
+ 'name' => $attach->label(),
+ 'mimetype' => $attach->mimetype(),
+ );
+ }
else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
$object['links'][] = $attach->uri();
}
}
}
/**
* Utility function to set attachment properties to the kolabformat object
*
* @param array Object data as hash array
* @param boolean True to always overwrite attachment information
*/
protected function set_attachments($object, $write = true)
{
// save attachments
$vattach = new vectorattachment;
foreach ((array) $object['_attachments'] as $cid => $attr) {
if (empty($attr))
continue;
$attach = new Attachment;
$attach->setLabel((string)$attr['name']);
$attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
if ($attach->isValid()) {
$vattach->push($attach);
$write = true;
}
else {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
), true);
}
}
foreach ((array) $object['links'] as $link) {
$attach = new Attachment;
$attach->setUri($link, 'unknown');
$vattach->push($attach);
$write = true;
}
if ($write) {
$this->obj->setAttachments($vattach);
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, Jul 8, 8:21 PM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1051937
Default Alt Text
(291 KB)

Event Timeline