diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 9c0a8929..5616b28b 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -1,2871 +1,2927 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * Copyright (C) 2014, Kolab Systems AG * * 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 . */ 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; private $ics_parts = array(); /** * Plugin initialization. */ function init() { $this->require_plugin('libcalendaring'); $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->register_task('calendar', 'calendar'); // load calendar configuration $this->load_config(); // load localizations $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print')); $this->timezone = $this->lib->timezone; $this->gmt_offset = $this->lib->gmt_offset; $this->dst_active = $this->lib->dst_active; $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active; require($this->home . '/lib/calendar_ui.php'); $this->ui = new calendar_ui($this); // catch iTIP confirmation requests that don're require a valid session if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) { $this->add_hook('startup', array($this, 'itip_attend_response')); } else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) { $this->add_hook('startup', array($this, 'ical_feed_export')); } else { // default startup routine $this->add_hook('startup', array($this, 'startup')); } $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('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('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','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) { $default_id = $this->rc->config->get('calendar_default_calendar'); $calendars = $this->driver->list_calendars(false, true); $calendar = $calendars[$default_id] ?: null; if (!$calendar || ($writeable && $calendar['readonly'])) { foreach ($calendars as $cal) { if ($cal['default']) { $calendar = $cal; break; } if (!$writeable || !$cal['readonly']) { $first = $cal; } } } return $calendar ?: $first; } /** * Render the main calendar view from skin template */ function calendar_view() { $this->rc->output->set_pagetitle($this->gettext('calendar')); // Add CSS stylesheets to the page header $this->ui->addCSS(); // Add JS files to the page header $this->ui->addJS(); $this->ui->init_templates(); $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','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 = get_input_value('view', RCUBE_INPUT_GPC); if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $this->rc->output->set_env('view', $view); if ($date = get_input_value('date', RCUBE_INPUT_GPC)) $this->rc->output->set_env('date', $date); if ($msgref = get_input_value('itip', RCUBE_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)) . ' — ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)), ); } if (!isset($no_override['calendar_event_coloring'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_coloring'; $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id)); $select_colors->add($this->gettext('coloringmode0'), 0); $select_colors->add($this->gettext('coloringmode1'), 1); $select_colors->add($this->gettext('coloringmode2'), 2); $select_colors->add($this->gettext('coloringmode3'), 3); $p['blocks']['view']['options']['eventcolors'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))), 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])), ); } // loading driver is expensive, don't do it if not needed $this->load_driver(); if (!isset($no_override['calendar_default_alarm_type'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_alarm'; $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); $select_type->add($this->gettext('none'), ''); foreach ($this->driver->alarm_types as $type) $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); $p['blocks']['view']['options']['alarmtype'] = array( 'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))), 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')), ); } if (!isset($no_override['calendar_default_alarm_offset'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_alarm'; $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); $preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); $p['blocks']['view']['options']['alarmoffset'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))), 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } if (!isset($no_override['calendar_default_calendar'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } // default calendar selection $field_id = 'rcmfd_default_calendar'; $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true)); foreach ((array)$this->driver->list_calendars(false, true) as $id => $prop) { $select_cal->add($prop['name'], strval($id)); if ($prop['default']) $default_calendar = $id; } $p['blocks']['view']['options']['defaultcalendar'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))), 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)), ); } $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) . ' ' . $category_color->show($color) . ' ' . $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('') . ' ' . $add_category->show(), ); $this->rc->output->add_script('function rcube_calendar_add_category(){ var name = $("#rcmfd_new_category").val(); if (name.length) { var input = $("").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name); var color = $("").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000"); var button = $("").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() }); $("
").append(input).append(" ").append(color).append(" ").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'])) . ' ' . rcube::Q($source['realname'] ?: $source['name'])); } $p['blocks']['birthdays']['options']['birthday_adressbooks'] = array( 'title' => rcube::Q($this->gettext('birthdayscalendarsources')), 'content' => join(html::br(), $sources), ); $field_id = 'rcmfd_birthdays_alarm'; $select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib); $select_type->add($this->gettext('none'), ''); foreach ($this->driver->alarm_types as $type) { $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); } $input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib); $select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib); foreach (array('-M','-H','-D') as $trigger) $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); $preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D')); $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array( 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))), 'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } return $p; } /** * Handler for preferences_save hook. * Executed on Calendar settings form submit. * * @param array Original parameters * @return array Modified parameters */ function preferences_save($p) { if ($p['section'] == 'calendar') { $this->load_driver(); // compose default alarm preset value $alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST); $default_alarm = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1]; $birthdays_alarm_offset = get_input_value('_birthdays_alarm_offset', RCUBE_INPUT_POST); $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval(get_input_value('_birthdays_alarm_value', RCUBE_INPUT_POST)) . $birthdays_alarm_offset[1]; $p['prefs'] = array( 'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST), 'calendar_timeslots' => intval(get_input_value('_timeslots', RCUBE_INPUT_POST)), 'calendar_first_day' => intval(get_input_value('_first_day', RCUBE_INPUT_POST)), 'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST)), 'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST)), 'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST)), 'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST)), 'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST), 'calendar_default_alarm_offset' => $default_alarm, 'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST), 'calendar_date_format' => null, // clear previously saved values 'calendar_time_format' => null, 'calendar_contact_birthdays' => get_input_value('_contact_birthdays', RCUBE_INPUT_POST) ? true : false, 'calendar_birthday_adressbooks' => (array)get_input_value('_birthday_adressbooks', RCUBE_INPUT_POST), 'calendar_birthdays_alarm_type' => get_input_value('_birthdays_alarm_type', RCUBE_INPUT_POST), 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null, 'calendar_itip_after_action' => intval(get_input_value('_after_action', RCUBE_INPUT_POST)), ); if ($p['prefs']['calendar_itip_after_action'] == 4) { $p['prefs']['calendar_itip_after_action'] = get_input_value('_after_action_folder', RCUBE_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) get_input_value('_categories', RCUBE_INPUT_POST); $colors = (array) get_input_value('_colors', RCUBE_INPUT_POST); foreach ($categories as $key => $name) { $color = preg_replace('/^#/', '', strval($colors[$key])); // rename categories in existing events -> driver's job if ($oldname = $old_categories[$key]) { $this->driver->replace_category($oldname, $name, $color); unset($old_categories[$key]); } else $this->driver->add_category($name, $color); $new_categories[$name] = $color; } // these old categories have been removed, alter events accordingly -> driver's job foreach ((array)$old_categories[$key] as $key => $name) { $this->driver->remove_category($name); } $p['prefs']['calendar_categories'] = $new_categories; } } return $p; } /** * Dispatcher for calendar actions initiated by the client */ function calendar_action() { $action = get_input_value('action', RCUBE_INPUT_GPC); $cal = get_input_value('c', RCUBE_INPUT_GPC); $success = $reload = false; if (isset($cal['showalarms'])) $cal['showalarms'] = intval($cal['showalarms']); switch ($action) { case "form-new": case "form-edit": echo $this->ui->calendar_editform($action, $cal); exit; case "new": $success = $this->driver->create_calendar($cal); $reload = true; break; case "edit": $success = $this->driver->edit_calendar($cal); $reload = true; break; case "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']); foreach ((array)$this->driver->search_calendars(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) 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, get_input_value('_reqid', RCUBE_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 = get_input_value('action', RCUBE_INPUT_GPC); $event = get_input_value('e', RCUBE_INPUT_POST, true); $success = $reload = $got_msg = false; // don't notify if modifying a recurring instance (really?) if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify']) unset($event['_notify']); // force notify if hidden + active else 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); 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']; $this->cleanup_event($event); } $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); $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; break; case "resize": $this->write_preprocess($event, $action); $success = $this->driver->resize_event($event); $reload = $event['_savemode'] ? 2 : 1; break; case "move": $this->write_preprocess($event, $action); $success = $this->driver->move_event($event); $reload = $success && $event['_savemode'] ? 2 : 1; break; case "remove": // remove previous deletes $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0; $this->rc->session->remove('calendar_event_undo'); // search for event if only UID is given if (!isset($event['calendar']) && $event['uid']) { if (!($event = $this->driver->get_event($event, true))) { break; } $undo_time = 0; } $success = $this->driver->remove_event($event, $undo_time < 1); $reload = (!$success || $event['_savemode']) ? 2 : 1; if ($undo_time > 0 && $success) { $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event); // display message with Undo link. $msg = html::span(null, $this->gettext('successremoval')) . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo')); $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time); $got_msg = true; } else if ($success) { $this->rc->output->show_message('calendar.successremoval', 'confirmation'); $got_msg = true; } // send iTIP reply that participant has declined the event if ($success && $event['decline']) { $emails = $this->get_user_emails(); foreach ($old['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $attendee; else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $old['attendees'][$i]['status'] = 'DECLINED'; $reply_sender = $attendee['email']; } } $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } break; case "undo": // Restore deleted event $event = $_SESSION['calendar_event_undo']['data']; if ($event) $success = $this->driver->restore_event($event); if ($success) { $this->rc->session->remove('calendar_event_undo'); $this->rc->output->show_message('calendar.successrestore', 'confirmation'); $got_msg = true; $reload = 2; } break; case "rsvp": $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); $status = get_input_value('status', RCUBE_INPUT_GPC); $reply_comment = $event['comment']; $ev = $this->driver->get_event($event); $ev['attendees'] = $event['attendees']; $event = $ev; if ($success = $this->driver->edit_rsvp($event, $status)) { $noreply = intval(get_input_value('noreply', RCUBE_INPUT_GPC)) || $status == 'needs-action' || $itip_sending === 0; $reload = $event['calendar'] != $ev['calendar'] ? 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; if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } } break; case "dismiss": $event['ids'] = explode(',', $event['id']); $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event); $success = $plugin['success']; foreach ($event['ids'] as $id) { if (strpos($id, 'cal:') === 0) $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']); } break; 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); $this->rc->output->command('display_message', $this->gettext('eventchangelognotavailable'), 'error'); } $got_msg = true; $reload = false; break; case "diff": $data = $this->driver->get_event_diff($event, $event['rev']); 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'] = $this->lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c'); $change['new'] = $this->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'); } // send out notifications if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) { // make sure we have the complete record $event = $action == 'remove' ? $old : $this->driver->get_event($event); // only notify if data really changed (TODO: do diff check on client already) if (!$old || $action == 'remove' || self::event_diff($event, $old)) { $sent = $this->notify_attendees($event, $old, $action, $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'); } } // 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); } } /** * Handler for load-requests from fullcalendar * This will return pure JSON formatted output */ function load_events() { $events = $this->driver->load_events( get_input_value('start', RCUBE_INPUT_GET), get_input_value('end', RCUBE_INPUT_GET), ($query = get_input_value('q', RCUBE_INPUT_GET)), get_input_value('source', RCUBE_INPUT_GET) ); echo $this->encode($events, !empty($query)); exit; } /** * 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); } 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; } foreach ($this->driver->list_calendars(true) as $cal) { $events = $this->driver->load_events( get_input_value('start', RCUBE_INPUT_GPC), get_input_value('end', RCUBE_INPUT_GPC), get_input_value('q', RCUBE_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))); } } } /** * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. * This will check for pending notifications and pass them to the client */ public function pending_alarms($p) { $this->load_driver(); $time = $p['time'] ?: time(); if ($alarms = $this->driver->pending_alarms($time)) { foreach ($alarms as $alarm) { $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal: $p['alarms'][] = $alarm; } } // get alarms for birthdays calendar if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') { $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db'); foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) { $alarm = libcalendaring::get_next_alarm($e); // overwrite alarm time with snooze value (or null if dismissed) if ($dismissed = $cache->get($e['id'])) $alarm['time'] = $dismissed['notifyat']; // add to list if alarm is set if ($alarm && $alarm['time'] && $alarm['time'] <= $time) { $e['id'] = 'cal:bday:' . $e['id']; $e['notifyat'] = $alarm['time']; $p['alarms'][] = $e; } } } return $p; } /** * Handler for alarm dismiss hook triggered by libcalendaring */ public function dismiss_alarms($p) { $this->load_driver(); foreach ((array)$p['ids'] as $id) { if (strpos($id, 'cal:bday:') === 0) { $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']); } else if (strpos($id, 'cal:') === 0) { $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']); } } return $p; } /** * Handler for check-recent requests which are accidentally sent to calendar taks */ function check_recent() { // NOP $this->rc->output->send(); } /** * Hook triggered when a contact is saved */ function contact_update($p) { // clear birthdays calendar cache if (!empty($p['record']['birthday'])) { $cache = $this->rc->get_cache('calendar.birthdays', 'db'); $cache->remove(); } } /** * */ function import_events() { // Upload progress update if (!empty($_GET['_progress'])) { rcube_upload_progress(); } @set_time_limit(0); // process uploaded file if there is no error $err = $_FILES['_data']['error']; if (!$err && $_FILES['_data']['tmp_name']) { $calendar = get_input_value('calendar', RCUBE_INPUT_GPC); $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0; // extract zip file if ($_FILES['_data']['type'] == 'application/zip') { $count = 0; if (class_exists('ZipArchive', false)) { $zip = new ZipArchive(); if ($zip->open($_FILES['_data']['tmp_name'])) { $randname = uniqid('zip-' . session_id(), true); $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname; mkdir($tmpdir, 0700); // extract each ical file from the archive and import it for ($i = 0; $i < $zip->numFiles; $i++) { $filename = $zip->getNameIndex($i); if (preg_match('/\.ics$/i', $filename)) { $tmpfile = $tmpdir . '/' . basename($filename); if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) { $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors); unlink($tmpfile); } } } rmdir($tmpdir); $zip->close(); } else { $errors = 1; $msg = 'Failed to open zip file.'; } } else { $errors = 1; $msg = 'Zip files are not supported for import.'; } } else { // attempt to import teh uploaded file directly $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors); } if ($count) { $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation'); $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true)); } else if (!$errors) { $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); $this->rc->output->command('plugin.import_success', array('source' => $calendar)); } else { $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : ''))); } } else { if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array( 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else { $msg = rcube_label('fileuploaderror'); } $this->rc->output->command('plugin.import_error', array('message' => $msg)); } $this->rc->output->send('iframe'); } /** * Helper function to parse and import a single .ics file */ private function import_from_file($filepath, $calendar, $rangestart, &$errors) { $user_email = $this->rc->user->get_username(); $ical = $this->get_ical(); $errors = !$ical->fopen($filepath); $count = $i = 0; foreach ($ical as $event) { // keep the browser connection alive on long import jobs if (++$i > 100 && $i % 100 == 0) { echo ""; ob_flush(); } // TODO: correctly handle recurring events which start before $rangestart if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) continue; $event['_owner'] = $user_email; $event['calendar'] = $calendar; if ($this->driver->new_event($event)) { $count++; } else { $errors++; } } return $count; } /** * Construct the ics file for exporting events to iCalendar format; */ function export_events($terminate = true) { $start = get_input_value('start', RCUBE_INPUT_GET); $end = get_input_value('end', RCUBE_INPUT_GET); if (!isset($start)) $start = 'today -1 year'; if (!is_numeric($start)) $start = strtotime($start . ' 00:00:00'); if (!$end) $end = 'today +10 years'; if (!is_numeric($end)) $end = strtotime($end . ' 23:59:59'); $event_id = get_input_value('id', RCUBE_INPUT_GET); $attachments = get_input_value('attachments', RCUBE_INPUT_GET); $calid = $filename = get_input_value('source', RCUBE_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))) { $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() { // process HTTP auth info if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host() $auth = $this->rc->plugins->exec_hook('authenticate', array( 'host' => $this->rc->autoselect_host(), 'user' => trim($_SERVER['PHP_AUTH_USER']), 'pass' => $_SERVER['PHP_AUTH_PW'], 'cookiecheck' => true, 'valid' => true, )); if ($auth['valid'] && !$auth['abort']) $this->rc->login($auth['user'], $auth['pass'], $auth['host']); } // require HTTP auth if (empty($_SESSION['user_id'])) { header('WWW-Authenticate: Basic realm="Roundcube Calendar"'); header('HTTP/1.0 401 Unauthorized'); exit; } // decode calendar feed hash $format = 'ics'; $calhash = get_input_value('_cal', RCUBE_INPUT_GET); if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) { $format = strtolower($m[1]); $calhash = preg_replace($suff_regex, '', $calhash); } if (!strpos($calhash, ':')) $calhash = base64_decode($calhash); list($user, $_GET['source']) = explode(':', $calhash, 2); // sanity check user if ($this->rc->user->get_username() == $user) { $this->load_driver(); $this->export_events(false); } else { header('HTTP/1.0 404 Not Found'); } // don't save session data session_destroy(); exit; } /** * */ function load_settings() { $this->lib->load_settings(); $this->defaults += $this->lib->defaults; $settings = array(); // configuration $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar'); $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']); $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']); $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']); $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']); $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']); $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']); $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']); $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_identities() as $rec) { if (!$identity) $identity = $rec; $identity['emails'][] = $rec['email']; $settings['identities'][$rec['identity_id']] = $rec['email']; } $identity['emails'][] = $this->rc->user->get_username(); $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails']))); } return $settings; } /** * Encode events as JSON * * @param array Events as array * @param boolean Add CSS class names according to calendar and categories * @return string JSON encoded events */ function encode($events, $addcss = false) { $json = array(); foreach ($events as $event) { $json[] = $this->_client_event($event, $addcss); } return json_encode($json); } /** * Convert an event object to be used on the client */ private function _client_event($event, $addcss = false) { // compose a human readable strings for alarms_text and recurrence_text if ($event['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']); } foreach ((array)$event['attachments'] as $k => $attachment) { $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); } // check for organizer in attendees list $organizer = null; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; break; } } if ($organizer === null && !empty($event['organizer'])) { $organizer = $event['organizer']; $organizer['role'] = 'ORGANIZER'; if (!is_array($event['attendees'])) $event['attendees'] = array(); array_unshift($event['attendees'], $organizer); } // mapping url => vurl because of the fullcalendar client script $event['vurl'] = $event['url']; unset($event['url']); return array( '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'), 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'), // 'changed' might be empty for event recurrences (Bug #2185) 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null, '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:///?_task=calendar&_action=randomdata&_num=500 */ public function generate_randomdata() { $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100; $cats = array_keys($this->driver->list_categories()); $cals = $this->driver->list_calendars(true); $count = 0; while ($count++ < $num) { $start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300; $duration = round(rand(30, 360) / 30) * 30 * 60; $allday = rand(0,20) > 18; $alarm = rand(-30,12) * 5; $fb = rand(0,2); if (date('G', $start) > 23) $start -= 3600; if ($allday) { $start = strtotime(date('Y-m-d 00:00:00', $start)); $duration = 86399; } $title = ''; $len = rand(2, 12); $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise."); // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"; for ($i = 0; $i < $len; $i++) $title .= $words[rand(0,count($words)-1)] . " "; $this->driver->new_event(array( 'uid' => $this->generate_uid(), 'start' => new DateTime('@'.$start), 'end' => new DateTime('@'.($start + $duration)), 'allday' => $allday, 'title' => rtrim($title), 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), 'categories' => $cats[array_rand($cats)], 'calendar' => array_rand($cals), 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '', 'priority' => rand(0,9), )); } $this->rc->output->redirect(''); } /** * Handler for attachments upload */ public function attachment_upload() { $this->lib->attachment_upload(self::SESSION_KEY, 'cal:'); } /** * Handler for attachments download/displaying */ public function attachment_get() { // show loading page if (!empty($_GET['_preload'])) { return $this->lib->attachment_loading_page(); } $event_id = get_input_value('_event', RCUBE_INPUT_GPC); $calendar = get_input_value('_cal', RCUBE_INPUT_GPC); $id = get_input_value('_id', RCUBE_INPUT_GPC); $event = array('id' => $event_id, 'calendar' => $calendar); $attachment = $this->driver->get_attachment($id, $event); // show part page if (!empty($_GET['_frame'])) { $this->lib->attachment = $attachment; $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame')); $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header')); $this->rc->output->send('calendar.attachment'); } // deliver attachment content else if ($attachment) { $attachment['body'] = $this->driver->get_attachment_body($id, $event); $this->lib->attachment_get($attachment); } // if we arrive here, the requested part was not found header('HTTP/1.1 404 Not Found'); exit; } /** * Prepares new/edited event properties before save */ private function 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); // 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; // check for organizer in attendees if ($action == 'new' || $action == 'edit') { if (!$event['attendees']) $event['attendees'] = array(); $emails = $this->get_user_emails(); $organizer = $owner = false; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $i; if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails)) $owner = $i; else if (!isset($attendee['rsvp'])) $event['attendees'][$i]['rsvp'] = true; 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']); } else if ($organizer === false && $action == 'new' && ($identity = $this->rc->user->get_identity($event['_identity'])) && $identity['email']) { array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED')); } } // mapping url => vurl because of the fullcalendar client script 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; // compose multipart message using PEAR:Mail_Mime $method = $action == 'remove' ? 'CANCEL' : 'REQUEST'; $message = $itip->compose_itip_message($event, $method); // list existing attendees from $old event $old_attendees = array(); foreach ((array)$old['attendees'] as $attendee) { $old_attendees[] = $attendee['email']; } // send to every attendee $sent = 0; $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; // 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; } // 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 = get_input_value('email', RCUBE_INPUT_GPC); $start = get_input_value('start', RCUBE_INPUT_GPC); $end = get_input_value('end', RCUBE_INPUT_GPC); // convert dates into unix timestamps if (!empty($start) && !is_numeric($start)) { $dts = new DateTime($start, $this->timezone); $start = $dts->format('U'); } if (!empty($end) && !is_numeric($end)) { $dte = new DateTime($end, $this->timezone); $end = $dte->format('U'); } if (!$start) $start = time(); if (!$end) $end = $start + 3600; $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE'); $status = 'UNKNOWN'; // if the backend has free-busy information $fblist = $this->driver->get_freebusy_list($email, $start, $end); if (is_array($fblist)) { $status = 'FREE'; foreach ($fblist as $slot) { list($from, $to, $type) = $slot; if ($from < $end && $to > $start) { $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY'; break; } } } // let this information be cached for 5min send_future_expire_header(300); echo $status; exit; } /** * Return a list of free/busy time slots within the given period * Echo data in JSON encoding */ public function freebusy_times() { $email = get_input_value('email', RCUBE_INPUT_GPC); $start = get_input_value('start', RCUBE_INPUT_GPC); $end = get_input_value('end', RCUBE_INPUT_GPC); $interval = intval(get_input_value('interval', RCUBE_INPUT_GPC)); $strformat = $interval > 60 ? 'Ymd' : 'YmdHis'; // convert dates into unix timestamps if (!empty($start) && !is_numeric($start)) { $dts = 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 = get_input_value('view', RCUBE_INPUT_GPC); if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $view = 'agendaDay'; $this->rc->output->set_env('view',$view); if ($date = get_input_value('date', RCUBE_INPUT_GPC)) $this->rc->output->set_env('date', $date); if ($range = get_input_value('range', RCUBE_INPUT_GPC)) $this->rc->output->set_env('listRange', intval($range)); if (isset($_REQUEST['sections'])) $this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC)); if ($search = get_input_value('search', RCUBE_INPUT_GPC)) { $this->rc->output->set_env('search', $search); $title .= ' "' . $search . '"'; } // Add CSS stylesheets to the page header $skin_path = $this->local_skin_path(); $this->include_stylesheet($skin_path . '/fullcalendar.css'); $this->include_stylesheet($skin_path . '/print.css'); // Add JS files to the page header $this->include_script('print.js'); $this->include_script('lib/js/fullcalendar.js'); $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css')); $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list')); $this->rc->output->set_pagetitle($title); $this->rc->output->send("calendar.print"); } /** * */ public function get_inline_ui() { foreach (array('save','cancel','savingdata') as $label) $texts['calendar.'.$label] = $this->gettext($label); $texts['calendar.new_event'] = $this->gettext('createfrommail'); $this->ui->init_templates(); $this->ui->calendar_list(); # set env['calendars'] echo $this->api->output->parse('calendar.eventedit', false, false); echo html::tag('script', array('type' => 'text/javascript'), "rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n". "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n". "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n". "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n". "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n". "rcmail.add_label(" . json_encode($texts) . ");\n" ); exit; } /** * Compare two event objects and return differing properties * * @param array Event A * @param array Event B * @return array List of differing event properties */ public static function event_diff($a, $b) { $diff = array(); $ignore = array('changed' => 1, 'attachments' => 1); foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { if (!$ignore[$key] && $a[$key] != $b[$key]) $diff[] = $key; } // only compare number of attachments if (count($a['attachments']) != count($b['attachments'])) $diff[] = 'attachments'; return $diff; } /**** 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_INPUT_GET), rcube_utils::get_input_value('end', RCUBE_INPUT_GET)); } echo $this->encode($events); exit; } /**** Event invitation plugin hooks ****/ /** * Handler for calendar/itip-status requests */ function event_itip_status() { $data = get_input_value('data', RCUBE_INPUT_POST, true); // find local copy of the referenced event $this->load_driver(); $existing = $this->driver->get_event($data, true, false, true); $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(false, true); $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['readonly']) { $calendar_select->add($calendar['name'], $calendar['id']); $numcals++; } } if ($numcals <= 1) $calendar_select = null; } if ($calendar_select) { $default_calendar = $this->get_default_calendar(true); $response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . ' ' . $calendar_select->show($this->rc->config->get('calendar_default_calendar', $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(false, true); + $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; // search for event if only UID is given if ($event = $this->driver->get_event(array('uid' => get_input_value('uid', RCUBE_INPUT_POST)), true)) { $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 = get_input_value('_t', RCUBE_INPUT_GPC); // read event info stored under the given token if ($invitation = $itip->get_invitation($token)) { $this->token = $token; $this->event = $invitation['event']; // show message about cancellation if ($invitation['cancelled']) { $this->invitestatus = html::div('rsvp-status declined', $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 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']['calendar'] = $calendar['id']; if ($this->driver->new_event($invitation['event'])) $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); } } } $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform')); $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox')); if (!$this->invitestatus) $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons')); $this->rc->output->set_pagetitle($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['all-day'] ? $this->gettext('allday') : + $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 $i => $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 = get_input_value('_uid', RCUBE_INPUT_POST); $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $mime_id = get_input_value('_part', RCUBE_INPUT_POST); $status = get_input_value('_status', RCUBE_INPUT_POST); $delete = intval(get_input_value('_del', RCUBE_INPUT_POST)); $noreply = intval(get_input_value('_noreply', RCUBE_INPUT_POST)) || $status == 'needs-action' || $itip_sending === 0; $error_msg = $this->gettext('errorimportingevent'); $success = false; // successfully parsed events? if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { // find writeable calendar to store event $cal_id = !empty($_REQUEST['_folder']) ? get_input_value('_folder', RCUBE_INPUT_POST) : null; $dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST'); $calendars = $this->driver->list_calendars(false, true); $calendar = $calendars[$cal_id]; // select default calendar except user explicitly selected 'none' if (!$calendar && !$dontsave) $calendar = $this->get_default_calendar(true); $metadata = array( 'uid' => $event['uid'], '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 ($event['attendees'][$i]['status'] != 'NEEDS-ACTION') unset($event['attendees'][$i]['rsvp']); // remove RSVP attribute $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $reply_sender = $attendee['email']; } } // add attendee with this user's default identity if not listed if (!$reply_sender) { $sender_identity = $this->rc->user->get_identity(); $event['attendees'][] = array( 'name' => $sender_identity['name'], 'email' => $sender_identity['email'], 'role' => 'OPT-PARTICIPANT', 'status' => strtoupper($status), ); $metadata['attendee'] = $sender_identity['email']; } } // save to calendar if ($calendar && !$calendar['readonly']) { $event['calendar'] = $calendar['id']; // check for existing event with the same UID $existing = $this->driver->get_event($event['uid'], true, false, true); if ($existing) { // only update attendee status if ($event['_method'] == 'REPLY') { // try to identify the attendee using the email sender address $existing_attendee = -1; foreach ($existing['attendees'] as $i => $attendee) { if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { $existing_attendee = $i; break; } } $event_attendee = null; foreach ($event['attendees'] as $attendee) { if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { $event_attendee = $attendee; $metadata['fallback'] = $attendee['status']; $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; break; } } // found matching attendee entry in both existing and new events if ($existing_attendee >= 0 && $event_attendee) { $existing['attendees'][$existing_attendee] = $event_attendee; $success = $this->driver->edit_event($existing); } // update the entire attendees block else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) { $existing['attendees'][] = $event_attendee; $success = $this->driver->edit_event($existing); } else { $error_msg = $this->gettext('newerversionexists'); } } // delete the event when declined (#1670) else if ($status == 'declined' && $delete) { $deleted = $this->driver->remove_event($existing, true); $success = true; } // import the (newer) event else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) { $event['id'] = $existing['id']; $event['calendar'] = $existing['calendar']; // 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['free_busy'] = 'free'; $success = $this->driver->edit_event($event); } else if (!empty($status)) { $existing['attendees'] = $event['attendees']; if ($status == 'declined') // show me as free when declined (#1670) $existing['free_busy'] = 'free'; $success = $this->driver->edit_event($existing); } else $error_msg = $this->gettext('newerversionexists'); } else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) { $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'] = get_input_value('_comment', RCUBE_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 = get_input_value('_uid', RCUBE_INPUT_POST); $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $mime_id = get_input_value('_part', RCUBE_INPUT_POST); if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') { $event['comment'] = get_input_value('_comment', RCUBE_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'); } } /** * Import the full payload from a mail message attachment */ public function mail_import_attachment() { $uid = get_input_value('_uid', RCUBE_INPUT_POST); $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $mime_id = get_input_value('_part', RCUBE_INPUT_POST); $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']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : null; $calendars = $this->driver->list_calendars(false, true); $calendar = $calendars[$cal_id] ?: $this->get_default_calendar(true); foreach ($events as $event) { // save to calendar if ($calendar && !$calendar['readonly'] && $event['_type'] == 'event') { $event['calendar'] = $calendar['id']; if (!$this->driver->get_event($event['uid'], true, false)) { $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 = get_input_value('_uid', RCUBE_INPUT_POST); $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $event = array(); // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); $message = new rcube_message($uid); if ($message->headers) { $event['title'] = trim($message->subject); $event['description'] = trim($message->first_text_part()); // copy mail attachments to event if ($message->attachments) { $eventid = 'cal:'; if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) { $_SESSION[self::SESSION_KEY] = array(); $_SESSION[self::SESSION_KEY]['id'] = $eventid; $_SESSION[self::SESSION_KEY]['attachments'] = array(); } foreach ((array)$message->attachments as $part) { $attachment = array( 'data' => $imap->get_message_part($uid, $part->mime_id, $part), 'size' => $part->size, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'group' => $eventid, ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); // store new attachment in session unset($attachment['status'], $attachment['abort'], $attachment['data']); $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' $event['attachments'][] = $attachment; } } } $this->rc->output->command('plugin.mail2event_dialog', $event); } else { $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); } $this->rc->output->send(); } /** * 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) { $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/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 76fbceec..7755aa70 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -1,301 +1,303 @@ CalDAV client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.'; $labels['findcalendars'] = 'Find calendars...'; $labels['searchterms'] = 'Search terms'; $labels['calsearchresults'] = 'Available Calendars'; $labels['calendarsubscribe'] = 'List permanently'; $labels['nocalendarsfound'] = 'No calendars found'; $labels['nrcalendarsfound'] = '$nr calendars found'; $labels['quickview'] = 'View only this calendar'; $labels['invitationspending'] = 'Pending invitations'; $labels['invitationsdeclined'] = 'Declined invitations'; $labels['changepartstat'] = 'Change participant status'; $labels['rsvpcomment'] = 'Invitation text'; // agenda view $labels['listrange'] = 'Range to display:'; $labels['listsections'] = 'Divide into:'; $labels['smartsections'] = 'Smart sections'; $labels['until'] = 'until'; $labels['today'] = 'Today'; $labels['tomorrow'] = 'Tomorrow'; $labels['thisweek'] = 'This week'; $labels['nextweek'] = 'Next week'; $labels['prevweek'] = 'Previous week'; $labels['thismonth'] = 'This month'; $labels['nextmonth'] = 'Next month'; $labels['weekofyear'] = 'Week'; $labels['pastevents'] = 'Past'; $labels['futureevents'] = 'Future'; // alarm/reminder settings $labels['showalarms'] = 'Show reminders'; $labels['defaultalarmtype'] = 'Default reminder setting'; $labels['defaultalarmoffset'] = 'Default reminder time'; // attendees $labels['attendee'] = 'Participant'; $labels['role'] = 'Role'; $labels['availability'] = 'Avail.'; $labels['confirmstate'] = 'Status'; $labels['addattendee'] = 'Add participant'; $labels['roleorganizer'] = 'Organizer'; $labels['rolerequired'] = 'Required'; $labels['roleoptional'] = 'Optional'; $labels['rolechair'] = 'Chair'; $labels['rolenonparticipant'] = 'Absent'; $labels['cutypeindividual'] = 'Individual'; $labels['cutypegroup'] = 'Group'; $labels['cutyperesource'] = 'Resource'; $labels['cutyperoom'] = 'Room'; $labels['availfree'] = 'Free'; $labels['availbusy'] = 'Busy'; $labels['availunknown'] = 'Unknown'; $labels['availtentative'] = 'Tentative'; $labels['availoutofoffice'] = 'Out of Office'; $labels['delegatedto'] = 'Delegated to: '; $labels['delegatedfrom'] = 'Delegated from: '; $labels['scheduletime'] = 'Find availability'; $labels['sendinvitations'] = 'Send invitations'; $labels['sendnotifications'] = 'Notify participants about modifications'; $labels['sendcancellation'] = 'Notify participants about event cancellation'; $labels['onlyworkinghours'] = 'Find availability within my working hours'; $labels['reqallattendees'] = 'Required/all participants'; $labels['prevslot'] = 'Previous Slot'; $labels['nextslot'] = 'Next Slot'; $labels['suggestedslot'] = 'Suggested Slot'; $labels['noslotfound'] = 'Unable to find a free time slot'; $labels['invitationsubject'] = 'You\'ve been invited to "$title"'; $labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application."; $labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url"; $labels['eventupdatesubject'] = '"$title" has been updated'; $labels['eventupdatesubjectempty'] = 'An event that concerns you has been updated'; $labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated event details which you can import to your calendar application."; $labels['eventcancelsubject'] = '"$title" has been canceled'; $labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe event has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated event details."; // invitation handling (overrides labels from libcalendaring) $labels['itipobjectnotfound'] = 'The event referred by this message was not found in your calendar.'; $labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees"; $labels['itipmailbodytentative'] = "\$sender has tentatively accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees"; $labels['itipmailbodydeclined'] = "\$sender has declined the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees"; $labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date"; $labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?'; $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?'; $labels['itipcomment'] = 'Invitation/notification comment'; $labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants'; $labels['notanattendee'] = 'You\'re not listed as an attendee of this event'; $labels['eventcancelled'] = 'The event has been cancelled'; $labels['saveincalendar'] = 'save in'; $labels['updatemycopy'] = 'Update in my calendar'; $labels['savetocalendar'] = 'Save to calendar'; $labels['openpreview'] = 'Check Calendar'; +$labels['noearlierevents'] = 'No earlier events'; +$labels['nolaterevents'] = 'No later events'; // resources $labels['resource'] = 'Resource'; $labels['addresource'] = 'Book resource'; $labels['findresources'] = 'Find resources'; $labels['resourcedetails'] = 'Details'; $labels['resourceavailability'] = 'Availability'; $labels['resourceowner'] = 'Owner'; $labels['resourceadded'] = 'The resource was added to your event'; // event dialog tabs $labels['tabsummary'] = 'Summary'; $labels['tabrecurrence'] = 'Recurrence'; $labels['tabattendees'] = 'Participants'; $labels['tabresources'] = 'Resources'; $labels['tabattachments'] = 'Attachments'; $labels['tabsharing'] = 'Sharing'; // messages $labels['deleteobjectconfirm'] = 'Do you really want to delete this event?'; $labels['deleteventconfirm'] = 'Do you really want to delete this event?'; $labels['deletecalendarconfirm'] = 'Do you really want to delete this calendar with all its events?'; $labels['deletecalendarconfirmrecursive'] = 'Do you really want to delete this calendar with all its events and sub-calendars?'; $labels['savingdata'] = 'Saving data...'; $labels['errorsaving'] = 'Failed to save changes.'; $labels['operationfailed'] = 'The requested operation failed.'; $labels['invalideventdates'] = 'Invalid dates entered! Please check your input.'; $labels['invalidcalendarproperties'] = 'Invalid calendar properties! Please set a valid name.'; $labels['searchnoresults'] = 'No events found in the selected calendars.'; $labels['successremoval'] = 'The event has been deleted successfully.'; $labels['successrestore'] = 'The event has been restored successfully.'; $labels['errornotifying'] = 'Failed to send notifications to event participants'; $labels['errorimportingevent'] = 'Failed to import the event'; $labels['importwarningexists'] = 'A copy of this event already exists in your calendar.'; $labels['newerversionexists'] = 'A newer version of this event already exists! Aborted.'; $labels['nowritecalendarfound'] = 'No calendar found to save the event'; $labels['importedsuccessfully'] = 'The event was successfully added to \'$calendar\''; $labels['updatedsuccessfully'] = 'The event was successfully updated in \'$calendar\''; $labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status'; $labels['itipsendsuccess'] = 'Invitation sent to participants.'; $labels['itipresponseerror'] = 'Failed to send the response to this event invitation'; $labels['itipinvalidrequest'] = 'This invitation is no longer valid'; $labels['sentresponseto'] = 'Successfully sent invitation response to $mailto'; $labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your calendar and not be sent to the organizer of the event.'; $labels['importsuccess'] = 'Successfully imported $nr events'; $labels['importnone'] = 'No events found to be imported'; $labels['importerror'] = 'An error occured while importing'; $labels['aclnorights'] = 'You do not have administrator rights on this calendar.'; $labels['changeeventconfirm'] = 'Change event'; $labels['removeeventconfirm'] = 'Remove event'; $labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?'; $labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to remove the current event only, this and all future occurences or all occurences of this event?'; $labels['currentevent'] = 'Current'; $labels['futurevents'] = 'Future'; $labels['allevents'] = 'All'; $labels['saveasnew'] = 'Save as new'; // birthdays calendar $labels['birthdays'] = 'Birthdays'; $labels['birthdayscalendar'] = 'Birthdays Calendar'; $labels['displaybirthdayscalendar'] = 'Display birthdays calendar'; $labels['birthdayscalendarsources'] = 'From these address books'; $labels['birthdayeventtitle'] = '$name\'s Birthday'; $labels['birthdayage'] = 'Age $age'; // history dialog $labels['eventchangelog'] = 'Change History'; $labels['eventdiff'] = 'Changes from revisions $rev'; $labels['revision'] = 'Revision'; $labels['user'] = 'User'; $labels['operation'] = 'Action'; $labels['actionappend'] = 'Saved'; $labels['actionmove'] = 'Moved'; $labels['actiondelete'] = 'Deleted'; $labels['compare'] = 'Compare'; $labels['showrevision'] = 'Show this version'; $labels['restore'] = 'Restore this version'; $labels['eventnotfound'] = 'Failed to load event data'; $labels['eventchangelognotavailable'] = 'Change history is not available for this event'; $labels['eventdiffnotavailable'] = 'No comparison possible for the selected revisions'; $labels['eventrestoreconfirm'] = 'Do you really want to restore revision $rev of this event? This will replace the current event with the old version.'; // (hidden) titles and labels for accessibility annotations $labels['arialabelminical'] = 'Calendar date selection'; $labels['arialabelcalendarview'] = 'Calendar view'; $labels['arialabelsearchform'] = 'Event search form'; $labels['arialabelquicksearchbox'] = 'Event search input'; $labels['arialabelcalsearchform'] = 'Calendars search form'; $labels['calendaractions'] = 'Calendar actions'; $labels['arialabeleventattendees'] = 'Event participants list'; $labels['arialabeleventresources'] = 'Event resources list'; $labels['arialabelresourcesearchform'] = 'Resources search form'; $labels['arialabelresourceselection'] = 'Available resources'; ?> diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css index f7e0c1ca..f8a4fa31 100644 --- a/plugins/calendar/skins/classic/calendar.css +++ b/plugins/calendar/skins/classic/calendar.css @@ -1,1721 +1,1757 @@ /*** Style for Calendar plugin ***/ body.calendarmain { overflow: hidden; } #taskbar a.button-calendar { background: url(images/calendar.png) 0px 1px no-repeat; } /* hack for IE 6/7 */ * html #taskbar a.button-calendar { background-image: url(images/calendar.gif); } #main { position: absolute; clear: both; top: 72px; left: 0; right: 0; bottom: 10px; } #calendarsidebar { position: absolute; top: 0px; left: 10px; bottom: 0; width: 230px; } #datepicker { position: relative; top: 42px; width: 100%; } #datepicker .ui-datepicker { width: 97% !important; box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none; } #datepicker .ui-datepicker-activerange a { border-color: #c33; color: #a22; } #datepicker .ui-datepicker-activerange a.ui-state-active { color: #fff; } #datepicker .ui-priority-secondary { opacity: 0.4; } #datepicker td.ui-datepicker-week-col { cursor: pointer; } #calendarsidebartoggle { position: absolute; left: 244px; width: 8px; top: 4px; bottom: 0; background: url(images/toggle.gif) 0 48% no-repeat transparent; cursor: pointer; } div.sidebarclosed { background-position: -8px 48% !important; } #calendarsidebartoggle:hover { background-color: #ddd; } #calendar { position: absolute; top: 4px; left: 256px; right: 10px; bottom: 0; } #print { width: 680px; } pre { font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; } #calendars { position: absolute; top: 228px; left: 0; bottom: 0; right: 0; background-color: #F9F9F9; border: 1px solid #999999; overflow: hidden; } #calendars .boxlistcontent { top: 43px; } #calendars .listsearchbox { padding: 2px 4px; } #calendarslist { list-style: none; margin: 0; padding: 0; } #attachmentlist li, #calendarslist li { margin: 0; padding: 1px; display: block; background: #fff; border-bottom: 1px solid #EBEBEB; white-space: nowrap; cursor: default; } #calendars .treelist li { margin: 0; padding: 0; position: relative; } #calendars .treelist ul li:last-child { border-bottom: 0; } #calendars .treelist li div.folder, #calendars .treelist li div.calendar { position: relative; height: 22px; } #calendars .treelist li span.calname { display: block; padding: 0px 30px 2px 2px; position: absolute; top: 4px; left: 38px; right: 40px; cursor: default; background: url(images/calendars.png) right 20px no-repeat; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #calendars .treelist li div.virtual > span.calname { color: #aaa; left: 20px; } #calendars .treelist.flat li span.calname { left: 24px; right: 22px; } #calendars .treelist li span.handle { display: inline-block; position: absolute; top: 5px; right: 6px; padding: 0; width: 12px; height: 12px; border-radius: 3px; font-size: 0.8em; } #calendars .treelist li a.subscribed { display: inline-block; position: absolute; top: 2px; right: 22px; 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 { background-position: 0 -126px; } #calendars .treelist div.subscribed a.subscribed { background-position: 0 -144px; } #calendars .treelist li input { position: absolute; top: 1px; left: 18px; } #calendars .treelist li div.treetoggle { top: -1px; left: 1px !important; } #calendars .treelist ul li div.treetoggle { left: 17px !important; } #calendars .treelist ul ul li div.treetoggle { left: 33px !important; } #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 { background-color: #ccc; } #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; } #calendarslist li.virtual span.calname { color: #666; } #calendars .searchresults .boxtitle { border-top: 1px solid #aaa; margin-bottom: 0; } #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: 0px; left: 0px; height: 35px; } #calendartoolbar a { padding-right: 10px; } #calendartoolbar a.button, #calendartoolbar a.buttonPas { display: block; float: left; width: 32px; height: 32px; padding: 0; margin-right: 10px; overflow: hidden; background: url(images/toolbar.png) 0 0 no-repeat transparent; opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */ } #calendartoolbar a.buttonPas { opacity: 0.35; } #calendartoolbar a.addeventSel { background-position: 0 -32px; } #calendartoolbar a.delete { background-position: -32px 0; } #calendartoolbar a.deleteSel { background-position: -32px -32px; } #calendartoolbar a.print { background-position: -64px 0; } #calendartoolbar a.printSel { background-position: -64px -32px; } #calendartoolbar a.import { background-position: -168px 0; } #calendartoolbar a.importSel { background-position: -168px -32px; } #calendartoolbar a.export { background-position: -128px 0; } #calendartoolbar a.exportSel { background-position: -128px -32px; } .calendarmain #quicksearchbar { top: 80px; right: 4px; } .calendarmain div.uidialog { display: none; } #user { position: absolute; top: 10px; right: 100px; left: 100px; text-align: center; } a.morelink { font-size: 90%; color: #C33; text-decoration: none; } a.morelink:hover { text-decoration: underline; } a.miniColors-trigger { margin-top: -3px; } #attachmentcontainer { position: absolute; top: 80px; left: 20px; right: 20px; bottom: 20px; } #attachmentframe { width: 100%; height: 100%; border: 1px solid #999999; background-color: #F9F9F9; } #partheader { position: absolute; top: 20px; left: 220px; right: 20px; height: 40px; } #partheader table td { padding-left: 2px; padding-right: 4px; vertical-align: middle; font-size: 11px; } #partheader table td.title { color: #666; font-weight: bold; } .attachments-list ul { margin: 0px; padding: 0px; list-style-image: none; list-style-type: none; } .attachments-list ul li { height: 18px; font-size: 12px; padding-top: 2px; padding-right: 8px; white-space: nowrap; } .attachments-list ul li img { padding-right: 2px; vertical-align: middle; } .attachments-list ul li a { text-decoration: none; } .attachments-list ul li a:hover { text-decoration: underline; } #attachmentlist { margin: 0 -0.8em; } #attachmentlist li { padding: 2px 2px 3px 0.8em; } #eventshow .attachments-list ul li { float: left; } #edit-attachments-form { padding-top: 1.2em; } #edit-attachments-form .formbuttons { margin: 0.5em 0; } .event-attendees span.attendee { padding-right: 18px; margin-right: 0.5em; background: url(images/attendee-status.gif) right 0 no-repeat; } .event-attendees span.attendee a.mailtolink { text-decoration: none; white-space: nowrap; } .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: 4px; padding-bottom: 3px; border-bottom: 1px solid #ddd; } /* jQuery UI overrides */ .calendarmain .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: left; } #eventshow h1 { font-size: 20px; margin: 0.1em 0 0.4em 0; } #eventshow label, #eventshow h5.label { font-weight: normal; font-size: 0.9em; color: #999; margin: 0 0 0.2em 0; } #eventshow { margin: 0 -0.2em; } #eventshow.status-cancelled { background: url(images/badge_cancelled.png) top right no-repeat; } #eventshow.sensitivity-private { background: url(images/badge_private.png) top right no-repeat; } #eventshow.sensitivity-confidential { background: url(images/badge_confidential.png) top right no-repeat; } .sensitivity-private #event-title { margin-right: 50px; } .sensitivity-confidential #event-title { margin-right: 60px; } #eventshow div.event-line { margin-top: 0.1em; margin-bottom: 0.3em; } #eventshow #event-url .event-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } #event-rsvp .itip-reply-controls { margin-top: 0.5em; } #eventshow .itip-reply-controls label { font-size: 1em; color: #333; } #event-partstat .changersvp { cursor: pointer; color: #333; text-decoration: none; } #event-partstat:hover .changersvp { text-decoration: underline; } #event-partstat .changersvp.accepted { color: #589b1e; } #event-partstat .changersvp.tentative { color: #f0bb1d; } #event-partstat .changersvp.declined { color: #ea0000; } #event-partstat .changersvp.delegated { color: #018be9; } #eventedit { position: relative; padding: 0.5em 0.1em; } #eventedit input.text, #eventedit textarea { width: 97%; } #eventtabs { position: relative; padding: 0; border: 0; border-radius: 0; } div.form-section, #eventshow div.event-section, #eventtabs div.event-section { margin-top: 0.2em; margin-bottom: 0.8em; } #eventtabs .tabsbar { position: absolute; top: 0; } #eventtabs .ui-tabs-panel { padding: 1em 0.8em; border: 1px solid #aaa; border-width: 0 1px 1px 1px; } #eventtabs .ui-tabs-nav { background: none; padding: 0; border-width: 0 0 1px 0; border-radius: 0; } #eventtabs .border-after { padding-bottom: 0.6em; margin-bottom: 0.6em; border-bottom: 1px solid #999; } #eventshow label, #eventedit label, .form-section label { display: inline-block; min-width: 7em; padding-right: 0.5em; } #eventedit .formtable td.label { min-width: 6em; } td.topalign { vertical-align: top; } #eventedit .edit-alarm-item { position: relative; padding-right: 30px; margin-bottom: 2px; } #eventedit .edit-alarm-buttons { position: absolute; top: 2px; right: 0; } #eventedit .edit-alarm-buttons a.iconlink { display: none; width: 18px; height: 17px; padding: 1px; text-indent: -5000px; overflow: hidden; } #eventedit .edit-alarm-buttons a.add-alarm { background: url(images/plus.png) 1px 1px no-repeat; } #eventedit .edit-alarm-buttons a.delete-alarm { background: url(images/delete.png) 1px 1px no-repeat; } #eventedit .edit-alarm-buttons a.delete-alarm, #eventedit .first .edit-alarm-buttons a.add-alarm { display: inline-block; } #eventedit .first .edit-alarm-buttons a.delete-alarm { display: none; } #eventedit label.weekday, #eventedit label.monthday { min-width: 3em; } #eventedit label.month { min-width: 5em; } #edit-recurrence-yearly-bymonthblock { margin-left: 7.5em; } #edit-recurrence-rdates { display: block; list-style: none; margin: 0 0 0.8em 0; padding: 0; max-height: 300px; overflow: auto; } #edit-recurrence-rdates li { display: block; position: relative; width: 14em; padding: 1px; } #edit-recurrence-rdates li a.delete { position: absolute; top: 1px; right: 0; } #eventedit .recurrence-form { display: none; } #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; background-color: #F7FDCB; border: 1px solid #C2D071; } .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; } #event-rsvp, #edit-attendees-notify { margin: 0.3em 0; padding: 0.5em; background-color: #F7FDCB; border: 1px solid #C2D071; } .edit-attendees-table { width: 100%; display: table; table-layout: fixed; border-collapse: collapse; border: 1px solid #ccc; } .edit-attendees-table th, .edit-attendees-table td { padding: 3px; border-bottom: 1px solid #ccc; text-align: left; } .edit-attendees-table th.role, .edit-attendees-table td.role { width: 8em; } .edit-attendees-table th.availability, .edit-attendees-table th.confirmstate, .edit-attendees-table td.availability, .edit-attendees-table td.confirmstate { width: 4em; } .edit-attendees-table th.options, .edit-attendees-table td.options { width: 2em; text-align: right; padding-right: 4px; } .edit-attendees-table th.invite, .edit-attendees-table td.invite { width: 24px; padding: 2px; white-space: nowrap; overflow: hidden; text-overflow: hidden; } #eventedit .edit-attendees-table th.invite label { display: none; } .edit-attendees-table th.name, .edit-attendees-table td.name { width: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .edit-attendees-table thead th, .edit-attendees-table thead td { background: url(images/listheader.gif) top left repeat-x #CCC; } #edit-attendees-form, #edit-resources-form { position: relative; margin-top: 1em; } #edit-attendees-form #edit-attendee-schedule, #edit-resources-form #edit-resource-find { position: absolute; top: 0; right: 0; } .edit-attendees-table select.edit-attendee-role { border: 0; padding: 2px; background: white; } .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: #0c0; } #schedule-freebusy-times td.busy, .availability img.availabilityicon.busy { background: #c00; } #schedule-freebusy-times td.tentative, .availability img.availabilityicon.tentative { background: #66d; } #schedule-freebusy-times td.out-of-office, .availability img.availabilityicon.out-of-office { background: #f0b400; } #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.gif) 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-collapse: collapse; margin: 0.5em 0; } #attendees-freebusy-table td.attendees { width: 18em; border: 1px solid #ccc; vertical-align: top; overflow: hidden; } #attendees-freebusy-table td.times { width: auto; vertical-align: top; border: 1px solid #ccc; } #attendees-freebusy-table div.scroll { position: relative; overflow: auto; } #attendees-freebusy-table h3.boxtitle { margin: 0; height: auto !important; border-color: #ccc; } .attendees-list .attendee { padding: 3px 4px 3px 1px; background: url(images/attendee-status.gif) 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: 3px; border: 1px solid #ccc; } #schedule-freebusy-times tr.dates th { border-color: #aaa; border-style: solid; border-width: 0 1px 0 1px; white-space: nowrap; } #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; } #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; right: 0; } #eventfreebusy .schedule-find-buttons { padding-bottom:0.5em; } #eventfreebusy .schedule-find-buttons button { min-width: 9em; text-align: center; } span.edit-alarm-set { white-space: nowrap; } a.dropdown-link { color: #CC0000; font-size: 12px; text-decoration: none; } a.dropdown-link:after { content: ' ▼'; font-size: 11px; color: #666; } #eventedit .ui-tabs-panel { min-height: 20em; } .alarm-item { margin: 0.4em 0 1em 0; } .alarm-item .event-title { font-size: 14px; margin: 0.1em 0 0.3em 0; } .alarm-item div.event-section { margin-top: 0.1em; margin-bottom: 0.3em; } .alarm-item .alarm-actions { margin-top: 0.4em; } .alarm-item div.alarm-actions a { color: #CC0000; margin-right: 0.8em; text-decoration: none; } a.alarm-action-snooze:after { content: ' ▼'; font-size: 10px; color: #666; } #alarm-snooze-dropdown { z-index: 5000; } .ui-dialog-buttonset a.dropdown-link { margin-right: 1em; } .ui-datepicker-calendar .ui-datepicker-today .ui-state-default { border-color: #cccccc; background: #ffffcc; color: #000; } .ui-datepicker-calendar .ui-datepicker-week-col { text-align: right; padding-right: 0.5em; } .ui-datepicker th { padding: 0.3em 0; font-size: 10px; } .ui-datepicker td span, .ui-datepicker td a { padding-left: 0.1em; } .ui-autocomplete { max-height: 160px; overflow-y: auto; overflow-x: hidden; } .ui-autocomplete .ui-menu-item { white-space: nowrap; } * html .ui-autocomplete { height: 160px; } span.spacer { padding-left: 3em; } #agendaoptions { position: absolute; left: 0; right: 0; bottom: 0; height: auto; z-index: 200; border: 1px solid #ccc; padding: 2px 5px 1px; font-size: 90%; } #agendaoptions label { color: #444; text-shadow: 1px 1px #eee; padding-right: 0.5em; } #calendar-kolabform { position: relative; padding-top: 24px; margin: 0 -8px; min-width: 660px; min-height: 400px; } #calendar-kolabform div.tabsbar { top: 0; right: 2px; left: 2px; height: 24px; } #calendar-kolabform fieldset.tabbed { background-color: #fff; margin-top: 0; } #calendar-kolabform span.tablink { background-color: #e8e8e9; background-image: -moz-linear-gradient(center top, #f4f4f4, #e6e6e6); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0.0, #f4f4f4), color-stop(1.0, #e6e6e6)); filter: progid:DXImageTransform.Microsoft.gradient(enabled='true', startColorstr=#f4f4f4, endColorstr=#e6e6e6, GradientType=0); height: 24px !important; } #calendar-kolabform span.tablink-selected { background: #fff; height: 25px !important; } #calendar-kolabform span.tablink a, #calendar-kolabform span.tablink-selected a { background: none; border: 1px solid #AAAAAA; border-top-left-radius: 2px; border-top-right-radius: 2px; padding: 4px 10px 0 10px; margin-left: 0; } #calendar-kolabform table td.title { font-weight: bold; white-space: nowrap; color: #666; padding-right: 10px; } #resource-dialog-right { position: absolute; top: 10px; left: 300px; right: 8px; bottom: 10px; } #resource-info, #resource-availability { position: absolute; top: 0; left: 0; right: 0; height: 48%; border: 1px solid #999; background-color: #F9F9F9; overflow: auto; } #resource-availability { top: auto; bottom: 0; height: 49%; overflow: hidden; } #resource-info .boxtitle, #resource-availability .boxtitle { margin-top: 0; } #resource-freebusy-calendar { position: absolute; top: 20px; 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; } #resources-list div.treetoggle { left: 3px !important; top: -2px; } #resources-list li ul div.treetoggle { left: 23px !important; } #resource-selection { position: absolute; top: 10px; bottom: 10px; left: 8px; width: 280px; border: 1px solid #999999; background-color: #F9F9F9; overflow: hidden; } #resource-selection .boxlistcontent { top: 25px; border-top: 1px solid #eee; } #resourcequicksearch { position: absolute; top: 3px; left: 7px; right: 4px; height: 17px; background: #fff; border: 1px solid #888; border-radius: 10px; -webkit-box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3); -moz-box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3); box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3); } #resourcesearchbox { position: absolute; top: 1px; left: 24px; width: 140px; height: 15px; font-size: 11px; padding: 0px; border: none; outline: none; background: #fff; } #resourcesearchreset { position: absolute; top: 2px; right: 2px; text-decoration: none; } #resource-details, #resource-details-owner { margin: 8px; } #resource-details td.title, #resource-details-owner td.title { color: #666; padding-right: 10px; min-width: 10em; } #resource-details-owner thead td { color: #333; font-size: 13px; font-weight: bold; } /* fullcalendar style overrides */ #calendar .fc-header-right { padding-right: 200px; padding-top: 0; } .rcube-fc-content { position: absolute !important; top: 38px; left: 0; right: 0; bottom: 0; overflow: hidden; } .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-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-grid .fc-event-time { font-weight: normal !important; padding-right: 0.3em; } .fc-event-cateories { font-style:italic; } .fc-event-location { font-size: 90%; } .fc-more-link { color: #999; padding-top: 1px; cursor: pointer; } .fc-agenda-slots td div { height: 22px; } .fc-mon, .fc-tue, .fc-wed, .fc-thu, .fc-fri { background-color: #fdfdfd; } .fc-widget-header { background-color: #fff; } .fc-icon-alarms, .fc-icon-sensitive, .fc-icon-recurring { display: inline-block; width: 11px; height: 11px; background: url(images/eventicons.gif) 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; } #calendar .fc-event-vert .fc-event-head, #calendar .fc-event-vert .fc-event-content { position: relative; z-index: 2; width: 100%; overflow: hidden; } .fc-view-list div.fc-list-header, .fc-view-table td.fc-list-header, .edit-attendees-table thead td { padding: 3px; background: #dddddd; background-image: -moz-linear-gradient(center top, #f4f4f4, #d2d2d2); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0.00, #f4f4f4), color-stop(1.00, #d2d2d2)); filter: progid:DXImageTransform.Microsoft.gradient(enabled='true', startColorstr=#f4f4f4, endColorstr=#d2d2d2, GradientType=0); font-weight: bold; color: #333; } .fc-view-list .fc-event-skin .fc-event-content { background: #F6F6F6; padding: 2px; } .fc-view-list .fc-event-skin .fc-event-title, .fc-view-list .fc-event-skin .fc-event-location { color: #333; } .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/calendar-small.png) 1px 1px no-repeat; } #messagemenu li a.calendarlink, #attachmentmenu li a.calendarlink { background-image: url(images/calendars.png); background-position: 7px -109px; background-repeat: no-repeat; } div.calendar-invitebox { min-height: 20px; margin: 5px 8px; padding: 3px 6px 6px 34px; border: 1px solid #C2D071; background: url(images/calendar.png) 6px 5px no-repeat #F7FDCB; } div.calendar-invitebox td.ititle { font-weight: bold; padding-right: 0.5em; } div.calendar-invitebox td.label { color: #666; padding-right: 1em; } #event-rsvp .rsvp-buttons, div.calendar-invitebox .itip-buttons div { margin-top: 0.5em; } #event-rsvp input.button, div.calendar-invitebox input.button, div.calendar-invitebox select { font-size: 11px; margin-right: 0.5em; } div.calendar-invitebox .folder-select { font-size: 11px; margin-left: 1em; } 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.declined, div.calendar-invitebox .rsvp-status.tentative, div.calendar-invitebox .rsvp-status.delegated, div.calendar-invitebox .rsvp-status.accepted { padding: 0 0 1px 22px; background: url(images/attendee-status.gif) 2px -20px no-repeat; } div.calendar-invitebox .rsvp-status.declined { background-position: 2px -40px; } div.calendar-invitebox .rsvp-status.tentative { background-position: 2px -60px; } div.calendar-invitebox .rsvp-status.delegated { background-position: 2px -180px; } +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; +} + +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: #000; + 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; margin: 80px auto; padding: 10px 10px 10px 90px; border: 1px solid #ccc; box-shadow: 1px 1px 24px #ccc; -moz-box-shadow: 1px 1px 18px #ccc; -webkit-box-shadow: #ccc 1px 1px 18px; background: url(images/invitation.png) 10px 10px no-repeat #fbfbfb; } .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%; } diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index 54e0141d..40d79fd5 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -1,2146 +1,2184 @@ /** * Roundcube Calendar plugin styles for skin "Larry" * * Copyright (c) 2012-2014, Kolab Systems AG * 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; } } body.calendar.attachmentwin #mainscreen { top: 32px; } #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: hidden; text-overflow: ellipsis; white-space: nowrap; color: #004458; } #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; } #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 -148px; background-color: transparent !important; } #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 .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; } #attachmentcontainer { position: absolute; top: 60px; left: 0px; right: 0px; bottom: 0px; } #attachmentframe { width: 100%; height: 100%; border: 0; background-color: #fff; border-radius: 4px; } #partheader { position: relative; padding: 3px 0; background: #f9f9f9; background: -moz-linear-gradient(top, #fff 0%, #e9e9e9 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fff), color-stop(100%,#e9e9e9)); background: -o-linear-gradient(top, #fff 0%, #e9e9e9 100%); background: -ms-linear-gradient(top, #fff 0%, #e9e9e9 100%); background: linear-gradient(top, #fff 0%, #e9e9e9 100%); } #partheader table td { color: #666; padding: 2px 8px; } #partheader table td.header { font-weight: bold; } #partheader table td.title a { color: #666; text-decoration: none; } #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-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: 5em; } #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 { 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; } #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; } #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; position: relative; } #eventedit .edit-attendees-table td.name select { width: 100%; } #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; } .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; } #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.55; 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; -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; border-color: #444 !important; cursor: default !important; } .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-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: #fdfdfd; } .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; } #messagemenu 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.label { color: #666; padding-right: 1em; } #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%; } diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php index a5b056f7..6740bfa5 100644 --- a/plugins/libcalendaring/lib/libcalendaring_itip.php +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php @@ -1,672 +1,675 @@ * * Copyright (C) 2011-2014, Kolab Systems AG * * 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 . */ class libcalendaring_itip { protected $rc; protected $lib; protected $plugin; protected $sender; protected $domain; protected $itip_send = false; protected $rsvp_actions = array('accepted','tentative','declined'); protected $rsvp_status = array('accepted','tentative','declined','delegated'); function __construct($plugin, $domain = 'libcalendaring') { $this->plugin = $plugin; $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->domain = $domain; $hook = $this->rc->plugins->exec_hook('calendar_load_itip', array('identity' => $this->rc->user->get_identity())); $this->sender = $hook['identity']; $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook')); $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook')); } public function set_sender_email($email) { if (!empty($email)) $this->sender['email'] = $email; } public function set_rsvp_actions($actions) { $this->rsvp_actions = (array)$actions; $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated')); } public function set_rsvp_status($status) { $this->rsvp_status = $status; } /** * Wrapper for rcube_plugin::gettext() * Checking for a label in different domains * * @see rcube::gettext() */ public function gettext($p) { $label = is_array($p) ? $p['name'] : $p; $domain = $this->domain; if (!$this->rc->text_exists($label, $domain)) { $domain = 'libcalendaring'; } return $this->rc->gettext($p, $domain); } /** * Send an iTip mail message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @param array Hash array with recipient data (name, email) * @param string Mail subject * @param string Mail body text label * @param object Mail_mime object with message data * @return boolean True on success, false on failure */ public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true) { if (!$this->sender['name']) $this->sender['name'] = $this->sender['email']; if (!$message) $message = $this->compose_itip_message($event, $method); $mailto = rcube_idn_to_ascii($recipient['email']); $headers = $message->headers(); $headers['To'] = format_email_recipient($mailto, $recipient['name']); $headers['Subject'] = $this->gettext(array( 'name' => $subject, 'vars' => array( 'title' => $event['title'], 'name' => $this->sender['name'] ) )); // compose a list of all event attendees $attendees_list = array(); foreach ((array)$event['attendees'] as $attendee) { $attendees_list[] = ($attendee['name'] && $attendee['email']) ? $attendee['name'] . ' <' . $attendee['email'] . '>' : ($attendee['name'] ? $attendee['name'] : $attendee['email']); } $mailbody = $this->gettext(array( 'name' => $bodytext, 'vars' => array( 'title' => $event['title'], 'date' => $this->lib->event_date_text($event, true), 'attendees' => join(",\n ", $attendees_list), 'sender' => $this->sender['name'], 'organizer' => $this->sender['name'], ) )); // if (!empty($event['comment'])) { // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment']; // } // append links for direct invitation replies if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) { $mailbody .= "\n\n" . $this->gettext(array( 'name' => 'invitationattendlinks', 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))), )); } else if ($method == 'CANCEL' && $event['cancelled']) { $this->cancel_itip_invitation($event); } $message->headers($headers, true); $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79)); // finally send the message $this->itip_send = true; $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error); $this->itip_send = false; return $sent; } /** * Plugin hook triggered by rcube::deliver_message() before delivering a message. * Here we can set the 'smtp_server' config option to '' in order to use * PHP's mail() function for unauthenticated email sending. */ public function before_send_hook($p) { if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') { $this->rc->config->set('smtp_server', ''); } return $p; } /** * Plugin hook to alter SMTP authentication. * This is used if iTip messages are to be sent from an unauthenticated session */ public function smtp_connect_hook($p) { // replace smtp auth settings if we're not in an authenticated session if ($this->itip_send && !$this->rc->user->ID) { foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) { $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]); } } return $p; } /** * Helper function to build a Mail_mime object to send an iTip message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @return object Mail_mime object with message data */ public function compose_itip_message($event, $method) { $from = rcube_idn_to_ascii($this->sender['email']); $from_utf = rcube_idn_to_utf8($from); $sender = format_email_recipient($from, $this->sender['name']); // truncate list attendees down to the recipient of the iTip Reply. // constraints for a METHOD:REPLY according to RFC 5546 if ($method == 'REPLY') { $replying_attendee = null; $reply_attendees = array(); foreach ($event['attendees'] as $attendee) { if ($attendee['role'] == 'ORGANIZER') { $reply_attendees[] = $attendee; } else if (strcasecmp($attedee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) { $replying_attendee = $attendee; unset($replying_attendee['rsvp']); // unset the RSVP attribute } } if ($replying_attendee) { $reply_attendees[] = $replying_attendee; $event['attendees'] = $reply_attendees; } } // set RSVP=TRUE for every attendee if not set else if ($method == 'REQUEST') { foreach ($event['attendees'] as $i => $attendee) { if (!isset($attendee['rsvp'])) { $event['attendees'][$i]['rsvp']= true; } } } // compose multipart message using PEAR:Mail_Mime $message = new Mail_mime("\r\n"); $message->setParam('text_encoding', 'quoted-printable'); $message->setParam('head_encoding', 'quoted-printable'); $message->setParam('head_charset', RCMAIL_CHARSET); $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed"); $message->setContentType('multipart/alternative'); // compose common headers array $headers = array( 'From' => $sender, 'Date' => $this->rc->user_date(), 'Message-ID' => $this->rc->gen_message_id(), 'X-Sender' => $from, ); if ($agent = $this->rc->config->get('useragent')) { $headers['User-Agent'] = $agent; } $message->headers($headers); // attach ics file for this event $ical = libcalendaring::get_ical(); $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false); $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics'; $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method); return $message; } /** * Handler for calendar/itip-status requests */ public function get_itip_status($event, $existing = null) { $action = $event['rsvp'] ? 'rsvp' : ''; $status = $event['fallback']; $latest = false; $html = ''; if (is_numeric($event['changed'])) $event['changed'] = new DateTime('@'.$event['changed']); // check if the given itip object matches the last state if ($existing) { $latest = (isset($event['sequence']) && $existing['sequence'] == $event['sequence']) || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']); } // determine action for REQUEST if ($event['method'] == 'REQUEST') { $html = html::div('rsvp-status', $this->gettext('acceptinvitation')); if ($existing) { $rsvp = $event['rsvp']; $emails = $this->lib->get_user_emails(); foreach ($existing['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $status = strtoupper($attendee['status']); break; } } } else { $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true); } $status_lc = strtolower($status); if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { $html = html::div('rsvp-status', $this->gettext('notanattendee')); $action = 'import'; } else if (in_array($status_lc, $this->rsvp_status)) { $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc); if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) { $action = ''; // nothing to do here, outdated invitation if ($status_lc == 'needs-action') $status_text = $this->gettext('outdatedinvitation'); } else if (!$existing && !$rsvp) { $action = 'import'; } else if ($latest && $status_lc != 'needs-action') { $action = 'update'; } $html = html::div('rsvp-status ' . $status_lc, $status_text); } } // determine action for REPLY else if ($event['method'] == 'REPLY') { // check whether the sender already is an attendee if ($existing) { $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : ''; $listed = false; foreach ($existing['attendees'] as $attendee) { if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) { $status_lc = strtolower($status); if (in_array($status_lc, $this->rsvp_status)) { $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array( 'name' => 'attendee' . $status_lc, 'vars' => array( 'delegatedto' => Q($attendee['delegated-to'] ?: '?'), ) ))); } $action = $attendee['status'] == $status || !$latest ? '' : 'update'; $listed = true; break; } } if (!$listed) { $html = html::div('rsvp-status', $this->gettext('itipnewattendee')); } } else { $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); $action = ''; } } else if ($event['method'] == 'CANCEL') { if (!$existing) { $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); $action = ''; } } return array( 'uid' => $event['uid'], 'id' => asciiwords($event['uid'], true), 'existing' => $existing ? true : false, 'saved' => $existing ? true : false, 'latest' => $latest, 'status' => $status, 'action' => $action, 'html' => $html, ); } /** * Build inline UI elements for iTip messages */ public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null) { $buttons = array(); $dom_id = asciiwords($event['uid'], true); $rsvp_status = 'unknown'; // pass some metadata about the event and trigger the asynchronous status check $changed = is_object($event['changed']) ? $event['changed'] : $message_date; $metadata = array( 'uid' => $event['uid'], 'changed' => $changed ? $changed->format('U') : 0, 'sequence' => intval($event['sequence']), 'method' => $method, 'task' => $task, ); // create buttons to be activated from async request checking existence of this event in local calendars $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading')); // on iTip REPLY we have two options: if ($method == 'REPLY') { $title = $this->gettext('itipreply'); foreach ($event['attendees'] as $attendee) { if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') { $metadata['attendee'] = $attendee['email']; $rsvp_status = strtoupper($attendee['status']); break; } } // 1. update the attendee status on our copy $update_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updateattendeestatus'), )); // 2. accept or decline a new or delegate attendee $accept_buttons = html::tag('input', array( 'type' => 'button', 'class' => "button accept", 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('acceptattendee'), )); $accept_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button decline", 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('declineattendee'), )); $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons); } // when receiving iTip REQUEST messages: else if ($method == 'REQUEST') { $emails = $this->lib->get_user_emails(); $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation'); $metadata['rsvp'] = true; + if (is_object($event['start'])) { + $metadata['date'] = $event['start']->format('U'); + } // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') { $this->rsvp_actions = array('accepted','declined'); $metadata['nosave'] = true; } // 1. display RSVP buttons (if the user was invited) foreach ($this->rsvp_actions as $method) { $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button $method", 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task', '$method', '$dom_id')", 'value' => $this->gettext('itip' . $method), )); } // add button to open calendar/preview if (!empty($preview_url)) { $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id; $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button preview", 'onclick' => "rcube_libcalendaring.open_itip_preview('" . JQ($preview_url) . "', '" . JQ($msgref) . "')", 'value' => $this->gettext('openpreview'), )); } // 2. update the local copy with minor changes $update_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updatemycopy'), )); // 3. Simply import the event without replying $import_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('importtocalendar'), )); // check my status foreach ($event['attendees'] as $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION'; break; } } // add itip reply message controls $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave'])); $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons); $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); } // for CANCEL messages, we can: else if ($method == 'CANCEL') { $title = $this->gettext('itipcancellation'); // 1. remove the event from our calendar $button_remove = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.remove_from_itip('" . JQ($event['uid']) . "', '$task', '" . JQ($event['title']) . "')", 'value' => $this->gettext('removefromcalendar'), )); // 2. update our copy with status=cancelled $button_update = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updatemycopy'), )); $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update); $rsvp_status = 'CANCELLED'; $metadata['rsvp'] = true; } // append generic import button if ($import_button) { $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button); } // TODO: add option/checkbox to delete this message after update // pass some metadata about the event and trigger the asynchronous status check $metadata['fallback'] = $rsvp_status; $metadata['rsvp'] = intval($metadata['rsvp']); $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . json_serialize($metadata) . ")", 'docready'); // get localized texts from the right domain foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee','declineattendeeconfirm','cancel') as $label) { $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); } // show event details with buttons return $this->itip_object_details_table($event, $title) . html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons)); } /** * Render an RSVP UI widget with buttons to respond on iTip invitations */ function itip_rsvp_buttons($attrib = array(), $actions = null) { $attrib += array('type' => 'button'); if (!$actions) $actions = $this->rsvp_actions; foreach ($actions as $method) { $buttons .= html::tag('input', array( 'type' => $attrib['type'], 'name' => $attrib['iname'], 'class' => 'button', 'rel' => $method, 'value' => $this->gettext('itip' . $method), )); } $buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id'])); return html::div($attrib, html::div('label', $this->gettext('acceptinvitation')) . html::div('rsvp-buttons', $buttons)); } /** * Render UI elements to control iTip reply message sending */ public function itip_rsvp_options_ui($dom_id, $disable = false) { $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3); // itip sending is entirely disabled if ($itip_sending === 0) { return ''; } // add checkbox to suppress itip reply message else if ($itip_sending >= 2) { $rsvp_additions = html::label(array('class' => 'noreply-toggle'), html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0)) . ' ' . $this->gettext('itipsuppressreply') ); } // add input field for reply comment $rsvp_additions .= html::a(array('href' => '#toggle', 'class' => 'reply-comment-toggle'), $this->gettext('itipeditresponse')); $rsvp_additions .= html::div('itip-reply-comment', html::tag('textarea', array('id' => 'reply-comment-'.$dom_id, 'cols' => 40, 'rows' => 6, 'style' => 'display:none', 'placeholder' => $this->gettext('itipcomment')), '') ); return $rsvp_additions; } /** * Render event/task details in a table */ function itip_object_details_table($event, $title) { $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); $table->add('ititle', $title); $table->add('title', Q($event['title'])); if ($event['start'] && $event['end']) { $table->add('label', $this->plugin->gettext('date'), $this->domain); $table->add('date', Q($this->lib->event_date_text($event))); } else if ($event['due'] && $event['_type'] == 'task') { $table->add('label', $this->plugin->gettext('date'), $this->domain); $table->add('date', Q($this->lib->event_date_text($event))); } if ($event['location']) { $table->add('label', $this->plugin->gettext('location'), $this->domain); $table->add('location', Q($event['location'])); } if ($event['comment']) { $table->add('label', $this->plugin->gettext('comment'), $this->domain); $table->add('location', Q($event['comment'])); } return $table->show(); } /** * Create iTIP invitation token for later replies via URL * * @param array Hash array with event properties * @param string Attendee email address * @return string Invitation token */ public function store_invitation($event, $attendee) { // empty stub return false; } /** * Mark invitations for the given event as cancelled * * @param array Hash array with event properties */ public function cancel_itip_invitation($event) { // empty stub return false; } /** * Utility function to get the value of a custom property */ public static function get_custom_property($event, $name) { $ret = false; if (is_array($event['x-custom'])) { array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) { if (strcasecmp($prop[0], $name) === 0) { $ret = $prop[1]; } }); } return $ret; } } diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js index b23a3fee..5b8ad5cd 100644 --- a/plugins/libcalendaring/libcalendaring.js +++ b/plugins/libcalendaring/libcalendaring.js @@ -1,995 +1,1009 @@ /** * Basic Javascript utilities for calendar-related plugins * * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this page. * * Copyright (C) 2012-2014, Kolab Systems AG * * 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 . * * @licend The above is the entire license notice * for the JavaScript code in this page. */ function rcube_libcalendaring(settings) { // member vars this.settings = settings || {}; this.alarm_ids = []; this.alarm_dialog = null; this.snooze_popup = null; this.dismiss_link = null; this.group2expand = {}; // abort if env isn't set if (!settings || !settings.date_format) return; // private vars var me = this; var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0); var client_timezone = new Date().getTimezoneOffset(); // general datepicker settings var datepicker_settings = { // translate from fullcalendar format to datepicker format dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'), firstDay : settings.first_day, dayNamesMin: settings.days_short, monthNames: settings.months, monthNamesShort: settings.months, changeMonth: false, showOtherMonths: true, selectOtherMonths: true }; /** * Quote html entities */ var Q = this.quote_html = function(str) { return String(str).replace(//g, '>').replace(/"/g, '"'); }; /** * Create a nice human-readable string for the date/time range */ this.event_date_text = function(event, voice) { if (!event.start) return ''; if (!event.end) event.end = event.start; var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000, until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — '; if (event.allDay) { fromto = this.format_datetime(event.start, 1, voice) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : ''); } else if (duration < 86400 && event.start.getDay() == event.end.getDay()) { fromto = this.format_datetime(event.start, 0, voice) + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : ''); } else { fromto = this.format_datetime(event.start, 0, voice) + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : ''); } return fromto; }; /** * From time and date strings to a real date object */ this.parse_datetime = function(time, date) { // we use the utility function from datepicker to parse dates var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date(); var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/); if (!isNaN(time_arr[0])) { date.setHours(time_arr[0]); if (time.match(/p[.m]*/i) && date.getHours() < 12) date.setHours(parseInt(time_arr[0]) + 12); else if (time.match(/a[.m]*/i) && date.getHours() == 12) date.setHours(0); } if (!isNaN(time_arr[1])) date.setMinutes(time_arr[1]); return date; } /** * Convert an ISO 8601 formatted date string from the server into a Date object. * Timezone information will be ignored, the server already provides dates in user's timezone. */ this.parseISO8601 = function(s) { // force d to be on check's YMD, for daylight savings purposes var fixDate = function(d, check) { if (+d) { // prevent infinite looping on invalid dates while (d.getDate() != check.getDate()) { d.setTime(+d + (d < check ? 1 : -1) * 3600000); } } } // derived from http://delete.me.uk/2005/03/iso8601.html var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); if (!m) { return null; } var date = new Date(m[1], 0, 1), check = new Date(m[1], 0, 1, 9, 0); if (m[3]) { date.setMonth(m[3] - 1); check.setMonth(m[3] - 1); } if (m[5]) { date.setDate(m[5]); check.setDate(m[5]); } fixDate(date, check); if (m[7]) { date.setHours(m[7]); } if (m[8]) { date.setMinutes(m[8]); } if (m[10]) { date.setSeconds(m[10]); } if (m[12]) { date.setMilliseconds(Number("0." + m[12]) * 1000); } fixDate(date, check); return date; } /** * Turn the given date into an ISO 8601 date string understandable by PHPs strtotime() */ this.date2ISO8601 = function(date) { var zeropad = function(num) { return (num < 10 ? '0' : '') + num; }; return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate()) + 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds()); }; /** * Format the given date object according to user's prefs */ this.format_datetime = function(date, mode, voice) { var res = ''; if (!mode || mode == 1) { res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings); } if (!mode) { res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' '; } if (!mode || mode == 2) { res += this.format_time(date, voice); } return res; } /** * Clone from fullcalendar.js */ this.format_time = function(date, voice) { var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; } var formatters = { s : function(d) { return d.getSeconds() }, ss : function(d) { return zeroPad(d.getSeconds()) }, m : function(d) { return d.getMinutes() }, mm : function(d) { return zeroPad(d.getMinutes()) }, h : function(d) { return d.getHours() % 12 || 12 }, hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, H : function(d) { return d.getHours() }, HH : function(d) { return zeroPad(d.getHours()) }, t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' } }; var i, i2, c, formatter, res = '', format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format']; for (i=0; i < format.length; i++) { c = format.charAt(i); for (i2=Math.min(i+2, format.length); i2 > i; i2--) { if (formatter = formatters[format.substring(i, i2)]) { res += formatter(date); i = i2 - 1; break; } } if (i2 == i) { res += c; } } return res; } /** * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings */ this.date2unixtime = function(date) { var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset); } /** * Turn a unix timestamp value into a Date object */ this.fromunixtime = function(ts) { ts -= gmt_offset * 3600; var date = new Date(ts * 1000), dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; if (dst_offset) // adjust DST offset date.setTime((ts + 3600) * 1000); return date; } /** * Simple plaintext to HTML converter, makig URLs clickable */ this.text2html = function(str, maxlen, maxlines) { var html = Q(String(str)); // limit visible text length if (maxlen) { var morelink = '... '+rcmail.gettext('showmore','libcalendaring')+'', lines = html.split(/\r?\n/), words, out = '', len = 0; for (var i=0; i < lines.length; i++) { len += lines[i].length; if (maxlines && i == maxlines - 1) { out += lines[i] + '\n' + morelink; maxlen = html.length * 2; } else if (len > maxlen) { len = out.length; words = lines[i].split(' '); for (var j=0; j < words.length; j++) { len += words[j].length + 1; out += words[j] + ' '; if (len > maxlen) { out += morelink; maxlen = html.length * 2; maxlines = 0; } } out += '\n'; } else out += lines[i] + '\n'; } if (maxlen > str.length) out += ''; html = out; } // simple link parser (similar to rcube_string_replacer class in PHP) var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})'; var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-'; var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig'); var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig'); var link_replace = function(matches, p1, p2) { var title = '', text = p2; if (p2 && p2.length > 55) { text = p2.substr(0, 45) + '...' + p2.substr(-8); title = p1 + p2; } return ''+p1+text+'' }; return html .replace(link_pattern, link_replace) .replace(mailto_pattern, '$1') .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"') .replace(/\n/g, "
"); }; this.init_alarms_edit = function(prefix, index) { var edit_type = $(prefix+' select.edit-alarm-type'), dom_id = edit_type.attr('id'); // register events on alarm fields edit_type.change(function(){ $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); }); $(prefix+' select.edit-alarm-offset').change(function(){ var mode = $(this).val() == '@' ? 'show' : 'hide'; $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode](); $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show'); }); $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings); $(prefix).on('click', 'a.delete-alarm', function(e){ if ($(this).closest('.edit-alarm-item').siblings().length > 0) { $(this).closest('.edit-alarm-item').remove(); } return false; }); // set a unique id attribute and set label reference accordingly if ((index || 0) > 0 && dom_id) { dom_id += ':' + (new Date().getTime()); edit_type.attr('id', dom_id); $(prefix+' label:first').attr('for', dom_id); } $(prefix).on('click', 'a.add-alarm', function(e){ var i = $(this).closest('.edit-alarm-item').siblings().length + 1; var item = $(this).closest('.edit-alarm-item').clone(false) .removeClass('first') .appendTo(prefix); me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); $('select.edit-alarm-type, select.edit-alarm-offset', item).change(); return false; }); } this.set_alarms_edit = function(prefix, valarms) { $(prefix + ' .edit-alarm-item:gt(0)').remove(); var i, alarm, domnode, val, offset; for (i=0; i < valarms.length; i++) { alarm = valarms[i]; if (!alarm.action) alarm.action = 'DISPLAY'; if (i == 0) { domnode = $(prefix + ' .edit-alarm-item').eq(0); } else { domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix); this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); } $('select.edit-alarm-type', domnode).val(alarm.action); if (String(alarm.trigger).match(/@(\d+)/)) { var ondate = this.fromunixtime(parseInt(RegExp.$1)); $('select.edit-alarm-offset', domnode).val('@'); $('input.edit-alarm-value', domnode).val(''); $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1)); $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2)); } else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) { val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3; $('input.edit-alarm-value', domnode).val(val); $('select.edit-alarm-offset', domnode).val(offset); } } // set correct visibility by triggering onchange handlers $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change(); }; this.serialize_alarms = function(prefix) { var valarms = []; $(prefix + ' .edit-alarm-item').each(function(i, elem) { var val, offset, alarm = { action: $('select.edit-alarm-type', elem).val() }; if (alarm.action) { offset = $('select.edit-alarm-offset', elem).val(); if (offset == '@') { alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val())); } else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) { alarm.trigger = offset[0] + val + offset[1]; } valarms.push(alarm); } }); return valarms; }; /***** Alarms handling *****/ /** * Display a notification for the given pending alarms */ this.display_alarms = function(alarms) { // clear old alert first if (this.alarm_dialog) this.alarm_dialog.dialog('destroy').remove(); this.alarm_dialog = $('
').attr('id', 'alarm-display'); var i, actions, adismiss, asnooze, alarm, html, event_ids = [], buttons = {}; for (i=0; i < alarms.length; i++) { alarm = alarms[i]; alarm.start = this.parseISO8601(alarm.start); alarm.end = this.parseISO8601(alarm.end); event_ids.push(alarm.id); html = '

