diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 152cc539..365ce9ea 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -1,2525 +1,2550 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * Copyright (C) 2012, 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 $driver; + public $resources_dir; public $home; // declare public to be used in other classes public $urlbase; public $timezone; public $timezone_offset; public $gmt_offset; public $ical; public $ui; public $defaults = array( 'calendar_default_view' => "agendaWeek", 'calendar_timeslots' => 2, 'calendar_work_start' => 6, 'calendar_work_end' => 18, 'calendar_agenda_range' => 60, 'calendar_agenda_sections' => 'smart', 'calendar_event_coloring' => 0, 'calendar_time_indicator' => true, 'calendar_allow_invite_shared' => false, ); private $ics_parts = array(); /** * Plugin initialization. */ function init() { $this->require_plugin('libcalendaring'); $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->register_task('calendar', 'calendar'); // load calendar configuration $this->load_config(); // load localizations $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print')); $this->timezone = $this->lib->timezone; $this->gmt_offset = $this->lib->gmt_offset; $this->dst_active = $this->lib->dst_active; $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active; require($this->home . '/lib/calendar_ui.php'); $this->ui = new calendar_ui($this); // catch iTIP confirmation requests that don're require a valid session if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) { $this->add_hook('startup', array($this, 'itip_attend_response')); } else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) { $this->add_hook('startup', array($this, 'ical_feed_export')); } else { // default startup routine $this->add_hook('startup', array($this, 'startup')); } } /** * Startup hook */ public function startup($args) { // the calendar module can be enabled/disabled by the kolab_auth plugin if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true)) return; // load Calendar user interface if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) { $this->ui->init(); // settings are required in (almost) every GUI step if ($args['action'] != 'attend') $this->rc->output->set_env('calendar_settings', $this->load_settings()); } if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') { if ($args['action'] != 'upload') { $this->load_driver(); } // register calendar actions $this->register_action('index', array($this, 'calendar_view')); $this->register_action('event', array($this, 'event_action')); $this->register_action('calendar', array($this, 'calendar_action')); $this->register_action('load_events', array($this, 'load_events')); $this->register_action('export_events', array($this, 'export_events')); $this->register_action('import_events', array($this, 'import_events')); $this->register_action('upload', array($this, 'attachment_upload')); $this->register_action('get-attachment', array($this, 'attachment_get')); $this->register_action('freebusy-status', array($this, 'freebusy_status')); $this->register_action('freebusy-times', array($this, 'freebusy_times')); $this->register_action('randomdata', array($this, 'generate_randomdata')); $this->register_action('print', array($this,'print_view')); $this->register_action('mailimportevent', array($this, 'mail_import_event')); $this->register_action('mailtoevent', array($this, 'mail_message2event')); $this->register_action('inlineui', array($this, 'get_inline_ui')); $this->register_action('check-recent', array($this, 'check_recent')); $this->register_action('resources-list', array($this, 'resources_list')); $this->register_action('resources-owner', array($this, 'resources_owner')); $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('message_load', array($this, 'mail_message_load')); $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } // add 'Create event' item to message menu if ($this->api->output->type == 'html') { $this->api->add_content(html::tag('li', null, $this->api->output->button(array( 'command' => 'calendar-create-from-mail', 'label' => 'calendar.createfrommail', 'type' => 'link', 'classact' => 'icon calendarlink active', 'class' => 'icon calendarlink', 'innerclass' => 'icon calendar', ))), 'messagemenu'); $this->api->output->add_label('calendar.createfrommail'); } } else if ($args['task'] == 'addressbook') { if ($this->rc->config->get('calendar_contact_birthdays')) { $this->add_hook('contact_update', array($this, 'contact_update')); $this->add_hook('contact_create', array($this, 'contact_update')); } } // add hooks to display alarms $this->add_hook('pending_alarms', array($this, 'pending_alarms')); $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms')); } /** * Helper method to load the backend driver according to local config */ private function load_driver() { if (is_object($this->driver)) return; $driver_name = $this->rc->config->get('calendar_driver', 'database'); $driver_class = $driver_name . '_driver'; require_once($this->home . '/drivers/calendar_driver.php'); require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); - switch ($driver_name) { - case "kolab": - $this->require_plugin('libkolab'); - default: - $this->driver = new $driver_class($this); - break; - } + $this->driver = new $driver_class($this); - if ($this->driver->undelete) - $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0; + if ($this->driver->undelete) + $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0; } /** * Load iTIP functions */ private function load_itip() { if (!$this->itip) { require_once($this->home . '/lib/calendar_itip.php'); $plugin = $this->rc->plugins->exec_hook('calendar_load_itip', array('identity' => null)); $this->itip = new calendar_itip($this, $plugin['identity']); } return $this->itip; } /** * Load iCalendar functions */ public function get_ical() { if (!$this->ical) { $this->ical = libcalendaring::get_ical(); } return $this->ical; } /** * Get properties of the calendar this user has specified as default */ public function get_default_calendar($writeable = false) { $default_id = $this->rc->config->get('calendar_default_calendar'); $calendars = $this->driver->list_calendars(false, true); $calendar = $calendars[$default_id] ?: null; if (!$calendar || ($writeable && $calendar['readonly'])) { foreach ($calendars as $cal) { if ($cal['default']) { $calendar = $cal; break; } if (!$writeable || !$cal['readonly']) { $first = $cal; } } } return $calendar ?: $first; } /** * Render the main calendar view from skin template */ function calendar_view() { $this->rc->output->set_pagetitle($this->gettext('calendar')); // Add CSS stylesheets to the page header $this->ui->addCSS(); // Add JS files to the page header $this->ui->addJS(); $this->ui->init_templates(); $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close'); // 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('resources', (bool)$this->driver->resources); + $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver')); $this->rc->output->set_env('mscolors', $this->driver->get_color_values()); $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list'))); $view = get_input_value('view', RCUBE_INPUT_GPC); if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $this->rc->output->set_env('view', $view); if ($date = get_input_value('date', RCUBE_INPUT_GPC)) $this->rc->output->set_env('date', $date); $this->rc->output->send("calendar.calendar"); } /** * Handler for preferences_sections_list hook. * Adds Calendar settings sections into preferences sections list. * * @param array Original parameters * @return array Modified parameters */ function preferences_sections_list($p) { $p['list']['calendar'] = array( 'id' => 'calendar', 'section' => $this->gettext('calendar'), ); return $p; } /** * Handler for preferences_list hook. * Adds options blocks into Calendar settings sections in Preferences. * * @param array Original parameters * @return array Modified parameters */ function preferences_list($p) { if ($p['section'] != 'calendar') { return $p; } $no_override = array_flip((array)$this->rc->config->get('dont_override')); $p['blocks']['view']['name'] = $this->gettext('mainoptions'); if (!isset($no_override['calendar_default_view'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_default_view'; $select = new html_select(array('name' => '_default_view', 'id' => $field_id)); $select->add($this->gettext('day'), "agendaDay"); $select->add($this->gettext('week'), "agendaWeek"); $select->add($this->gettext('month'), "month"); $select->add($this->gettext('agenda'), "table"); $p['blocks']['view']['options']['default_view'] = array( 'title' => html::label($field_id, Q($this->gettext('default_view'))), 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])), ); } if (!isset($no_override['calendar_timeslots'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_timeslot'; $choices = array('1', '2', '3', '4', '6'); $select = new html_select(array('name' => '_timeslots', 'id' => $field_id)); $select->add($choices); $p['blocks']['view']['options']['timeslots'] = array( 'title' => html::label($field_id, Q($this->gettext('timeslots'))), 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))), ); } if (!isset($no_override['calendar_first_day'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_firstday'; $select = new html_select(array('name' => '_first_day', 'id' => $field_id)); $select->add(rcube_label('sunday'), '0'); $select->add(rcube_label('monday'), '1'); $select->add(rcube_label('tuesday'), '2'); $select->add(rcube_label('wednesday'), '3'); $select->add(rcube_label('thursday'), '4'); $select->add(rcube_label('friday'), '5'); $select->add(rcube_label('saturday'), '6'); $p['blocks']['view']['options']['first_day'] = array( 'title' => html::label($field_id, Q($this->gettext('first_day'))), 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))), ); } if (!isset($no_override['calendar_first_hour'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']))); $select_hours = new html_select(); for ($h = 0; $h < 24; $h++) $select_hours->add(date($time_format, mktime($h, 0, 0)), $h); $field_id = 'rcmfd_firsthour'; $p['blocks']['view']['options']['first_hour'] = array( 'title' => html::label($field_id, Q($this->gettext('first_hour'))), 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)), ); } if (!isset($no_override['calendar_work_start'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_workstart'; $p['blocks']['view']['options']['workinghours'] = array( 'title' => html::label($field_id, Q($this->gettext('workinghours'))), 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) . ' — ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)), ); } if (!isset($no_override['calendar_event_coloring'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_coloring'; $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id)); $select_colors->add($this->gettext('coloringmode0'), 0); $select_colors->add($this->gettext('coloringmode1'), 1); $select_colors->add($this->gettext('coloringmode2'), 2); $select_colors->add($this->gettext('coloringmode3'), 3); $p['blocks']['view']['options']['eventcolors'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))), 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])), ); } // loading driver is expensive, don't do it if not needed $this->load_driver(); if (!isset($no_override['calendar_default_alarm_type'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_alarm'; $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); $select_type->add($this->gettext('none'), ''); foreach ($this->driver->alarm_types as $type) $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); $p['blocks']['view']['options']['alarmtype'] = array( 'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))), 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')), ); } if (!isset($no_override['calendar_default_alarm_offset'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_alarm'; $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); $preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); $p['blocks']['view']['options']['alarmoffset'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))), 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } if (!isset($no_override['calendar_default_calendar'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } // default calendar selection $field_id = 'rcmfd_default_calendar'; $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true)); foreach ((array)$this->driver->list_calendars(false, true) as $id => $prop) { $select_cal->add($prop['name'], strval($id)); if ($prop['default']) $default_calendar = $id; } $p['blocks']['view']['options']['defaultcalendar'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))), 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)), ); } // category definitions if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) { $p['blocks']['categories']['name'] = $this->gettext('categories'); if (!$p['current']) { $p['blocks']['categories']['content'] = true; return $p; } $categories = (array) $this->driver->list_categories(); $categories_list = ''; foreach ($categories as $name => $color) { $key = md5($name); $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name); $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category'))); $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable)); $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6)); $hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : ''; $categories_list .= html::div(null, $hidden . $category_name->show($name) . ' ' . $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'); // include color picker $this->include_script('lib/js/jquery.miniColors.min.js'); $this->include_stylesheet($this->local_skin_path() . '/jquery.miniColors.css'); $this->rc->output->set_env('mscolors', $this->driver->get_color_values()); $this->rc->output->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready'); } // virtual birthdays calendar if (!isset($no_override['calendar_contact_birthdays'])) { $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar'); if (!$p['current']) { $p['blocks']['birthdays']['content'] = true; return $p; } $field_id = 'rcmfd_contact_birthdays'; $input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)')); $p['blocks']['birthdays']['options']['contact_birthdays'] = array( 'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')), 'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0), ); $input_attrib = array( 'class' => 'calendar_birthday_props', 'disabled' => !$this->rc->config->get('calendar_contact_birthdays'), ); $sources = array(); $checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib); foreach ($this->rc->get_address_sources(false, true) as $source) { $active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : ''; $sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . ' ' . 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, ); // categories if (!$this->driver->nocategories) { $old_categories = $new_categories = array(); foreach ($this->driver->list_categories() as $name => $color) { $old_categories[md5($name)] = $name; } $categories = (array) get_input_value('_categories', RCUBE_INPUT_POST); $colors = (array) get_input_value('_colors', RCUBE_INPUT_POST); foreach ($categories as $key => $name) { $color = preg_replace('/^#/', '', strval($colors[$key])); // rename categories in existing events -> driver's job if ($oldname = $old_categories[$key]) { $this->driver->replace_category($oldname, $name, $color); unset($old_categories[$key]); } else $this->driver->add_category($name, $color); $new_categories[$name] = $color; } // these old categories have been removed, alter events accordingly -> driver's job foreach ((array)$old_categories[$key] as $key => $name) { $this->driver->remove_category($name); } $p['prefs']['calendar_categories'] = $new_categories; } } return $p; } /** * Dispatcher for calendar actions initiated by the client */ function calendar_action() { $action = get_input_value('action', RCUBE_INPUT_GPC); $cal = get_input_value('c', RCUBE_INPUT_GPC); $success = $reload = false; if (isset($cal['showalarms'])) $cal['showalarms'] = intval($cal['showalarms']); switch ($action) { case "form-new": case "form-edit": echo $this->ui->calendar_editform($action, $cal); exit; case "new": $success = $this->driver->create_calendar($cal); $reload = true; break; case "edit": $success = $this->driver->edit_calendar($cal); $reload = true; break; case "remove": if ($success = $this->driver->remove_calendar($cal)) $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id'])); break; case "subscribe": if (!$this->driver->subscribe_calendar($cal)) $this->rc->output->show_message($this->gettext('errorsaving'), 'error'); return; } if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else { $error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :''); $this->rc->output->show_message($error_msg, 'error'); } $this->rc->output->command('plugin.unlock_saving'); if ($success && $reload) $this->rc->output->command('plugin.reload_view'); } /** * Dispatcher for event actions initiated by the client */ function event_action() { $action = get_input_value('action', RCUBE_INPUT_GPC); $event = get_input_value('e', RCUBE_INPUT_POST, true); $success = $reload = $got_msg = false; // don't notify if modifying a recurring instance (really?) if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify']) unset($event['_notify']); // read old event data in order to find changes if (($event['_notify'] || $event['decline']) && $action != 'new') $old = $this->driver->get_event($event); switch ($action) { case "new": // create UID for new event $event['uid'] = $this->generate_uid(); $this->prepare_event($event, $action); if ($success = $this->driver->new_event($event)) { $event['id'] = $event['uid']; $this->cleanup_event($event); } $reload = $success && $event['recurrence'] ? 2 : 1; break; case "edit": $this->prepare_event($event, $action); if ($success = $this->driver->edit_event($event)) $this->cleanup_event($event); $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; break; case "resize": $this->prepare_event($event, $action); $success = $this->driver->resize_event($event); $reload = $event['_savemode'] ? 2 : 1; break; case "move": $this->prepare_event($event, $action); $success = $this->driver->move_event($event); $reload = $success && $event['_savemode'] ? 2 : 1; break; case "remove": // remove previous deletes $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0; $this->rc->session->remove('calendar_event_undo'); // search for event if only UID is given if (!isset($event['calendar']) && $event['uid']) { if (!($event = $this->driver->get_event($event, true))) { break; } $undo_time = 0; } $success = $this->driver->remove_event($event, $undo_time < 1); $reload = (!$success || $event['_savemode']) ? 2 : 1; if ($undo_time > 0 && $success) { $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event); // display message with Undo link. $msg = html::span(null, $this->gettext('successremoval')) . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo')); $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time); $got_msg = true; } else if ($success) { $this->rc->output->show_message('calendar.successremoval', 'confirmation'); $got_msg = true; } // send iTIP reply that participant has declined the event if ($success && $event['decline']) { $emails = $this->get_user_emails(); foreach ($old['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $attendee; else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $old['attendees'][$i]['status'] = 'DECLINED'; $reply_sender = $attendee['email']; } } $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } break; case "undo": // Restore deleted event $event = $_SESSION['calendar_event_undo']['data']; if ($event) $success = $this->driver->restore_event($event); if ($success) { $this->rc->session->remove('calendar_event_undo'); $this->rc->output->show_message('calendar.successrestore', 'confirmation'); $got_msg = true; $reload = 2; } break; case "rsvp-status": $action = 'rsvp'; $status = $event['fallback']; $latest = false; $html = html::div('rsvp-status', $status != 'CANCELLED' ? $this->gettext('acceptinvitation') : ''); if (is_numeric($event['changed'])) $event['changed'] = new DateTime('@'.$event['changed']); $this->load_driver(); if ($existing = $this->driver->get_event($event, true, false, true)) { $latest = ($event['sequence'] && $existing['sequence'] == $event['sequence']) || (!$event['sequence'] && $existing['changed'] && $existing['changed'] >= $event['changed']); $emails = $this->get_user_emails(); foreach ($existing['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $status = $attendee['status']; break; } } } else { // get a list of writeable calendars $calendars = $this->driver->list_calendars(false, true); $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto', 'is_escaped' => true)); $numcals = 0; foreach ($calendars as $calendar) { if (!$calendar['readonly']) { $calendar_select->add($calendar['name'], $calendar['id']); $numcals++; } } if ($numcals <= 1) $calendar_select = null; } if ($status == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { $html = html::div('rsvp-status', $this->gettext('notanattendee')); $action = 'import'; } else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) { $html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status))); if ($existing['sequence'] > $event['sequence'] || (!$event['sequence'] && $existing['changed'] && $existing['changed'] > $event['changed'])) { $action = ''; // nothing to do here, outdated invitation } } $default_calendar = $calendar_select ? $this->get_default_calendar(true) : null; $this->rc->output->command('plugin.update_event_rsvp_status', array( 'uid' => $event['uid'], 'id' => asciiwords($event['uid'], true), 'saved' => $existing ? true : false, 'latest' => $latest, 'status' => $status, 'action' => $action, 'html' => $html, 'select' => $calendar_select ? html::span('calendar-select', $this->gettext('saveincalendar') . ' ' . $calendar_select->show($this->rc->config->get('calendar_default_calendar', $default_calendar['id']))) : '', )); return; case "rsvp": $ev = $this->driver->get_event($event); $ev['attendees'] = $event['attendees']; $event = $ev; if ($success = $this->driver->edit_event($event)) { $status = get_input_value('status', RCUBE_INPUT_GPC); $organizer = null; foreach ($event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; break; } } $itip = $this->load_itip(); if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } break; case "dismiss": $event['ids'] = explode(',', $event['id']); $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event); $success = $plugin['success']; foreach ($event['ids'] as $id) { if (strpos($id, 'cal:') === 0) $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']); } break; } // show confirmation/error message if (!$got_msg) { if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else $this->rc->output->show_message('calendar.errorsaving', 'error'); } // send out notifications if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) { // make sure we have the complete record $event = $action == 'remove' ? $old : $this->driver->get_event($event); // only notify if data really changed (TODO: do diff check on client already) if (!$old || $action == 'remove' || self::event_diff($event, $old)) { $sent = $this->notify_attendees($event, $old, $action); if ($sent > 0) $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); else if ($sent < 0) $this->rc->output->show_message('calendar.errornotifying', 'error'); } } // unlock client $this->rc->output->command('plugin.unlock_saving'); // update event object on the client or trigger a complete refretch if too complicated if ($reload) { $args = array('source' => $event['calendar']); if ($reload > 1) $args['refetch'] = true; else if ($success && $action != 'remove') $args['update'] = $this->_client_event($this->driver->get_event($event)); $this->rc->output->command('plugin.refresh_calendar', $args); } } /** * Handler for load-requests from fullcalendar * This will return pure JSON formatted output */ function load_events() { $events = $this->driver->load_events( get_input_value('start', RCUBE_INPUT_GET), get_input_value('end', RCUBE_INPUT_GET), ($query = get_input_value('q', RCUBE_INPUT_GET)), get_input_value('source', RCUBE_INPUT_GET) ); echo $this->encode($events, !empty($query)); exit; } /** * Handler for keep-alive requests * This will check for updated data in active calendars and sync them to the client */ public function refresh($attr) { // refresh the entire calendar every 10th time to also sync deleted events if (rand(0,10) == 10) { $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true)); return; } foreach ($this->driver->list_calendars(true) as $cal) { $events = $this->driver->load_events( get_input_value('start', RCUBE_INPUT_GET), get_input_value('end', RCUBE_INPUT_GET), get_input_value('q', RCUBE_INPUT_GET), $cal['id'], 1, $attr['last'] ); foreach ($events as $event) { $this->rc->output->command('plugin.refresh_calendar', array('source' => $cal['id'], 'update' => $this->_client_event($event))); } } } /** * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. * This will check for pending notifications and pass them to the client */ public function pending_alarms($p) { $this->load_driver(); $time = $p['time'] ?: time(); if ($alarms = $this->driver->pending_alarms($time)) { foreach ($alarms as $alarm) { $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal: $p['alarms'][] = $alarm; } } // get alarms for birthdays calendar if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') { $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db'); foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) { $alarm = libcalendaring::get_next_alarm($e); // overwrite alarm time with snooze value (or null if dismissed) if ($dismissed = $cache->get($e['id'])) $alarm['time'] = $dismissed['notifyat']; // add to list if alarm is set if ($alarm && $alarm['time'] && $alarm['time'] <= $time) { $e['id'] = 'cal:bday:' . $e['id']; $e['notifyat'] = $alarm['time']; $p['alarms'][] = $e; } } } return $p; } /** * Handler for alarm dismiss hook triggered by libcalendaring */ public function dismiss_alarms($p) { $this->load_driver(); foreach ((array)$p['ids'] as $id) { if (strpos($id, 'cal:bday:') === 0) { $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']); } else if (strpos($id, 'cal:') === 0) { $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']); } } return $p; } /** * Handler for check-recent requests which are accidentally sent to calendar taks */ function check_recent() { // NOP $this->rc->output->send(); } /** * Hook triggered when a contact is saved */ function contact_update($p) { // clear birthdays calendar cache if (!empty($p['record']['birthday'])) { $cache = $this->rc->get_cache('calendar.birthdays', 'db'); $cache->remove(); } } /** * */ function import_events() { // Upload progress update if (!empty($_GET['_progress'])) { rcube_upload_progress(); } @set_time_limit(0); // process uploaded file if there is no error $err = $_FILES['_data']['error']; if (!$err && $_FILES['_data']['tmp_name']) { $calendar = get_input_value('calendar', RCUBE_INPUT_GPC); $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0; // extract zip file if ($_FILES['_data']['type'] == 'application/zip') { $count = 0; if (class_exists('ZipArchive', false)) { $zip = new ZipArchive(); if ($zip->open($_FILES['_data']['tmp_name'])) { $randname = uniqid('zip-' . session_id(), true); $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname; mkdir($tmpdir, 0700); // extract each ical file from the archive and import it for ($i = 0; $i < $zip->numFiles; $i++) { $filename = $zip->getNameIndex($i); if (preg_match('/\.ics$/i', $filename)) { $tmpfile = $tmpdir . '/' . basename($filename); if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) { $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors); unlink($tmpfile); } } } rmdir($tmpdir); $zip->close(); } else { $errors = 1; $msg = 'Failed to open zip file.'; } } else { $errors = 1; $msg = 'Zip files are not supported for import.'; } } else { // attempt to import teh uploaded file directly $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors); } if ($count) { $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation'); $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true)); } else if (!$errors) { $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); $this->rc->output->command('plugin.import_success', array('source' => $calendar)); } else { $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : ''))); } } else { if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array( 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else { $msg = rcube_label('fileuploaderror'); } $this->rc->output->command('plugin.import_error', array('message' => $msg)); } $this->rc->output->send('iframe'); } /** * Helper function to parse and import a single .ics file */ private function import_from_file($filepath, $calendar, $rangestart, &$errors) { $user_email = $this->rc->user->get_username(); $ical = $this->get_ical(); $errors = !$ical->fopen($filepath); $count = $i = 0; foreach ($ical as $event) { // keep the browser connection alive on long import jobs if (++$i > 100 && $i % 100 == 0) { echo ""; ob_flush(); } // TODO: correctly handle recurring events which start before $rangestart if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) continue; $event['_owner'] = $user_email; $event['calendar'] = $calendar; if ($this->driver->new_event($event)) { $count++; } else { $errors++; } } return $count; } /** * Construct the ics file for exporting events to iCalendar format; */ function export_events($terminate = true) { $start = get_input_value('start', RCUBE_INPUT_GET); $end = get_input_value('end', RCUBE_INPUT_GET); if (!isset($start)) $start = 'today -1 year'; if (!is_numeric($start)) $start = strtotime($start . ' 00:00:00'); if (!$end) $end = 'today +10 years'; if (!is_numeric($end)) $end = strtotime($end . ' 23:59:59'); $attachments = get_input_value('attachments', RCUBE_INPUT_GET); $calid = $calname = get_input_value('source', RCUBE_INPUT_GET); $calendars = $this->driver->list_calendars(); if ($calendars[$calid]) { $calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid; $calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii if (empty($calname)) $calname = $calid; $events = $this->driver->load_events($start, $end, null, $calid, 0); } else $events = array(); header("Content-Type: text/calendar"); header("Content-Disposition: inline; filename=".$calname.'.ics'); $this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null); if ($terminate) exit; } /** * Handler for iCal feed requests */ function ical_feed_export() { // process HTTP auth info if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host() $auth = $this->rc->plugins->exec_hook('authenticate', array( 'host' => $this->rc->autoselect_host(), 'user' => trim($_SERVER['PHP_AUTH_USER']), 'pass' => $_SERVER['PHP_AUTH_PW'], 'cookiecheck' => true, 'valid' => true, )); if ($auth['valid'] && !$auth['abort']) $this->rc->login($auth['user'], $auth['pass'], $auth['host']); } // require HTTP auth if (empty($_SESSION['user_id'])) { header('WWW-Authenticate: Basic realm="Roundcube Calendar"'); header('HTTP/1.0 401 Unauthorized'); exit; } // decode calendar feed hash $format = 'ics'; $calhash = get_input_value('_cal', RCUBE_INPUT_GET); if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) { $format = strtolower($m[1]); $calhash = preg_replace($suff_regex, '', $calhash); } if (!strpos($calhash, ':')) $calhash = base64_decode($calhash); list($user, $_GET['source']) = explode(':', $calhash, 2); // sanity check user if ($this->rc->user->get_username() == $user) { $this->load_driver(); $this->export_events(false); } else { header('HTTP/1.0 404 Not Found'); } // don't save session data session_destroy(); exit; } /** * */ function load_settings() { $this->lib->load_settings(); $this->defaults += $this->lib->defaults; $settings = array(); // configuration $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar'); $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']); $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']); $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']); $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']); $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']); $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']); $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']); // get user identity to create default attendee if ($this->ui->screen == 'calendar') { foreach ($this->rc->user->list_identities() as $rec) { if (!$identity) $identity = $rec; $identity['emails'][] = $rec['email']; $settings['identities'][$rec['identity_id']] = $rec['email']; } $identity['emails'][] = $this->rc->user->get_username(); $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails']))); } return $settings; } /** * Encode events as JSON * * @param array Events as array * @param boolean Add CSS class names according to calendar and categories * @return string JSON encoded events */ function encode($events, $addcss = false) { $json = array(); foreach ($events as $event) { $json[] = $this->_client_event($event, $addcss); } return json_encode($json); } /** * Convert an event object to be used on the client */ private function _client_event($event, $addcss = false) { // compose a human readable strings for alarms_text and recurrence_text if ($event['alarms']) $event['alarms_text'] = libcalendaring::alarms_text($event['alarms']); if ($event['recurrence']) { $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']); if ($event['recurrence']['UNTIL']) $event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c'); unset($event['recurrence']['EXCEPTIONS']); } foreach ((array)$event['attachments'] as $k => $attachment) { $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); } // check for organizer in attendees list $organizer = null; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; break; } } if ($organizer === null && !empty($event['organizer'])) { $organizer = $event['organizer']; $organizer['role'] = 'ORGANIZER'; if (!is_array($event['attendees'])) $event['attendees'] = array(); array_unshift($event['attendees'], $organizer); } // mapping url => vurl because of the fullcalendar client script $event['vurl'] = $event['url']; unset($event['url']); return array( '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'), 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'), // 'changed' might be empty for event recurrences (Bug #2185) 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null, 'title' => strval($event['title']), 'description' => strval($event['description']), 'location' => strval($event['location']), 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true), 'allDay' => ($event['allday'] == 1), ) + $event; } /** * Render localized text describing the recurrence rule of an event */ private function _recurrence_text($rrule) { // derive missing FREQ and INTERVAL from RDATE list if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { $first = $rrule['RDATE'][0]; $second = $rrule['RDATE'][1]; if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) { $diff = $first->diff($second); foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) { if ($diff->$k != 0) { $rrule['FREQ'] = $freq; $rrule['INTERVAL'] = $diff->$k; break; } } } $rrule['UNTIL'] = end($rrule['RDATE']); } // TODO: finish this $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']); $details = ''; switch ($rrule['FREQ']) { case 'DAILY': $freq .= $this->gettext('days'); break; case 'WEEKLY': $freq .= $this->gettext('weeks'); break; case 'MONTHLY': $freq .= $this->gettext('months'); break; case 'YEARLY': $freq .= $this->gettext('years'); break; } if ($rrule['INTERVAL'] <= 1) $freq = $this->gettext(strtolower($rrule['FREQ'])); if ($rrule['COUNT']) $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); else if ($rrule['UNTIL']) $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']))); else $until = $this->gettext('forever'); return rtrim($freq . $details . ', ' . $until); } /** * Generate a unique identifier for an event */ public function generate_uid() { return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); } /** * TEMPORARY: generate random event data for testing * Create events by opening http:///?_task=calendar&_action=randomdata&_num=500 */ public function generate_randomdata() { $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100; $cats = array_keys($this->driver->list_categories()); $cals = $this->driver->list_calendars(true); $count = 0; while ($count++ < $num) { $start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300; $duration = round(rand(30, 360) / 30) * 30 * 60; $allday = rand(0,20) > 18; $alarm = rand(-30,12) * 5; $fb = rand(0,2); if (date('G', $start) > 23) $start -= 3600; if ($allday) { $start = strtotime(date('Y-m-d 00:00:00', $start)); $duration = 86399; } $title = ''; $len = rand(2, 12); $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise."); // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"; for ($i = 0; $i < $len; $i++) $title .= $words[rand(0,count($words)-1)] . " "; $this->driver->new_event(array( 'uid' => $this->generate_uid(), 'start' => new DateTime('@'.$start), 'end' => new DateTime('@'.($start + $duration)), 'allday' => $allday, 'title' => rtrim($title), 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), 'categories' => $cats[array_rand($cats)], 'calendar' => array_rand($cals), 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '', 'priority' => rand(0,9), )); } $this->rc->output->redirect(''); } /** * Handler for attachments upload */ public function attachment_upload() { $this->lib->attachment_upload(self::SESSION_KEY, 'cal:'); } /** * Handler for attachments download/displaying */ public function attachment_get() { // show loading page if (!empty($_GET['_preload'])) { return $this->lib->attachment_loading_page(); } $event_id = get_input_value('_event', RCUBE_INPUT_GPC); $calendar = get_input_value('_cal', RCUBE_INPUT_GPC); $id = get_input_value('_id', RCUBE_INPUT_GPC); $event = array('id' => $event_id, 'calendar' => $calendar); $attachment = $this->driver->get_attachment($id, $event); // show part page if (!empty($_GET['_frame'])) { $this->lib->attachment = $attachment; $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame')); $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header')); $this->rc->output->send('calendar.attachment'); } // deliver attachment content else if ($attachment) { $attachment['body'] = $this->driver->get_attachment_body($id, $event); $this->lib->attachment_get($attachment); } // if we arrive here, the requested part was not found header('HTTP/1.1 404 Not Found'); exit; } /** * Prepares new/edited event properties before save */ private function prepare_event(&$event, $action) { // convert dates into DateTime objects in user's current timezone $event['start'] = new DateTime($event['start'], $this->timezone); $event['end'] = new DateTime($event['end'], $this->timezone); // start/end is all we need for 'move' action (#1480) if ($action == 'move') { return; } if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL'])) $event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone); $attachments = array(); $eventid = 'cal:'.$event['id']; if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) { if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) { foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) { if (is_array($event['attachments']) && in_array($id, $event['attachments'])) { $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment); } } } } $event['attachments'] = $attachments; // check for organizer in attendees if ($action == 'new' || $action == 'edit') { if (!$event['attendees']) $event['attendees'] = array(); $emails = $this->get_user_emails(); $organizer = $owner = false; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $i; if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails)) $owner = $i; else if (!isset($attendee['rsvp'])) $event['attendees'][$i]['rsvp'] = true; } // set new organizer identity if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) { $event['attendees'][$organizer]['name'] = $identity['name']; $event['attendees'][$organizer]['email'] = $identity['email']; } // set owner as organizer if yet missing if ($organizer === false && $owner !== false) { $event['attendees'][$owner]['role'] = 'ORGANIZER'; unset($event['attendees'][$owner]['rsvp']); } else if ($organizer === false && $action == 'new' && ($identity = $this->rc->user->get_identity($event['_identity'])) && $identity['email']) { array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED')); } } // mapping url => vurl because of the fullcalendar client script 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') { if ($action == 'remove') { $event['cancelled'] = true; $is_cancelled = true; } $itip = $this->load_itip(); $emails = $this->get_user_emails(); // compose multipart message using PEAR:Mail_Mime $method = $action == 'remove' ? 'CANCEL' : 'REQUEST'; $message = $itip->compose_itip_message($event, $method); // list existing attendees from $old event $old_attendees = array(); foreach ((array)$old['attendees'] as $attendee) { $old_attendees[] = $attendee['email']; } // send to every attendee $sent = 0; foreach ((array)$event['attendees'] as $attendee) { // skip myself for obvious reasons if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) continue; // which template to use for mail text $is_new = !in_array($attendee['email'], $old_attendees); $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody'); $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty')); // finally send the message if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message)) $sent++; else $sent = -100; } return $sent; } /** * Echo simple free/busy status text for the given user and time range */ public function freebusy_status() { $email = get_input_value('email', RCUBE_INPUT_GPC); $start = get_input_value('start', RCUBE_INPUT_GPC); $end = get_input_value('end', RCUBE_INPUT_GPC); // convert dates into unix timestamps if (!empty($start) && !is_numeric($start)) { $dts = new DateTime($start, $this->timezone); $start = $dts->format('U'); } if (!empty($end) && !is_numeric($end)) { $dte = new DateTime($end, $this->timezone); $end = $dte->format('U'); } if (!$start) $start = time(); if (!$end) $end = $start + 3600; $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE'); $status = 'UNKNOWN'; // if the backend has free-busy information $fblist = $this->driver->get_freebusy_list($email, $start, $end); if (is_array($fblist)) { $status = 'FREE'; foreach ($fblist as $slot) { list($from, $to, $type) = $slot; if ($from < $end && $to > $start) { $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY'; break; } } } // let this information be cached for 5min send_future_expire_header(300); echo $status; exit; } /** * Return a list of free/busy time slots within the given period * Echo data in JSON encoding */ public function freebusy_times() { $email = get_input_value('email', RCUBE_INPUT_GPC); $start = get_input_value('start', RCUBE_INPUT_GPC); $end = get_input_value('end', RCUBE_INPUT_GPC); $interval = intval(get_input_value('interval', RCUBE_INPUT_GPC)); $strformat = $interval > 60 ? 'Ymd' : 'YmdHis'; // convert dates into unix timestamps if (!empty($start) && !is_numeric($start)) { $dts = new DateTime($start, $this->timezone); $start = $dts->format('U'); } if (!empty($end) && !is_numeric($end)) { $dte = new DateTime($end, $this->timezone); $end = $dte->format('U'); } if (!$start) $start = time(); if (!$end) $end = $start + 86400 * 30; if (!$interval) $interval = 60; // 1 hour if (!$dte) { $dts = new DateTime('@'.$start); $dts->setTimezone($this->timezone); } $fblist = $this->driver->get_freebusy_list($email, $start, $end); $slots = array(); // build a list from $start till $end with blocks representing the fb-status for ($s = 0, $t = $start; $t <= $end; $s++) { $status = self::FREEBUSY_UNKNOWN; $t_end = $t + $interval * 60; $dt = new DateTime('@'.$t); $dt->setTimezone($this->timezone); // determine attendee's status if (is_array($fblist)) { $status = self::FREEBUSY_FREE; foreach ($fblist as $slot) { list($from, $to, $type) = $slot; if ($from < $t_end && $to > $t) { $status = isset($type) ? $type : self::FREEBUSY_BUSY; if ($status == self::FREEBUSY_BUSY) // can't get any worse :-) break; } } } $slots[$s] = $status; $times[$s] = intval($dt->format($strformat)); $t = $t_end; } $dte = new DateTime('@'.$t_end); $dte->setTimezone($this->timezone); // let this information be cached for 5min send_future_expire_header(300); echo json_encode(array( 'email' => $email, 'start' => $dts->format('c'), 'end' => $dte->format('c'), 'interval' => $interval, 'slots' => $slots, 'times' => $times, )); exit; } /** * Handler for printing calendars */ public function print_view() { $title = $this->gettext('print'); $view = get_input_value('view', RCUBE_INPUT_GPC); if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $view = 'agendaDay'; $this->rc->output->set_env('view',$view); if ($date = get_input_value('date', RCUBE_INPUT_GPC)) $this->rc->output->set_env('date', $date); if ($range = get_input_value('range', RCUBE_INPUT_GPC)) $this->rc->output->set_env('listRange', intval($range)); if (isset($_REQUEST['sections'])) $this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC)); if ($search = get_input_value('search', RCUBE_INPUT_GPC)) { $this->rc->output->set_env('search', $search); $title .= ' "' . $search . '"'; } // Add CSS stylesheets to the page header $skin_path = $this->local_skin_path(); $this->include_stylesheet($skin_path . '/fullcalendar.css'); $this->include_stylesheet($skin_path . '/print.css'); // Add JS files to the page header $this->include_script('print.js'); $this->include_script('lib/js/fullcalendar.js'); $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css')); $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list')); $this->rc->output->set_pagetitle($title); $this->rc->output->send("calendar.print"); } /** * */ public function get_inline_ui() { foreach (array('save','cancel','savingdata') as $label) $texts['calendar.'.$label] = $this->gettext($label); $texts['calendar.new_event'] = $this->gettext('createfrommail'); $this->ui->init_templates(); $this->ui->calendar_list(); # set env['calendars'] echo $this->api->output->parse('calendar.eventedit', false, false); echo html::tag('script', array('type' => 'text/javascript'), "rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n". "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n". "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n". "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n". "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n". "rcmail.add_label(" . json_encode($texts) . ");\n" ); exit; } /** * Compare two event objects and return differing properties * * @param array Event A * @param array Event B * @return array List of differing event properties */ public static function event_diff($a, $b) { $diff = array(); $ignore = array('changed' => 1, 'attachments' => 1); foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { if (!$ignore[$key] && $a[$key] != $b[$key]) $diff[] = $key; } // only compare number of attachments if (count($a['attachments']) != count($b['attachments'])) $diff[] = 'attachments'; return $diff; } /**** 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('_id', rcube_utils::INPUT_GPC); $maxnum = (int)$this->rc->config->get('autocomplete_max', 15); $results = array(); - foreach ($this->driver->load_resources($search, $maxnum) as $rec) { - $results[] = array( - 'name' => $rec['name'], - 'email' => $rec['email'], - 'type' => $rec['_type'], - ); + 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(); - foreach ($this->driver->load_resources() as $rec) { - $rec['dn'] = rcube_ldap::dn_decode($rec['ID']); - $data[] = $rec; + + 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() { - $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); - $data = $this->driver->get_resource_owner($id); + 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(); } /**** Event invitation plugin hooks ****/ /** * Handler for URLs that allow an invitee to respond on his invitation mail */ public function itip_attend_response($p) { if ($p['action'] == 'attend') { $this->ui->init(); $this->rc->output->set_env('task', 'calendar'); // override some env vars $this->rc->output->set_env('refresh_interval', 0); $this->rc->output->set_pagetitle($this->gettext('calendar')); $itip = $this->load_itip(); $token = get_input_value('_t', RCUBE_INPUT_GPC); // read event info stored under the given token if ($invitation = $itip->get_invitation($token)) { $this->token = $token; $this->event = $invitation['event']; // show message about cancellation if ($invitation['cancelled']) { $this->invitestatus = html::div('rsvp-status declined', $this->gettext('eventcancelled')); } // save submitted RSVP status else if (!empty($_POST['rsvp'])) { $status = null; foreach (array('accepted','tentative','declined') as $method) { if ($_POST['rsvp'] == $this->gettext('itip' . $method)) { $status = $method; break; } } // send itip reply to organizer if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) { $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status))); } else $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1); // if user is logged in... if ($this->rc->user->ID) { $this->load_driver(); $invitation = $itip->get_invitation($token); // save the event to his/her default calendar if not yet present if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar(true))) { $invitation['event']['calendar'] = $calendar['id']; if ($this->driver->new_event($invitation['event'])) $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); } } } $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform')); $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox')); if (!$this->invitestatus) $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons')); $this->rc->output->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event['title']); } else $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1); $this->rc->output->send('calendar.itipattend'); } } /** * */ public function itip_event_inviteform($attrib) { $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token)); return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show(); } /** * Check mail message structure of there are .ics files attached */ public function mail_message_load($p) { $this->message = $p['object']; $itip_part = null; // check all message parts for .ics files foreach ((array)$this->message->mime_parts as $part) { if ($this->is_vcalendar($part)) { if ($part->ctype_parameters['method']) $itip_part = $part->mime_id; else $this->ics_parts[] = $part->mime_id; } } // priorize part with method parameter if ($itip_part) $this->ics_parts = array($itip_part); } /** * Add UI element to copy event invitations or updates to the calendar */ public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) if (!empty($this->ics_parts)) { $this->get_ical(); } $html = ''; foreach ($this->ics_parts as $mime_id) { $part = $this->message->mime_parts[$mime_id]; $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET; $events = $this->ical->import($this->message->get_part_content($mime_id), $charset); $title = $this->gettext('title'); $date = rcube_utils::anytodatetime($this->message->headers->date); // successfully parsed events? if (empty($events)) continue; // show a box for every event in the file foreach ($events as $idx => $event) { // define buttons according to method if ($this->ical->method == 'REPLY') { $title = $this->gettext('itipreply'); $buttons = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')", 'value' => $this->gettext('updateattendeestatus'), )); } else if ($this->ical->method == 'REQUEST') { $emails = $this->get_user_emails(); $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation'); // add (hidden) buttons and activate them from asyncronous request foreach (array('accepted','tentative','declined') as $method) { $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button $method", 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')", 'value' => $this->gettext('itip' . $method), )); } $import_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')", 'value' => $this->gettext('importtocalendar'), )); // check my status $status = 'unknown'; foreach ($event['attendees'] as $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $status = strtoupper($attendee['status']); break; } } $dom_id = asciiwords($event['uid'], true); $buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons); $buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button); $buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading')); $changed = is_object($event['changed']) ? $event['changed'] : $date; $script = json_serialize(array( 'uid' => $event['uid'], 'changed' => $changed ? $changed->format('U') : 0, 'sequence' => intval($event['sequence']), 'fallback' => $status, )); $this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready'); } else if ($this->ical->method == 'CANCEL') { $title = $this->gettext('itipcancellation'); // create buttons to be activated from async request checking existence of this event in local calendars $button_import = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')", 'value' => $this->gettext('importtocalendar'), )); $button_remove = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')", 'value' => $this->gettext('removefromcalendar'), )); $dom_id = asciiwords($event['uid'], true); $buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove); $buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import); $buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading')); $changed = is_object($event['changed']) ? $event['changed'] : $date; $script = json_serialize(array( 'uid' => $event['uid'], 'changed' => $changed ? $changed->format('U') : 0, 'sequence' => intval($event['sequence']), 'fallback' => 'CANCELLED', )); $this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready'); } else { $buttons = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')", 'value' => $this->gettext('importtocalendar'), )); } // show event details with buttons $html .= html::div('calendar-invitebox', $this->ui->event_details_table($event, $title) . $buttons_pre . html::div('rsvp-buttons', $buttons)); // limit listing if ($idx >= 3) break; } } // prepend event boxes to message body if ($html) { $this->ui->init(); $p['content'] = $html . $p['content']; $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm'); } return $p; } /** * Handler for POST request to import an event attached to a mail message */ public function mail_import_event() { $uid = get_input_value('_uid', RCUBE_INPUT_POST); $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $mime_id = get_input_value('_part', RCUBE_INPUT_POST); $status = get_input_value('_status', RCUBE_INPUT_POST); $delete = intval(get_input_value('_del', RCUBE_INPUT_POST)); $charset = RCMAIL_CHARSET; // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); if ($uid && $mime_id) { list($mime_id, $index) = explode(':', $mime_id); $part = $imap->get_message_part($uid, $mime_id); if ($part->ctype_parameters['charset']) $charset = $part->ctype_parameters['charset']; $headers = $imap->get_message_headers($uid); } $events = $this->get_ical()->import($part, $charset); $error_msg = $this->gettext('errorimportingevent'); $success = false; // successfully parsed events? if (!empty($events) && ($event = $events[$index])) { // find writeable calendar to store event $cal_id = !empty($_REQUEST['_calendar']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : null; $calendars = $this->driver->list_calendars(false, true); $calendar = $calendars[$cal_id] ?: $this->get_default_calendar(true); // update my attendee status according to submitted method if (!empty($status)) { $organizer = null; $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $event['attendees'][$i]['status'] = strtoupper($status); $reply_sender = $attendee['email']; } } // add attendee with this user's default identity if not listed if (!$reply_sender) { $sender_identity = $this->rc->user->get_identity(); $event['attendees'][] = array( 'name' => $sender_identity['name'], 'email' => $sender_identity['email'], 'role' => 'OPT-PARTICIPANT', 'status' => strtoupper($status), ); } } // save to calendar if ($calendar && !$calendar['readonly']) { $event['calendar'] = $calendar['id']; // check for existing event with the same UID $existing = $this->driver->get_event($event['uid'], true, false, true); if ($existing) { // only update attendee status if ($this->ical->method == 'REPLY') { // try to identify the attendee using the email sender address $sender = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : ''; $sender_utf = rcube_idn_to_utf8($sender); $existing_attendee = -1; foreach ($existing['attendees'] as $i => $attendee) { if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) { $existing_attendee = $i; break; } } $event_attendee = null; foreach ($event['attendees'] as $attendee) { if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) { $event_attendee = $attendee; break; } } // found matching attendee entry in both existing and new events if ($existing_attendee >= 0 && $event_attendee) { $existing['attendees'][$existing_attendee] = $event_attendee; $success = $this->driver->edit_event($existing); } // update the entire attendees block else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) { $existing['attendees'][] = $event_attendee; $success = $this->driver->edit_event($existing); } else { $error_msg = $this->gettext('newerversionexists'); } } // delete the event when declined (#1670) else if ($status == 'declined' && $delete) { $deleted = $this->driver->remove_event($existing, true); $success = true; } // import the (newer) event else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) { $event['id'] = $existing['id']; $event['calendar'] = $existing['calendar']; if ($status == 'declined') // show me as free when declined (#1670) $event['free_busy'] = 'free'; $success = $this->driver->edit_event($event); } else if (!empty($status)) { $existing['attendees'] = $event['attendees']; if ($status == 'declined') // show me as free when declined (#1670) $existing['free_busy'] = 'free'; $success = $this->driver->edit_event($existing); } else $error_msg = $this->gettext('newerversionexists'); } else if (!$existing && $status != 'declined') { $success = $this->driver->new_event($event); } else if ($status == 'declined') $error_msg = null; } else if ($status == 'declined') $error_msg = null; else $error_msg = $this->gettext('nowritecalendarfound'); } if ($success) { $message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : 'importedsuccessfully'); $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); $this->rc->output->command('plugin.fetch_event_rsvp_status', array( 'uid' => $event['uid'], 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0, 'sequence' => intval($event['sequence']), 'fallback' => strtoupper($status), )); $error_msg = null; } else if ($error_msg) $this->rc->output->command('display_message', $error_msg, 'error'); // send iTip reply if ($this->ical->method == 'REQUEST' && $organizer && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) { $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } $this->rc->output->send(); } /** * Read email message and return contents for a new event based on that message */ public function mail_message2event() { $uid = get_input_value('_uid', RCUBE_INPUT_POST); $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $event = array(); // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); $message = new rcube_message($uid); if ($message->headers) { $event['title'] = trim($message->subject); $event['description'] = trim($message->first_text_part()); // copy mail attachments to event if ($message->attachments) { $eventid = 'cal:'; if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) { $_SESSION[self::SESSION_KEY] = array(); $_SESSION[self::SESSION_KEY]['id'] = $eventid; $_SESSION[self::SESSION_KEY]['attachments'] = array(); } foreach ((array)$message->attachments as $part) { $attachment = array( 'data' => $imap->get_message_part($uid, $part->mime_id, $part), 'size' => $part->size, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'group' => $eventid, ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); // store new attachment in session unset($attachment['status'], $attachment['abort'], $attachment['data']); $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' $event['attachments'][] = $attachment; } } } $this->rc->output->command('plugin.mail2event_dialog', $event); } else { $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); } $this->rc->output->send(); } /** * Checks if specified message part is a vcalendar data * * @param rcube_message_part Part object * @return boolean True if part is of type vcard */ private function is_vcalendar($part) { return ( in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || // Apple sends files as application/x-any (!?) ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) ); } /** * Get a list of email addresses of the current user (from login and identities) */ private function get_user_emails() { $emails = array(); $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails)); $emails = array_map('strtolower', $plugin['emails']); if ($plugin['abort']) { return $emails; } $emails[] = $this->rc->user->get_username(); foreach ($this->rc->user->list_identities() as $identity) $emails[] = strtolower($identity['email']); return array_unique($emails); } /** * Build an absolute URL with the given parameters */ public function get_url($param = array()) { $param += array('task' => 'calendar'); $schema = 'http'; $default_port = 80; if (rcube_https_check()) { $schema = 'https'; $default_port = 443; } $url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']); if ($_SERVER['SERVER_PORT'] != $default_port) $url .= ':' . $_SERVER['SERVER_PORT']; if (dirname($_SERVER['SCRIPT_NAME']) != '/') $url .= dirname($_SERVER['SCRIPT_NAME']); $url .= preg_replace('!^\./!', '/', $this->rc->url($param)); return $url; } public function ical_feed_hash($source) { return base64_encode($this->rc->user->get_username() . ':' . $source); } } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index f39c4c04..986ea8c3 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -1,3254 +1,3254 @@ /** * Client UI Javascript for the Calendar plugin * * @version @package_version@ * @author Lazlo Westerhof * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * Copyright (C) 2012, 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 . */ // Roundcube calendar UI client class function rcube_calendar_ui(settings) { // extend base class rcube_calendar.call(this, settings); /*** member vars ***/ this.is_loading = false; this.selected_event = null; this.selected_calendar = null; this.search_request = null; this.saving_lock; /*** private vars ***/ var DAY_MS = 86400000; var HOUR_MS = 3600000; var me = this; var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0); var client_timezone = new Date().getTimezoneOffset(); var day_clicked = day_clicked_ts = 0; var ignore_click = false; var event_defaults = { free_busy:'busy', alarms:'' }; var event_attendees = []; var attendees_list; var resources_list; var resources_treelist; var resources_data = {}; var resources_index = []; var resource_owners = {}; var freebusy_ui = { workinhoursonly:false, needsupdate:false }; var freebusy_data = {}; var current_view = null; var exec_deferred = bw.ie6 ? 5 : 1; var sensitivitylabels = { 'public':rcmail.gettext('public','calendar'), 'private':rcmail.gettext('private','calendar'), 'confidential':rcmail.gettext('confidential','calendar') }; var ui_loading = rcmail.set_busy(true, 'loading'); // 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 }; /*** imports ***/ var Q = this.quote_html; var text2html = this.text2html; var event_date_text = this.event_date_text; var parse_datetime = this.parse_datetime; var date2unixtime = this.date2unixtime; var fromunixtime = this.fromunixtime; var parseISO8601 = this.parseISO8601; var init_alarms_edit = this.init_alarms_edit; /*** private methods ***/ // same as str.split(delimiter) but it ignores delimiters within quoted strings var explode_quoted_string = function(str, delimiter) { var result = [], strlen = str.length, q, p, i, char, last; for (q = p = i = 0; i < strlen; i++) { char = str.charAt(i); if (char == '"' && last != '\\') { q = !q; } else if (!q && char == delimiter) { result.push(str.substring(p, i)); p = i + 1; } last = char; } result.push(str.substr(p)); return result; }; // Change the first charcter to uppercase var ucfirst = function(str) { return str.charAt(0).toUpperCase() + str.substr(1); }; // clone the given date object and optionally adjust time var clone_date = function(date, adjust) { var d = new Date(date.getTime()); // set time to 00:00 if (adjust == 1) { d.setHours(0); d.setMinutes(0); } // set time to 23:59 else if (adjust == 2) { d.setHours(23); d.setMinutes(59); } return d; }; // fix date if jumped over a DST change var fix_date = function(date) { if (date.getHours() == 23) date.setTime(date.getTime() + HOUR_MS); else if (date.getHours() > 0) date.setHours(0); }; // turn the given date into an ISO 8601 date string understandable by PHPs strtotime() var date2servertime = function(date) { return date.getFullYear()+'-'+zeropad(date.getMonth()+1)+'-'+zeropad(date.getDate()) + 'T'+zeropad(date.getHours())+':'+zeropad(date.getMinutes())+':'+zeropad(date.getSeconds()); } var date2timestring = function(date, dateonly) { return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14)); } var zeropad = function(num) { return (num < 10 ? '0' : '') + num; } var render_link = function(url) { var islink = false, href = url; if (url.match(/^[fhtpsmailo]+?:\/\//i)) { islink = true; } else if (url.match(/^[a-z0-9.-:]+(\/|$)/i)) { islink = true; href = 'http://' + url; } return islink ? '' + Q(url) + '' : Q(url); } // determine whether the given date is on a weekend var is_weekend = function(date) { return date.getDay() == 0 || date.getDay() == 6; }; var is_workinghour = function(date) { if (settings['work_start'] > settings['work_end']) return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end']; else return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end']; }; // check if the event has 'real' attendees, excluding the current user var has_attendees = function(event) { return (event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email)); }; // check if the current user is an attendee of this event var is_attendee = function(event, role, email) { var emails = email ? ';'+email.toLowerCase() : settings.identity.emails; for (var i=0; event.attendees && i < event.attendees.length; i++) { if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) return event.attendees[i]; } return false; }; // check if the current user is the organizer var is_organizer = function(event, email) { return is_attendee(event, 'ORGANIZER', email) || !event.id; }; var load_attachment = function(event, att) { var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar); // open attachment in frame if it's of a supported mimetype if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) { if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) { return; } } rcmail.goto_url('get-attachment', qstring+'&_download=1', false); }; // build event attachments list var event_show_attachments = function(list, container, event, edit) { var i, id, len, img, content, li, elem, ul = document.createElement('UL'); ul.className = 'attachmentslist'; for (i=0, len=list.length; i now shown in dialog title // $('#event-date').html(Q(me.event_date_text(event))).show(); if (event.recurrence && event.recurrence_text) $('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text)); if (event.alarms && event.alarms_text) $('#event-alarm').show().children('.event-text').html(Q(event.alarms_text)); if (calendar.name) $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).removeClass().addClass('event-text').addClass('cal-'+calendar.id); if (event.categories) $('#event-category').show().children('.event-text').html(Q(event.categories)).removeClass().addClass('event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, '')); if (event.free_busy) $('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar'))); if (event.priority > 0) { var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ]; $('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority])); } if (event.sensitivity && event.sensitivity != 'public') { $('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity])); $dialog.addClass('sensitivity-'+event.sensitivity); } // create attachments list if ($.isArray(event.attachments)) { event_show_attachments(event.attachments, $('#event-attachments').children('.event-text'), event); if (event.attachments.length > 0) { $('#event-attachments').show(); } } else if (calendar.attachments) { // fetch attachments, some drivers doesn't set 'attachments' prop of the event? } // list event attendees if (calendar.attendees && event.attendees) { // sort resources to the end event.attendees.sort(function(a,b) { var j = a.cutype == 'RESOURCE' ? 1 : 0, k = b.cutype == 'RESOURCE' ? 1 : 0; return (j - k); }); var data, dispname, organizer = false, rsvp = false, line, morelink, html = '',overflow = ''; for (var j=0; j < event.attendees.length; j++) { data = event.attendees[j]; dispname = Q(data.name || data.email); if (data.email) { dispname = '' + dispname + ''; if (data.role == 'ORGANIZER') organizer = true; else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) && settings.identity.emails.indexOf(';'+data.email) >= 0) rsvp = data.status.toLowerCase(); } line = '' + dispname + ' '; if (morelink) overflow += line; else html += line; // stop listing attendees if (j == 7 && event.attendees.length >= 7) { morelink = $('').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1)); } } if (html && (event.attendees.length > 1 || !organizer)) { $('#event-attendees').show() .children('.event-text') .html(html) .find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; }); // display all attendees in a popup when clicking the "more" link if (morelink) { $('#event-attendees .event-text').append(morelink); morelink.click(function(e){ rcmail.show_popup_dialog( '
' + html + overflow + '
', rcmail.gettext('tabattendees','calendar'), null, { width:450, modal:false }); $('#all-event-attendees a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; }); return false; }) } } $('#event-rsvp')[(rsvp && !is_organizer(event) ? 'show' : 'hide')](); $('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+rsvp+']').prop('disabled', true); } var buttons = {}; if (calendar.editable && event.editable !== false) { buttons[rcmail.gettext('edit', 'calendar')] = function() { event_edit_dialog('edit', event); }; buttons[rcmail.gettext('remove', 'calendar')] = function() { me.delete_event(event); $dialog.dialog('close'); }; } else { buttons[rcmail.gettext('close', 'calendar')] = function(){ $dialog.dialog('close'); }; } // open jquery UI dialog $dialog.dialog({ modal: false, resizable: !bw.ie6, closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons title: Q(me.event_date_text(event)), open: function() { $dialog.parent().find('.ui-button').first().focus(); }, close: function() { $dialog.dialog('destroy').hide(); }, buttons: buttons, minWidth: 320, width: 420 }).show(); // set dialog size according to content me.dialog_resize($dialog.get(0), $dialog.height(), 420); /* // add link for "more options" drop-down $('') .attr('href', '#') .html('More Options') .addClass('dropdown-link') .click(function(){ return false; }) .insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first()); */ }; // bring up the event dialog (jquery-ui popup) var event_edit_dialog = function(action, event) { // close show dialog first $("#eventshow:ui-dialog").dialog('close'); var $dialog = $('
'); var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' }; me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults) event = me.selected_event; // change reference to clone freebusy_ui.needsupdate = false; // reset dialog first $('#eventtabs').get(0).reset(); // allow other plugins to do actions when event form is opened rcmail.triggerEvent('calendar-event-init', {o: event}); // event details var title = $('#edit-title').val(event.title || ''); var location = $('#edit-location').val(event.location || ''); var description = $('#edit-description').html(event.description || ''); var vurl = $('#edit-url').val(event.vurl || ''); var categories = $('#edit-categories').val(event.categories); var calendars = $('#edit-calendar').val(event.calendar); var freebusy = $('#edit-free-busy').val(event.free_busy); var priority = $('#edit-priority').val(event.priority); var sensitivity = $('#edit-sensitivity').val(event.sensitivity); var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000); var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration); var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show(); var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); var allday = $('#edit-allday').get(0); var notify = $('#edit-attendees-donotify').get(0); var invite = $('#edit-attendees-invite').get(0); notify.checked = has_attendees(event), invite.checked = true; if (event.allDay) { starttime.val("12:00").hide(); endtime.val("13:00").hide(); allday.checked = true; } else { allday.checked = false; } // set alarm(s) // TODO: support multiple alarm entries if (event.alarms || action != 'new') { if (typeof event.alarms == 'string') event.alarms = event.alarms.split(';'); var valarms = event.alarms || ['']; for (var alarm, i=0; i < valarms.length; i++) { alarm = String(valarms[i]).split(':'); if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY'; $('#eventedit select.edit-alarm-type').val(alarm[1]); if (alarm[0].match(/@(\d+)/)) { var ondate = fromunixtime(parseInt(RegExp.$1)); $('#eventedit select.edit-alarm-offset').val('@'); $('#eventedit input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format'])); $('#eventedit input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format'])); } else if (alarm[0].match(/([-+])(\d+)([MHD])/)) { $('#eventedit input.edit-alarm-value').val(RegExp.$2); $('#eventedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3); } } } // set correct visibility by triggering onchange handlers $('#eventedit select.edit-alarm-type, #eventedit select.edit-alarm-offset').change(); // enable/disable alarm property according to backend support $('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')](); // check categories drop-down: add value if not exists if (event.categories && !categories.find("option[value='"+event.categories+"']").length) { $(' '; list_html += '
' + role_html + dispname + '
'; // clone attendees data for local modifications freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data); } // add total row list_html += '
 