' + Q(alarm.title) + '

'; html += '
' + Q(alarm.location || '') + '
'; html += '
' + Q(this.event_date_text(alarm)) + '
'; adismiss = $('').html(rcmail.gettext('dismiss','libcalendaring')).click(function(){ me.dismiss_link = $(this); me.dismiss_alarm(me.dismiss_link.data('id'), 0); }); asnooze = $('').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){ me.snooze_dropdown($(this), e); e.stopPropagation(); return false; }); actions = $('
').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id)); $('
').addClass('alarm-item').html(html).append(actions).appendTo(this.alarm_dialog); } buttons[rcmail.gettext('close')] = function() { $(this).dialog('close'); }; buttons[rcmail.gettext('dismissall','libcalendaring')] = function() { // submit dismissed event_ids to server me.dismiss_alarm(me.alarm_ids.join(','), 0); $(this).dialog('close'); }; this.alarm_dialog.appendTo(document.body).dialog({ modal: false, resizable: true, closeOnEscape: false, dialogClass: 'alarms', title: rcmail.gettext('alarmtitle','libcalendaring'), buttons: buttons, open: function() { setTimeout(function() { me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); }, 5); }, close: function() { $('#alarm-snooze-dropdown').hide(); $(this).dialog('destroy').remove(); me.alarm_dialog = null; me.alarm_ids = null; }, drag: function(event, ui) { $('#alarm-snooze-dropdown').hide(); } }); this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog'); this.alarm_ids = event_ids; }; /** * Show a drop-down menu with a selection of snooze times */ this.snooze_dropdown = function(link, event) { if (!this.snooze_popup) { this.snooze_popup = $('#alarm-snooze-dropdown'); // create popup if not found if (!this.snooze_popup.length) { this.snooze_popup = $('
').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body); this.snooze_popup.html(rcmail.env.snooze_select) } $('#alarm-snooze-dropdown a').click(function(e){ var time = String(this.href).replace(/.+#/, ''); me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time); return false; }); } // hide visible popup if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) { rcmail.command('menu-close', 'alarm-snooze-dropdown'); this.dismiss_link = null; } else { // open popup below the clicked link rcmail.command('menu-open', 'alarm-snooze-dropdown', link.get(0), event); this.snooze_popup.data('id', link.data('id')); this.dismiss_link = link; } }; /** * Dismiss or snooze alarms for the given event */ this.dismiss_alarm = function(id, snooze) { rcmail.command('menu-close', 'alarm-snooze-dropdown'); rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } }); // remove dismissed alarm from list if (this.dismiss_link) { this.dismiss_link.closest('div.alarm-item').hide(); var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; }); if (new_ids.length) this.alarm_ids = new_ids; else this.alarm_dialog.dialog('close'); } this.dismiss_link = null; }; /***** Recurrence form handling *****/ /** * Install event handlers on recurrence form elements */ this.init_recurrence_edit = function(prefix) { // toggle recurrence frequency forms $('#edit-recurrence-frequency').change(function(e){ var freq = $(this).val().toLowerCase(); $('.recurrence-form').hide(); if (freq) { $('#recurrence-form-'+freq).show(); if (freq != 'rdate') $('#recurrence-form-until').show(); } }); $('#recurrence-form-rdate input.button.add').click(function(e){ var dt, dv = $('#edit-recurrence-rdate-input').val(); if (dv && (dt = me.parse_datetime('12:00', dv))) { me.add_rdate(dt); me.sort_rdates(); $('#edit-recurrence-rdate-input').val('') } else { $('#edit-recurrence-rdate-input').select(); } }); $('#edit-recurrence-rdates').on('click', 'a.delete', function(e){ $(this).closest('li').remove(); return false; }); $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); $('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); }); $('#edit-recurrence-rdate-input').datepicker(datepicker_settings); }; /** * Set recurrence form according to the given event/task record */ this.set_recurrence_edit = function(rec) { var recurrence = $('#edit-recurrence-frequency').val(rec.recurrence ? rec.recurrence.FREQ || (rec.recurrence.RDATE ? 'RDATE' : '') : '').change(), interval = $('.recurrence-form select.edit-recurrence-interval').val(rec.recurrence ? rec.recurrence.INTERVAL || 1 : 1), rrtimes = $('#edit-recurrence-repeat-times').val(rec.recurrence ? rec.recurrence.COUNT || 1 : 1), rrenddate = $('#edit-recurrence-enddate').val(rec.recurrence && rec.recurrence.UNTIL ? this.format_datetime(this.parseISO8601(rec.recurrence.UNTIL), 1) : ''); $('.recurrence-form input.edit-recurrence-until:checked').prop('checked', false); $('#edit-recurrence-rdates').html(''); var weekdays = ['SU','MO','TU','WE','TH','FR','SA'], rrepeat_id = '#edit-recurrence-repeat-forever'; if (rec.recurrence && rec.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count'; else if (rec.recurrence && rec.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until'; $(rrepeat_id).prop('checked', true); if (rec.recurrence && rec.recurrence.BYDAY && rec.recurrence.FREQ == 'WEEKLY') { var wdays = rec.recurrence.BYDAY.split(','); $('input.edit-recurrence-weekly-byday').val(wdays); } if (rec.recurrence && rec.recurrence.BYMONTHDAY) { $('input.edit-recurrence-monthly-bymonthday').val(String(rec.recurrence.BYMONTHDAY).split(',')); $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']); } if (rec.recurrence && rec.recurrence.BYDAY && (rec.recurrence.FREQ == 'MONTHLY' || rec.recurrence.FREQ == 'YEARLY')) { var byday, section = rec.recurrence.FREQ.toLowerCase(); if ((byday = String(rec.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) { $('#edit-recurrence-'+section+'-prefix').val(byday[1]); $('#edit-recurrence-'+section+'-byday').val(byday[2]); } $('input.edit-recurrence-'+section+'-mode').val(['BYDAY']); } else if (rec.start) { $('#edit-recurrence-monthly-byday').val(weekdays[rec.start.getDay()]); } if (rec.recurrence && rec.recurrence.BYMONTH) { $('input.edit-recurrence-yearly-bymonth').val(String(rec.recurrence.BYMONTH).split(',')); } else if (rec.start) { $('input.edit-recurrence-yearly-bymonth').val([String(rec.start.getMonth()+1)]); } if (rec.recurrence && rec.recurrence.RDATE) { $.each(rec.recurrence.RDATE, function(i,rdate){ me.add_rdate(me.parseISO8601(rdate)); }); } }; /** * Gather recurrence settings from form */ this.serialize_recurrence = function(timestr) { var recurrence = '', freq = $('#edit-recurrence-frequency').val(); if (freq != '') { recurrence = { FREQ: freq, INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val() }; var until = $('input.edit-recurrence-until:checked').val(); if (until == 'count') recurrence.COUNT = $('#edit-recurrence-repeat-times').val(); else if (until == 'until') recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val())); if (freq == 'WEEKLY') { var byday = []; $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); }); if (byday.length) recurrence.BYDAY = byday.join(','); } else if (freq == 'MONTHLY') { var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = []; if (mode == 'BYMONTHDAY') { $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); }); if (bymonday.length) recurrence.BYMONTHDAY = bymonday.join(','); } else recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val(); } else if (freq == 'YEARLY') { var byday, bymonth = []; $('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); }); if (bymonth.length) recurrence.BYMONTH = bymonth.join(','); if ((byday = $('#edit-recurrence-yearly-byday').val())) recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday; } else if (freq == 'RDATE') { recurrence = { RDATE:[] }; // take selected but not yet added date into account if ($('#edit-recurrence-rdate-input').val() != '') { $('#recurrence-form-rdate input.button.add').click(); } $('#edit-recurrence-rdates li').each(function(i, li){ recurrence.RDATE.push($(li).attr('data-value')); }); } } return recurrence; }; // add the given date to the RDATE list this.add_rdate = function(date) { var li = $('
  • ') .attr('data-value', this.date2ISO8601(date)) .html('' + Q(this.format_datetime(date, 1)) + '') .appendTo('#edit-recurrence-rdates'); $('').attr('href', '#del') .addClass('iconbutton delete') .html(rcmail.get_label('delete', 'libcalendaring')) .attr('title', rcmail.get_label('delete', 'libcalendaring')) .appendTo(li); }; // re-sort the list items by their 'data-value' attribute this.sort_rdates = function() { var mylist = $('#edit-recurrence-rdates'), listitems = mylist.children('li').get(); listitems.sort(function(a, b) { var compA = $(a).attr('data-value'); var compB = $(b).attr('data-value'); return (compA < compB) ? -1 : (compA > compB) ? 1 : 0; }) $.each(listitems, function(idx, item) { mylist.append(item); }); }; /***** Attendee form handling *****/ // expand the given contact group into individual event/task attendees this.expand_attendee_group = function(e, add, remove) { var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'), role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected'); this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove } // copy group role from the according form element if (role_select.length) { this.group2expand[id].data.role = role_select.val(); } // register callback handler if (!this._expand_attendee_listener) { this._expand_attendee_listener = this.expand_attendee_callback; rcmail.addEventListener('plugin.expand_attendee_callback', function(result) { me._expand_attendee_listener(result); }); } rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading')); }; // callback from server to expand an attendee group this.expand_attendee_callback = function(result) { var attendee, id = result.id, data = this.group2expand[id], row = $(data.link).closest('tr'); // replace group entry with all members returned by the server if (data && data.adder && result.members && result.members.length) { for (var i=0; i < result.members.length; i++) { attendee = result.members[i]; attendee.role = data.data.role; attendee.cutype = 'INDIVIDUAL'; attendee.status = 'NEEDS-ACTION'; data.adder(attendee, null, row); } if (data.remover) { data.remover(data.link, id) } else { row.remove(); } delete this.group2expand[id]; } else { rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error'); } }; } ////// static methods /** * */ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id) { // ask user to delete the declined event from the local calendar (#1670) var del = false; if (rcmail.env.rsvp_saved && status == 'declined') { del = confirm(rcmail.gettext('itip.declinedeleteconfirm')); } var noreply = 0, comment = ''; if (dom_id) { noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0; if (!noreply) comment = $('#reply-comment-'+dom_id).val(); } rcmail.http_post(task + '/mailimportitip', { _uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _part: mime_id, _folder: $('#itip-saveto').val(), _status: status, _del: del?1:0, _noreply: noreply, _comment: comment }, rcmail.set_busy(true, 'itip.savingdata')); return false; }; /** * */ rcube_libcalendaring.remove_from_itip = function(uid, task, title) { if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) { rcmail.http_post(task + '/itip-remove', { uid: uid }, rcmail.set_busy(true, 'itip.savingdata') ); } }; /** * */ rcube_libcalendaring.decline_attendee_reply = function(mime_id, task) { // show dialog for entering a comment and send to server var html = '
    ' + rcmail.gettext('itip.declineattendeeconfirm') + '
    ' + ''; var dialog, buttons = []; buttons.push({ text: rcmail.gettext('declineattendee', 'itip'), click: function() { rcmail.http_post(task + '/itip-decline-reply', { _uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _part: mime_id, _comment: $('#itip-decline-comment', window.parent.document).val() }, rcmail.set_busy(true, 'itip.savingdata')); dialog.dialog("close"); } }); buttons.push({ text: rcmail.gettext('cancel', 'itip'), click: function() { dialog.dialog('close'); } }); dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, { width: 460, open: function() { $(this).parent().find('.ui-button').first().addClass('mainaction'); $('#itip-decline-comment').focus(); } }); return false; }; /** * */ rcube_libcalendaring.fetch_itip_object_status = function(p) { rcmail.http_post(p.task + '/itip-status', { data: p }); }; /** * */ rcube_libcalendaring.update_itip_object_status = function(p) { rcmail.env.rsvp_saved = p.saved; rcmail.env.itip_existing = p.existing; // hide all elements first $('#itip-buttons-'+p.id+' > div').hide(); $('#rsvp-'+p.id+' .folder-select').remove(); if (p.html) { // append/replace rsvp status display $('#loading-'+p.id).next('.rsvp-status').remove(); $('#loading-'+p.id).hide().after(p.html); } // enable/disable rsvp buttons if (p.action == 'rsvp') { $('#rsvp-'+p.id+' input.button').prop('disabled', false) .filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest); } // show rsvp/import buttons (with calendar selector) $('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select); + + // show itip box appendix after replacing the given placeholders + if (p.append && p.append.selector) { + var elem = $(p.append.selector); + if (p.append.replacements) { + $.each(p.append.replacements, function(k, html) { + elem.html(elem.html().replace(k, html)); + }); + } + else if (p.append.html) { + elem.html(p.append.html) + } + elem.show(); + } }; /** * Callback from server after an iTip message has been processed */ rcube_libcalendaring.itip_message_processed = function(metadata) { if (metadata.after_action) { setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200); } else { rcube_libcalendaring.fetch_itip_object_status(metadata); } }; /** * After-action on iTip request message. Action types: * 0 - no action * 1 - move to Trash * 2 - delete the message * 3 - flag as deleted * folder_name - move the message to the specified folder */ rcube_libcalendaring.itip_after_action = function(action) { if (!action) { return; } var rc = rcmail.is_framed() ? parent.rcmail : rcmail; if (action === 2) { rc.permanently_remove_messages(); } else if (action === 3) { rc.mark_message('delete'); } else { rc.move_messages(action === 1 ? rc.env.trash_mailbox : action); } }; /** * Open the calendar preview for the current iTip event */ rcube_libcalendaring.open_itip_preview = function(url, msgref) { if (!rcmail.env.itip_existing) url += '&itip=' + escape(msgref); var win = rcmail.open_window(url); }; // extend jQuery (function($){ $.fn.serializeJSON = function(){ var json = {}; jQuery.map($(this).serializeArray(), function(n, i) { json[n['name']] = n['value']; }); return json; }; })(jQuery); /* libcalendaring plugin initialization */ window.rcmail && rcmail.addEventListener('init', function(evt) { if (rcmail.env.libcal_settings) { var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings); rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); }); } rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status) .addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status) .addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed); $('.rsvp-buttons').on('click', 'a.reply-comment-toggle', function(e){ $(this).hide().parent().find('textarea').show().focus(); }); });