'; list_html += '
' + rcmail.gettext('reqallattendees','calendar') + '
'; $('#schedule-attendees-list').html(list_html) .unbind('click.roleicons') .bind('click.roleicons', function(e){ // toggle attendee status upon click on icon if (e.target.id && e.target.id.match(/rcmlia(.+)/)) { var attendee, domid = RegExp.$1, roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'NON-PARTICIPANT', 'CHAIR' ]; if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') { var req = attendee.role != 'OPT-PARTICIPANT' && attendee.role != 'NON-PARTICIPANT'; var j = $.inArray(attendee.role, roles); j = (j+1) % roles.length; attendee.role = roles[j]; $(e.target).parent().removeClass().addClass('attendee '+String(attendee.role).toLowerCase()); // update total display if required-status changed if (req != (roles[j] != 'OPT-PARTICIPANT' && roles[j] != 'NON-PARTICIPANT')) { compute_freebusy_totals(); update_freebusy_display(attendee.email); } } } return false; }); // enable/disable buttons $('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime())); // dialog buttons var buttons = {}; buttons[rcmail.gettext('select', 'calendar')] = function() { $('#edit-startdate').val(freebusy_ui.startdate.val()); $('#edit-starttime').val(freebusy_ui.starttime.val()); $('#edit-enddate').val(freebusy_ui.enddate.val()); $('#edit-endtime').val(freebusy_ui.endtime.val()); // write role changes back to main dialog $('select.edit-attendee-role').each(function(i, elem){ if (event_attendees[i] && freebusy_ui.attendees[i]) { event_attendees[i].role = freebusy_ui.attendees[i].role; $(elem).val(event_attendees[i].role); } }); if (freebusy_ui.needsupdate) update_freebusy_status(me.selected_event); freebusy_ui.needsupdate = false; $dialog.dialog("close"); }; buttons[rcmail.gettext('cancel', 'calendar')] = function() { $dialog.dialog("close"); }; $dialog.dialog({ modal: true, resizable: true, closeOnEscape: (!bw.ie6 && !bw.ie7), title: rcmail.gettext('scheduletime', 'calendar'), open: function() { $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().focus(); }, close: function() { if (bw.ie6) $("#edit-attendees-table").css('visibility','visible'); $dialog.dialog("destroy").hide(); }, resizeStop: function() { render_freebusy_overlay(); }, buttons: buttons, minWidth: 640, width: 850 }).show(); // hide edit dialog on IE6 because of drop-down elements if (bw.ie6) $("#edit-attendees-table").css('visibility','hidden'); // adjust dialog size to fit grid without scrolling var gridw = $('#schedule-freebusy-times').width(); var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1; me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow)); // fetch data from server freebusy_ui.loading = 0; load_freebusy_data(freebusy_ui.start, freebusy_ui.interval); }; // render an HTML table showing free-busy status for all the event attendees var render_freebusy_grid = function(delta) { if (delta) { freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta); fix_date(freebusy_ui.start); // skip weekends if in workinhoursonly-mode if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) { while (is_weekend(freebusy_ui.start)) freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta); fix_date(freebusy_ui.start); } freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays); } var dayslots = Math.floor(1440 / freebusy_ui.interval); var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format); var lastdate, datestr, css, curdate = new Date(), allday = (freebusy_ui.interval == 1440), times_css = (allday ? 'allday ' : ''), dates_row = '', times_row = '', slots_row = ''; for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) { curdate.setTime(t); datestr = fc.fullCalendar('formatDate', curdate, date_format); if (datestr != lastdate) { if (lastdate && !allday) break; dates_row += '' + Q(datestr) + ''; lastdate = datestr; } // set css class according to working hours css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours'; times_row += '' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + ''; slots_row += ' '; t += freebusy_ui.interval * 60000; } dates_row += ''; times_row += ''; // render list of attendees var domid, data, list_html = '', times_html = ''; for (var i=0; i < event_attendees.length; i++) { data = event_attendees[i]; domid = String(data.email).replace(rcmail.identifier_expr, ''); times_html += '' + slots_row + ''; } // add line for all/required attendees times_html += ' '; times_html += '' + slots_row + ''; var table = $('#schedule-freebusy-times'); table.children('thead').html(dates_row + times_row); table.children('tbody').html(times_html); // initialize event handlers on grid if (!freebusy_ui.grid_events) { freebusy_ui.grid_events = true; table.children('thead').click(function(e){ // move event to the clicked date/time if (e.target.id && e.target.id.match(/t-(\d+)/)) { var newstart = new Date(RegExp.$1 * 1000); // set time to 00:00 if (me.selected_event.allDay) { newstart.setMinutes(0); newstart.setHours(0); } update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000)); render_freebusy_overlay(); } }) } // if we have loaded free-busy data, show it if (!freebusy_ui.loading) { if (freebusy_ui.start < freebusy_data.start || freebusy_ui.end > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) { load_freebusy_data(freebusy_ui.start, freebusy_ui.interval); } else { for (var email, i=0; i < event_attendees.length; i++) { if ((email = event_attendees[i].email)) update_freebusy_display(email); } } } // render current event date/time selection over grid table // use timeout to let the dom attributes (width/height/offset) be set first window.setTimeout(function(){ render_freebusy_overlay(); }, 10); }; // render overlay element over the grid to visiualize the current event date/time var render_freebusy_overlay = function() { var overlay = $('#schedule-event-time'); if (me.selected_event.end.getTime() <= freebusy_ui.start.getTime() || me.selected_event.start.getTime() >= freebusy_ui.end.getTime()) { overlay.hide(); if (overlay.data('isdraggable')) overlay.draggable('disable'); } else { var table = $('#schedule-freebusy-times'), width = 0, pos = { top:table.children('thead').height(), left:0 }, eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)), eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60, slotstart = date2unixtime(freebusy_ui.start), slotsize = freebusy_ui.interval * 60, slotend, fraction, $cell; // iterate through slots to determine position and size of the overlay table.children('thead').find('td').each(function(i, cell){ slotend = slotstart + slotsize - 1; // event starts in this slot: compute left if (eventstart >= slotstart && eventstart <= slotend) { fraction = 1 - (slotend - eventstart) / slotsize; pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction); } // event ends in this slot: compute width if (eventend >= slotstart && eventend <= slotend) { fraction = 1 - (slotend - eventend) / slotsize; width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left; } slotstart = slotstart + slotsize; }); if (!width) width = table.width() - pos.left; // overlay is visible if (width > 0) { overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show(); // configure draggable if (!overlay.data('isdraggable')) { overlay.draggable({ axis: 'x', scroll: true, stop: function(e, ui){ // convert pixels to time var px = ui.position.left; var range_p = $('#schedule-freebusy-times').width(); var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime(); var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p)); newstart.setSeconds(0); newstart.setMilliseconds(0); // snap to day boundaries if (me.selected_event.allDay) { if (newstart.getHours() >= 12) // snap to next day newstart.setTime(newstart.getTime() + DAY_MS); newstart.setMinutes(0); newstart.setHours(0); } else { // round to 5 minutes var round = newstart.getMinutes() % 5; if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000); else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000); } // update event times and display update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000)); if (me.selected_event.allDay) render_freebusy_overlay(); } }).data('isdraggable', true); } else overlay.draggable('enable'); } else overlay.draggable('disable').hide(); } }; // fetch free-busy information for each attendee from server var load_freebusy_data = function(from, interval) { var start = new Date(from.getTime() - DAY_MS * 2); // start 2 days before event fix_date(start); var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days freebusy_ui.numrequired = 0; freebusy_data.all = []; freebusy_data.required = []; // load free-busy information for every attendee var domid, email; for (var i=0; i < event_attendees.length; i++) { if ((email = event_attendees[i].email)) { domid = String(email).replace(rcmail.identifier_expr, ''); $('#rcmli' + domid).addClass('loading'); freebusy_ui.loading++; $.ajax({ type: 'GET', dataType: 'json', url: rcmail.url('freebusy-times'), data: { email:email, start:date2servertime(clone_date(start, 1)), end:date2servertime(clone_date(end, 2)), interval:interval, _remote:1 }, success: function(data) { freebusy_ui.loading--; // find attendee var attendee = null; for (var i=0; i < event_attendees.length; i++) { if (freebusy_ui.attendees[i].email == data.email) { attendee = freebusy_ui.attendees[i]; break; } } // copy data to member var var ts, req = attendee.role != 'OPT-PARTICIPANT'; freebusy_data.start = parseISO8601(data.start); freebusy_data[data.email] = {}; for (var i=0; i < data.slots.length; i++) { ts = data.times[i] + ''; freebusy_data[data.email][ts] = data.slots[i]; // set totals if (!freebusy_data.required[ts]) freebusy_data.required[ts] = [0,0,0,0]; if (req) freebusy_data.required[ts][data.slots[i]]++; if (!freebusy_data.all[ts]) freebusy_data.all[ts] = [0,0,0,0]; freebusy_data.all[ts][data.slots[i]]++; } freebusy_data.end = parseISO8601(data.end); freebusy_data.interval = data.interval; // hide loading indicator var domid = String(data.email).replace(rcmail.identifier_expr, ''); $('#rcmli' + domid).removeClass('loading'); // update display update_freebusy_display(data.email); } }); // count required attendees if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT') freebusy_ui.numrequired++; } } }; // re-calculate total status after role change var compute_freebusy_totals = function() { freebusy_ui.numrequired = 0; freebusy_data.all = []; freebusy_data.required = []; var email, req, status; for (var i=0; i < event_attendees.length; i++) { if (!(email = event_attendees[i].email)) continue; req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT'; if (req) freebusy_ui.numrequired++; for (var ts in freebusy_data[email]) { if (!freebusy_data.required[ts]) freebusy_data.required[ts] = [0,0,0,0]; if (!freebusy_data.all[ts]) freebusy_data.all[ts] = [0,0,0,0]; status = freebusy_data[email][ts]; freebusy_data.all[ts][status]++; if (req) freebusy_data.required[ts][status]++; } } }; // update free-busy grid with status loaded from server var update_freebusy_display = function(email) { var status_classes = ['unknown','free','busy','tentative','out-of-office']; var domid = String(email).replace(rcmail.identifier_expr, ''); var row = $('#fbrow' + domid); var rowall = $('#fbrowall').children(); var dateonly = freebusy_ui.interval > 60, t, ts = date2timestring(freebusy_ui.start, dateonly), curdate = new Date(), fbdata = freebusy_data[email]; if (fbdata && fbdata[ts] !== undefined && row.length) { t = freebusy_ui.start.getTime(); row.children().each(function(i, cell){ curdate.setTime(t); ts = date2timestring(curdate, dateonly); cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown'); // also update total row if all data was loaded if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) { var workinghours = cell.className.indexOf('workinghours') >= 0; var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown'; req_status = freebusy_data.required[ts][2] ? 'busy' : 'free'; for (var j=1; j < status_classes.length; j++) { if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired) req_status = status_classes[j]; if (freebusy_data.all[ts][j] == event_attendees.length) all_status = status_classes[j]; } cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status; } t += freebusy_ui.interval * 60000; }); } }; // write changed event date/times back to form fields var update_freebusy_dates = function(start, end) { // fix all-day evebt times if (me.selected_event.allDay) { var numdays = Math.floor((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / DAY_MS); start.setHours(12); start.setMinutes(0); end.setTime(start.getTime() + numdays * DAY_MS); end.setHours(13); end.setMinutes(0); } me.selected_event.start = start; me.selected_event.end = end; freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format'])); freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format'])); freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format'])); freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format'])); freebusy_ui.needsupdate = true; }; // attempt to find a time slot where all attemdees are available var freebusy_find_slot = function(dir) { // exit if free-busy data isn't available yet if (!freebusy_data || !freebusy_data.start) return false; var event = me.selected_event, eventstart = clone_date(event.start, event.allDay ? 1 : 0).getTime(), // calculate with integers eventend = clone_date(event.end, event.allDay ? 2 : 0).getTime(), duration = eventend - eventstart - (event.allDay ? HOUR_MS : 0), // make sure we don't cross day borders on DST change sinterval = freebusy_data.interval * 60000, intvlslots = 1, numslots = Math.ceil(duration / sinterval), checkdate, slotend, email, ts, slot, slotdate = new Date(); // shift event times to next possible slot eventstart += sinterval * intvlslots * dir; eventend += sinterval * intvlslots * dir; // iterate through free-busy slots and find candidates var candidatecount = 0, candidatestart = candidateend = success = false; for (slot = dir > 0 ? freebusy_data.start.getTime() : freebusy_data.end.getTime() - sinterval; (dir > 0 && slot < freebusy_data.end.getTime()) || (dir < 0 && slot >= freebusy_data.start.getTime()); slot += sinterval * dir) { slotdate.setTime(slot); // fix slot if just crossed a DST change if (event.allDay) { fix_date(slotdate); slot = slotdate.getTime(); } slotend = slot + sinterval; if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip continue; // respect workingours setting if (freebusy_ui.workinhoursonly) { if (is_weekend(slotdate) || (freebusy_data.interval <= 60 && !is_workinghour(slotdate))) { // skip off-hours candidatestart = candidateend = false; candidatecount = 0; continue; } } if (!candidatestart) candidatestart = slot; // check freebusy data for all attendees ts = date2timestring(slotdate, freebusy_data.interval > 60); for (var i=0; i < event_attendees.length; i++) { if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][ts] > 1) { candidatestart = candidateend = false; break; } } // occupied slot if (!candidatestart) { slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir; candidatecount = 0; continue; } // set candidate end to slot end time candidatecount++; if (dir < 0 && !candidateend) candidateend = slotend; // if candidate is big enough, this is it! if (candidatecount == numslots) { if (dir > 0) { event.start.setTime(candidatestart); event.end.setTime(candidatestart + duration); } else { event.end.setTime(candidateend); event.start.setTime(candidateend - duration); } success = true; break; } } // update event date/time display if (success) { update_freebusy_dates(event.start, event.end); // move freebusy grid if necessary var offset = Math.ceil((event.start.getTime() - freebusy_ui.end.getTime()) / DAY_MS); if (event.start.getTime() >= freebusy_ui.end.getTime()) render_freebusy_grid(Math.max(1, offset)); else if (event.end.getTime() <= freebusy_ui.start.getTime()) render_freebusy_grid(Math.min(-1, offset)); else render_freebusy_overlay(); var now = new Date(); $('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime())); } else { alert(rcmail.gettext('noslotfound','calendar')); } }; // update event properties and attendees availability if event times have changed var event_times_changed = function() { if (me.selected_event) { var allday = $('#edit-allday').get(0); me.selected_event.allDay = allday.checked; me.selected_event.start = parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val()); me.selected_event.end = parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val()); if (event_attendees) freebusy_ui.needsupdate = true; $('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000)); } }; // add the given list of participants var add_attendees = function(names, params) { names = explode_quoted_string(names.replace(/,\s*$/, ''), ','); // parse name/email pairs var item, email, name, success = false; for (var i=0; i < names.length; i++) { email = name = ''; item = $.trim(names[i]); if (!item.length) { continue; } // address in brackets without name (do nothing) else if (item.match(/^<[^@]+@[^>]+>$/)) { email = item.replace(/[<>]/g, ''); } // address without brackets and without name (add brackets) else if (rcube_check_email(item)) { email = item; } // address with name else if (item.match(/([^\s<@]+@[^>]+)>*$/)) { email = RegExp.$1; name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, ''); } if (email) { add_attendee($.extend({ email:email, name:name }, params)); success = true; } else { alert(rcmail.gettext('noemailwarning')); } } return success; }; // add the given attendee to the list var add_attendee = function(data, readonly) { if (!me.selected_event) return false; // check for dupes... var exists = false; $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); }); if (exists) return false; var calendar = me.selected_event && me.calendars[me.selected_event.calendar] ? me.calendars[me.selected_event.calendar] : me.calendars[me.selected_calendar]; var dispname = Q(data.name || data.email); if (data.email) dispname = '' + dispname + ''; // role selection var organizer = data.role == 'ORGANIZER'; var opts = {}; if (organizer) opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer'); opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired'); opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional'); opts['NON-PARTICIPANT'] = rcmail.gettext('calendar.rolenonparticipant'); if (data.cutype != 'RESOURCE') opts['CHAIR'] = rcmail.gettext('calendar.rolechair'); if (organizer && !readonly) dispname = rcmail.env['identities-selector']; var select = ''; // availability var avail = data.email ? 'loading' : 'unknown'; // delete icon var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); var dellink = '' + icon + ''; var html = '' + select + '' + '' + dispname + '' + '' + '' + Q(data.status || '') + '' + '' + (organizer || readonly ? '' : dellink) + ''; - var table = calendar.resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list; + var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list; var tr = $('') .addClass(String(data.role).toLowerCase()) .html(html) .appendTo(table); tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; }); // select organizer identity if (data.identity_id) $('#edit-identities-list').val(data.identity_id); // check free-busy status if (avail == 'loading') { check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event); } event_attendees.push(data); return true; }; // iterate over all attendees and update their free-busy status display var update_freebusy_status = function(event) { attendees_list.find('img.availabilityicon').each(function(i,v) { var email, icon = $(this); if (email = icon.attr('data-email')) check_freebusy_status(icon, email, event); }); freebusy_ui.needsupdate = false; }; // load free-busy status from server and update icon accordingly var check_freebusy_status = function(icon, email, event) { var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false }; if (!calendar.freebusy) { $(icon).removeClass().addClass('availabilityicon unknown'); return; } icon = $(icon).removeClass().addClass('availabilityicon loading'); $.ajax({ type: 'GET', dataType: 'html', url: rcmail.url('freebusy-status'), data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 }, success: function(status){ icon.removeClass('loading').addClass(String(status).toLowerCase()); }, error: function(){ icon.removeClass('loading').addClass('unknown'); } }); }; // remove an attendee from the list var remove_attendee = function(elem, id) { $(elem).closest('tr').remove(); event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) }); }; // open a dialog to display detailed free-busy information and to find free slots var event_resources_dialog = function() { var $dialog = $('#eventresourcesdialog'), event = me.selected_event; if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); // dialog buttons var buttons = {}; buttons[rcmail.gettext('addresource', 'calendar')] = function() { rcmail.command('add-resource'); }; buttons[rcmail.gettext('close')] = function() { $dialog.dialog("close"); }; // open jquery UI dialog $dialog.dialog({ modal: true, resizable: true, closeOnEscape: true, title: rcmail.gettext('findresources', 'calendar'), close: function() { $dialog.dialog('destroy').hide(); }, buttons: buttons, width: 900, height: 500 }).show(); // define add-button as main action $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd'); me.dialog_resize($dialog.get(0), 540, Math.min(1000, $(window).width() - 50)); // initialize the treelist widget if (!resources_treelist) { resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, { id_prefix: 'rcres', id_encode: rcmail.html_identifier_encode, id_decode: rcmail.html_identifier_decode, selectable: true }); resources_treelist.addEventListener('select', function(node) { if (resources_data[node.id]) { resource_showinfo(resources_data[node.id]); rcmail.enable_command('add-resource', me.selected_event ? true : false); } else { rcmail.enable_command('add-resource', false); $(rcmail.gui_objects.resourceinfo).hide(); $(rcmail.gui_objects.resourceownerinfo).hide(); } }); // fetch (all) resource data from server me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock); rcmail.http_request('resources-list', {}, me.loading_lock); // register button rcmail.register_button('add-resource', 'rcmbtncalresadd', 'uibutton'); } else { resources_treelist.select('__none__'); } }; // render the resource details UI box var resource_showinfo = function(resource) { // inline function to render a resource attribute function render_attrib(value) { if (typeof value == 'boolean') { return value ? rcmail.get_label('yes') : rcmail.get_label('no'); } return value; } if (rcmail.gui_objects.resourceinfo) { var tr, table = $(rcmail.gui_objects.resourceinfo).show().find('tbody').html(''), attribs = $.extend({ name:resource.name }, resource.attributes||{}) attribs.description = resource.description; for (var k in attribs) { if (typeof attribs[k] == 'undefined') continue; table.append($('').addClass(k) .append('' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '') .append('' + text2html(render_attrib(attribs[k])) + '') ); } $(rcmail.gui_objects.resourceownerinfo).hide(); if (resource.owner) { // display cached data if (resource_owners[resource.owner]) { resource_owner_load(resource_owners[resource.owner]); } else { // fetch owner data from server me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock); rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock); } } } }; // callback from server for resource listing var resource_data_load = function(data) { data.sort(function(a,b) { var j = a._type == 'collection' ? 1 : 0, k = b._type == 'collection' ? 1 : 0; return k != j ? (j - k) : (a.name < b.name ? 1 : 0); }); // assign parent-relations $.each(data, function(i, rec) { - resources_data[rec.dn] = rec; - resources_index.push(rec.dn); + resources_data[rec.ID] = rec; + resources_index.push(rec.ID); if (rec.members) { $.each(rec.members, function(j, m){ - resources_data[m].parent_id = rec.dn; + resources_data[m].parent_id = rec.ID; }); } }); resources_index.reverse(); resource_render_list(resources_index); rcmail.set_busy(false, null, me.loading_lock); }; // renders the given list of resource records into the treelist var resource_render_list = function(index) { var rec, link; resources_treelist.reset(); $.each(index, function(i, dn) { if (rec = resources_data[dn]) { link = $('').attr('href', '#') - .attr('rel', rec.dn) + .attr('rel', rec.ID) .html(Q(rec.name)); - resources_treelist.insert({ id:rec.dn, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false); + resources_treelist.insert({ id:rec.ID, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false); } }); }; // callback from server for owner information display var resource_owner_load = function(data) { if (data) { // cache this! resource_owners[data.ID] = data; var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html(''); for (var k in data) { if (k == 'event' || k == 'ID') continue; table.append($('').addClass(k) .append('' + Q(ucfirst(rcmail.get_label('owner'+k, 'calendar'))) + '') .append('' + text2html(data[k]) + '') ); } table.parent().show(); } } // quick-filter the loaded resource data var resource_search = function() { var dataset, rec, q = $('#resourcesearchbox').val().toLowerCase(); if (q.length && resources_data) { dataset = []; // search by iterating over all resource records for (var dn in resources_data) { rec = resources_data[dn]; if (String(rec.name).toLowerCase().indexOf(q) >= 0) { - dataset.push(rec.dn); + dataset.push(rec.ID); } } resource_render_list(dataset); // select single match if (dataset.length == 1) { resources_treelist.select(dataset[0]); } } else { $('#resourcesearchbox').val(''); } }; // var reset_resource_search = function() { $('#resourcesearchbox').val('').focus(); resource_render_list(resources_index); }; // var add_resource2event = function() { var resource = resources_data[resources_treelist.get_selection()]; if (resource) { if (add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource))) rcmail.display_message(rcmail.get_label('resourceadded', 'calendar'), 'confirmation'); } } // when the user accepts or declines an event invitation var event_rsvp = function(response) { if (me.selected_event && me.selected_event.attendees && response) { // update attendee status for (var data, i=0; i < me.selected_event.attendees.length; i++) { data = me.selected_event.attendees[i]; if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) data.status = response.toUpperCase(); } event_show_dialog(me.selected_event); // submit status change to server me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata'); rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response }); } } // post the given event data to server var update_event = function(action, data) { me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata'); rcmail.http_post('calendar/event', { action:action, e:data }); // render event temporarily into the calendar if ((data.start && data.end) || data.id) { var event = data.id ? $.extend(fc.fullCalendar('clientEvents', function(e){ return e.id == data.id; })[0], data) : data; if (data.start) event.start = data.start; if (data.end) event.end = data.end; if (data.allday !== undefined) event.allDay = data.allday; event.editable = false; event.temp = true; event.className = 'fc-event-cal-'+data.calendar+' fc-event-temp'; fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event); } }; // mouse-click handler to check if the show dialog is still open and prevent default action var dialog_check = function(e) { var showd = $("#eventshow"); if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) { showd.dialog('close'); e.stopImmediatePropagation(); ignore_click = true; return false; } else if (ignore_click) { window.setTimeout(function(){ ignore_click = false; }, 20); return false; } return true; }; // display confirm dialog when modifying/deleting an event var update_event_confirm = function(action, event, data) { if (!data) data = event; var decline = false, notify = false, html = '', cal = me.calendars[event.calendar]; // event has attendees, ask whether to notify them if (has_attendees(event)) { if (is_organizer(event)) { notify = true; html += '
' + '
'; } else if (action == 'remove' && is_attendee(event)) { decline = true; html += '
' + '
'; } else { html += '
' + rcmail.gettext('localchangeswarning', 'calendar') + '
'; } } // recurring event: user needs to select the savemode if (event.recurrence) { html += '
' + rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '
' + '
'; } // show dialog if (html) { var $dialog = $('
').html(html); $dialog.find('a.button').button().click(function(e){ data._savemode = String(this.href).replace(/.+#/, ''); if ($dialog.find('input.confirm-attendees-donotify').get(0)) data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0; if (decline && $dialog.find('input.confirm-attendees-decline:checked')) data.decline = 1; update_event(action, data); $dialog.dialog("destroy").hide(); return false; }); var buttons = []; if (!event.recurrence) { buttons.push({ text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'), click: function() { data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0; data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0; update_event(action, data); $(this).dialog("close"); } }); } buttons.push({ text: rcmail.gettext('cancel', 'calendar'), click: function() { $(this).dialog("close"); } }); $dialog.dialog({ modal: true, width: 460, dialogClass: 'warning', title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'), buttons: buttons, open: function() { $dialog.parent().find('.ui-button').first().focus(); }, close: function(){ $dialog.dialog("destroy").hide(); if (!rcmail.busy) fc.fullCalendar('refetchEvents'); } }).addClass('event-update-confirm').show(); return false; } // show regular confirm box when deleting else if (action == 'remove' && !cal.undelete) { if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar'))) return false; } // do update update_event(action, data); return true; }; var update_agenda_toolbar = function() { $('#agenda-listrange').val(fc.fullCalendar('option', 'listRange')); $('#agenda-listsections').val(fc.fullCalendar('option', 'listSections')); } /*** fullcalendar event handlers ***/ var fc_event_render = function(event, element, view) { if (view.name != 'list' && view.name != 'table') { var prefix = event.sensitivity && event.sensitivity != 'public' ? String(sensitivitylabels[event.sensitivity]).toUpperCase()+': ' : ''; element.attr('title', prefix + event.title); } if (view.name != 'month') { if (event.location) { element.find('div.fc-event-title').after('
@ ' + Q(event.location) + '
'); } if (event.sensitivity && event.sensitivity != 'public') element.find('div.fc-event-time').append(''); if (event.recurrence) element.find('div.fc-event-time').append(''); if (event.alarms) element.find('div.fc-event-time').append(''); } }; /*** public methods ***/ /** * Remove saving lock and free the UI for new input */ this.unlock_saving = function() { if (me.saving_lock) rcmail.set_busy(false, null, me.saving_lock); }; // opens calendar day-view in a popup this.fisheye_view = function(date) { $('#fish-eye-view:ui-dialog').dialog('close'); // create list of active event sources var src, cals = {}, sources = []; for (var id in this.calendars) { src = $.extend({}, this.calendars[id]); src.editable = false; src.url = null; src.events = []; if (src.active) { cals[id] = src; sources.push(src); } } // copy events already loaded var events = fc.fullCalendar('clientEvents'); for (var event, i=0; i< events.length; i++) { event = events[i]; if (event.source && (src = cals[event.source.id])) { src.events.push(event); } } var h = $(window).height() - 50; var dialog = $('
') .attr('id', 'fish-eye-view') .dialog({ modal: true, width: 680, height: h, title: $.fullCalendar.formatDate(date, 'dddd ' + settings['date_long']), close: function(){ dialog.dialog("destroy"); me.fisheye_date = null; } }) .fullCalendar({ header: { left: '', center: '', right: '' }, height: h - 50, defaultView: 'agendaDay', date: date.getDate(), month: date.getMonth(), year: date.getFullYear(), ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone eventSources: sources, monthNames : settings['months'], monthNamesShort : settings['months_short'], dayNames : settings['days'], dayNamesShort : settings['days_short'], firstDay : settings['first_day'], firstHour : settings['first_hour'], slotMinutes : 60/settings['timeslots'], timeFormat: { '': settings['time_format'] }, axisFormat : settings['time_format'], columnFormat: { day: 'dddd ' + settings['date_short'] }, titleFormat: { day: 'dddd ' + settings['date_long'] }, allDayText: rcmail.gettext('all-day', 'calendar'), currentTimeIndicator: settings.time_indicator, eventRender: fc_event_render, eventClick: function(event) { event_show_dialog(event); } }); this.fisheye_date = date; }; //public method to show the print dialog. this.print_calendars = function(view) { if (!view) view = fc.fullCalendar('getView').name; var date = fc.fullCalendar('getDate') || new Date(); var range = fc.fullCalendar('option', 'listRange'); var sections = fc.fullCalendar('option', 'listSections'); rcmail.open_window(rcmail.url('print', { view: view, date: date2unixtime(date), range: range, sections: sections, search: this.search_query }), true, true); }; // public method to bring up the new event dialog this.add_event = function(templ) { if (this.selected_calendar) { var now = new Date(); var date = fc.fullCalendar('getDate'); if (typeof date != 'Date') date = now; date.setHours(now.getHours()+1); date.setMinutes(0); var end = new Date(date.getTime()); end.setHours(date.getHours()+1); event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {})); } }; // delete the given event after showing a confirmation dialog this.delete_event = function(event) { // show confirm dialog for recurring events, use jquery UI dialog return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees }); }; // opens a jquery UI dialog with event properties (or empty for creating a new calendar) this.calendar_edit_dialog = function(calendar) { // close show dialog first var $dialog = $("#calendarform"); if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); if (!calendar) calendar = { name:'', color:'cc0000', editable:true, showalarms:true }; var form, name, color, alarms; $dialog.html(rcmail.get_label('loading')); $.ajax({ type: 'GET', dataType: 'html', url: rcmail.url('calendar'), data: { action:(calendar.id ? 'form-edit' : 'form-new'), c:{ id:calendar.id } }, success: function(data) { $dialog.html(data); // resize and reposition dialog window form = $('#calendarpropform'); me.dialog_resize('#calendarform', form.height(), form.width()); name = $('#calendar-name').prop('disabled', !calendar.editable).val(calendar.editname || calendar.name); color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color, colorValues:rcmail.env.mscolors }); alarms = $('#calendar-showalarms').prop('checked', calendar.showalarms).get(0); name.select(); } }); // dialog buttons var buttons = {}; buttons[rcmail.gettext('save', 'calendar')] = function() { // form is not loaded if (!form || !form.length) return; // TODO: do some input validation if (!name.val() || name.val().length < 2) { alert(rcmail.gettext('invalidcalendarproperties', 'calendar')); name.select(); return; } // post data to server var data = form.serializeJSON(); if (data.color) data.color = data.color.replace(/^#/, ''); if (calendar.id) data.id = calendar.id; if (alarms) data.showalarms = alarms.checked ? 1 : 0; me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata'); rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data }); $dialog.dialog("close"); }; buttons[rcmail.gettext('cancel', 'calendar')] = function() { $dialog.dialog("close"); }; // open jquery UI dialog $dialog.dialog({ modal: true, resizable: true, closeOnEscape: false, title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'), open: function() { $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction'); }, close: function() { $dialog.html('').dialog("destroy").hide(); }, buttons: buttons, minWidth: 400, width: 420 }).show(); }; this.calendar_remove = function(calendar) { if (confirm(rcmail.gettext(calendar.children ? 'deletecalendarconfirmrecursive' : 'deletecalendarconfirm', 'calendar'))) { rcmail.http_post('calendar', { action:'remove', c:{ id:calendar.id } }); return true; } return false; }; this.calendar_destroy_source = function(id) { var delete_ids = []; if (this.calendars[id]) { // find sub-calendars if (this.calendars[id].children) { for (var child_id in this.calendars) { if (String(child_id).indexOf(id) == 0) delete_ids.push(child_id); } } else { delete_ids.push(id); } } // delete all calendars in the list for (var i=0; i < delete_ids.length; i++) { id = delete_ids[i]; fc.fullCalendar('removeEventSource', this.calendars[id]); $(rcmail.get_folder_li(id, 'rcmlical')).remove(); $('#edit-calendar option[value="'+id+'"]').remove(); delete this.calendars[id]; } }; // open a dialog to upload an .ics file with events to be imported this.import_events = function(calendar) { // close show dialog first var $dialog = $("#eventsimport"), form = rcmail.gui_objects.importform; if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); if (calendar) $('#event-import-calendar').val(calendar.id); var buttons = {}; buttons[rcmail.gettext('import', 'calendar')] = function() { if (form && form.elements._data.value) { rcmail.async_upload_form(form, 'import_events', function(e) { rcmail.set_busy(false, null, me.saving_lock); $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable'); // display error message if no sophisticated response from server arrived (e.g. iframe load error) if (me.import_succeeded === null) rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error'); }); // display upload indicator (with extended timeout) var timeout = rcmail.env.request_timeout; rcmail.env.request_timeout = 600; me.import_succeeded = null; me.saving_lock = rcmail.set_busy(true, 'uploading'); $('.ui-dialog-buttonpane button', $dialog.parent()).button('disable'); // restore settings rcmail.env.request_timeout = timeout; } }; buttons[rcmail.gettext('cancel', 'calendar')] = function() { $dialog.dialog("close"); }; // open jquery UI dialog $dialog.dialog({ modal: true, resizable: false, closeOnEscape: false, title: rcmail.gettext('importevents', 'calendar'), open: function() { $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction'); }, close: function() { $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable'); $dialog.dialog("destroy").hide(); }, buttons: buttons, width: 520 }).show(); }; // callback from server if import succeeded this.import_success = function(p) { this.import_succeeded = true; $("#eventsimport:ui-dialog").dialog('close'); rcmail.set_busy(false, null, me.saving_lock); rcmail.gui_objects.importform.reset(); if (p.refetch) this.refresh(p); }; // callback from server to report errors on import this.import_error = function(p) { this.import_succeeded = false; rcmail.set_busy(false, null, me.saving_lock); rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error'); } // open a dialog to select calendars for export this.export_events = function(calendar) { // close show dialog first var $dialog = $("#eventsexport"), form = rcmail.gui_objects.exportform; if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); if (calendar) $('#event-export-calendar').val(calendar.id); $('#event-export-range').change(function(e){ var custom = $('option:selected', this).val() == 'custom', input = $('#event-export-startdate') input.parent()[(custom?'show':'hide')](); if (custom) input.select(); }) var buttons = {}; buttons[rcmail.gettext('export', 'calendar')] = function() { if (form) { var start = 0, range = $('#event-export-range option:selected', this).val(), source = $('#event-export-calendar option:selected').val(), attachmt = $('#event-export-attachments').get(0).checked; if (range == 'custom') start = date2unixtime(parse_datetime('00:00', $('#event-export-startdate').val())); else if (range > 0) start = 'today -' + range + ' months'; rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 }); } }; buttons[rcmail.gettext('cancel', 'calendar')] = function() { $dialog.dialog("close"); }; // open jquery UI dialog $dialog.dialog({ modal: true, resizable: false, closeOnEscape: false, title: rcmail.gettext('exporttitle', 'calendar'), open: function() { $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction'); }, close: function() { $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable'); $dialog.dialog("destroy").hide(); }, buttons: buttons, width: 520 }).show(); }; // show URL of the given calendar in a dialog box this.showurl = function(calendar) { var $dialog = $('#calendarurlbox'); if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); if (calendar.feedurl) { if (calendar.caldavurl) { $('#caldavurl').val(calendar.caldavurl); $('#calendarcaldavurl').show(); } else { $('#calendarcaldavurl').hide(); } $dialog.dialog({ resizable: true, closeOnEscape: true, title: rcmail.gettext('showurl', 'calendar'), close: function() { $dialog.dialog("destroy").hide(); }, width: 520 }).show(); $('#calfeedurl').val(calendar.feedurl).select(); } }; // refresh the calendar view after saving event data this.refresh = function(p) { var source = me.calendars[p.source]; if (source && (p.refetch || (p.update && !source.active))) { // activate event source if new event was added to an invisible calendar if (!source.active) { source.active = true; fc.fullCalendar('addEventSource', source); $('#' + rcmail.get_folder_li(source.id, 'rcmlical').id + ' input').prop('checked', true); } else fc.fullCalendar('refetchEvents', source); } // add/update single event object else if (source && p.update) { var event = p.update; event.temp = false; event.editable = source.editable; var existing = fc.fullCalendar('clientEvents', event._id); if (existing.length) { $.extend(existing[0], event); fc.fullCalendar('updateEvent', existing[0]); // remove old recurrence instances if (event.recurrence && !event.recurrence_id) fc.fullCalendar('removeEvents', function(e){ return e._id.indexOf(event._id+'-') == 0; }); } else { event.source = source; // link with source fc.fullCalendar('renderEvent', event); } // refresh fish-eye view if (me.fisheye_date) me.fisheye_view(me.fisheye_date); } // refetch all calendars else if (p.refetch) { fc.fullCalendar('refetchEvents'); } // remove temp events fc.fullCalendar('removeEvents', function(e){ return e.temp; }); }; // modify query parameters for refresh requests this.before_refresh = function(query) { var view = fc.fullCalendar('getView'); query.start = date2unixtime(view.visStart); query.end = date2unixtime(view.visEnd); if (this.search_query) query.q = this.search_query; return query; }; // reload the calendar view by keeping the current date/view selection this.reload_view = function() { var query = { view: fc.fullCalendar('getView').name }, date = fc.fullCalendar('getDate'); if (date) query.date = date2unixtime(date); rcmail.redirect(rcmail.url('', query)); } // update browser location to remember current view this.update_state = function() { var query = { view: current_view }, date = fc.fullCalendar('getDate'); if (date) query.date = date2unixtime(date); if (window.history.replaceState) window.history.replaceState({}, document.title, rcmail.url('', query).replace('&_action=', '')); }; this.resource_search = resource_search; this.reset_resource_search = reset_resource_search; this.add_resource2event = add_resource2event; this.resource_data_load = resource_data_load; this.resource_owner_load = resource_owner_load; /*** event searching ***/ // execute search this.quicksearch = function() { if (rcmail.gui_objects.qsearchbox) { var q = rcmail.gui_objects.qsearchbox.value; if (q != '') { var id = 'search-'+q; var sources = []; if (this._search_message) rcmail.hide_message(this._search_message); for (var sid in this.calendars) { if (this.calendars[sid]) { this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '') + '&q='+escape(q); sources.push(sid); } } id += '@'+sources.join(','); // ignore if query didn't change if (this.search_request == id) { return; } // remember current view else if (!this.search_request) { this.default_view = fc.fullCalendar('getView').name; } this.search_request = id; this.search_query = q; // change to list view fc.fullCalendar('option', 'listSections', 'month') .fullCalendar('option', 'listRange', Math.max(60, settings['agenda_range'])) .fullCalendar('changeView', 'table'); update_agenda_toolbar(); // refetch events with new url (if not already triggered by changeView) if (!this.is_loading) fc.fullCalendar('refetchEvents'); } else // empty search input equals reset this.reset_quicksearch(); } }; // reset search and get back to normal event listing this.reset_quicksearch = function() { $(rcmail.gui_objects.qsearchbox).val(''); if (this._search_message) rcmail.hide_message(this._search_message); if (this.search_request) { // hide bottom links of agenda view fc.find('.fc-list-content > .fc-listappend').hide(); // restore original event sources and view mode from fullcalendar fc.fullCalendar('option', 'listSections', settings['agenda_sections']) .fullCalendar('option', 'listRange', settings['agenda_range']); update_agenda_toolbar(); for (var sid in this.calendars) { if (this.calendars[sid]) this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, ''); } if (this.default_view) fc.fullCalendar('changeView', this.default_view); if (!this.is_loading) fc.fullCalendar('refetchEvents'); this.search_request = this.search_query = null; } }; // callback if all sources have been fetched from server this.events_loaded = function(count) { var addlinks, append = ''; // enhance list view when searching if (this.search_request) { if (!count) { this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice'); append = '
' + rcmail.gettext('searchnoresults', 'calendar') + '
'; } append += ''; addlinks = true; } if (fc.fullCalendar('getView').name == 'table') { var container = fc.find('.fc-list-content > .fc-listappend'); if (append) { if (!container.length) container = $('
').appendTo(fc.find('.fc-list-content')); container.html(append).show(); } else if (container.length) container.hide(); // add links to adjust search date range if (addlinks) { var lc = container.find('.fc-bottomlinks'); $('').attr('href', '#').html(rcmail.gettext('searchearlierdates', 'calendar')).appendTo(lc).click(function(){ fc.fullCalendar('incrementDate', 0, -1, 0); }); lc.append(" "); $('').attr('href', '#').html(rcmail.gettext('searchlaterdates', 'calendar')).appendTo(lc).click(function(){ var range = fc.fullCalendar('option', 'listRange'); if (range < 90) { fc.fullCalendar('option', 'listRange', fc.fullCalendar('option', 'listRange') + 30).fullCalendar('render'); update_agenda_toolbar(); } else fc.fullCalendar('incrementDate', 0, 1, 0); }); } } if (this.fisheye_date) this.fisheye_view(this.fisheye_date); }; // resize and reposition (center) the dialog window this.dialog_resize = function(id, height, width) { var win = $(window), w = win.width(), h = win.height(); $(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) }) .dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?) }; // adjust calendar view size this.view_resize = function() { var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').outerHeight() : 0; fc.fullCalendar('option', 'height', $('#calendar').height() - footer); }; /*** startup code ***/ // create list of event sources AKA calendars this.calendars = {}; var id, li, cal, active, color, brightness, event_sources = []; for (id in rcmail.env.calendars) { cal = rcmail.env.calendars[id]; this.calendars[id] = $.extend({ url: "./?_task=calendar&_action=load_events&source="+escape(id), editable: !cal.readonly, className: 'fc-event-cal-'+id, id: id }, cal); // choose black text color when background is bright, white otherwise if (color = settings.event_coloring % 2 ? '' : '#' + cal.color) { if (/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.test(color)) { // use information about brightness calculation found at // http://javascriptrules.com/2009/08/05/css-color-brightness-contrast-using-javascript/ brightness = (parseInt(RegExp.$1, 16) * 299 + parseInt(RegExp.$2, 16) * 587 + parseInt(RegExp.$3, 16) * 114) / 1000; if (brightness > 125) this.calendars[id].textColor = 'black'; } } this.calendars[id].color = color; if ((active = cal.active || false)) { event_sources.push(this.calendars[id]); } // init event handler on calendar list checkbox if ((li = rcmail.get_folder_li(id, 'rcmlical'))) { $('#'+li.id+' input').click(function(e){ var id = $(this).data('id'); if (me.calendars[id]) { // add or remove event source on click var action; if (this.checked) { action = 'addEventSource'; me.calendars[id].active = true; } else { action = 'removeEventSource'; me.calendars[id].active = false; } // add/remove event source fc.fullCalendar(action, me.calendars[id]); rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } }); } }).data('id', id).get(0).checked = active; $(li).click(function(e){ var id = $(this).data('id'); rcmail.select_folder(id, 'rcmlical'); rcmail.enable_command('calendar-edit', true); rcmail.enable_command('calendar-remove', 'calendar-showurl', true); me.selected_calendar = id; }) .dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); }) .data('id', id); } if (!cal.readonly && !this.selected_calendar) { this.selected_calendar = id; rcmail.enable_command('addevent', true); } } // select default calendar if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly) this.selected_calendar = settings.default_calendar; var viewdate = new Date(); if (rcmail.env.date) viewdate.setTime(fromunixtime(rcmail.env.date)); // initalize the fullCalendar plugin var fc = $('#calendar').fullCalendar({ header: { right: 'prev,next today', center: 'title', left: 'agendaDay,agendaWeek,month,table' }, aspectRatio: 1, date: viewdate.getDate(), month: viewdate.getMonth(), year: viewdate.getFullYear(), ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone height: $('#calendar').height(), eventSources: event_sources, monthNames : settings['months'], monthNamesShort : settings['months_short'], dayNames : settings['days'], dayNamesShort : settings['days_short'], firstDay : settings['first_day'], firstHour : settings['first_hour'], slotMinutes : 60/settings['timeslots'], timeFormat: { '': settings['time_format'], agenda: settings['time_format'] + '{ - ' + settings['time_format'] + '}', list: settings['time_format'] + '{ - ' + settings['time_format'] + '}', table: settings['time_format'] + '{ - ' + settings['time_format'] + '}' }, axisFormat : settings['time_format'], columnFormat: { month: 'ddd', // Mon week: 'ddd ' + settings['date_short'], // Mon 9/7 day: 'dddd ' + settings['date_short'], // Monday 9/7 table: settings['date_agenda'] }, titleFormat: { month: 'MMMM yyyy', week: settings['dates_long'], day: 'dddd ' + settings['date_long'], table: settings['dates_long'] }, listPage: 1, // advance one day in agenda view listRange: settings['agenda_range'], listSections: settings['agenda_sections'], tableCols: ['handle', 'date', 'time', 'title', 'location'], defaultView: rcmail.env.view || settings['default_view'], allDayText: rcmail.gettext('all-day', 'calendar'), buttonText: { prev: (bw.ie6 ? ' << ' : ' ◄ '), next: (bw.ie6 ? ' >> ' : ' ► '), today: settings['today'], day: rcmail.gettext('day', 'calendar'), week: rcmail.gettext('week', 'calendar'), month: rcmail.gettext('month', 'calendar'), table: rcmail.gettext('agenda', 'calendar') }, listTexts: { until: rcmail.gettext('until', 'calendar'), past: rcmail.gettext('pastevents', 'calendar'), today: rcmail.gettext('today', 'calendar'), tomorrow: rcmail.gettext('tomorrow', 'calendar'), thisWeek: rcmail.gettext('thisweek', 'calendar'), nextWeek: rcmail.gettext('nextweek', 'calendar'), thisMonth: rcmail.gettext('thismonth', 'calendar'), nextMonth: rcmail.gettext('nextmonth', 'calendar'), future: rcmail.gettext('futureevents', 'calendar'), week: rcmail.gettext('weekofyear', 'calendar') }, selectable: true, selectHelper: false, currentTimeIndicator: settings.time_indicator, loading: function(isLoading) { me.is_loading = isLoading; this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading); // trigger callback if (!isLoading) me.events_loaded($(this).fullCalendar('clientEvents').length); }, // event rendering eventRender: fc_event_render, // render element indicating more (invisible) events overflowRender: function(data, element) { element.html(rcmail.gettext('andnmore', 'calendar').replace('$nr', data.count)) .click(function(e){ me.fisheye_view(data.date); }); }, // callback for date range selection select: function(start, end, allDay, e, view) { var range_select = (!allDay || start.getDate() != end.getDate()) if (dialog_check(e) && range_select) event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar }); if (range_select || ignore_click) view.calendar.unselect(); }, // callback for clicks in all-day box dayClick: function(date, allDay, e, view) { var now = new Date().getTime(); if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000); return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar }); } if (!ignore_click) { view.calendar.gotoDate(date); if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth()) view.calendar.select(date, date, allDay); } day_clicked = date.getTime(); day_clicked_ts = now; }, // callback when a specific event is clicked eventClick: function(event) { if (!event.temp) event_show_dialog(event); }, // callback when an event was dragged and finally dropped eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) { if (event.end == null || event.end.getTime() < event.start.getTime()) { event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS)); } // moved to all-day section: set times to 12:00 - 13:00 if (allDay && !event.allDay) { event.start.setHours(12); event.start.setMinutes(0); event.start.setSeconds(0); event.end.setHours(13); event.end.setMinutes(0); event.end.setSeconds(0); } // moved from all-day section: set times to working hours else if (event.allDay && !allDay) { var newstart = event.start.getTime(); revertFunc(); // revert to get original duration var numdays = Math.max(1, Math.round((event.end.getTime() - event.start.getTime()) / DAY_MS)) - 1; event.start = new Date(newstart); event.end = new Date(newstart + numdays * DAY_MS); event.end.setHours(settings['work_end'] || 18); event.end.setMinutes(0); if (event.end.getTime() < event.start.getTime()) event.end = new Date(newstart + HOUR_MS); } // send move request to server var data = { id: event.id, calendar: event.calendar, start: date2servertime(event.start), end: date2servertime(event.end), allday: allDay?1:0 }; update_event_confirm('move', event, data); }, // callback for event resizing eventResize: function(event, delta) { // sanitize event dates if (event.allDay) event.start.setHours(12); if (!event.end || event.end.getTime() < event.start.getTime()) event.end = new Date(event.start.getTime() + HOUR_MS); // send resize request to server var data = { id: event.id, calendar: event.calendar, start: date2servertime(event.start), end: date2servertime(event.end) }; update_event_confirm('resize', event, data); }, viewDisplay: function(view) { $('#agendaoptions')[view.name == 'table' ? 'show' : 'hide'](); if (minical) { window.setTimeout(function(){ minical.datepicker('setDate', fc.fullCalendar('getDate')); }, exec_deferred); if (view.name != current_view) me.view_resize(); current_view = view.name; me.update_state(); } }, viewRender: function(view) { if (fc && view.name == 'month') fc.fullCalendar('option', 'maxHeight', Math.floor((view.element.parent().height()-18) / 6) - 35); } }); // format time string var formattime = function(hour, minutes, start) { var time, diff, unit, duration = '', d = new Date(); d.setHours(hour); d.setMinutes(minutes); time = $.fullCalendar.formatDate(d, settings['time_format']); if (start) { diff = Math.floor((d.getTime() - start.getTime()) / 60000); if (diff > 0) { unit = 'm'; if (diff >= 60) { unit = 'h'; diff = Math.round(diff / 3) / 20; } duration = ' (' + diff + unit + ')'; } } return [time, duration]; }; var autocomplete_times = function(p, callback) { /* Time completions */ var result = []; var now = new Date(); var st, start = (String(this.element.attr('id')).indexOf('endtime') > 0 && (st = $('#edit-starttime').val()) && $('#edit-startdate').val() == $('#edit-enddate').val()) ? parse_datetime(st, '') : null; var full = p.term - 1 > 0 || p.term.length > 1; var hours = start ? start.getHours() : (full ? parse_datetime(p.term, '') : now).getHours(); var step = 15; var minutes = hours * 60 + (full ? 0 : now.getMinutes()); var min = Math.ceil(minutes / step) * step % 60; var hour = Math.floor(Math.ceil(minutes / step) * step / 60); // list hours from 0:00 till now for (var h = start ? start.getHours() : 0; h < hours; h++) result.push(formattime(h, 0, start)); // list 15min steps for the next two hours for (; h < hour + 2 && h < 24; h++) { while (min < 60) { result.push(formattime(h, min, start)); min += step; } min = 0; } // list the remaining hours till 23:00 while (h < 24) result.push(formattime((h++), 0, start)); return callback(result); }; var autocomplete_open = function(event, ui) { // scroll to current time var $this = $(this); var widget = $this.autocomplete('widget'); var menu = $this.data('autocomplete').menu; var amregex = /^(.+)(a[.m]*)/i; var pmregex = /^(.+)(a[.m]*)/i; var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1'); var li, html; widget.css('width', '10em'); widget.children().each(function(){ li = $(this); html = li.children().first().html().replace(/\s+\(.+\)$/, '').replace(amregex, '0:$1').replace(pmregex, '1:$1'); if (html.indexOf(val) == 0) menu._scrollIntoView(li); }); }; // if start date is changed, shift end date according to initial duration var shift_enddate = function(dateText) { var newstart = parse_datetime('0', dateText); var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000); $('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format'])); event_times_changed(); }; // Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. // Uses the default $.datepicker.iso8601Week() function but takes firstDay setting into account. // This is a temporary fix until http://bugs.jqueryui.com/ticket/8420 is resolved. var iso8601Week = datepicker_settings.calculateWeek = function(date) { var mondayOffset = Math.abs(1 - datepicker_settings.firstDay); return $.datepicker.iso8601Week(new Date(date.getTime() + mondayOffset * 86400000)); }; var minical; var init_calendar_ui = function() { // initialize small calendar widget using jQuery UI datepicker minical = $('#datepicker').datepicker($.extend(datepicker_settings, { inline: true, showWeek: true, changeMonth: true, changeYear: true, onSelect: function(dateText, inst) { ignore_click = true; var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText); fc.fullCalendar('gotoDate', d).fullCalendar('select', d, d, true); }, onChangeMonthYear: function(year, month, inst) { minical.data('year', year).data('month', month); }, beforeShowDay: function(date) { var view = fc.fullCalendar('getView'); var active = view.visStart && date.getTime() >= view.visStart.getTime() && date.getTime() < view.visEnd.getTime(); return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), '']; } })) // set event handler for clicks on calendar week cell of the datepicker widget .click(function(e) { var cell = $(e.target); if (e.target.tagName == 'TD' && cell.hasClass('ui-datepicker-week-col')) { var base_date = minical.datepicker('getDate'); if (minical.data('month')) base_date.setMonth(minical.data('month')-1); if (minical.data('year')) base_date.setYear(minical.data('year')); base_date.setHours(12); base_date.setDate(base_date.getDate() - ((base_date.getDay() + 6) % 7) + datepicker_settings.firstDay); var day_off = base_date.getDay() - datepicker_settings.firstDay; var base_kw = iso8601Week(base_date); var target_kw = parseInt(cell.html()); var diff = (target_kw - base_kw) * 7 * DAY_MS; // select monday of the chosen calendar week var date = new Date(base_date.getTime() - day_off * DAY_MS + diff); fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek'); minical.datepicker('setDate', date); } }); if (rcmail.env.date) { var viewdate = new Date(); viewdate.setTime(fromunixtime(rcmail.env.date)); minical.datepicker('setDate', viewdate); } // init event dialog $('#eventtabs').tabs({ show: function(event, ui) { if (ui.panel.id == 'event-panel-attendees' || ui.panel.id == 'event-panel-resources') { var tab = ui.panel.id == 'event-panel-resources' ? 'resource' : 'attendee'; $('#edit-'+tab+'-name').select(); // update free-busy status if needed if (freebusy_ui.needsupdate && me.selected_event) update_freebusy_status(me.selected_event); // add current user as organizer if non added yet if (!event_attendees.length) { add_attendee($.extend({ role:'ORGANIZER' }, settings.identity)); $('#edit-attendees-form .attendees-invitebox').show(); } } } }); $('#edit-enddate').datepicker(datepicker_settings); $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); }); $('#edit-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed); $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); }); // configure drop-down menu on time input fields based on jquery UI autocomplete $('#edit-starttime, #edit-endtime, #eventedit input.edit-alarm-time') .attr('autocomplete', "off") .autocomplete({ delay: 100, minLength: 1, source: autocomplete_times, open: autocomplete_open, change: event_times_changed, select: function(event, ui) { $(this).val(ui.item[0]); return false; } }) .click(function() { // show drop-down upon clicks $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " "); }).each(function(){ $(this).data('autocomplete')._renderItem = function(ul, item) { return $('
  • ') .data('item.autocomplete', item) .append('' + item[0] + item[1] + '') .appendTo(ul); }; }); // register events on alarm fields init_alarms_edit('#eventedit'); // toggle recurrence frequency forms $('#edit-recurrence-frequency').change(function(e){ var freq = $(this).val().toLowerCase(); $('.recurrence-form').hide(); if (freq) $('#recurrence-form-'+freq+', #recurrence-form-until').show(); }); $('#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); }); $('#event-export-startdate').datepicker(datepicker_settings); // init attendees autocompletion var ac_props; // parallel autocompletion if (rcmail.env.autocomplete_threads > 0) { ac_props = { threads: rcmail.env.autocomplete_threads, sources: rcmail.env.autocomplete_sources }; } rcmail.init_address_input_events($('#edit-attendee-name'), ac_props); rcmail.addEventListener('autocomplete_insert', function(e){ if (e.field.name == 'participant') { $('#edit-attendee-add').click(); } else if (e.field.name == 'resource' && e.data && e.data.email) { add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })); e.field.value = ''; } }); $('#edit-attendee-add').click(function(){ var input = $('#edit-attendee-name'); rcmail.ksearch_blur(); if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) { input.val(''); } }); rcmail.init_address_input_events($('#edit-resource-name'), { action:'calendar/resources-autocomplete' }); $('#edit-resource-add').click(function(){ var input = $('#edit-resource-name'); rcmail.ksearch_blur(); if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })) { input.val(''); } }); $('#edit-resource-find').click(function(){ event_resources_dialog(); return false; }); // keep these two checkboxes in sync $('#edit-attendees-donotify, #edit-attendees-invite').click(function(){ $('#edit-attendees-donotify, #edit-attendees-invite').prop('checked', this.checked); }); $('#edit-attendee-schedule').click(function(){ event_freebusy_dialog(); }); $('#shedule-freebusy-prev').html(bw.ie6 ? '<<' : '◄').button().click(function(){ render_freebusy_grid(-1); }); $('#shedule-freebusy-next').html(bw.ie6 ? '>>' : '►').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset(); $('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); }); $('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); }); $('#schedule-freebusy-workinghours').click(function(){ freebusy_ui.workinhoursonly = this.checked; $('#workinghourscss').remove(); if (this.checked) $('').appendTo('head'); }); $('#event-rsvp input.button').click(function(){ event_rsvp($(this).attr('rel')) }); $('#agenda-listrange').change(function(e){ settings['agenda_range'] = parseInt($(this).val()); fc.fullCalendar('option', 'listRange', settings['agenda_range']).fullCalendar('render'); // TODO: save new settings in prefs }).val(settings['agenda_range']); $('#agenda-listsections').change(function(e){ settings['agenda_sections'] = $(this).val(); fc.fullCalendar('option', 'listSections', settings['agenda_sections']).fullCalendar('render'); // TODO: save new settings in prefs }).val(fc.fullCalendar('option', 'listSections')); // hide event dialog when clicking somewhere into document $(document).bind('mousedown', dialog_check); rcmail.set_busy(false, 'loading', ui_loading); } // initialize more UI elements (deferred) window.setTimeout(init_calendar_ui, exec_deferred); // add proprietary css styles if not IE if (!bw.ie) $('div.fc-content').addClass('rcube-fc-content'); // IE supresses 2nd click event when double-clicking if (bw.ie && bw.vendver < 9) { $('div.fc-content').bind('dblclick', function(e){ if (!$(this).hasClass('fc-widget-header') && fc.fullCalendar('getView').name != 'table') { var date = fc.fullCalendar('getDate'); var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000); event_edit_dialog('new', { start:date, end:enddate, allDay:true, calendar:me.selected_calendar }); } }); } } // end rcube_calendar class /* calendar plugin initialization */ window.rcmail && rcmail.addEventListener('init', function(evt) { // configure toolbar buttons rcmail.register_command('addevent', function(){ cal.add_event(); }, true); rcmail.register_command('print', function(){ cal.print_calendars(); }, true); // configure list operations rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true); rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false); rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false); rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true); rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false); // search and export events rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true); rcmail.register_command('search', function(){ cal.quicksearch(); }, true); rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true); // resource invitation dialog rcmail.register_command('search-resource', function(){ cal.resource_search(); }, true); rcmail.register_command('reset-resource-search', function(){ cal.reset_resource_search(); }, true); rcmail.register_command('add-resource', function(){ cal.add_resource2event(); }, false); // register callback commands rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); }); rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); }); rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); }); rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); }); rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); }); rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); }); rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); }); rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); }); // let's go var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings)); $(window).resize(function(e) { // check target due to bugs in jquery // http://bugs.jqueryui.com/ticket/7514 // http://bugs.jquery.com/ticket/9841 if (e.target == window) { cal.view_resize(); } }).resize(); // show calendars list when ready $('#calendars').css('visibility', 'inherit'); // show toolbar $('#toolbar').show(); }); diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist index 9a472a7a..f09d30f0 100644 --- a/plugins/calendar/config.inc.php.dist +++ b/plugins/calendar/config.inc.php.dist @@ -1,138 +1,145 @@ . | | | +-------------------------------------------------------------------------+ | Author: Lazlo Westerhof | | Thomas Bruederli | +-------------------------------------------------------------------------+ */ // backend type (database, google, kolab) $rcmail_config['calendar_driver'] = "database"; // default calendar view (agendaDay, agendaWeek, month) $rcmail_config['calendar_default_view'] = "agendaWeek"; // show a birthdays calendar from the user's address book(s) $rcmail_config['calendar_contact_birthdays'] = false; // mapping of Roundcube date formats to calendar formats (long/short/agenda) // should be in sync with 'date_formats' in main config $rcmail_config['calendar_date_format_sets'] = array( 'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'), 'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'), 'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'), 'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'), 'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'), 'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'), 'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'), ); // general date format (only set if different from default date format and not user configurable) // $rcmail_config['calendar_date_format'] = "yyyy-MM-dd"; // time format (only set if different from default date format) // $rcmail_config['calendar_time_format'] = "HH:mm"; // short date format (used for column titles) // $rcmail_config['calendar_date_short'] = 'M-d'; // long date format (used for calendar title) // $rcmail_config['calendar_date_long'] = 'MMM d yyyy'; // date format used for agenda view // $rcmail_config['calendar_date_agenda'] = 'ddd MM-dd'; // timeslots per hour (1, 2, 3, 4, 6) $rcmail_config['calendar_timeslots'] = 2; // show this number of days in agenda view $rcmail_config['calendar_agenda_range'] = 60; // first day of the week (0-6) $rcmail_config['calendar_first_day'] = 1; // first hour of the calendar (0-23) $rcmail_config['calendar_first_hour'] = 6; // working hours begin $rcmail_config['calendar_work_start'] = 6; // working hours end $rcmail_config['calendar_work_end'] = 18; // show line at current time of the day $rcmail_config['calendar_time_indicator'] = true; // default alarm settings for new events. // this is only a preset when a new event dialog opens // possible values are , DISPLAY, EMAIL $rcmail_config['calendar_default_alarm_type'] = ''; // default alarm offset for new events. // use ical-style offset values like "-1H" (one hour before) or "+30M" (30 minutes after) $rcmail_config['calendar_default_alarm_offset'] = '-15M'; // how to colorize events: // 0: according to calendar color // 1: according to category color // 2: calendar for outer, category for inner color // 3: category for outer, calendar for inner color $rcmail_config['calendar_event_coloring'] = 0; // event categories $rcmail_config['calendar_categories'] = array( 'Personal' => 'c0c0c0', 'Work' => 'ff0000', 'Family' => '00ff00', 'Holiday' => 'ff6600', ); // enable users to invite/edit attendees for shared events organized by others $rcmail_config['calendar_allow_invite_shared'] = false; // allow users to accecpt iTip invitations who are no explicitly listed as attendee. // this can be the case if invitations are sent to mailing lists or alias email addresses. $rcmail_config['calendar_allow_itip_uninvited'] = true; // enable asynchronous free-busy triggering after data changed $rcmail_config['calendar_freebusy_trigger'] = false; // SMTP server host used to send (anonymous) itip messages. // Set to '' in order to use PHP's mail() function for email delivery. // To override the SMTP port or connection method, provide a full URL like 'tls://somehost:587' $rcmail_config['calendar_itip_smtp_server'] = null; // SMTP username used to send (anonymous) itip messages $rcmail_config['calendar_itip_smtp_user'] = 'smtpauth'; // SMTP password used to send (anonymous) itip messages $rcmail_config['calendar_itip_smtp_pass'] = '123456'; // Base URL to build fully qualified URIs to access calendars via CALDAV // The following replacement variables are supported: // %h - Current HTTP host // %u - Current webmail user name // %n - Calendar name // %i - Calendar UUID // $rcmail_config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i'; +// Driver to provide a resource directory ('ldap' is the only implementation yet). +// Leave empty or commented to disable resources support. +// $rcmail_config['calendar_resources_driver'] = 'ldap'; + +// LDAP directory configuration to find avilable resources for events +// $rcmail_config['calendar_resources_directory'] = array(/* ldap_public-like address book configuration */) + ?> diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 3d8d083a..20f9f164 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -1,583 +1,532 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * 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 . */ /** * Struct of an internal event object how it is passed from/to the driver classes: * * $event = array( * 'id' => 'Event ID used for editing', * 'uid' => 'Unique identifier of this event', * 'calendar' => 'Calendar identifier to add event to or where the event is stored', * 'start' => DateTime, // Event start date/time as DateTime object * 'end' => DateTime, // Event end date/time as DateTime object * 'allday' => true|false, // Boolean flag if this is an all-day event * 'changed' => DateTime, // Last modification date of event * 'title' => 'Event title/summary', * 'location' => 'Location string', * 'description' => 'Event description', * 'url' => 'URL to more information', * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY', * 'INTERVAL' => 1...n, * 'UNTIL' => DateTime, * 'COUNT' => 1..n, // number of times * // + more properties (see http://www.kanzaki.com/docs/ical/recur.html) * 'EXDATE' => array(), // list of DateTime objects of exception Dates/Times * 'EXCEPTIONS' => array(), list of event objects which denote exceptions in the recurrence chain * ), * 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event * 'categories' => 'Event category', * 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as * 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest) * 'sensitivity' => 'public|private|confidential', // Event sensitivity * 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event) * 'attachments' => array( // List of attachments * 'name' => 'File name', * 'mimetype' => 'Content type', * 'size' => 1..n, // in bytes * 'id' => 'Attachment identifier' * ), * 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated * 'attendees' => array( // List of event participants * 'name' => 'Participant name', * 'email' => 'Participant e-mail address', // used as identifier * 'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR', * 'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED' * 'rsvp' => true|false, * ), * * '_savemode' => 'all|future|current|new', // How changes on recurring event should be handled * '_notify' => true|false, // whether to notify event attendees about changes * '_fromcalendar' => 'Calendar identifier where the event was stored before', * ); */ /** * Interface definition for calendar driver classes */ abstract class calendar_driver { const BIRTHDAY_CALENDAR_ID = '__bdays__'; // features supported by backend public $alarms = false; public $attendees = false; - public $resources = false; public $freebusy = false; public $attachments = false; public $undelete = false; // event undelete action public $categoriesimmutable = false; public $alarm_types = array('DISPLAY'); public $alarm_absolute = true; public $last_error; protected $default_categories = array( 'Personal' => 'c0c0c0', 'Work' => 'ff0000', 'Family' => '00ff00', 'Holiday' => 'ff6600', ); /** * Get a list of available calendars from this source * * @param bool $active Return only active calendars * @param bool $personal Return only personal calendars * * @return array List of calendars */ abstract function list_calendars($active = false, $personal = false); /** * Create a new calendar assigned to the current user * * @param array Hash array with calendar properties * name: Calendar name * color: The color of the calendar * showalarms: True if alarms are enabled * @return mixed ID of the calendar on success, False on error */ abstract function create_calendar($prop); /** * Update properties of an existing calendar * * @param array Hash array with calendar properties * id: Calendar Identifier * name: Calendar name * color: The color of the calendar * showalarms: True if alarms are enabled (if supported) * @return boolean True on success, Fales on failure */ abstract function edit_calendar($prop); /** * Set active/subscribed state of a calendar * * @param array Hash array with calendar properties * id: Calendar Identifier * active: True if calendar is active, false if not * @return boolean True on success, Fales on failure */ abstract function subscribe_calendar($prop); /** * Delete the given calendar with all its contents * * @param array Hash array with calendar properties * id: Calendar Identifier * @return boolean True on success, Fales on failure */ abstract function remove_calendar($prop); /** * Add a single event to the database * * @param array Hash array with event properties (see header of this file) * @return mixed New event ID on success, False on error */ abstract function new_event($event); /** * Update an event entry with the given data * * @param array Hash array with event properties (see header of this file) * @return boolean True on success, False on error */ abstract function edit_event($event); /** * Move a single event * * @param array Hash array with event properties: * id: Event identifier * start: Event start date/time as DateTime object * end: Event end date/time as DateTime object * allday: Boolean flag if this is an all-day event * @return boolean True on success, False on error */ abstract function move_event($event); /** * Resize a single event * * @param array Hash array with event properties: * id: Event identifier * start: Event start date/time as DateTime object with timezone * end: Event end date/time as DateTime object with timezone * @return boolean True on success, False on error */ abstract function resize_event($event); /** * Remove a single event from the database * * @param array Hash array with event properties: * id: Event identifier * @param boolean Remove event irreversible (mark as deleted otherwise, * if supported by the backend) * * @return boolean True on success, False on error */ abstract function remove_event($event, $force = true); /** * Restores a single deleted event (if supported) * * @param array Hash array with event properties: * id: Event identifier * * @return boolean True on success, False on error */ public function restore_event($event) { return false; } /** * Return data of a single event * * @param mixed UID string or hash array with event properties: * id: Event identifier * calendar: Calendar identifier (optional) * @param boolean If true, only writeable calendars shall be searched * @param boolean If true, only active calendars shall be searched * @param boolean If true, only personal calendars shall be searched * * @return array Event object as hash array */ abstract function get_event($event, $writeable = false, $active = false, $personal = false); /** * Get events from source. * * @param integer Event's new start (unix timestamp) * @param integer Event's new end (unix timestamp) * @param string Search query (optional) * @param mixed List of calendar IDs to load events from (either as array or comma-separated string) * @param boolean Include virtual/recurring events (optional) * @param integer Only list events modified since this time (unix timestamp) * @return array A list of event objects (see header of this file for struct of an event) */ abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null); /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of calendar IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array: * id: Event identifier * uid: Unique identifier of this event * start: Event start date/time as DateTime object * end: Event end date/time as DateTime object * allday: Boolean flag if this is an all-day event * title: Event title/summary * location: Location string */ abstract function pending_alarms($time, $calendars = null); /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string Event identifier * @param integer Suspend the alarm for this number of seconds */ abstract function dismiss_alarm($event_id, $snooze = 0); /** * Check the given event object for validity * * @param array Event object as hash array * @return boolean True if valid, false if not */ public function validate($event) { $valid = true; if (!is_object($event['start']) || !is_a($event['start'], 'DateTime')) $valid = false; if (!is_object($event['end']) || !is_a($event['end'], 'DateTime')) $valid = false; return $valid; } /** * Get list of event's attachments. * Drivers can return list of attachments as event property. * If they will do not do this list_attachments() method will be used. * * @param array $event Hash array with event properties: * id: Event identifier * calendar: Calendar identifier * * @return array List of attachments, each as hash array: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function list_attachments($event) { } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $event Hash array with event properties: * id: Event identifier * calendar: Calendar identifier * * @return array Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $event) { } /** * Get attachment body * * @param string $id Attachment identifier * @param array $event Hash array with event properties: * id: Event identifier * calendar: Calendar identifier * * @return string Attachment body */ public function get_attachment_body($id, $event) { } /** * List availabale categories * The default implementation reads them from config/user prefs */ public function list_categories() { $rcmail = rcube::get_instance(); return $rcmail->config->get('calendar_categories', $this->default_categories); } /** * Create a new category */ public function add_category($name, $color) { } /** * Remove the given category */ public function remove_category($name) { } /** * Update/replace a category */ public function replace_category($oldname, $name, $color) { } /** * Fetch free/busy information from a person within the given range * * @param string E-mail address of attendee * @param integer Requested period start date/time as unix timestamp * @param integer Requested period end date/time as unix timestamp * * @return array List of busy timeslots within the requested range */ public function get_freebusy_list($email, $start, $end) { return false; } /** * Callback function to produce driver-specific calendar create/edit form * * @param string Request action 'form-edit|form-new' * @param array Calendar properties (e.g. id, color) * @param array Edit form fields * * @return string HTML content of the form */ public function calendar_form($action, $calendar, $formfields) { $html = ''; foreach ($formfields as $field) { $html .= html::div('form-section', html::label($field['id'], $field['label']) . $field['value']); } return $html; } /** * Return a (limited) list of color values to be used for calendar and category coloring * * @return mixed List for colors as hex values or false if no presets should be shown */ public function get_color_values() { return false; } /** * Compose a list of birthday events from the contact records in the user's address books. * * This is a default implementation using Roundcube's address book API. * It can be overriden with a more optimized version by the individual drivers. * * @param integer Event's new start (unix timestamp) * @param integer Event's new end (unix timestamp) * @param string Search query (optional) * @param integer Only list events modified since this time (unix timestamp) * @return array A list of event records */ public function load_birthday_events($start, $end, $search = null, $modifiedsince = null) { // ignore update requests for simplicity reasons if (!empty($modifiedsince)) { return array(); } // convert to DateTime for comparisons $start = new DateTime('@'.$start); $end = new DateTime('@'.$end); // extract the current year $year = $start->format('Y'); $year2 = $end->format('Y'); $events = array(); $search = mb_strtolower($search); $rcmail = rcmail::get_instance(); $cache = $rcmail->get_cache('calendar.birthdays', 'db', 3600); $cache->expunge(); $alarm_type = $rcmail->config->get('calendar_birthdays_alarm_type', ''); $alarm_offset = $rcmail->config->get('calendar_birthdays_alarm_offset', '-1D'); $alarms = $alarm_type ? $alarm_offset . ':' . $alarm_type : null; // let the user select the address books to consider in prefs $selected_sources = $rcmail->config->get('calendar_birthday_adressbooks'); $sources = $selected_sources ?: array_keys($rcmail->get_address_sources(false, true)); foreach ($sources as $source) { $abook = $rcmail->get_address_book($source); // skip LDAP address books unless selected by the user if (!$abook || ($abook instanceof rcube_ldap && empty($selected_sources))) { continue; } $abook->set_pagesize(10000); // check for cached results $cache_records = array(); $cached = $cache->get($source); // iterate over (cached) contacts foreach (($cached ?: $abook->search('*', '', 2, true, true, array('birthday'))) as $contact) { if (is_array($contact) && !empty($contact['birthday'])) { try { if (is_array($contact['birthday'])) $contact['birthday'] = reset($contact['birthday']); $bday = $contact['birthday'] instanceof DateTime ? $contact['birthday'] : new DateTime($contact['birthday'], new DateTimezone('UTC')); $birthyear = $bday->format('Y'); } catch (Exception $e) { console('BIRTHDAY PARSE ERROR: ' . $e); continue; } $display_name = rcube_addressbook::compose_display_name($contact); $event_title = $rcmail->gettext(array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name)), 'calendar'); // add stripped record to cache if (empty($cached)) { $cache_records[] = array( 'ID' => $contact['ID'], 'name' => $display_name, 'birthday' => $bday->format('Y-m-d'), ); } // filter by search term (only name is involved here) if (!empty($search) && strpos(mb_strtolower($event_title), $search) === false) { continue; } // quick-and-dirty recurrence computation: just replace the year $bday->setDate($year, $bday->format('n'), $bday->format('j')); $bday->setTime(12, 0, 0); // date range reaches over multiple years: use end year if not in range if (($bday > $end || $bday < $start) && $year2 != $year) { $bday->setDate($year2, $bday->format('n'), $bday->format('j')); $year = $year2; } // birthday is within requested range if ($bday <= $end && $bday >= $start) { $age = $year - $birthyear; $event = array( 'id' => md5('bday_' . $contact['ID'] . $year), 'calendar' => self::BIRTHDAY_CALENDAR_ID, 'title' => $event_title, 'description' => $rcmail->gettext(array('name' => 'birthdayage', 'vars' => array('age' => $age)), 'calendar'), // Add more contact information to description block? 'allday' => true, 'start' => $bday, 'alarms' => $alarms, ); $event['end'] = clone $bday; $event['end']->add(new DateInterval('PT1H')); $events[] = $event; } } } // store collected contacts in cache if (empty($cached)) { $cache->write($source, $cache_records); } } return $events; } - /** - * Store alarm dismissal for birtual birthay events - * - * @param string Event identifier - * @param integer Suspend the alarm for this number of seconds - */ - public function dismiss_birthday_alarm($event_id, $snooze = 0) - { - $rcmail = rcmail::get_instance(); - $cache = $rcmail->get_cache('calendar.birthdayalarms', 'db', 86400 * 30); - $cache->remove($event_id); - - // compute new notification time or disable if not snoozed - $notifyat = $snooze > 0 ? time() + $snooze : null; - $cache->set($event_id, array('snooze' => $snooze, 'notifyat' => $notifyat)); - - return true; - } - - - /** - * Fetch resource objects to be displayed for booking - * - * @param string Search query (optional) - * @return array List of resource records available for booking - */ - public function load_resources($query = null) - { - return array(); - } - - /** - * Return properties of a single resource - * - * @param mixed UID string - * @return array Resource object as hash array - */ - public function get_resource($uid) - { - return null; - } - - /** - * - */ - public function get_resource_owner($id) - { - return null; - } - } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 1bf4e17f..0bcab593 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -1,1391 +1,1288 @@ * @author Aleksander Machniak * * 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 . */ require_once(dirname(__FILE__) . '/kolab_calendar.php'); class kolab_driver extends calendar_driver { // features this backend supports public $alarms = true; public $attendees = true; public $freebusy = true; public $attachments = true; public $undelete = true; public $alarm_types = array('DISPLAY'); public $categoriesimmutable = true; private $rc; private $cal; private $calendars; private $has_writeable = false; private $freebusy_trigger = false; /** * Default constructor */ public function __construct($cal) { + $cal->require_plugin('libkolab'); + $this->cal = $cal; $this->rc = $cal->rc; $this->_read_calendars(); $this->cal->register_action('push-freebusy', array($this, 'push_freebusy')); $this->cal->register_action('calendar-acl', array($this, 'calendar_acl')); $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false); if (kolab_storage::$version == '2.0') { $this->alarm_types = array('DISPLAY'); $this->alarm_absolute = false; } - - if ($this->rc->config->get('calendar_resources_directory')) { - $this->resources = true; - } } /** * Read available calendars from server */ private function _read_calendars() { // already read sources if (isset($this->calendars)) return $this->calendars; // get all folders that have "event" type, sorted by namespace/name $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event')); $this->calendars = array(); foreach ($folders as $folder) { $calendar = new kolab_calendar($folder->name, $this->cal); $this->calendars[$calendar->id] = $calendar; if (!$calendar->readonly) $this->has_writeable = true; } return $this->calendars; } /** * Get a list of available calendars from this source * * @param bool $active Return only active calendars * @param bool $personal Return only personal calendars * * @return array List of calendars */ public function list_calendars($active = false, $personal = false) { // attempt to create a default calendar for this user if (!$this->has_writeable) { if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) { unset($this->calendars); $this->_read_calendars(); } } $folders = $this->filter_calendars(false, $active, $personal); $calendars = $names = array(); // include virtual folders for a full folder tree if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index',''))) $folders = kolab_storage::folder_hierarchy($folders); foreach ($folders as $id => $cal) { $fullname = $cal->get_name(); $listname = kolab_storage::folder_displayname($fullname, $names); // special handling for virtual folders if ($cal->virtual) { $calendars[$cal->id] = array( 'id' => $cal->id, 'name' => $fullname, 'listname' => $listname, 'virtual' => true, 'readonly' => true, ); } else { $calendars[$cal->id] = array( 'id' => $cal->id, 'name' => $fullname, 'listname' => $listname, 'editname' => $cal->get_foldername(), 'color' => $cal->get_color(), 'readonly' => $cal->readonly, 'showalarms' => $cal->alarms, 'class_name' => $cal->get_namespace(), 'default' => $cal->storage->default, 'active' => $cal->storage->is_active(), 'owner' => $cal->get_owner(), 'children' => true, // TODO: determine if that folder indeed has child folders 'caldavurl' => $cal->get_caldav_url(), ); } } // append the virtual birthdays calendar if ($this->rc->config->get('calendar_contact_birthdays', false)) { $id = self::BIRTHDAY_CALENDAR_ID; $prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs if (!$active || $prefs[$id]['active']) { $calendars[$id] = array( 'id' => $id, 'name' => $this->cal->gettext('birthdays'), 'listname' => $this->cal->gettext('birthdays'), 'color' => $prefs[$id]['color'], 'active' => $prefs[$id]['active'], 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'), 'class_name' => 'birthdays', 'readonly' => true, 'default' => false, 'children' => false, ); } } return $calendars; } /** * Get list of calendars according to specified filters * * @param bool $writeable Return only writeable calendars * @param bool $active Return only active calendars * @param bool $personal Return only personal calendars * * @return array List of calendars */ protected function filter_calendars($writeable = false, $active = false, $personal = false) { $calendars = array(); $plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array( 'list' => $this->calendars, 'calendars' => $calendars, 'writeable' => $writeable, 'active' => $active, 'personal' => $personal, )); if ($plugin['abort']) { return $plugin['calendars']; } foreach ($this->calendars as $cal) { if (!$cal->ready) { continue; } if ($writeable && $cal->readonly) { continue; } if ($active && !$cal->storage->is_active()) { continue; } if ($personal && $cal->get_namespace() != 'personal') { continue; } $calendars[$cal->id] = $cal; } return $calendars; } /** * Create a new calendar assigned to the current user * * @param array Hash array with calendar properties * name: Calendar name * color: The color of the calendar * @return mixed ID of the calendar on success, False on error */ public function create_calendar($prop) { $prop['type'] = 'event'; $prop['active'] = true; $prop['subscribed'] = true; $folder = kolab_storage::folder_update($prop); if ($folder === false) { $this->last_error = $this->cal->gettext(kolab_storage::$last_error); return false; } // create ID $id = kolab_storage::folder_id($folder); // save color in user prefs (temp. solution) $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); if (isset($prop['color'])) $prefs['kolab_calendars'][$id]['color'] = $prop['color']; if (isset($prop['showalarms'])) $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_calendars'][$id]) $this->rc->user->save_prefs($prefs); return $id; } /** * Update properties of an existing calendar * * @see calendar_driver::edit_calendar() */ public function edit_calendar($prop) { if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) { $prop['oldname'] = $cal->get_realname(); $newfolder = kolab_storage::folder_update($prop); if ($newfolder === false) { $this->last_error = $this->cal->gettext(kolab_storage::$last_error); return false; } // create ID $id = kolab_storage::folder_id($newfolder); } else { $id = $prop['id']; } // fallback to local prefs $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']); if (isset($prop['color'])) $prefs['kolab_calendars'][$id]['color'] = $prop['color']; if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID) $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : ''; else if (isset($prop['showalarms'])) $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if (!empty($prefs['kolab_calendars'][$id])) $this->rc->user->save_prefs($prefs); return true; } /** * Set active/subscribed state of a calendar * * @see calendar_driver::subscribe_calendar() */ public function subscribe_calendar($prop) { if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) { return $cal->storage->activate($prop['active']); } else { // save state in local prefs $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); $prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active']; $this->rc->user->save_prefs($prefs); return true; } return false; } /** * Delete the given calendar with all its contents * * @see calendar_driver::remove_calendar() */ public function remove_calendar($prop) { if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) { $folder = $cal->get_realname(); if (kolab_storage::folder_delete($folder)) { // remove color in user prefs (temp. solution) $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); unset($prefs['kolab_calendars'][$prop['id']]); $this->rc->user->save_prefs($prefs); return true; } else $this->last_error = kolab_storage::$last_error; } return false; } /** * Fetch a single event * * @see calendar_driver::get_event() * @return array Hash array with event properties, false if not found */ public function get_event($event, $writeable = false, $active = false, $personal = false) { if (is_array($event)) { $id = $event['id'] ? $event['id'] : $event['uid']; $cal = $event['calendar']; } else { $id = $event; } if ($cal) { if ($storage = $this->calendars[$cal]) { return $storage->get_event($id); } } // iterate over all calendar folders and search for the event ID else { foreach ($this->filter_calendars($writeable, $active, $personal) as $calendar) { if ($result = $calendar->get_event($id)) { return $result; } } } return false; } /** * Add a single event to the database * * @see calendar_driver::new_event() */ public function new_event($event) { if (!$this->validate($event)) return false; $cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars)); if ($storage = $this->calendars[$cid]) { // handle attachments to add if (!empty($event['attachments'])) { foreach ($event['attachments'] as $idx => $attachment) { // we'll read file contacts into memory, Horde/Kolab classes does the same // So we cannot save memory, rcube_imap class can do this better $event['attachments'][$idx]['content'] = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']); } } $success = $storage->insert_event($event); if ($success && $this->freebusy_trigger) { $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); $this->freebusy_trigger = false; // disable after first execution (#2355) } return $success; } return false; } /** * Update an event entry with the given data * * @see calendar_driver::new_event() * @return boolean True on success, False on error */ public function edit_event($event) { return $this->update_event($event); } /** * Move a single event * * @see calendar_driver::move_event() * @return boolean True on success, False on error */ public function move_event($event) { if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) { unset($ev['sequence']); return $this->update_event($event + $ev); } return false; } /** * Resize a single event * * @see calendar_driver::resize_event() * @return boolean True on success, False on error */ public function resize_event($event) { if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) { unset($ev['sequence']); return $this->update_event($event + $ev); } return false; } /** * Remove a single event * * @param array Hash array with event properties: * id: Event identifier * @param boolean Remove record(s) irreversible (mark as deleted otherwise) * * @return boolean True on success, False on error */ public function remove_event($event, $force = true) { $success = false; $savemode = $event['_savemode']; if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) { $event['_savemode'] = $savemode; $savemode = 'all'; $master = $event; $this->rc->session->remove('calendar_restore_event_data'); // read master if deleting a recurring event if ($event['recurrence'] || $event['recurrence_id']) { $master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event; $savemode = $event['_savemode']; } // removing an exception instance if ($event['recurrence_id']) { $i = $event['_instance'] - 1; if (!empty($master['recurrence']['EXCEPTIONS'][$i])) { unset($master['recurrence']['EXCEPTIONS'][$i]); } } switch ($savemode) { case 'current': $_SESSION['calendar_restore_event_data'] = $master; // removing the first instance => just move to next occurence if ($master['id'] == $event['id']) { $recurring = reset($storage->_get_recurring_events($event, $event['start'], null, $event['id'].'-1')); // no future instances found: delete the master event (bug #1677) if (!$recurring['start']) { $success = $storage->delete_event($master, $force); break; } $master['start'] = $recurring['start']; $master['end'] = $recurring['end']; if ($master['recurrence']['COUNT']) $master['recurrence']['COUNT']--; } else { // add exception to master event $master['recurrence']['EXDATE'][] = $event['start']; } $success = $storage->update_event($master); break; case 'future': if ($master['id'] != $event['id']) { $_SESSION['calendar_restore_event_data'] = $master; // set until-date on master event $master['recurrence']['UNTIL'] = clone $event['start']; $master['recurrence']['UNTIL']->sub(new DateInterval('P1D')); unset($master['recurrence']['COUNT']); // if all future instances are deleted, remove recurrence rule entirely (bug #1677) if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) $master['recurrence'] = array(); $success = $storage->update_event($master); break; } default: // 'all' is default $success = $storage->delete_event($master, $force); break; } } if ($success && $this->freebusy_trigger) $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); return $success; } /** * Restore a single deleted event * * @param array Hash array with event properties: * id: Event identifier * @return boolean True on success, False on error */ public function restore_event($event) { if ($storage = $this->calendars[$event['calendar']]) { if (!empty($_SESSION['calendar_restore_event_data'])) $success = $storage->update_event($_SESSION['calendar_restore_event_data']); else $success = $storage->restore_event($event); if ($success && $this->freebusy_trigger) $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); return $success; } return false; } /** * Wrapper to update an event object depending on the given savemode */ private function update_event($event) { if (!($storage = $this->calendars[$event['calendar']])) return false; // move event to another folder/calendar if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) { if (!($fromcalendar = $this->calendars[$event['_fromcalendar']])) return false; if ($event['_savemode'] != 'new') { if (!$fromcalendar->storage->move($event['id'], $storage->get_realname())) return false; $fromcalendar = $storage; } } else $fromcalendar = $storage; $success = false; $savemode = 'all'; $attachments = array(); $old = $master = $fromcalendar->get_event($event['id']); if (!$old || !$old['start']) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to load event object to update: id=" . $event['id']), true, false); return false; } // delete existing attachment(s) if (!empty($event['deleted_attachments'])) { foreach ($event['deleted_attachments'] as $attachment) { if (!empty($old['attachments'])) { foreach ($old['attachments'] as $idx => $att) { if ($att['id'] == $attachment) { $old['attachments'][$idx]['_deleted'] = true; } } } } unset($event['deleted_attachments']); } // handle attachments to add if (!empty($event['attachments'])) { foreach ($event['attachments'] as $attachment) { // skip entries without content (could be existing ones) if (!$attachment['data'] && !$attachment['path']) continue; $attachments[] = array( 'name' => $attachment['name'], 'mimetype' => $attachment['mimetype'], 'content' => $attachment['data'], 'path' => $attachment['path'], ); } } $event['attachments'] = array_merge((array)$old['attachments'], $attachments); // modify a recurring event, check submitted savemode to do the right things if ($old['recurrence'] || $old['recurrence_id']) { $master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old; $savemode = $event['_savemode']; } // keep saved exceptions (not submitted by the client) if ($old['recurrence']['EXDATE']) $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE']; if ($old['recurrence']['EXCEPTIONS']) $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS']; switch ($savemode) { case 'new': // save submitted data as new (non-recurring) event $event['recurrence'] = array(); $event['uid'] = $this->cal->generate_uid(); // copy attachment data to new event foreach ((array)$event['attachments'] as $idx => $attachment) { if (!$attachment['data']) $attachment['data'] = $fromcalendar->get_attachment_body($attachment['id'], $event); } $success = $storage->insert_event($event); break; case 'future': case 'current': // recurring instances shall not store recurrence rules $event['recurrence'] = array(); $event['thisandfuture'] = $savemode == 'future'; // remove some internal properties which should not be saved unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']); // save properties to a recurrence exception instance if ($old['recurrence_id']) { $i = $old['_instance'] - 1; if (!empty($master['recurrence']['EXCEPTIONS'][$i])) { $master['recurrence']['EXCEPTIONS'][$i] = $event; $success = $storage->update_event($master, $old['id']); break; } } // save as new exception to master event $master['recurrence']['EXCEPTIONS'][] = $event; $success = $storage->update_event($master); break; default: // 'all' is default $event['id'] = $master['id']; $event['uid'] = $master['uid']; // use start date from master but try to be smart on time or duration changes $old_start_date = $old['start']->format('Y-m-d'); $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i'); $old_duration = $old['end']->format('U') - $old['start']->format('U'); $new_start_date = $event['start']->format('Y-m-d'); $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i'); $new_duration = $event['end']->format('U') - $event['start']->format('U'); $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; // shifted or resized if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { $event['start'] = $master['start']->add($old['start']->diff($event['start'])); $event['end'] = clone $event['start']; $event['end']->add(new DateInterval('PT'.$new_duration.'S')); // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event() if ($old_start_date != $new_start_date) { if (strlen($event['recurrence']['BYDAY']) == 2) unset($event['recurrence']['BYDAY']); if ($old['recurrence']['BYMONTH'] == $old['start']->format('n')) unset($event['recurrence']['BYMONTH']); } } // dates did not change, use the ones from master else if ($event['start'] == $old['start'] && $event['end'] == $old['end']) { $event['start'] = $master['start']; $event['end'] = $master['end']; } // unset _dateonly flags in (cached) date objects unset($event['start']->_dateonly, $event['end']->_dateonly); $success = $storage->update_event($event); break; } if ($success && $this->freebusy_trigger) $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); return $success; } /** * Get events from source. * * @param integer Event's new start (unix timestamp) * @param integer Event's new end (unix timestamp) * @param string Search query (optional) * @param mixed List of calendar IDs to load events from (either as array or comma-separated string) * @param boolean Include virtual events (optional) * @param integer Only list events modified since this time (unix timestamp) * @return array A list of event records */ public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null) { if ($calendars && is_string($calendars)) $calendars = explode(',', $calendars); $query = array(); if ($modifiedsince) $query[] = array('changed', '>=', $modifiedsince); $events = $categories = array(); foreach (array_keys($this->calendars) as $cid) { if ($calendars && !in_array($cid, $calendars)) continue; $events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual, $query)); $categories += $this->calendars[$cid]->categories; } // add events from the address books birthday calendar if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) { $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince)); } // add new categories to user prefs $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories); if ($newcats = array_diff(array_map('strtolower', array_keys($categories)), array_map('strtolower', array_keys($old_categories)))) { foreach ($newcats as $category) $old_categories[$category] = ''; // no color set yet $this->rc->user->save_prefs(array('calendar_categories' => $old_categories)); } return $events; } /** * Get a list of pending alarms to be displayed to the user * * @see calendar_driver::pending_alarms() */ public function pending_alarms($time, $calendars = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) return array(); if ($calendars && is_string($calendars)) $calendars = explode(',', $calendars); $time = $slot + $interval; $events = array(); $query = array(array('tags', '=', 'x-has-alarms')); foreach ($this->calendars as $cid => $calendar) { // skip calendars with alarms disabled if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars))) continue; foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) { // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($e); if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') { $id = $e['id']; $events[$id] = $e; $events[$id]['notifyat'] = $alarm['time']; } } } // get alarm information stored in local database if (!empty($events)) { $event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events)); $result = $this->rc->db->query(sprintf( "SELECT * FROM kolab_alarms WHERE event_id IN (%s) AND user_id=?", join(',', $event_ids), $this->rc->db->now() ), $this->rc->user->ID ); while ($result && ($e = $this->rc->db->fetch_assoc($result))) { $dbdata[$e['event_id']] = $e; } } $alarms = array(); foreach ($events as $id => $e) { // skip dismissed if ($dbdata[$id]['dismissed']) continue; // snooze function may have shifted alarm time $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $e['notifyat']; if ($notifyat <= $time) $alarms[] = $e; } return $alarms; } /** * Feedback after showing/sending an alarm notification * * @see calendar_driver::dismiss_alarm() */ public function dismiss_alarm($event_id, $snooze = 0) { // delete old alarm entry $this->rc->db->query( "DELETE FROM kolab_alarms WHERE event_id=? AND user_id=?", $event_id, $this->rc->user->ID ); // set new notifyat time or unset if not snoozed $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; $query = $this->rc->db->query( "INSERT INTO kolab_alarms (event_id, user_id, dismissed, notifyat) VALUES(?, ?, ?, ?)", $event_id, $this->rc->user->ID, $snooze > 0 ? 0 : 1, $notifyat ); return $this->rc->db->affected_rows($query); } /** * List attachments from the given event */ public function list_attachments($event) { if (!($storage = $this->calendars[$event['calendar']])) return false; $event = $storage->get_event($event['id']); return $event['attachments']; } /** * Get attachment properties */ public function get_attachment($id, $event) { if (!($storage = $this->calendars[$event['calendar']])) return false; $event = $storage->get_event($event['id']); if ($event && !empty($event['attachments'])) { foreach ($event['attachments'] as $att) { if ($att['id'] == $id) { return $att; } } } return null; } /** * Get attachment body * @see calendar_driver::get_attachment_body() */ public function get_attachment_body($id, $event) { if (!($cal = $this->calendars[$event['calendar']])) return false; return $cal->storage->get_attachment($event['id'], $id); } /** * List availabale categories * The default implementation reads them from config/user prefs */ public function list_categories() { // FIXME: complete list with categories saved in config objects (KEP:12) return $this->rc->config->get('calendar_categories', $this->default_categories); } /** * Fetch free/busy information from a person within the given range */ public function get_freebusy_list($email, $start, $end) { if (empty($email)/* || $end < time()*/) return false; // map vcalendar fbtypes to internal values $fbtypemap = array( 'FREE' => calendar::FREEBUSY_FREE, 'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE, 'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF, 'OOF' => calendar::FREEBUSY_OOF); // ask kolab server first try { $request_config = array( 'store_body' => true, 'follow_redirects' => true, ); $request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config); $response = $request->send(); // authentication required if ($response->getStatus() == 401) { $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password'])); $response = $request->send(); } if ($response->getStatus() == 200) $fbdata = $response->getBody(); unset($request, $response); } catch (Exception $e) { PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage()); } // get free-busy url from contacts if (!$fbdata) { $fburl = null; foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) { $abook = $this->rc->get_address_book($book); if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) { while ($contact = $result->iterate()) { if ($fburl = $contact['freebusyurl']) { $fbdata = @file_get_contents($fburl); break; } } } if ($fbdata) break; } } // parse free-busy information using Horde classes if ($fbdata) { $ical = $this->cal->get_ical(); $ical->import($fbdata); if ($fb = $ical->freebusy) { $result = array(); foreach ($fb['periods'] as $tuple) { list($from, $to, $type) = $tuple; $result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY); } // we take 'dummy' free-busy lists as "unknown" if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy')) return false; // set period from $start till the begin of the free-busy information as 'unknown' if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) { array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN)); } // pad period till $end with status 'unknown' if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) { $result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN); } return $result; } } return false; } /** * Handler to push folder triggers when sent from client. * Used to push free-busy changes asynchronously after updating an event */ public function push_freebusy() { // make shure triggering completes set_time_limit(0); ignore_user_abort(true); $cal = get_input_value('source', RCUBE_INPUT_GPC); if (!($cal = $this->calendars[$cal])) return false; // trigger updates on folder $trigger = $cal->storage->trigger(); if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed triggering folder. Error was " . $trigger->getMessage()), true, false); } exit; } /** * Callback function to produce driver-specific calendar create/edit form * * @param string Request action 'form-edit|form-new' * @param array Calendar properties (e.g. id, color) * @param array Edit form fields * * @return string HTML content of the form */ public function calendar_form($action, $calendar, $formfields) { if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) { $folder = $cal->get_realname(); // UTF7 $color = $cal->get_color(); } else { $folder = ''; $color = ''; } $hidden_fields[] = array('name' => 'oldname', 'value' => $folder); $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); $form = array(); if (strlen($folder)) { $path_imap = explode($delim, $folder); array_pop($path_imap); // pop off name part $path_imap = implode($path_imap, $delim); $options = $storage->folder_info($folder); } else { $path_imap = ''; } // General tab $form['props'] = array( 'name' => $this->rc->gettext('properties'), ); // Disable folder name input if (!empty($options) && ($options['norename'] || $options['protected'])) { $input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name')); $formfields['name']['value'] = kolab_storage::object_name($folder) . $input_name->show($folder); } // calendar name (default field) $form['props']['fieldsets']['location'] = array( 'name' => $this->rc->gettext('location'), 'content' => array( 'name' => $formfields['name'] ), ); if (!empty($options) && ($options['norename'] || $options['protected'])) { // prevent user from moving folder $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap); } else { $select = kolab_storage::folder_selector('event', array('name' => 'parent'), $folder); $form['props']['fieldsets']['location']['content']['path'] = array( 'label' => $this->cal->gettext('parentcalendar'), 'value' => $select->show(strlen($folder) ? $path_imap : ''), ); } // calendar color (default field) $form['props']['fieldsets']['settings'] = array( 'name' => $this->rc->gettext('settings'), 'content' => array( 'color' => $formfields['color'], 'showalarms' => $formfields['showalarms'], ), ); if ($action != 'form-new') { $form['sharing'] = array( 'name' => Q($this->cal->gettext('tabsharing')), 'content' => html::tag('iframe', array( 'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)), 'width' => '100%', 'height' => 350, 'border' => 0, 'style' => 'border:0'), ''), ); } $this->form_html = ''; if (is_array($hidden_fields)) { foreach ($hidden_fields as $field) { $hiddenfield = new html_hiddenfield($field); $this->form_html .= $hiddenfield->show() . "\n"; } } // Create form output foreach ($form as $tab) { if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) { $content = ''; foreach ($tab['fieldsets'] as $fieldset) { $subcontent = $this->get_form_part($fieldset); if ($subcontent) { $content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n"; } } } else { $content = $this->get_form_part($tab); } if ($content) { $this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n"; } } // Parse form template for skin-dependent stuff $this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html')); return $this->rc->output->parse('calendar.kolabform', false, false); } /** * Handler for template object */ public function calendar_form_html() { return $this->form_html; } /** * Helper function used in calendar_form_content(). Creates a part of the form. */ private function get_form_part($form) { $content = ''; if (is_array($form['content']) && !empty($form['content'])) { $table = new html_table(array('cols' => 2)); foreach ($form['content'] as $col => $colprop) { $colprop['id'] = '_'.$col; $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col); $table->add('title', sprintf('', $colprop['id'], Q($label))); $table->add(null, $colprop['value']); } $content = $table->show(); } else { $content = $form['content']; } return $content; } /** * Handler to render ACL form for a calendar folder */ public function calendar_acl() { $this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form')); $this->rc->output->send('calendar.kolabacl'); } /** * Handler for ACL form template object */ public function calendar_acl_form() { $calid = get_input_value('_id', RCUBE_INPUT_GPC); if ($calid && ($cal = $this->calendars[$calid])) { $folder = $cal->get_realname(); // UTF7 $color = $cal->get_color(); } else { $folder = ''; $color = ''; } $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); $form = array(); if (strlen($folder)) { $path_imap = explode($delim, $folder); array_pop($path_imap); // pop off name part $path_imap = implode($path_imap, $delim); $options = $storage->folder_info($folder); // Allow plugins to modify the form content (e.g. with ACL form) $plugin = $this->rc->plugins->exec_hook('calendar_form_kolab', array('form' => $form, 'options' => $options, 'name' => $folder)); } if (!$plugin['form']['sharing']['content']) $plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights')); return $plugin['form']['sharing']['content']; } /** * Return a (limited) list of color values to be used for calendar and category coloring * * @return mixed List for colors as hex values or false if no presets should be shown */ public function get_color_values() { // selection from http://msdn.microsoft.com/en-us/library/aa358802%28v=VS.85%29.aspx return array('000000','006400','2F4F4F','800000','808000','008000', '008080','000080','800080','4B0082','191970','8B0000','008B8B', '00008B','8B008B','556B2F','8B4513','228B22','6B8E23','2E8B57', 'B8860B','483D8B','A0522D','0000CD','A52A2A','00CED1','696969', '20B2AA','9400D3','B22222','C71585','3CB371','D2691E','DC143C', 'DAA520','00FA9A','4682B4','7CFC00','9932CC','FF0000','FF4500', 'FF8C00','FFA500','FFD700','FFFF00','9ACD32','32CD32','00FF00', '00FF7F','00FFFF','5F9EA0','00BFFF','0000FF','FF00FF','808080', '708090','CD853F','8A2BE2','778899','FF1493','48D1CC','1E90FF', '40E0D0','4169E1','6A5ACD','BDB76B','BA55D3','CD5C5C','ADFF2F', '66CDAA','FF6347','8FBC8B','DA70D6','BC8F8F','9370DB','DB7093', 'FF7F50','6495ED','A9A9A9','F4A460','7B68EE','D2B48C','E9967A', 'DEB887','FF69B4','FA8072','F08080','EE82EE','87CEEB','FFA07A', 'F0E68C','DDA0DD','90EE90','7FFFD4','C0C0C0','87CEFA','B0C4DE', '98FB98','ADD8E6','B0E0E6','D8BFD8','EEE8AA','AFEEEE','D3D3D3', 'FFDEAD'); } - - private function resurces_ldap() - { - if (!isset($this->resources_dir)) { - $this->resources_dir = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true); - } - - return $this->resources_dir->ready ? $this->resources_dir : null; - } - - - /** - * Fetch resource objects to be displayed for booking - * - * @param string Search query (optional) - * @return array List of resource records available for booking - */ - public function load_resources($query = null, $num = 5000) - { - if (!($ldap = $this->resurces_ldap())) { - return array(); - } - - // TODO: apply paging - $ldap->set_pagesize($num); - - if (isset($query)) { - $results = $ldap->search('*', $query, 0, true, true); - } - else { - $results = $ldap->list_records(); - } - - if ($results instanceof ArrayAccess) { - foreach ($results as $i => $rec) { - $results[$i] = $this->decode_resource($rec); - } - } - - return $results; - } - - /** - * Return properties of a single resource - * - * @param mixed UID string - * @return array Resource object as hash array - */ - public function get_resource($uid) - { - $rec = null; - - if ($ldap = $this->resurces_ldap()) { - $rec = $ldap->get_record($uid); - - if (!empty($rec)) { - $rec = $this->decode_resource($rec); - } - } - - return $rec; - } - - /** - * - */ - public function get_resource_owner($dn) - { - $owner = null; - - if ($ldap = $this->resurces_ldap()) { - $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true); - $owner['ID'] = rcube_ldap::dn_decode($owner['ID']); - unset($owner['_raw_attrib'], $owner['_type']); - } - - return $owner; - } - - /** - * Extract JSON-serialized attributes - */ - private function decode_resource($rec) - { - if (is_array($rec['attributes']) && $rec['attributes'][0]) { - $attributes = array(); - - foreach ($rec['attributes'] as $sattr) { - $attr = @json_decode($sattr, true); - $attributes += $attr; - } - - $rec['attributes'] = $attributes; - } - - // remove unused cruft - unset($rec['_raw_attrib']); - - return $rec; - } - } diff --git a/plugins/calendar/drivers/ldap/resources_driver_ldap.php b/plugins/calendar/drivers/ldap/resources_driver_ldap.php new file mode 100644 index 00000000..23439b64 --- /dev/null +++ b/plugins/calendar/drivers/ldap/resources_driver_ldap.php @@ -0,0 +1,146 @@ + + * + * 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 . + */ + +/** + * LDAP-based resource directory implementation + */ +class resources_driver_ldap extends resources_driver +{ + private $rc; + private $cal; + private $ldap; + + /** + * Default constructor + */ + function __construct($cal) + { + $this->cal = $cal; + $this->rc = $cal->rc; + } + + /** + * Fetch resource objects to be displayed for booking + * + * @param string Search query (optional) + * @return array List of resource records available for booking + */ + public function load_resources($query = null, $num = 5000) + { + if (!($ldap = $this->connect())) { + return array(); + } + + // TODO: apply paging + $ldap->set_pagesize($num); + + if (isset($query)) { + $results = $ldap->search('*', $query, 0, true, true); + } + else { + $results = $ldap->list_records(); + } + + if ($results instanceof ArrayAccess) { + foreach ($results as $i => $rec) { + $results[$i] = $this->decode_resource($rec); + } + } + + return $results; + } + + /** + * Return properties of a single resource + * + * @param string Unique resource identifier + * @return array Resource object as hash array + */ + public function get_resource($dn) + { + $rec = null; + + if ($ldap = $this->connect()) { + $rec = $ldap->get_record(rcube_ldap::dn_encode($dn)); + + if (!empty($rec)) { + $rec = $this->decode_resource($rec); + } + } + + return $rec; + } + + /** + * Return properties of a resource owner + * + * @param string Owner identifier + * @return array Resource object as hash array + */ + public function get_resource_owner($dn) + { + $owner = null; + + if ($ldap = $this->connect()) { + $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true); + $owner['ID'] = rcube_ldap::dn_decode($owner['ID']); + unset($owner['_raw_attrib'], $owner['_type']); + } + + return $owner; + } + + /** + * Extract JSON-serialized attributes + */ + private function decode_resource($rec) + { + $rec['ID'] = rcube_ldap::dn_decode($rec['ID']); + + if (is_array($rec['attributes']) && $rec['attributes'][0]) { + $attributes = array(); + + foreach ($rec['attributes'] as $sattr) { + $attr = @json_decode($sattr, true); + $attributes += $attr; + } + + $rec['attributes'] = $attributes; + } + + // remove unused cruft + unset($rec['_raw_attrib']); + + return $rec; + } + + private function connect() + { + if (!isset($this->ldap)) { + $this->ldap = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true); + } + + return $this->ldap->ready ? $this->ldap : null; + } + +} \ No newline at end of file diff --git a/plugins/calendar/drivers/resources_driver.php b/plugins/calendar/drivers/resources_driver.php new file mode 100644 index 00000000..b1fed9c8 --- /dev/null +++ b/plugins/calendar/drivers/resources_driver.php @@ -0,0 +1,58 @@ + + * + * 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 . + */ + + +/** + * Interface definition for a resources directory driver classe + */ +abstract class resources_driver +{ + + /** + * Fetch resource objects to be displayed for booking + * + * @param string Search query (optional) + * @return array List of resource records available for booking + */ + abstract public function load_resources($query = null); + + /** + * Return properties of a single resource + * + * @param string Unique resource identifier + * @return array Resource object as hash array + */ + abstract public function get_resource($id); + + /** + * Return properties of a resource owner + * + * @param string Owner identifier + * @return array Resource object as hash array + */ + public function get_resource_owner($id) + { + return null; + } + +} diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 710898f6..14bc422f 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -1,912 +1,911 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * Copyright (C) 2012, 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_ui { private $rc; private $cal; private $ready = false; public $screen; function __construct($cal) { $this->cal = $cal; $this->rc = $cal->rc; $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other'; } /** * Calendar UI initialization and requests handlers */ public function init() { if ($this->ready) // already done return; // add taskbar button $this->cal->add_button(array( 'command' => 'calendar', 'class' => 'button-calendar', 'classsel' => 'button-calendar button-selected', 'innerclass' => 'button-inner', 'label' => 'calendar.calendar', ), 'taskbar'); // load basic client script $this->cal->include_script('calendar_base.js'); $skin_path = $this->cal->local_skin_path(); $this->cal->include_stylesheet($skin_path . '/calendar.css'); $this->ready = true; } /** * Register handler methods for the template engine */ public function init_templates() { $this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css')); $this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list')); $this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select')); $this->cal->register_handler('plugin.identity_select', array($this, 'identity_select')); $this->cal->register_handler('plugin.category_select', array($this, 'category_select')); $this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select')); $this->cal->register_handler('plugin.priority_select', array($this, 'priority_select')); $this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select')); $this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select')); $this->cal->register_handler('plugin.recurrence_form', array($this, 'recurrence_form')); $this->cal->register_handler('plugin.attachments_form', array($this, 'attachments_form')); $this->cal->register_handler('plugin.attachments_list', array($this, 'attachments_list')); $this->cal->register_handler('plugin.filedroparea', array($this, 'file_drop_area')); $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list')); $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form')); $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form')); $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list')); $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form')); $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info')); $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table')); $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify')); $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning')); $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons')); $this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options')); $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form')); $this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form')); $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template } /** * Adds CSS stylesheets to the page header */ public function addCSS() { $skin_path = $this->cal->local_skin_path(); $this->cal->include_stylesheet($skin_path . '/fullcalendar.css'); $this->cal->include_stylesheet($skin_path . '/jquery.miniColors.css'); } /** * Adds JS files to the page header */ public function addJS() { $this->cal->include_script('calendar_ui.js'); $this->cal->include_script('lib/js/fullcalendar.js'); $this->cal->include_script('lib/js/jquery.miniColors.min.js'); $this->rc->output->include_script('treelist.js'); } /** * */ function calendar_css($attrib = array()) { $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']); $categories = $this->cal->driver->list_categories(); $css = "\n"; foreach ((array)$categories as $class => $color) { if (empty($color)) continue; $class = 'cat-' . asciiwords(strtolower($class), true); $css .= ".$class { color: #$color }\n"; if ($mode > 0) { if ($mode == 2) { $css .= ".fc-event-$class .fc-event-bg {"; $css .= " opacity: 0.9;"; $css .= " filter: alpha(opacity=90);"; } else { $css .= ".fc-event-$class.fc-event-skin, "; $css .= ".fc-event-$class .fc-event-skin, "; $css .= ".fc-event-$class .fc-event-inner {"; } $css .= " background-color: #" . $color . ";"; if ($mode % 2) $css .= " border-color: #$color;"; $css .= "}\n"; } } $calendars = $this->cal->driver->list_calendars(); foreach ((array)$calendars as $id => $prop) { if (!$prop['color']) continue; $color = $prop['color']; $class = 'cal-' . asciiwords($id, true); $css .= "li.$class, #eventshow .$class { color: #$color }\n"; if ($mode != 1) { if ($mode == 3) { $css .= ".fc-event-$class .fc-event-bg {"; $css .= " opacity: 0.9;"; $css .= " filter: alpha(opacity=90);"; } else { $css .= ".fc-event-$class, "; $css .= ".fc-event-$class .fc-event-inner {"; } if (!$attrib['printmode']) $css .= " background-color: #$color;"; if ($mode % 2 == 0) $css .= " border-color: #$color;"; $css .= "}\n"; } $css .= ".$class .handle { background-color: #$color; }"; } return html::tag('style', array('type' => 'text/css'), $css); } /** * */ function calendar_list($attrib = array()) { $calendars = $this->cal->driver->list_calendars(); $li = ''; foreach ((array)$calendars as $id => $prop) { if ($attrib['activeonly'] && !$prop['active']) continue; unset($prop['user_id']); $prop['alarms'] = $this->cal->driver->alarms; $prop['attendees'] = $this->cal->driver->attendees; - $prop['resources'] = $this->cal->driver->resources; $prop['freebusy'] = $this->cal->driver->freebusy; $prop['attachments'] = $this->cal->driver->attachments; $prop['undelete'] = $this->cal->driver->undelete; $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed')); if (!$prop['virtual']) $jsenv[$id] = $prop; $html_id = html_identifier($id); $class = 'cal-' . asciiwords($id, true); $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : ''; if ($prop['virtual']) $class .= ' virtual'; else if ($prop['readonly']) $class .= ' readonly'; if ($prop['class_name']) $class .= ' '.$prop['class_name']; $li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class), ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') . html::span('handle', ' ')) . html::span(array('class' => 'calname', 'title' => $title), $prop['listname'])); } $this->rc->output->set_env('calendars', $jsenv); $this->rc->output->add_gui_object('folderlist', $attrib['id']); return html::tag('ul', $attrib, $li, html::$common_attrib); } /** * */ function angenda_options($attrib = array()) { $attrib += array('id' => 'agendaoptions'); $attrib['style'] .= 'display:none'; $select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange')); $select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->gettext('days')), $days); foreach (array(2,5,7,14,30,60,90) as $days) $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->gettext('days')), $days); $html .= html::label('agenda-listrange', $this->cal->gettext('listrange')); $html .= $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range'])); $select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections')); $select_sections->add('---', ''); foreach (array('day' => 'days', 'week' => 'weeks', 'month' => 'months', 'smart' => 'smartsections') as $val => $label) $select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->cal->gettext($label))), $val); $html .= html::span('spacer', ' '); $html .= html::label('agenda-listsections', $this->cal->gettext('listsections')); $html .= $select_sections->show($this->rc->config->get('calendar_agenda_sections', $this->cal->defaults['calendar_agenda_sections'])); return html::div($attrib, $html); } /** * Render a HTML select box for calendar selection */ function calendar_select($attrib = array()) { $attrib['name'] = 'calendar'; $attrib['is_escaped'] = true; $select = new html_select($attrib); foreach ((array)$this->cal->driver->list_calendars() as $id => $prop) { if (!$prop['readonly']) $select->add($prop['name'], $id); } return $select->show(null); } /** * Render a HTML select box for user identity selection */ function identity_select($attrib = array()) { $attrib['name'] = 'identity'; $select = new html_select($attrib); $identities = $this->rc->user->list_identities(); foreach ($identities as $ident) { $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']); } return $select->show(null); } /** * Render a HTML select box to select an event category */ function category_select($attrib = array()) { $attrib['name'] = 'categories'; $select = new html_select($attrib); $select->add('---', ''); foreach (array_keys((array)$this->cal->driver->list_categories()) as $cat) { $select->add($cat, $cat); } return $select->show(null); } /** * Render a HTML select box for free/busy/out-of-office property */ function freebusy_select($attrib = array()) { $attrib['name'] = 'freebusy'; $select = new html_select($attrib); $select->add($this->cal->gettext('free'), 'free'); $select->add($this->cal->gettext('busy'), 'busy'); $select->add($this->cal->gettext('outofoffice'), 'outofoffice'); $select->add($this->cal->gettext('tentative'), 'tentative'); return $select->show(null); } /** * Render a HTML select for event priorities */ function priority_select($attrib = array()) { $attrib['name'] = 'priority'; $select = new html_select($attrib); $select->add('---', '0'); $select->add('1 '.$this->cal->gettext('highest'), '1'); $select->add('2 '.$this->cal->gettext('high'), '2'); $select->add('3 ', '3'); $select->add('4 ', '4'); $select->add('5 '.$this->cal->gettext('normal'), '5'); $select->add('6 ', '6'); $select->add('7 ', '7'); $select->add('8 '.$this->cal->gettext('low'), '8'); $select->add('9 '.$this->cal->gettext('lowest'), '9'); return $select->show(null); } /** * Render HTML input for sensitivity selection */ function sensitivity_select($attrib = array()) { $attrib['name'] = 'sensitivity'; $select = new html_select($attrib); $select->add($this->cal->gettext('public'), 'public'); $select->add($this->cal->gettext('private'), 'private'); $select->add($this->cal->gettext('confidential'), 'confidential'); return $select->show(null); } /** * Render HTML form for alarm configuration */ function alarm_select($attrib = array()) { return $this->cal->lib->alarm_select($attrib, $this->cal->driver->alarm_types, $this->cal->driver->alarm_absolute); } /** * */ function edit_attendees_notify($attrib = array()) { $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1)); return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications'))); } /** * Generate the form for recurrence settings */ function recurring_event_warning($attrib = array()) { $attrib['id'] = 'edit-recurring-warning'; $radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode')); $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' . html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' . html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' . html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew')); return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form)); } /** * Generate the form for recurrence settings */ function recurrence_form($attrib = array()) { switch ($attrib['part']) { // frequency selector case 'frequency': $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency')); $select->add($this->cal->gettext('never'), ''); $select->add($this->cal->gettext('daily'), 'DAILY'); $select->add($this->cal->gettext('weekly'), 'WEEKLY'); $select->add($this->cal->gettext('monthly'), 'MONTHLY'); $select->add($this->cal->gettext('yearly'), 'YEARLY'); $html = html::label('edit-frequency', $this->cal->gettext('frequency')) . $select->show(''); break; // daily recurrence case 'daily': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily')); $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('days'))); break; // weekly recurrence form case 'weekly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly')); $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('weeks'))); // weekday selection $daymap = array('sun','mon','tue','wed','thu','fri','sat'); $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday')); $first = $this->rc->config->get('calendar_first_day', 1); for ($weekdays = '', $j = $first; $j <= $first+6; $j++) { $d = $j % 7; $weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->cal->gettext($daymap[$d])) . ' '; } $html .= html::div($attrib, html::label(null, $this->cal->gettext('bydays')) . $weekdays); break; // monthly recurrence form case 'monthly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly')); $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('months'))); $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday')); for ($monthdays = '', $d = 1; $d <= 31; $d++) { $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d); $monthdays .= $d % 7 ? ' ' : html::br(); } // rule selectors $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode')); $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->cal->gettext('each'))); $table->add(null, $monthdays); $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->cal->gettext('onevery'))); $table->add(null, $this->rrule_selectors($attrib['part'])); $html .= html::div($attrib, $table->show()); break; // annually recurrence form case 'yearly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly')); $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('years'))); // month selector $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'); $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth')); for ($months = '', $m = 1; $m <= 12; $m++) { $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->cal->gettext($monthmap[$m])); $months .= $m % 4 ? ' ' : html::br(); } $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months); // day rule selection $html .= html::div($attrib, html::label(null, $this->cal->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---')); break; // end of recurrence form case 'until': $radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until')); $select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times')); $input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10")); $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); $table->add('label', ucfirst($this->cal->gettext('recurrencend'))); $table->add(null, html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' . $this->cal->gettext('forever'))); $table->add('label', ''); $table->add(null, $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count')) . ' ' . $this->cal->gettext(array( 'name' => 'forntimes', 'vars' => array('nr' => $select->show(1))) )); $table->add('label', ''); $table->add(null, $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until')) . ' ' . $this->cal->gettext('untildate') . ' ' . $input->show('')); $html = $table->show(); break; } return $html; } /** * Input field for interval selection */ private function interval_selector($attrib) { $select = new html_select($attrib); $select->add(range(1,30), range(1,30)); return $select; } /** * Drop-down menus for recurrence rules like "each last sunday of" */ private function rrule_selectors($part, $noselect = null) { // rule selectors $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix")); if ($noselect) $select_prefix->add($noselect, ''); $select_prefix->add(array( $this->cal->gettext('first'), $this->cal->gettext('second'), $this->cal->gettext('third'), $this->cal->gettext('fourth'), $this->cal->gettext('last') ), array(1, 2, 3, 4, -1)); $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday")); if ($noselect) $select_wday->add($noselect, ''); $daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday'); $first = $this->rc->config->get('calendar_first_day', 1); for ($j = $first; $j <= $first+6; $j++) { $d = $j % 7; $select_wday->add($this->cal->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2))); } return $select_prefix->show() . ' ' . $select_wday->show(); } /** * Form for uploading and importing events */ function events_import_form($attrib = array()) { if (!$attrib['id']) $attrib['id'] = 'rcmImportForm'; // Get max filesize, enable upload progress bar $max_filesize = rcube_upload_init(); $accept = '.ics, text/calendar, text/x-vcalendar, application/ics'; if (class_exists('ZipArchive', false)) { $accept .= ', .zip, application/zip'; } $input = new html_inputfield(array( 'type' => 'file', 'name' => '_data', 'size' => $attrib['uploadfieldsize'], 'accept' => $accept)); $select = new html_select(array('name' => '_range', 'id' => 'event-import-range')); $select->add(array( $this->cal->gettext('onemonthback'), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))), $this->cal->gettext('all'), ), array('1','2','3','6','12',0)); $html .= html::div('form-section', html::div(null, $input->show()) . html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) ); $html .= html::div('form-section', html::label('event-import-calendar', $this->cal->gettext('calendar')) . $this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar')) ); $html .= html::div('form-section', html::label('event-import-range', $this->cal->gettext('importrange')) . $select->show(1) ); $this->rc->output->add_gui_object('importform', $attrib['id']); $this->rc->output->add_label('import'); return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')), 'method' => "post", 'enctype' => 'multipart/form-data', 'id' => $attrib['id']), $html ); } /** * Form to select options for exporting events */ function events_export_form($attrib = array()) { if (!$attrib['id']) $attrib['id'] = 'rcmExportForm'; $html .= html::div('form-section', html::label('event-export-calendar', $this->cal->gettext('calendar')) . $this->calendar_select(array('name' => 'calendar', 'id' => 'event-export-calendar')) ); $select = new html_select(array('name' => 'range', 'id' => 'event-export-range')); $select->add(array( $this->cal->gettext('all'), $this->cal->gettext('onemonthback'), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))), $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))), $this->cal->gettext('customdate'), ), array(0,'1','2','3','6','12','custom')); $startdate = new html_inputfield(array('name' => 'start', 'size' => 11, 'id' => 'event-export-startdate')); $html .= html::div('form-section', html::label('event-export-range', $this->cal->gettext('exportrange')) . $select->show(0) . html::span(array('style'=>'display:none'), $startdate->show()) ); $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'event-export-attachments', 'value' => 1)); $html .= html::div('form-section', html::label('event-export-range', $this->cal->gettext('exportattachments')) . $checkbox->show(1) ); $this->rc->output->add_gui_object('exportform', $attrib['id']); return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'export_events')), 'method' => "post", 'id' => $attrib['id']), $html ); } /** * Generate the form for event attachments upload */ function attachments_form($attrib = array()) { // add ID if not given if (!$attrib['id']) $attrib['id'] = 'rcmUploadForm'; // Get max filesize, enable upload progress bar $max_filesize = rcube_upload_init(); $button = new html_inputfield(array('type' => 'button')); $input = new html_inputfield(array( 'type' => 'file', 'name' => '_attachments[]', 'multiple' => 'multiple', 'size' => $attrib['attachmentfieldsize'])); return html::div($attrib, html::div(null, $input->show()) . html::div('formbuttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction', 'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) . html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) ); } /** * Register UI object for HTML5 drag & drop file upload */ function file_drop_area($attrib = array()) { if ($attrib['id']) { $this->rc->output->add_gui_object('filedrop', $attrib['id']); $this->rc->output->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments')); } } /** * Generate HTML element for attachments list */ function attachments_list($attrib = array()) { if (!$attrib['id']) $attrib['id'] = 'rcmAttachmentList'; $skin_path = $this->cal->local_skin_path(); if ($attrib['deleteicon']) { $_SESSION[calendar::SESSION_KEY . '_deleteicon'] = $skin_path . $attrib['deleteicon']; $this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']); } if ($attrib['cancelicon']) $this->rc->output->set_env('cancelicon', $skin_path . $attrib['cancelicon']); if ($attrib['loadingicon']) $this->rc->output->set_env('loadingicon', $skin_path . $attrib['loadingicon']); $this->rc->output->add_gui_object('attachmentlist', $attrib['id']); $this->attachmentlist_id = $attrib['id']; return html::tag('ul', $attrib, '', html::$common_attrib); } /** * Handler for calendar form template. * The form content could be overriden by the driver */ function calendar_editform($action, $calendar = array()) { // compose default calendar form fields $input_name = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20)); $input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 6)); $formfields = array( 'name' => array( 'label' => $this->cal->gettext('name'), 'value' => $input_name->show($calendar['name']), 'id' => 'calendar-name', ), 'color' => array( 'label' => $this->cal->gettext('color'), 'value' => $input_color->show($calendar['color']), 'id' => 'calendar-color', ), ); if ($this->cal->driver->alarms) { $checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1)); $formfields['showalarms'] = array( 'label' => $this->cal->gettext('showalarms'), 'value' => $checkbox->show($calendar['showalarms']?1:0), 'id' => 'calendar-showalarms', ); } // allow driver to extend or replace the form content return html::tag('form', array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'), $this->cal->driver->calendar_form($action, $calendar, $formfields) ); } /** * */ function attendees_list($attrib = array()) { $table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); $table->add_header('role', $this->cal->gettext('role')); $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee')); $table->add_header('availability', $this->cal->gettext('availability')); $table->add_header('confirmstate', $this->cal->gettext('confirmstate')); $table->add_header('options', ''); return $table->show($attrib); } /** * */ function attendees_form($attrib = array()) { $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30)); $checkbox = new html_checkbox(array('name' => 'invite', 'id' => 'edit-attendees-invite', 'value' => 1)); return html::div($attrib, html::div(null, $input->show() . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) . html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->cal->gettext('sendinvitations'))) ); } /** * */ function resources_form($attrib = array()) { $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'size' => 30)); return html::div($attrib, html::div(null, $input->show() . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...'))) ); } /** * */ function resources_list($attrib = array()) { $attrib += array('id' => 'calendar-resources-list'); $this->rc->output->add_gui_object('resourceslist', $attrib['id']); return html::tag('ul', $attrib, '', html::$common_attrib); } /** * */ public function resource_info($attrib = array()) { $attrib += array('id' => 'calendar-resources-info'); $this->rc->output->add_gui_object('resourceinfo', $attrib['id']); $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner'); $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border'); return html::tag('table', $attrib, html::tag('tbody', null, ''), $table_attrib) . html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib, html::tag('thead', null, html::tag('tr', null, html::tag('td', array('colspan' => 2), Q($this->cal->gettext('resourceowner'))) ) ) . html::tag('tbody', null, ''), $table_attrib); } /** * GUI object 'searchform' for the resource finder dialog * * @param array Named parameters * @return string HTML code for the gui object */ function resources_search_form($attrib) { $attrib += array('command' => 'search-resource', 'id' => 'rcmcalresqsearchbox', 'autocomplete' => 'off'); $attrib['name'] = '_q'; $input_q = new html_inputfield($attrib); $out = $input_q->show(); // add form tag around text field $out = $this->rc->output->form_tag(array( 'name' => "rcmcalresoursqsearchform", 'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('" . $attrib['command'] . "'); return false", 'style' => "display:inline"), $out); return $out; } /** * */ function attendees_freebusy_table($attrib = array()) { $table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0)); $table->add('attendees', html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) . html::div('timesheader', ' ') . html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '') ); $table->add('times', html::div('scroll', html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) . html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), ' ') ) ); return $table->show($attrib); } /** * Render event details in a table */ function event_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'])); $table->add('label', $this->cal->gettext('date')); $table->add('location', Q($this->cal->lib->event_date_text($event))); if ($event['location']) { $table->add('label', $this->cal->gettext('location')); $table->add('location', Q($event['location'])); } return $table->show(); } /** * */ function event_invitebox($attrib = array()) { if ($this->cal->event) { return html::div($attrib, $this->event_details_table($this->cal->event, $this->cal->gettext('itipinvitation')) . $this->cal->invitestatus ); } return ''; } function event_rsvp_buttons($attrib = array()) { $attrib += array('type' => 'button'); foreach (array('accepted','tentative','declined') as $method) { $buttons .= html::tag('input', array( 'type' => $attrib['type'], 'name' => $attrib['iname'], 'class' => 'button', 'rel' => $method, 'value' => $this->cal->gettext('itip' . $method), )); } return html::div($attrib, html::div('label', $this->cal->gettext('acceptinvitation')) . html::div('rsvp-buttons', $buttons)); } } diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html index 574cded2..96be36ea 100644 --- a/plugins/calendar/skins/larry/templates/calendar.html +++ b/plugins/calendar/skins/larry/templates/calendar.html @@ -1,255 +1,247 @@ <roundcube:object name="pagetitle" />

    Event Title

    Location
    From-To
    Default
    -