diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index a50db591..d674522b 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -1,3311 +1,3311 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * Copyright (C) 2014-2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class calendar extends rcube_plugin { const FREEBUSY_UNKNOWN = 0; const FREEBUSY_FREE = 1; const FREEBUSY_BUSY = 2; const FREEBUSY_TENTATIVE = 3; const FREEBUSY_OOF = 4; const SESSION_KEY = 'calendar_temp'; public $task = '?(?!logout).*'; public $rc; public $lib; public $resources_dir; public $home; // declare public to be used in other classes public $urlbase; public $timezone; public $timezone_offset; public $gmt_offset; public $ui; public $defaults = array( 'calendar_default_view' => "agendaWeek", 'calendar_timeslots' => 2, 'calendar_work_start' => 6, 'calendar_work_end' => 18, 'calendar_agenda_range' => 60, 'calendar_agenda_sections' => 'smart', 'calendar_event_coloring' => 0, 'calendar_time_indicator' => true, 'calendar_allow_invite_shared' => false, 'calendar_itip_send_option' => 3, 'calendar_itip_after_action' => 0, ); private $ical; private $itip; private $driver; /** * Plugin initialization. */ function init() { $this->require_plugin('libcalendaring'); $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->register_task('calendar', 'calendar'); // load calendar configuration $this->load_config(); // load localizations $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print')); $this->timezone = $this->lib->timezone; $this->gmt_offset = $this->lib->gmt_offset; $this->dst_active = $this->lib->dst_active; $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active; require($this->home . '/lib/calendar_ui.php'); $this->ui = new calendar_ui($this); // catch iTIP confirmation requests that don're require a valid session if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) { $this->add_hook('startup', array($this, 'itip_attend_response')); } else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) { $this->add_hook('startup', array($this, 'ical_feed_export')); } else { // default startup routine $this->add_hook('startup', array($this, 'startup')); } $this->add_hook('user_delete', array($this, 'user_delete')); } /** * Startup hook */ public function startup($args) { // the calendar module can be enabled/disabled by the kolab_auth plugin if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true)) return; // load Calendar user interface if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'preview')) { $this->ui->init(); // settings are required in (almost) every GUI step if ($args['action'] != 'attend') $this->rc->output->set_env('calendar_settings', $this->load_settings()); } if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') { if ($args['action'] != 'upload') { $this->load_driver(); } // register calendar actions $this->register_action('index', array($this, 'calendar_view')); $this->register_action('event', array($this, 'event_action')); $this->register_action('calendar', array($this, 'calendar_action')); $this->register_action('count', array($this, 'count_events')); $this->register_action('load_events', array($this, 'load_events')); $this->register_action('export_events', array($this, 'export_events')); $this->register_action('import_events', array($this, 'import_events')); $this->register_action('upload', array($this, 'attachment_upload')); $this->register_action('get-attachment', array($this, 'attachment_get')); $this->register_action('freebusy-status', array($this, 'freebusy_status')); $this->register_action('freebusy-times', array($this, 'freebusy_times')); $this->register_action('randomdata', array($this, 'generate_randomdata')); $this->register_action('print', array($this,'print_view')); $this->register_action('mailimportitip', array($this, 'mail_import_itip')); $this->register_action('mailimportattach', array($this, 'mail_import_attachment')); $this->register_action('mailtoevent', array($this, 'mail_message2event')); $this->register_action('inlineui', array($this, 'get_inline_ui')); $this->register_action('check-recent', array($this, 'check_recent')); $this->register_action('itip-status', array($this, 'event_itip_status')); $this->register_action('itip-remove', array($this, 'event_itip_remove')); $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply')); $this->register_action('itip-delegate', array($this, 'mail_itip_delegate')); $this->register_action('resources-list', array($this, 'resources_list')); $this->register_action('resources-owner', array($this, 'resources_owner')); $this->register_action('resources-calendar', array($this, 'resources_calendar')); $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete')); $this->add_hook('refresh', array($this, 'refresh')); // remove undo information... if ($undo = $_SESSION['calendar_event_undo']) { // ...after timeout $undo_time = $this->rc->config->get('undo_timeout', 0); if ($undo['ts'] < time() - $undo_time) { $this->rc->session->remove('calendar_event_undo'); // @TODO: do EXPUNGE on kolab objects? } } } else if ($args['task'] == 'settings') { // add hooks for Calendar settings $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list')); $this->add_hook('preferences_list', array($this, 'preferences_list')); $this->add_hook('preferences_save', array($this, 'preferences_save')); } else if ($args['task'] == 'mail') { // hooks to catch event invitations on incoming mails if ($args['action'] == 'show' || $args['action'] == 'preview') { $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } // add 'Create event' item to message menu if ($this->api->output->type == 'html') { $this->api->add_content(html::tag('li', null, $this->api->output->button(array( 'command' => 'calendar-create-from-mail', 'label' => 'calendar.createfrommail', 'type' => 'link', 'classact' => 'icon calendarlink active', 'class' => 'icon calendarlink', 'innerclass' => 'icon calendar', ))), 'messagemenu'); $this->api->output->add_label('calendar.createfrommail'); } $this->add_hook('messages_list', array($this, 'mail_messages_list')); $this->add_hook('message_compose', array($this, 'mail_message_compose')); } else if ($args['task'] == 'addressbook') { if ($this->rc->config->get('calendar_contact_birthdays')) { $this->add_hook('contact_update', array($this, 'contact_update')); $this->add_hook('contact_create', array($this, 'contact_update')); } } // add hooks to display alarms $this->add_hook('pending_alarms', array($this, 'pending_alarms')); $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms')); } /** * Helper method to load the backend driver according to local config */ private function load_driver() { if (is_object($this->driver)) return; $driver_name = $this->rc->config->get('calendar_driver', 'database'); $driver_class = $driver_name . '_driver'; require_once($this->home . '/drivers/calendar_driver.php'); require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); $this->driver = new $driver_class($this); if ($this->driver->undelete) $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0; } /** * Load iTIP functions */ private function load_itip() { if (!$this->itip) { require_once($this->home . '/lib/calendar_itip.php'); $this->itip = new calendar_itip($this); if ($this->rc->config->get('kolab_invitation_calendars')) $this->itip->set_rsvp_actions(array('accepted','tentative','declined','delegated','needs-action')); } return $this->itip; } /** * Load iCalendar functions */ public function get_ical() { if (!$this->ical) { $this->ical = libcalendaring::get_ical(); } return $this->ical; } /** * Get properties of the calendar this user has specified as default */ public function get_default_calendar($sensitivity = null) { $default_id = $this->rc->config->get('calendar_default_calendar'); $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_WRITEABLE); $calendar = $calendars[$default_id] ?: null; if (!$calendar || $sensitivity) { foreach ($calendars as $cal) { if ($sensitivity && $cal['subtype'] == $sensitivity) { $calendar = $cal; break; } if ($cal['default'] && $cal['editable']) { $calendar = $cal; } if ($cal['editable']) { $first = $cal; } } } return $calendar ?: $first; } /** * Render the main calendar view from skin template */ function calendar_view() { $this->rc->output->set_pagetitle($this->gettext('calendar')); // Add CSS stylesheets to the page header $this->ui->addCSS(); // Add JS files to the page header $this->ui->addJS(); $this->ui->init_templates(); $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close'); $this->rc->output->add_label('libcalendaring.itipaccepted','libcalendaring.itiptentative','libcalendaring.itipdeclined','libcalendaring.itipdelegated','libcalendaring.expandattendeegroup','libcalendaring.expandattendeegroupnodata'); // initialize attendees autocompletion rcube_autocomplete_init(); $this->rc->output->set_env('timezone', $this->timezone->getName()); $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false); $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver')); $this->rc->output->set_env('mscolors', jqueryui::get_color_values()); $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer')))); $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC); if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $this->rc->output->set_env('view', $view); if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) $this->rc->output->set_env('date', $date); if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC)) $this->rc->output->set_env('itip_events', $this->itip_events($msgref)); $this->rc->output->send("calendar.calendar"); } /** * Handler for preferences_sections_list hook. * Adds Calendar settings sections into preferences sections list. * * @param array Original parameters * @return array Modified parameters */ function preferences_sections_list($p) { $p['list']['calendar'] = array( 'id' => 'calendar', 'section' => $this->gettext('calendar'), ); return $p; } /** * Handler for preferences_list hook. * Adds options blocks into Calendar settings sections in Preferences. * * @param array Original parameters * @return array Modified parameters */ function preferences_list($p) { if ($p['section'] != 'calendar') { return $p; } $no_override = array_flip((array)$this->rc->config->get('dont_override')); $p['blocks']['view']['name'] = $this->gettext('mainoptions'); if (!isset($no_override['calendar_default_view'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_default_view'; $select = new html_select(array('name' => '_default_view', 'id' => $field_id)); $select->add($this->gettext('day'), "agendaDay"); $select->add($this->gettext('week'), "agendaWeek"); $select->add($this->gettext('month'), "month"); $select->add($this->gettext('agenda'), "table"); $p['blocks']['view']['options']['default_view'] = array( 'title' => html::label($field_id, Q($this->gettext('default_view'))), 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])), ); } if (!isset($no_override['calendar_timeslots'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_timeslot'; $choices = array('1', '2', '3', '4', '6'); $select = new html_select(array('name' => '_timeslots', 'id' => $field_id)); $select->add($choices); $p['blocks']['view']['options']['timeslots'] = array( 'title' => html::label($field_id, Q($this->gettext('timeslots'))), 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))), ); } if (!isset($no_override['calendar_first_day'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_firstday'; $select = new html_select(array('name' => '_first_day', 'id' => $field_id)); $select->add(rcube_label('sunday'), '0'); $select->add(rcube_label('monday'), '1'); $select->add(rcube_label('tuesday'), '2'); $select->add(rcube_label('wednesday'), '3'); $select->add(rcube_label('thursday'), '4'); $select->add(rcube_label('friday'), '5'); $select->add(rcube_label('saturday'), '6'); $p['blocks']['view']['options']['first_day'] = array( 'title' => html::label($field_id, Q($this->gettext('first_day'))), 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))), ); } if (!isset($no_override['calendar_first_hour'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']))); $select_hours = new html_select(); for ($h = 0; $h < 24; $h++) $select_hours->add(date($time_format, mktime($h, 0, 0)), $h); $field_id = 'rcmfd_firsthour'; $p['blocks']['view']['options']['first_hour'] = array( 'title' => html::label($field_id, Q($this->gettext('first_hour'))), 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)), ); } if (!isset($no_override['calendar_work_start'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_workstart'; $p['blocks']['view']['options']['workinghours'] = array( 'title' => html::label($field_id, Q($this->gettext('workinghours'))), 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) . ' — ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)), ); } if (!isset($no_override['calendar_event_coloring'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_coloring'; $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id)); $select_colors->add($this->gettext('coloringmode0'), 0); $select_colors->add($this->gettext('coloringmode1'), 1); $select_colors->add($this->gettext('coloringmode2'), 2); $select_colors->add($this->gettext('coloringmode3'), 3); $p['blocks']['view']['options']['eventcolors'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))), 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])), ); } // loading driver is expensive, don't do it if not needed $this->load_driver(); if (!isset($no_override['calendar_default_alarm_type'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_alarm'; $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); $select_type->add($this->gettext('none'), ''); foreach ($this->driver->alarm_types as $type) $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); $p['blocks']['view']['options']['alarmtype'] = array( 'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))), 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')), ); } if (!isset($no_override['calendar_default_alarm_offset'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_alarm'; $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); $p['blocks']['view']['options']['alarmoffset'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))), 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } if (!isset($no_override['calendar_default_calendar'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } // default calendar selection $field_id = 'rcmfd_default_calendar'; $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true)); foreach ((array)$this->driver->list_calendars(calendar_driver::FILTER_PERSONAL) as $id => $prop) { $select_cal->add($prop['name'], strval($id)); if ($prop['default']) $default_calendar = $id; } $p['blocks']['view']['options']['defaultcalendar'] = array( 'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))), 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)), ); } $p['blocks']['itip']['name'] = $this->gettext('itipoptions'); // Invitations handling if (!isset($no_override['calendar_itip_after_action'])) { if (!$p['current']) { $p['blocks']['itip']['content'] = true; return $p; } $field_id = 'rcmfd_after_action'; $select = new html_select(array('name' => '_after_action', 'id' => $field_id, 'onchange' => "\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()")); $select->add($this->gettext('afternothing'), ''); $select->add($this->gettext('aftertrash'), 1); $select->add($this->gettext('afterdelete'), 2); $select->add($this->gettext('afterflagdeleted'), 3); $select->add($this->gettext('aftermoveto'), 4); $val = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']); if ($val !== null && $val !== '' && !is_int($val)) { $folder = $val; $val = 4; } $folders = $this->rc->folder_selector(array( 'id' => $field_id . '_select', 'name' => '_after_action_folder', 'maxlength' => 30, 'folder_filter' => 'mail', 'folder_rights' => 'w', 'style' => $val !== 4 ? 'display:none' : '', )); $p['blocks']['itip']['options']['after_action'] = array( 'title' => html::label($field_id, Q($this->gettext('afteraction'))), 'content' => $select->show($val) . $folders->show($folder), ); } // category definitions if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) { $p['blocks']['categories']['name'] = $this->gettext('categories'); if (!$p['current']) { $p['blocks']['categories']['content'] = true; return $p; } $categories = (array) $this->driver->list_categories(); $categories_list = ''; foreach ($categories as $name => $color) { $key = md5($name); $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name); $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category'))); $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable)); $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6)); $hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : ''; $categories_list .= html::div(null, $hidden . $category_name->show($name) . ' ' . $category_color->show($color) . ' ' . $category_remove->show()); } $p['blocks']['categories']['options']['category_' . $name] = array( 'content' => html::div(array('id' => 'calendarcategories'), $categories_list), ); $field_id = 'rcmfd_new_category'; $new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30)); $add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()")); $p['blocks']['categories']['options']['categories'] = array( 'content' => $new_category->show('') . ' ' . $add_category->show(), ); $this->rc->output->add_script('function rcube_calendar_add_category(){ var name = $("#rcmfd_new_category").val(); if (name.length) { var input = $("").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name); var color = $("").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000"); var button = $("").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() }); $("
").append(input).append(" ").append(color).append(" ").append(button).appendTo("#calendarcategories"); color.miniColors({ colorValues:(rcmail.env.mscolors || []) }); $("#rcmfd_new_category").val(""); } }'); $this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){ if (event.which == 13) { rcube_calendar_add_category(); event.preventDefault(); } }); ', 'docready'); // load miniColors js/css files jqueryui::miniColors(); } // virtual birthdays calendar if (!isset($no_override['calendar_contact_birthdays'])) { $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar'); if (!$p['current']) { $p['blocks']['birthdays']['content'] = true; return $p; } $field_id = 'rcmfd_contact_birthdays'; $input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)')); $p['blocks']['birthdays']['options']['contact_birthdays'] = array( 'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')), 'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0), ); $input_attrib = array( 'class' => 'calendar_birthday_props', 'disabled' => !$this->rc->config->get('calendar_contact_birthdays'), ); $sources = array(); $checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib); foreach ($this->rc->get_address_sources(false, true) as $source) { $active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : ''; $sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . ' ' . rcube::Q($source['realname'] ?: $source['name'])); } $p['blocks']['birthdays']['options']['birthday_adressbooks'] = array( 'title' => rcube::Q($this->gettext('birthdayscalendarsources')), 'content' => join(html::br(), $sources), ); $field_id = 'rcmfd_birthdays_alarm'; $select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib); $select_type->add($this->gettext('none'), ''); foreach ($this->driver->alarm_types as $type) { $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); } $input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib); $select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib); foreach (array('-M','-H','-D') as $trigger) $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D')); $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array( 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))), 'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } return $p; } /** * Handler for preferences_save hook. * Executed on Calendar settings form submit. * * @param array Original parameters * @return array Modified parameters */ function preferences_save($p) { if ($p['section'] == 'calendar') { $this->load_driver(); // compose default alarm preset value $alarm_offset = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST); $alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST); $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1]; $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST); $birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST); $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1]; $p['prefs'] = array( 'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST), 'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)), 'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)), 'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)), 'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)), 'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)), 'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)), 'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST), 'calendar_default_alarm_offset' => $default_alarm, 'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST), 'calendar_date_format' => null, // clear previously saved values 'calendar_time_format' => null, 'calendar_contact_birthdays' => rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST) ? true : false, 'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST), 'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST), 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null, 'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)), ); if ($p['prefs']['calendar_itip_after_action'] == 4) { $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true); } // categories if (!$this->driver->nocategories) { $old_categories = $new_categories = array(); foreach ($this->driver->list_categories() as $name => $color) { $old_categories[md5($name)] = $name; } $categories = (array) rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST); $colors = (array) rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST); foreach ($categories as $key => $name) { $color = preg_replace('/^#/', '', strval($colors[$key])); // rename categories in existing events -> driver's job if ($oldname = $old_categories[$key]) { $this->driver->replace_category($oldname, $name, $color); unset($old_categories[$key]); } else $this->driver->add_category($name, $color); $new_categories[$name] = $color; } // these old categories have been removed, alter events accordingly -> driver's job foreach ((array)$old_categories[$key] as $key => $name) { $this->driver->remove_category($name); } $p['prefs']['calendar_categories'] = $new_categories; } } return $p; } /** * Dispatcher for calendar actions initiated by the client */ function calendar_action() { $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $cal = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC); $success = $reload = false; if (isset($cal['showalarms'])) $cal['showalarms'] = intval($cal['showalarms']); switch ($action) { case "form-new": case "form-edit": echo $this->ui->calendar_editform($action, $cal); exit; case "new": $success = $this->driver->create_calendar($cal); $reload = true; break; case "edit": $success = $this->driver->edit_calendar($cal); $reload = true; break; case "delete": if ($success = $this->driver->delete_calendar($cal)) $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id'])); break; case "subscribe": if (!$this->driver->subscribe_calendar($cal)) $this->rc->output->show_message($this->gettext('errorsaving'), 'error'); return; case "search": $results = array(); $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); foreach ((array) $this->driver->search_calendars($query, $source) as $id => $prop) { $editname = $prop['editname']; unset($prop['editname']); // force full name to be displayed $prop['active'] = false; // let the UI generate HTML and CSS representation for this calendar $html = $this->ui->calendar_list_item($id, $prop, $jsenv); $cal = $jsenv[$id]; $cal['editname'] = $editname; $cal['html'] = $html; if (!empty($prop['color'])) $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode); $results[] = $cal; } // report more results available if ($this->driver->search_more_results) $this->rc->output->show_message('autocompletemore', 'info'); $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC)); return; } if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else { $error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :''); $this->rc->output->show_message($error_msg, 'error'); } $this->rc->output->command('plugin.unlock_saving'); if ($success && $reload) $this->rc->output->command('plugin.reload_view'); } /** * Dispatcher for event actions initiated by the client */ function event_action() { $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true); $success = $reload = $got_msg = false; // force notify if hidden + active if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1) $event['_notify'] = 1; // read old event data in order to find changes if (($event['_notify'] || $event['_decline']) && $action != 'new') { $old = $this->driver->get_event($event); // load main event if savemode is 'all' or if deleting 'future' events if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline'])) && $old['recurrence_id']) { $old['id'] = $old['recurrence_id']; $old = $this->driver->get_event($old); } } switch ($action) { case "new": // create UID for new event $event['uid'] = $this->generate_uid(); $this->write_preprocess($event, $action); if ($success = $this->driver->new_event($event)) { $event['id'] = $event['uid']; $event['_savemode'] = 'all'; $this->cleanup_event($event); $this->event_save_success($event, null, $action, true); } $reload = $success && $event['recurrence'] ? 2 : 1; break; case "edit": $this->write_preprocess($event, $action); if ($success = $this->driver->edit_event($event)) { $this->cleanup_event($event); $this->event_save_success($event, $old, $action, $success); } $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; break; case "resize": $this->write_preprocess($event, $action); if ($success = $this->driver->resize_event($event)) { $this->event_save_success($event, $old, $action, $success); } $reload = $event['_savemode'] ? 2 : 1; break; case "move": $this->write_preprocess($event, $action); if ($success = $this->driver->move_event($event)) { $this->event_save_success($event, $old, $action, $success); } $reload = $success && $event['_savemode'] ? 2 : 1; break; case "remove": // remove previous deletes $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0; $this->rc->session->remove('calendar_event_undo'); // search for event if only UID is given if (!isset($event['calendar']) && $event['uid']) { if (!($event = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) { break; } $undo_time = 0; } $success = $this->driver->remove_event($event, $undo_time < 1); $reload = (!$success || $event['_savemode']) ? 2 : 1; if ($undo_time > 0 && $success) { $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event); // display message with Undo link. $msg = html::span(null, $this->gettext('successremoval')) . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", - JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo')); + rcmail_output::JS_OBJECT_NAME, rcmail_output::JS_OBJECT_NAME)), $this->gettext('undo')); $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time); $got_msg = true; } else if ($success) { $this->rc->output->show_message('calendar.successremoval', 'confirmation'); $got_msg = true; } // send cancellation for the main event if ($event['_savemode'] == 'all') { unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']); } // send an update for the main event's recurrence rule instead of a cancellation message else if ($event['_savemode'] == 'future' && $success !== false && $success !== true) { $event['_savemode'] = 'all'; // force event_save_success() to load master event $action = 'edit'; $success = true; } // send iTIP reply that participant has declined the event if ($success && $event['_decline']) { $emails = $this->get_user_emails(); foreach ($old['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $attendee; else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $old['attendees'][$i]['status'] = 'DECLINED'; $reply_sender = $attendee['email']; } } if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) { $old['thisandfuture'] = true; } $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } else if ($success) { $this->event_save_success($event, $old, $action, $success); } break; case "undo": // Restore deleted event $event = $_SESSION['calendar_event_undo']['data']; if ($event) $success = $this->driver->restore_event($event); if ($success) { $this->rc->session->remove('calendar_event_undo'); $this->rc->output->show_message('calendar.successrestore', 'confirmation'); $got_msg = true; $reload = 2; } break; case "rsvp": $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST); $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST); $reply_comment = $event['comment']; $this->write_preprocess($event, 'edit'); $ev = $this->driver->get_event($event); $ev['attendees'] = $event['attendees']; $ev['free_busy'] = $event['free_busy']; $ev['_savemode'] = $event['_savemode']; // send invitation to delegatee + add it as attendee if ($status == 'delegated' && $event['to']) { $itip = $this->load_itip(); if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) { $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); $noreply = false; } } $event = $ev; // compose a list of attendees affected by this change $updated_attendees = array_filter(array_map(function($j) use ($event) { return $event['attendees'][$j]; }, $attendees)); if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) { $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC); $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0; $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1; $organizer = null; $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $reply_sender = $attendee['email']; } } if (!$noreply) { $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); $event['comment'] = $reply_comment; $event['thisandfuture'] = $event['_savemode'] == 'future'; if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } // refresh all calendars if ($event['calendar'] != $ev['calendar']) { $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true)); $reload = 0; } } break; case "dismiss": $event['ids'] = explode(',', $event['id']); $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event); $success = $plugin['success']; foreach ($event['ids'] as $id) { if (strpos($id, 'cal:') === 0) $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']); } break; case "changelog": $data = $this->driver->get_event_changelog($event); if (is_array($data) && !empty($data)) { $lib = $this->lib; $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); array_walk($data, function(&$change) use ($lib, $dtformat) { if ($change['date']) { $dt = $lib->adjust_timezone($change['date']); if ($dt instanceof DateTime) $change['date'] = $this->rc->format_date($dt, $dtformat, false); } }); $this->rc->output->command('plugin.render_event_changelog', $data); } else { $this->rc->output->command('plugin.render_event_changelog', false); } $got_msg = true; $reload = false; break; case "diff": $data = $this->driver->get_event_diff($event, $event['rev1'], $event['rev2']); if (is_array($data)) { // convert some properties, similar to self::_client_event() $lib = $this->lib; array_walk($data['changes'], function(&$change, $i) use ($event, $lib) { // convert date cols foreach (array('start','end','created','changed') as $col) { if ($change['property'] == $col) { $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c'); $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c'); } } // create textual representation for alarms and recurrence if ($change['property'] == 'alarms') { if (is_array($change['old'])) $change['old_'] = libcalendaring::alarm_text($change['old']); if (is_array($change['new'])) $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'recurrence') { if (is_array($change['old'])) $change['old_'] = $lib->recurrence_text($change['old']); if (is_array($change['new'])) $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'attachments') { if (is_array($change['old'])) $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']); if (is_array($change['new'])) $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']); } // compute a nice diff of description texts if ($change['property'] == 'description') { $change['diff_'] = libkolab::html_diff($change['old'], $change['new']); } }); $this->rc->output->command('plugin.event_show_diff', $data); } else { $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); } $got_msg = true; $reload = false; break; case "show": if ($event = $this->driver->get_event_revison($event, $event['rev'])) { $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event)); } else { $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error'); } $got_msg = true; $reload = false; break; case "restore": if ($success = $this->driver->restore_event_revision($event, $event['rev'])) { $_event = $this->driver->get_event($event); $reload = $_event['recurrence'] ? 2 : 1; $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation'); $this->rc->output->command('plugin.close_history_dialog'); } else { $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error'); $reload = 0; } $got_msg = true; break; } // show confirmation/error message if (!$got_msg) { if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else $this->rc->output->show_message('calendar.errorsaving', 'error'); } // unlock client $this->rc->output->command('plugin.unlock_saving'); // update event object on the client or trigger a complete refretch if too complicated if ($reload) { $args = array('source' => $event['calendar']); if ($reload > 1) $args['refetch'] = true; else if ($success && $action != 'remove') $args['update'] = $this->_client_event($this->driver->get_event($event), true); $this->rc->output->command('plugin.refresh_calendar', $args); } } /** * Helper method sending iTip notifications after successful event updates */ private function event_save_success(&$event, $old, $action, $success) { // $success is a new event ID if ($success !== true) { // send update notification on the main event if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) { $master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']), 0, true); unset($master['_instance'], $master['recurrence_date']); $sent = $this->notify_attendees($master, null, $action, $event['_comment'], false); if ($sent < 0) $this->rc->output->show_message('calendar.errornotifying', 'error'); $event['attendees'] = $master['attendees']; // this tricks us into the next if clause } // delete old reference if saved as new if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') { $old = null; } $event['id'] = $success; $event['_savemode'] = 'all'; } // send out notifications if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) { $_savemode = $event['_savemode']; // send notification for the main event when savemode is 'all' if ($action != 'remove' && $_savemode == 'all' && ($event['recurrence_id'] || $old['recurrence_id'] || ($old && $old['id'] != $event['id']))) { $event['id'] = $event['recurrence_id'] ?: ($old['recurrence_id'] ?: $old['id']); $event = $this->driver->get_event($event, 0, true); unset($event['_instance'], $event['recurrence_date']); } else { // make sure we have the complete record $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true); } $event['_savemode'] = $_savemode; if ($old) { $old['thisandfuture'] = $_savemode == 'future'; } // only notify if data really changed (TODO: do diff check on client already) if (!$old || $action == 'remove' || self::event_diff($event, $old)) { $sent = $this->notify_attendees($event, $old, $action, $event['_comment']); if ($sent > 0) $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); else if ($sent < 0) $this->rc->output->show_message('calendar.errornotifying', 'error'); } } } /** * Handler for load-requests from fullcalendar * This will return pure JSON formatted output */ function load_events() { $events = $this->driver->load_events( rcube_utils::get_input_value('start', rcube_utils::INPUT_GET), rcube_utils::get_input_value('end', rcube_utils::INPUT_GET), ($query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET)), rcube_utils::get_input_value('source', rcube_utils::INPUT_GET) ); echo $this->encode($events, !empty($query)); exit; } /** * Handler for requests fetching event counts for calendars */ public function count_events() { // don't update session on these requests (avoiding race conditions) $this->rc->session->nowrite = true; $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); if (!$start) { $start = new DateTime('today 00:00:00', $this->timezone); $start = $start->format('U'); } $counts = $this->driver->count_events( rcube_utils::get_input_value('source', rcube_utils::INPUT_GET), $start, rcube_utils::get_input_value('end', rcube_utils::INPUT_GET) ); $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); } /** * Load event data from an iTip message attachment */ public function itip_events($msgref) { $path = explode('/', $msgref); $msg = array_pop($path); $mbox = join('/', $path); list($uid, $mime_id) = explode('#', $msg); $events = array(); if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { $partstat = 'NEEDS-ACTION'; /* $user_emails = $this->lib->get_user_emails(); foreach ($event['attendees'] as $attendee) { if (in_array($attendee['email'], $user_emails)) { $partstat = $attendee['status']; break; } } */ $event['id'] = $event['uid']; $event['temporary'] = true; $event['readonly'] = true; $event['calendar'] = '--invitation--itip'; $event['className'] = 'fc-invitation-' . strtolower($partstat); $event['_mbox'] = $mbox; $event['_uid'] = $uid; $event['_part'] = $mime_id; $events[] = $this->_client_event($event, true); // add recurring instances if (!empty($event['recurrence'])) { foreach ($this->driver->get_recurring_events($event, $event['start']) as $recurring) { $recurring['temporary'] = true; $recurring['readonly'] = true; $recurring['calendar'] = '--invitation--itip'; $events[] = $this->_client_event($recurring, true); } } } return $events; } /** * Handler for keep-alive requests * This will check for updated data in active calendars and sync them to the client */ public function refresh($attr) { // refresh the entire calendar every 10th time to also sync deleted events if (rand(0,10) == 10) { $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true)); return; } $counts = array(); foreach ($this->driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) { $events = $this->driver->load_events( rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), $cal['id'], 1, $attr['last'] ); foreach ($events as $event) { $this->rc->output->command('plugin.refresh_calendar', array('source' => $cal['id'], 'update' => $this->_client_event($event))); } // refresh count for this calendar if ($cal['counts']) { $today = new DateTime('today 00:00:00', $this->timezone); $counts += $this->driver->count_events($cal['id'], $today->format('U')); } } if (!empty($counts)) { $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); } } /** * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. * This will check for pending notifications and pass them to the client */ public function pending_alarms($p) { $this->load_driver(); $time = $p['time'] ?: time(); if ($alarms = $this->driver->pending_alarms($time)) { foreach ($alarms as $alarm) { $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal: $p['alarms'][] = $alarm; } } // get alarms for birthdays calendar if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') { $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db'); foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) { $alarm = libcalendaring::get_next_alarm($e); // overwrite alarm time with snooze value (or null if dismissed) if ($dismissed = $cache->get($e['id'])) $alarm['time'] = $dismissed['notifyat']; // add to list if alarm is set if ($alarm && $alarm['time'] && $alarm['time'] <= $time) { $e['id'] = 'cal:bday:' . $e['id']; $e['notifyat'] = $alarm['time']; $p['alarms'][] = $e; } } } return $p; } /** * Handler for alarm dismiss hook triggered by libcalendaring */ public function dismiss_alarms($p) { $this->load_driver(); foreach ((array)$p['ids'] as $id) { if (strpos($id, 'cal:bday:') === 0) { $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']); } else if (strpos($id, 'cal:') === 0) { $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']); } } return $p; } /** * Handler for check-recent requests which are accidentally sent to calendar taks */ function check_recent() { // NOP $this->rc->output->send(); } /** * Hook triggered when a contact is saved */ function contact_update($p) { // clear birthdays calendar cache if (!empty($p['record']['birthday'])) { $cache = $this->rc->get_cache('calendar.birthdays', 'db'); $cache->remove(); } } /** * */ function import_events() { // Upload progress update if (!empty($_GET['_progress'])) { rcube_upload_progress(); } @set_time_limit(0); // process uploaded file if there is no error $err = $_FILES['_data']['error']; if (!$err && $_FILES['_data']['tmp_name']) { $calendar = rcube_utils::get_input_value('calendar', rcube_utils::INPUT_GPC); $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0; // extract zip file if ($_FILES['_data']['type'] == 'application/zip') { $count = 0; if (class_exists('ZipArchive', false)) { $zip = new ZipArchive(); if ($zip->open($_FILES['_data']['tmp_name'])) { $randname = uniqid('zip-' . session_id(), true); $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname; mkdir($tmpdir, 0700); // extract each ical file from the archive and import it for ($i = 0; $i < $zip->numFiles; $i++) { $filename = $zip->getNameIndex($i); if (preg_match('/\.ics$/i', $filename)) { $tmpfile = $tmpdir . '/' . basename($filename); if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) { $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors); unlink($tmpfile); } } } rmdir($tmpdir); $zip->close(); } else { $errors = 1; $msg = 'Failed to open zip file.'; } } else { $errors = 1; $msg = 'Zip files are not supported for import.'; } } else { // attempt to import teh uploaded file directly $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors); } if ($count) { $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation'); $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true)); } else if (!$errors) { $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); $this->rc->output->command('plugin.import_success', array('source' => $calendar)); } else { $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : ''))); } } else { if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array( 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else { $msg = rcube_label('fileuploaderror'); } $this->rc->output->command('plugin.import_error', array('message' => $msg)); } $this->rc->output->send('iframe'); } /** * Helper function to parse and import a single .ics file */ private function import_from_file($filepath, $calendar, $rangestart, &$errors) { $user_email = $this->rc->user->get_username(); $ical = $this->get_ical(); $errors = !$ical->fopen($filepath); $count = $i = 0; foreach ($ical as $event) { // keep the browser connection alive on long import jobs if (++$i > 100 && $i % 100 == 0) { echo ""; ob_flush(); } // TODO: correctly handle recurring events which start before $rangestart if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) continue; $event['_owner'] = $user_email; $event['calendar'] = $calendar; if ($this->driver->new_event($event)) { $count++; } else { $errors++; } } return $count; } /** * Construct the ics file for exporting events to iCalendar format; */ function export_events($terminate = true) { $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET); if (!isset($start)) $start = 'today -1 year'; if (!is_numeric($start)) $start = strtotime($start . ' 00:00:00'); if (!$end) $end = 'today +10 years'; if (!is_numeric($end)) $end = strtotime($end . ' 23:59:59'); $event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET); $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET); $calid = $filename = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET); $calendars = $this->driver->list_calendars(); $events = array(); if ($calendars[$calid]) { $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid; $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii if (!empty($event_id)) { if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id), 0, true)) { if ($event['recurrence_id']) { $event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']), 0, true); } $events = array($event); $filename = asciiwords($event['title']); if (empty($filename)) $filename = 'event'; } } else { $events = $this->driver->load_events($start, $end, null, $calid, 0); if (empty($filename)) $filename = $calid; } } header("Content-Type: text/calendar"); header("Content-Disposition: inline; filename=".$filename.'.ics'); $this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null); if ($terminate) exit; } /** * Handler for iCal feed requests */ function ical_feed_export() { $session_exists = !empty($_SESSION['user_id']); // process HTTP auth info if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host() $auth = $this->rc->plugins->exec_hook('authenticate', array( 'host' => $this->rc->autoselect_host(), 'user' => trim($_SERVER['PHP_AUTH_USER']), 'pass' => $_SERVER['PHP_AUTH_PW'], 'cookiecheck' => true, 'valid' => true, )); if ($auth['valid'] && !$auth['abort']) $this->rc->login($auth['user'], $auth['pass'], $auth['host']); } // require HTTP auth if (empty($_SESSION['user_id'])) { header('WWW-Authenticate: Basic realm="Roundcube Calendar"'); header('HTTP/1.0 401 Unauthorized'); exit; } // decode calendar feed hash $format = 'ics'; $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET); if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) { $format = strtolower($m[1]); $calhash = preg_replace($suff_regex, '', $calhash); } if (!strpos($calhash, ':')) $calhash = base64_decode($calhash); list($user, $_GET['source']) = explode(':', $calhash, 2); // sanity check user if ($this->rc->user->get_username() == $user) { $this->load_driver(); $this->export_events(false); } else { header('HTTP/1.0 404 Not Found'); } // don't save session data if (!$session_exists) session_destroy(); exit; } /** * */ function load_settings() { $this->lib->load_settings(); $this->defaults += $this->lib->defaults; $settings = array(); // configuration $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar'); $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']); $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']); $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']); $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']); $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']); $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']); $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']); $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false); $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); // get user identity to create default attendee if ($this->ui->screen == 'calendar') { foreach ($this->rc->user->list_emails() as $rec) { if (!$identity) $identity = $rec; $identity['emails'][] = $rec['email']; $settings['identities'][$rec['identity_id']] = $rec['email']; } $identity['emails'][] = $this->rc->user->get_username(); $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails']))); } return $settings; } /** * Encode events as JSON * * @param array Events as array * @param boolean Add CSS class names according to calendar and categories * @return string JSON encoded events */ function encode($events, $addcss = false) { $json = array(); foreach ($events as $event) { $json[] = $this->_client_event($event, $addcss); } return json_encode($json); } /** * Convert an event object to be used on the client */ private function _client_event($event, $addcss = false) { // compose a human readable strings for alarms_text and recurrence_text if ($event['valarms']) { $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']); $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']); } if ($event['recurrence']) { $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']); $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']); unset($event['recurrence_date']); } foreach ((array)$event['attachments'] as $k => $attachment) { $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); } // convert link URIs references into structs if (array_key_exists('links', $event)) { foreach ((array)$event['links'] as $i => $link) { if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) { $event['links'][$i] = $msgref; } } } // check for organizer in attendees list $organizer = null; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) { $event['attendees'][$i]['noreply'] = true; } else { unset($event['attendees'][$i]['noreply']); } } if ($organizer === null && !empty($event['organizer'])) { $organizer = $event['organizer']; $organizer['role'] = 'ORGANIZER'; if (!is_array($event['attendees'])) $event['attendees'] = array(); array_unshift($event['attendees'], $organizer); } // Convert HTML description into plain text if ($this->is_html($event)) { $h2t = new rcube_html2text($event['description'], false, true, 0); $event['description'] = trim($h2t->get_text()); } // mapping url => vurl because of the fullcalendar client script $event['vurl'] = $event['url']; unset($event['url']); return array( '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'), 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'), // 'changed' might be empty for event recurrences (Bug #2185) 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null, 'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null, 'title' => strval($event['title']), 'description' => strval($event['description']), 'location' => strval($event['location']), 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true) . rtrim(' ' . $event['className']), 'allDay' => ($event['allday'] == 1), ) + $event; } /** * Generate a unique identifier for an event */ public function generate_uid() { return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); } /** * TEMPORARY: generate random event data for testing * Create events by opening http:///?_task=calendar&_action=randomdata&_num=500&_date=2014-08-01&_dev=120 */ public function generate_randomdata() { @set_time_limit(0); $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100; $date = $_REQUEST['_date'] ?: 'now'; $dev = $_REQUEST['_dev'] ?: 30; $cats = array_keys($this->driver->list_categories()); $cals = $this->driver->list_calendars(calendar_driver::FILTER_ACTIVE); $count = 0; while ($count++ < $num) { $spread = intval($dev) * 86400; // days $refdate = strtotime($date); $start = round(($refdate + rand(-$spread, $spread)) / 600) * 600; $duration = round(rand(30, 360) / 30) * 30 * 60; $allday = rand(0,20) > 18; $alarm = rand(-30,12) * 5; $fb = rand(0,2); if (date('G', $start) > 23) $start -= 3600; if ($allday) { $start = strtotime(date('Y-m-d 00:00:00', $start)); $duration = 86399; } $title = ''; $len = rand(2, 12); $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise."); // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"; for ($i = 0; $i < $len; $i++) $title .= $words[rand(0,count($words)-1)] . " "; $this->driver->new_event(array( 'uid' => $this->generate_uid(), 'start' => new DateTime('@'.$start), 'end' => new DateTime('@'.($start + $duration)), 'allday' => $allday, 'title' => rtrim($title), 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), 'categories' => $cats[array_rand($cats)], 'calendar' => array_rand($cals), 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '', 'priority' => rand(0,9), )); } $this->rc->output->redirect(''); } /** * Handler for attachments upload */ public function attachment_upload() { $this->lib->attachment_upload(self::SESSION_KEY, 'cal-'); } /** * Handler for attachments download/displaying */ public function attachment_get() { // show loading page if (!empty($_GET['_preload'])) { return $this->lib->attachment_loading_page(); } $event_id = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC); $calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC); $event = array('id' => $event_id, 'calendar' => $calendar, 'rev' => $rev); $attachment = $this->driver->get_attachment($id, $event); // show part page if (!empty($_GET['_frame'])) { $this->lib->attachment = $attachment; $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame')); $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header')); $this->rc->output->send('calendar.attachment'); } // deliver attachment content else if ($attachment) { $attachment['body'] = $this->driver->get_attachment_body($id, $event); $this->lib->attachment_get($attachment); } // if we arrive here, the requested part was not found header('HTTP/1.1 404 Not Found'); exit; } /** * Determine whether the given event description is HTML formatted */ private function is_html($event) { // check for opening and closing or tags return (preg_match('/<(html|body)(\s+[a-z]|>)/', $event['description'], $m) && strpos($event['description'], '') > 0); } /** * Prepares new/edited event properties before save */ private function write_preprocess(&$event, $action) { // convert dates into DateTime objects in user's current timezone $event['start'] = new DateTime($event['start'], $this->timezone); $event['end'] = new DateTime($event['end'], $this->timezone); $event['allday'] = (bool)$event['allday']; // start/end is all we need for 'move' action (#1480) if ($action == 'move') { return; } // convert the submitted recurrence settings if (is_array($event['recurrence'])) { $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']); } // convert the submitted alarm values if ($event['valarms']) { $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']); } $attachments = array(); $eventid = 'cal-'.$event['id']; if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) { if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) { foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) { if (is_array($event['attachments']) && in_array($id, $event['attachments'])) { $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment); } } } } $event['attachments'] = $attachments; // convert link references into simple URIs if (array_key_exists('links', $event)) { $event['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$event['links']); } // check for organizer in attendees if ($action == 'new' || $action == 'edit') { if (!$event['attendees']) $event['attendees'] = array(); $emails = $this->get_user_emails(); $organizer = $owner = false; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $i; if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails)) $owner = $i; if (!isset($attendee['rsvp'])) $event['attendees'][$i]['rsvp'] = true; else if (is_string($attendee['rsvp'])) $event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1'; } // set new organizer identity if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) { $event['attendees'][$organizer]['name'] = $identity['name']; $event['attendees'][$organizer]['email'] = $identity['email']; } // set owner as organizer if yet missing if ($organizer === false && $owner !== false) { $event['attendees'][$owner]['role'] = 'ORGANIZER'; unset($event['attendees'][$owner]['rsvp']); } } // mapping url => vurl because of the fullcalendar client script if (array_key_exists('vurl', $event)) { $event['url'] = $event['vurl']; unset($event['vurl']); } } /** * Releases some resources after successful event save */ private function cleanup_event(&$event) { // remove temp. attachment files if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) { $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid)); $this->rc->session->remove(self::SESSION_KEY); } } /** * Send out an invitation/notification to all event attendees */ private function notify_attendees($event, $old, $action = 'edit', $comment = null, $rsvp = null) { if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) { $event['cancelled'] = true; $is_cancelled = true; } if ($rsvp === null) $rsvp = !$old || $event['sequence'] > $old['sequence']; $itip = $this->load_itip(); $emails = $this->get_user_emails(); $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); // add comment to the iTip attachment $event['comment'] = $comment; // set a valid recurrence-id if this is a recurrence instance libcalendaring::identify_recurrence_instance($event); // compose multipart message using PEAR:Mail_Mime $method = $action == 'remove' ? 'CANCEL' : 'REQUEST'; $message = $itip->compose_itip_message($event, $method, $rsvp); // list existing attendees from $old event $old_attendees = array(); foreach ((array)$old['attendees'] as $attendee) { $old_attendees[] = $attendee['email']; } // send to every attendee $sent = 0; $current = array(); foreach ((array)$event['attendees'] as $attendee) { $current[] = strtolower($attendee['email']); // skip myself for obvious reasons if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) continue; // skip if notification is disabled for this attendee if ($attendee['noreply'] && $itip_notify & 2) continue; // skip if this attendee has delegated and set RSVP=FALSE if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) continue; // which template to use for mail text $is_new = !in_array($attendee['email'], $old_attendees); $is_rsvp = $is_new || $event['sequence'] > $old['sequence']; $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody'); $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty')); $event['comment'] = $comment; // finally send the message if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp)) $sent++; else $sent = -100; } // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions // send CANCEL message to removed attendees foreach ((array)$old['attendees'] as $attendee) { if ($attendee['role'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) continue; $vevent = $old; $vevent['cancelled'] = $is_cancelled; $vevent['attendees'] = array($attendee); $vevent['comment'] = $comment; if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody')) $sent++; else $sent = -100; } return $sent; } /** * Echo simple free/busy status text for the given user and time range */ public function freebusy_status() { $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC); $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC); $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC); // convert dates into unix timestamps if (!empty($start) && !is_numeric($start)) { $dts = new DateTime($start, $this->timezone); $start = $dts->format('U'); } if (!empty($end) && !is_numeric($end)) { $dte = new DateTime($end, $this->timezone); $end = $dte->format('U'); } if (!$start) $start = time(); if (!$end) $end = $start + 3600; $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE'); $status = 'UNKNOWN'; // if the backend has free-busy information $fblist = $this->driver->get_freebusy_list($email, $start, $end); if (is_array($fblist)) { $status = 'FREE'; foreach ($fblist as $slot) { list($from, $to, $type) = $slot; if ($from < $end && $to > $start) { $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY'; break; } } } // let this information be cached for 5min send_future_expire_header(300); echo $status; exit; } /** * Return a list of free/busy time slots within the given period * Echo data in JSON encoding */ public function freebusy_times() { $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC); $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC); $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC); $interval = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC)); $strformat = $interval > 60 ? 'Ymd' : 'YmdHis'; // convert dates into unix timestamps if (!empty($start) && !is_numeric($start)) { $dts = rcube_utils::anytodatetime($start, $this->timezone); $start = $dts ? $dts->format('U') : null; } if (!empty($end) && !is_numeric($end)) { $dte = rcube_utils::anytodatetime($end, $this->timezone); $end = $dte ? $dte->format('U') : null; } if (!$start) $start = time(); if (!$end) $end = $start + 86400 * 30; if (!$interval) $interval = 60; // 1 hour if (!$dte) { $dts = new DateTime('@'.$start); $dts->setTimezone($this->timezone); } $fblist = $this->driver->get_freebusy_list($email, $start, $end); $slots = array(); // build a list from $start till $end with blocks representing the fb-status for ($s = 0, $t = $start; $t <= $end; $s++) { $status = self::FREEBUSY_UNKNOWN; $t_end = $t + $interval * 60; $dt = new DateTime('@'.$t); $dt->setTimezone($this->timezone); // determine attendee's status if (is_array($fblist)) { $status = self::FREEBUSY_FREE; foreach ($fblist as $slot) { list($from, $to, $type) = $slot; // check for possible all-day times if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') { // shift into the user's timezone for sane matching $from -= $this->gmt_offset; $to -= $this->gmt_offset; } if ($from < $t_end && $to > $t) { $status = isset($type) ? $type : self::FREEBUSY_BUSY; if ($status == self::FREEBUSY_BUSY) // can't get any worse :-) break; } } } $slots[$s] = $status; $times[$s] = intval($dt->format($strformat)); $t = $t_end; } $dte = new DateTime('@'.$t_end); $dte->setTimezone($this->timezone); // let this information be cached for 5min send_future_expire_header(300); echo json_encode(array( 'email' => $email, 'start' => $dts->format('c'), 'end' => $dte->format('c'), 'interval' => $interval, 'slots' => $slots, 'times' => $times, )); exit; } /** * Handler for printing calendars */ public function print_view() { $title = $this->gettext('print'); $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC); if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $view = 'agendaDay'; $this->rc->output->set_env('view',$view); if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) $this->rc->output->set_env('date', $date); if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC)) $this->rc->output->set_env('listRange', intval($range)); if (isset($_REQUEST['sections'])) $this->rc->output->set_env('listSections', rcube_utils::get_input_value('sections', rcube_utils::INPUT_GPC)); if ($search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC)) { $this->rc->output->set_env('search', $search); $title .= ' "' . $search . '"'; } // Add CSS stylesheets to the page header $skin_path = $this->local_skin_path(); $this->include_stylesheet($skin_path . '/fullcalendar.css'); $this->include_stylesheet($skin_path . '/print.css'); // Add JS files to the page header $this->include_script('print.js'); $this->include_script('lib/js/fullcalendar.js'); $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css')); $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list')); $this->rc->output->set_pagetitle($title); $this->rc->output->send("calendar.print"); } /** * */ public function get_inline_ui() { foreach (array('save','cancel','savingdata') as $label) $texts['calendar.'.$label] = $this->gettext($label); $texts['calendar.new_event'] = $this->gettext('createfrommail'); $this->ui->init_templates(); $this->ui->calendar_list(); # set env['calendars'] echo $this->api->output->parse('calendar.eventedit', false, false); echo html::tag('script', array('type' => 'text/javascript'), "rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n". "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n". "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n". "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n". "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n". "rcmail.add_label(" . json_encode($texts) . ");\n" ); exit; } /** * Compare two event objects and return differing properties * * @param array Event A * @param array Event B * @return array List of differing event properties */ public static function event_diff($a, $b) { $diff = array(); $ignore = array('changed' => 1, 'attachments' => 1); foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { if (!$ignore[$key] && $key[0] != '_' && $a[$key] != $b[$key]) $diff[] = $key; } // only compare number of attachments if (count($a['attachments']) != count($b['attachments'])) $diff[] = 'attachments'; return $diff; } /** * Update attendee properties on the given event object * * @param array The event object to be altered * @param array List of hash arrays each represeting an updated/added attendee */ public static function merge_attendee_data(&$event, $attendees, $removed = null) { if (!empty($attendees) && !is_array($attendees[0])) { $attendees = array($attendees); } foreach ($attendees as $attendee) { $found = false; foreach ($event['attendees'] as $i => $candidate) { if ($candidate['email'] == $attendee['email']) { $event['attendees'][$i] = $attendee; $found = true; break; } } if (!$found) { $event['attendees'][] = $attendee; } } // filter out removed attendees if (!empty($removed)) { $event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) { return !in_array($attendee['email'], $removed); }); } } /**** Resource management functions ****/ /** * Getter for the configured implementation of the resource directory interface */ private function resources_directory() { if (is_object($this->resources_dir)) { return $this->resources_dir; } if ($driver_name = $this->rc->config->get('calendar_resources_driver')) { $driver_class = 'resources_driver_' . $driver_name; require_once($this->home . '/drivers/resources_driver.php'); require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); $this->resources_dir = new $driver_class($this); } return $this->resources_dir; } /** * Handler for resoruce autocompletion requests */ public function resources_autocomplete() { $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true); $sid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC); $maxnum = (int)$this->rc->config->get('autocomplete_max', 15); $results = array(); if ($directory = $this->resources_directory()) { foreach ($directory->load_resources($search, $maxnum) as $rec) { $results[] = array( 'name' => $rec['name'], 'email' => $rec['email'], 'type' => $rec['_type'], ); } } $this->rc->output->command('ksearch_query_results', $results, $search, $sid); $this->rc->output->send(); } /** * Handler for load-requests for resource data */ function resources_list() { $data = array(); if ($directory = $this->resources_directory()) { foreach ($directory->load_resources() as $rec) { $data[] = $rec; } } $this->rc->output->command('plugin.resource_data', $data); $this->rc->output->send(); } /** * Handler for requests loading resource owner information */ function resources_owner() { if ($directory = $this->resources_directory()) { $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $data = $directory->get_resource_owner($id); } $this->rc->output->command('plugin.resource_owner', $data); $this->rc->output->send(); } /** * Deliver event data for a resource's calendar */ function resources_calendar() { $events = array(); if ($directory = $this->resources_directory()) { $events = $directory->get_resource_calendar( rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('start', rcube_utils::INPUT_GET), rcube_utils::get_input_value('end', rcube_utils::INPUT_GET)); } echo $this->encode($events); exit; } /**** Event invitation plugin hooks ****/ /** * Handler for calendar/itip-status requests */ function event_itip_status() { $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); // find local copy of the referenced event $this->load_driver(); $existing = $this->driver->get_event($data, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL); $itip = $this->load_itip(); $response = $itip->get_itip_status($data, $existing); // get a list of writeable calendars to save new events to if (!$existing && !$data['nosave'] && $response['action'] == 'rsvp' || $response['action'] == 'import') { $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL); $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true)); $calendar_select->add('--', ''); $numcals = 0; foreach ($calendars as $calendar) { if ($calendar['editable']) { $calendar_select->add($calendar['name'], $calendar['id']); $numcals++; } } if ($numcals <= 1) $calendar_select = null; } if ($calendar_select) { $default_calendar = $this->get_default_calendar($data['sensitivity']); $response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . ' ' . $calendar_select->show($default_calendar['id'])); } else if ($data['nosave']) { $response['select'] = html::tag('input', array('type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => '')); } // render small agenda view for the respective day if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') { $event_start = rcube_utils::anytodatetime($data['date']); $day_start = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone); $day_end = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone); // get events on that day from the user's personal calendars $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL); $events = $this->driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars)); usort($events, function($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; }); $before = $after = array(); foreach ($events as $event) { // TODO: skip events with free_busy == 'free' ? if ($event['uid'] == $data['uid'] || $event['end'] < $day_start || $event['start'] > $day_end) continue; else if ($event['start'] < $event_start) $before[] = $this->mail_agenda_event_row($event); else $after[] = $this->mail_agenda_event_row($event); } $response['append'] = array( 'selector' => '.calendar-agenda-preview', 'replacements' => array( '%before%' => !empty($before) ? join("\n", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')), '%after%' => !empty($after) ? join("\n", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')), ), ); } $this->rc->output->command('plugin.update_itip_object_status', $response); } /** * Handler for calendar/itip-remove requests */ function event_itip_remove() { $success = false; $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST); $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST); // search for event if only UID is given if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $instance), calendar_driver::FILTER_WRITEABLE)) { $event['_savemode'] = $savemode; $success = $this->driver->remove_event($event, true); } if ($success) { $this->rc->output->show_message('calendar.successremoval', 'confirmation'); } else { $this->rc->output->show_message('calendar.errorsaving', 'error'); } } /** * Handler for URLs that allow an invitee to respond on his invitation mail */ public function itip_attend_response($p) { if ($p['action'] == 'attend') { $this->ui->init(); $this->rc->output->set_env('task', 'calendar'); // override some env vars $this->rc->output->set_env('refresh_interval', 0); $this->rc->output->set_pagetitle($this->gettext('calendar')); $itip = $this->load_itip(); $token = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC); // read event info stored under the given token if ($invitation = $itip->get_invitation($token)) { $this->token = $token; $this->event = $invitation['event']; // show message about cancellation if ($invitation['cancelled']) { $this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled')); } // save submitted RSVP status else if (!empty($_POST['rsvp'])) { $status = null; foreach (array('accepted','tentative','declined') as $method) { if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) { $status = $method; break; } } // send itip reply to organizer $invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) { $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave'.strtolower($status))); } else $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1); // if user is logged in... if ($this->rc->user->ID) { $this->load_driver(); $invitation = $itip->get_invitation($token); // save the event to his/her default calendar if not yet present if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) { $invitation['event']['calendar'] = $calendar['id']; if ($this->driver->new_event($invitation['event'])) $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); } } } $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform')); $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox')); if (!$this->invitestatus) { $this->itip->set_rsvp_actions(array('accepted','tentative','declined')); $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons')); } $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']); } else $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1); $this->rc->output->send('calendar.itipattend'); } } /** * */ public function itip_event_inviteform($attrib) { $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token)); return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show(); } /** * */ private function mail_agenda_event_row($event, $class = '') { $time = $event['allday'] ? $this->gettext('all-day') : $this->rc->format_date($event['start'], $this->rc->config->get('time_format')) . ' - ' . $this->rc->format_date($event['end'], $this->rc->config->get('time_format')); return html::div(rtrim('event-row ' . $class), html::span('event-date', $time) . html::span('event-title', Q($event['title'])) ); } /** * */ public function mail_messages_list($p) { if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) { foreach ($p['messages'] as $header) { $part = new StdClass; $part->mimetype = $header->ctype; if (libcalendaring::part_is_vcalendar($part)) { $header->list_flags['attachmentClass'] = 'ical'; } else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) { // TODO: fetch bodystructure and search for ical parts. Maybe too expensive? if (!empty($header->structure) && is_array($header->structure->parts)) { foreach ($header->structure->parts as $part) { if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) { $header->list_flags['attachmentClass'] = 'ical'; break; } } } } } } } /** * Add UI element to copy event invitations or updates to the calendar */ public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) if (!empty($this->lib->ical_parts)) { $this->get_ical(); $this->load_itip(); } $html = ''; $has_events = false; $ical_objects = $this->lib->get_mail_ical_objects(); // show a box for every event in the file foreach ($ical_objects as $idx => $event) { if ($event['_type'] != 'event') // skip non-event objects (#2928) continue; $has_events = true; // get prepared inline UI for this event object if ($ical_objects->method) { $append = ''; // prepare a small agenda preview to be filled with actual event data on async request if ($ical_objects->method == 'REQUEST') { $append = html::div('calendar-agenda-preview', html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' . html::span('date', $this->rc->format_date($event['start'], $this->rc->config->get('date_format'))) ) . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%'); } $html .= html::div('calendar-invitebox', $this->itip->mail_itip_inline_ui( $event, $ical_objects->method, $ical_objects->mime_id . ':' . $idx, 'calendar', rcube_utils::anytodatetime($ical_objects->message_date), $this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $event['start']->format('U') ) . $append ); } // limit listing if ($idx >= 3) break; } // prepend event boxes to message body if ($html) { $this->ui->init(); $p['content'] = $html . $p['content']; $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm'); } // add "Save to calendar" button into attachment menu if ($has_events) { $this->add_button(array( 'id' => 'attachmentsavecal', 'name' => 'attachmentsavecal', 'type' => 'link', 'wrapper' => 'li', 'command' => 'attachment-save-calendar', 'class' => 'icon calendarlink', 'classact' => 'icon calendarlink active', 'innerclass' => 'icon calendar', 'label' => 'calendar.savetocalendar', ), 'attachmentmenu'); } return $p; } /** * Handler for POST request to import an event attached to a mail message */ public function mail_import_itip() { $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST); $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST)); $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)); $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0; $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST); $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST); $error_msg = $this->gettext('errorimportingevent'); $success = false; $delegate = null; if ($status == 'delegated') { $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false); $delegate = reset($delegates); if (empty($delegate) || empty($delegate['mailto'])) { $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error'); return; } } // successfully parsed events? if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { // forward iTip request to delegatee if ($delegate) { $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST)); $itip = $this->load_itip(); if ($itip->delegate_to($event, $delegate, $rsvpme ? true : false)) { $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); } else { $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } // the delegator is set to non-participant, thus save as non-blocking $event['free_busy'] = 'free'; } // find writeable calendar to store event $cal_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null; $dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST'); $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL); $calendar = $calendars[$cal_id]; // select default calendar except user explicitly selected 'none' if (!$calendar && !$dontsave) $calendar = $this->get_default_calendar($event['sensitivity']); $metadata = array( 'uid' => $event['uid'], '_instance' => $event['_instance'], 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0, 'sequence' => intval($event['sequence']), 'fallback' => strtoupper($status), 'method' => $event['_method'], 'task' => 'calendar', ); // update my attendee status according to submitted method if (!empty($status)) { $organizer = null; $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $event['attendees'][$i]['status'] = strtoupper($status); if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $reply_sender = $attendee['email']; $event_attendee = $attendee; } } // add attendee with this user's default identity if not listed if (!$reply_sender) { $sender_identity = $this->rc->user->list_emails(true); $event['attendees'][] = array( 'name' => $sender_identity['name'], 'email' => $sender_identity['email'], 'role' => 'OPT-PARTICIPANT', 'status' => strtoupper($status), ); $metadata['attendee'] = $sender_identity['email']; } } // save to calendar if ($calendar && $calendar['editable']) { // check for existing event with the same UID $existing = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL); if ($existing) { // forward savemode for correct updates of recurring events $existing['_savemode'] = $savemode ?: $event['_savemode']; // only update attendee status if ($event['_method'] == 'REPLY') { // try to identify the attendee using the email sender address $existing_attendee = -1; $existing_attendee_emails = array(); foreach ($existing['attendees'] as $i => $attendee) { $existing_attendee_emails[] = $attendee['email']; if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { $existing_attendee = $i; } } $event_attendee = null; $update_attendees = array(); foreach ($event['attendees'] as $attendee) { if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { $event_attendee = $attendee; $update_attendees[] = $attendee; $metadata['fallback'] = $attendee['status']; $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; if ($attendee['status'] != 'DELEGATED') { break; } } // also copy delegate attendee else if (!empty($attendee['delegated-from']) && (stripos($attendee['delegated-from'], $event['_sender']) !== false || stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) { $update_attendees[] = $attendee; if (!in_array($attendee['email'], $existing_attendee_emails)) { $existing['attendees'][] = $attendee; } } } // if delegatee has declined, set delegator's RSVP=True if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) { foreach ($existing['attendees'] as $i => $attendee) { if ($attendee['email'] == $event_attendee['delegated-from']) { $existing['attendees'][$i]['rsvp'] = true; break; } } } // found matching attendee entry in both existing and new events if ($existing_attendee >= 0 && $event_attendee) { $existing['attendees'][$existing_attendee] = $event_attendee; $success = $this->driver->update_attendees($existing, $update_attendees); } // update the entire attendees block else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) { $existing['attendees'][] = $event_attendee; $success = $this->driver->update_attendees($existing, $update_attendees); } else { $error_msg = $this->gettext('newerversionexists'); } } // delete the event when declined (#1670) else if ($status == 'declined' && $delete) { $deleted = $this->driver->remove_event($existing, true); $success = true; } // import the (newer) event else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) { $event['id'] = $existing['id']; $event['calendar'] = $existing['calendar']; // preserve my participant status for regular updates if (empty($status)) { $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { foreach ($existing['attendees'] as $j => $_attendee) { if ($attendee['email'] == $_attendee['email']) { $event['attendees'][$i] = $existing['attendees'][$j]; break; } } } } } // set status=CANCELLED on CANCEL messages if ($event['_method'] == 'CANCEL') $event['status'] = 'CANCELLED'; // show me as free when declined (#1670) if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') $event['free_busy'] = 'free'; $success = $this->driver->edit_event($event); } else if (!empty($status)) { $existing['attendees'] = $event['attendees']; if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // show me as free when declined (#1670) $existing['free_busy'] = 'free'; $success = $this->driver->edit_event($existing); } else $error_msg = $this->gettext('newerversionexists'); } else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) { if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') { $event['free_busy'] = 'free'; } // if the RSVP reply only refers to a single instance: // store unmodified master event with current instance as exception if (!empty($instance) && !empty($savemode) && $savemode != 'all') { $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event'); if ($master['recurrence'] && !$master['_instance']) { // compute recurring events until this instance's date if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) { $recurrence_date->setTime(23,59,59); foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) { if ($recurring['_instance'] == $instance) { // copy attendees block with my partstat to exception $recurring['attendees'] = $event['attendees']; $master['recurrence']['EXCEPTIONS'][] = $recurring; $event = $recurring; // set reference for iTip reply break; } } $master['calendar'] = $event['calendar'] = $calendar['id']; $success = $this->driver->new_event($master); } else { $master = null; } } else { $master = null; } } // save to the selected/default calendar if (!$master) { $event['calendar'] = $calendar['id']; $success = $this->driver->new_event($event); } } else if ($status == 'declined') $error_msg = null; } else if ($status == 'declined' || $dontsave) $error_msg = null; else $error_msg = $this->gettext('nowritecalendarfound'); } if ($success) { $message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully')); $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); } if ($success || $dontsave) { $metadata['calendar'] = $event['calendar']; $metadata['nosave'] = $dontsave; $metadata['rsvp'] = intval($metadata['rsvp']); $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']); $this->rc->output->command('plugin.itip_message_processed', $metadata); $error_msg = null; } else if ($error_msg) { $this->rc->output->command('display_message', $error_msg, 'error'); } // send iTip reply if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) { $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } $this->rc->output->send(); } /** * Handler for calendar/itip-remove requests */ function mail_itip_decline_reply() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') { $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); foreach ($event['attendees'] as $_attendee) { if ($_attendee['role'] != 'ORGANIZER') { $attendee = $_attendee; break; } } $itip = $this->load_itip(); if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel')) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } else { $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } } /** * Handler for calendar/itip-delegate requests */ function mail_itip_delegate() { // forward request to mail_import_itip() with the right status $_POST['_status'] = $_REQUEST['_status'] = 'delegated'; $this->mail_import_itip(); } /** * Import the full payload from a mail message attachment */ public function mail_import_attachment() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); - $charset = RCMAIL_CHARSET; + $charset = RCUBE_CHARSET; // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); if ($uid && $mime_id) { $part = $imap->get_message_part($uid, $mime_id); if ($part->ctype_parameters['charset']) $charset = $part->ctype_parameters['charset']; // $headers = $imap->get_message_headers($uid); if ($part) { $events = $this->get_ical()->import($part, $charset); } } $success = $existing = 0; if (!empty($events)) { // find writeable calendar to store event $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null; $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL); foreach ($events as $event) { // save to calendar $calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']); if ($calendar && $calendar['editable'] && $event['_type'] == 'event') { $event['calendar'] = $calendar['id']; if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) { $success += (bool)$this->driver->new_event($event); } else { $existing++; } } } } if ($success) { $this->rc->output->command('display_message', $this->gettext(array( 'name' => 'importsuccess', 'vars' => array('nr' => $success), )), 'confirmation'); } else if ($existing) { $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning'); } else { $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error'); } } /** * Read email message and return contents for a new event based on that message */ public function mail_message2event() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $event = array(); // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); $message = new rcube_message($uid); if ($message->headers) { $event['title'] = trim($message->subject); $event['description'] = trim($message->first_text_part()); $this->load_driver(); // add a reference to the email message if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) { $event['links'] = array($msgref); } // copy mail attachments to event else if ($message->attachments) { $eventid = 'cal-'; if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) { $_SESSION[self::SESSION_KEY] = array(); $_SESSION[self::SESSION_KEY]['id'] = $eventid; $_SESSION[self::SESSION_KEY]['attachments'] = array(); } foreach ((array)$message->attachments as $part) { $attachment = array( 'data' => $imap->get_message_part($uid, $part->mime_id, $part), 'size' => $part->size, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'group' => $eventid, ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); // store new attachment in session unset($attachment['status'], $attachment['abort'], $attachment['data']); $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' $event['attachments'][] = $attachment; } } } $this->rc->output->command('plugin.mail2event_dialog', $event); } else { $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); } $this->rc->output->send(); } /** * Handler for the 'message_compose' plugin hook. This will check for * a compose parameter 'calendar_event' and create an attachment with the * referenced event in iCal format */ public function mail_message_compose($args) { // set the submitted event ID as attachment if (!empty($args['param']['calendar_event'])) { $this->load_driver(); list($cal, $id) = explode(':', $args['param']['calendar_event'], 2); if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) { $filename = asciiwords($event['title']); if (empty($filename)) $filename = 'event'; // save ics to a temp file and register as attachment $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal'); file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body'))); $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar'); $args['param']['subject'] = $event['title']; } } return $args; } /** * Get a list of email addresses of the current user (from login and identities) */ public function get_user_emails() { return $this->lib->get_user_emails(); } /** * Build an absolute URL with the given parameters */ public function get_url($param = array()) { $param += array('task' => 'calendar'); return $this->rc->url($param, true, true); } public function ical_feed_hash($source) { return base64_encode($this->rc->user->get_username() . ':' . $source); } /** * Handler for user_delete plugin hook */ public function user_delete($args) { // delete itipinvitations entries related to this user $db = $this->rc->get_dbh(); $table_itipinvitations = $db->table_name('itipinvitations', true); $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID); $this->load_driver(); return $this->driver->user_delete($args); } /** * Magic getter for public access to protected members */ public function __get($name) { switch ($name) { case 'ical': return $this->get_ical(); case 'itip': return $this->load_itip(); case 'driver': $this->load_driver(); return $this->driver; } return null; } } diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 2912bdff..5478f355 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -1,881 +1,881 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof * Copyright (C) 2014, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class calendar_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.status_select', array($this, 'status_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->cal->lib, '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.resource_calendar', array($this, 'resource_calendar')); $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.object_changelog_table', array('libkolab', 'object_changelog_table')); $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'); } /** * 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->rc->output->include_script('treelist.js'); // include kolab folderlist widget if available if (in_array('libkolab', $this->cal->api->loaded_plugins())) { $this->cal->api->include_script('libkolab/js/folderlist.js'); $this->cal->api->include_script('libkolab/js/audittrail.js'); } jqueryui::miniColors(); } /** * */ 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; $css .= $this->calendar_css_classes($id, $prop, $mode); } return html::tag('style', array('type' => 'text/css'), $css); } /** * */ public function calendar_css_classes($id, $prop, $mode) { $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"; } return $css . ".$class .handle { background-color: #$color; }\n"; } /** * */ function calendar_list($attrib = array()) { $html = ''; $jsenv = array(); $tree = true; $calendars = $this->cal->driver->list_calendars(0, $tree); // walk folder tree if (is_object($tree)) { $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib); // append birthdays calendar which isn't part of $tree if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) { $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal); } else { $calendars = array(); // clear array for flat listing } } else { // fall-back to flat folder listing $attrib['class'] .= ' flat'; } foreach ((array)$calendars as $id => $prop) { if ($attrib['activeonly'] && !$prop['active']) continue; $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']), $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']) ); } $this->rc->output->set_env('calendars', $jsenv); $this->rc->output->add_gui_object('calendarslist', $attrib['id']); return html::tag('ul', $attrib, $html, html::$common_attrib); } /** * Return html for a structured list
    for the folder tree */ public function list_tree_html($node, $data, &$jsenv, $attrib) { $out = ''; foreach ($node->children as $folder) { $id = $folder->id; $prop = $data[$id]; $is_collapsed = false; // TODO: determine this somehow? $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']); if (!empty($folder->children)) { $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $this->list_tree_html($folder, $data, $jsenv, $attrib)); } if (strlen($content)) { $out .= html::tag('li', array( 'id' => 'rcmlical' . rcube_utils::html_identifier($id), 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''), ), $content); } } return $out; } /** * Helper method to build a calendar list item (HTML content and js data) */ public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false) { // enrich calendar properties with settings from the driver if (!$prop['virtual']) { unset($prop['user_id']); $prop['alarms'] = $this->cal->driver->alarms; $prop['attendees'] = $this->cal->driver->attendees; $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')); $jsenv[$id] = $prop; } $classes = array('calendar', 'cal-' . asciiwords($id, true)); $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ? - html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : ''); + html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : ''); if ($prop['virtual']) $classes[] = 'virtual'; else if (!$prop['editable']) $classes[] = 'readonly'; if ($prop['subscribed']) $classes[] = 'subscribed'; if ($prop['subscribed'] === 2) $classes[] = 'partial'; if ($prop['class']) $classes[] = $prop['class']; $content = ''; if (!$activeonly || $prop['active']) { $label_id = 'cl:' . $id; $content = html::div(join(' ', $classes), html::span(array('class' => 'calname', 'id' => $label_id, 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) . ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id), '') . html::span('actions', ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->cal->gettext('removelist')), ' ') : '') . html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->cal->gettext('quickview'), 'role' => 'checkbox', 'aria-checked' => 'false'), '') . (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '') ) . html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ') ) ); } return $content; } /** * */ 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->lib->gettext('days')), $days); foreach (array(2,5,7,14,30,60,90,180,365) as $days) $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->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' => 'libcalendaring.days', 'week' => 'libcalendaring.weeks', 'month' => 'libcalendaring.months', 'smart' => 'calendar.smartsections') as $val => $label) $select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->rc->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['editable'] || strpos($prop['rights'], 'i') !== false) $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_emails(); 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 status property */ function status_select($attrib = array()) { $attrib['name'] = 'status'; $select = new html_select($attrib); $select->add('---', ''); $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED'); $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED'); //$select->add($this->cal->gettext('tentative'), 'TENTATIVE'); 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'); // out-of-office is not supported by libkolabxml (#3220) // $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)); } /** * 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)))) + html::div('formbuttons', $button->show($this->rc->gettext('upload'), array('class' => 'button mainaction', + 'onclick' => rcmail_output::JS_OBJECT_NAME . ".upload_file(this.form)"))) . + html::div('hint', $this->rc->gettext(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()) { // add "noreply" checkbox to attendees table only $invitations = strpos($attrib['id'], 'attend') !== false; $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite')); $table = new html_table(array('cols' => 5 + intval($invitations), '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')); if ($invitations) { $table->add_header(array('class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')), $invite->show(1) . html::label('edit-attendees-invite', $this->cal->gettext('sendinvitations'))); } $table->add_header('options', ''); // hide invite column if disabled by config $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']); if ($invitations && !($itip_notify & 2)) { $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']); $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css)); } return $table->show($attrib); } /** * */ function attendees_form($attrib = array()) { $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30)); $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment', 'rows' => 4, 'cols' => 55, 'title' => $this->cal->gettext('itipcommenttitle'))); 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-commentbox', html::label(null, $this->cal->gettext('itipcomment') . $textarea->show())) ); } /** * */ 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'); // copy address book labels for owner details to client $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address'); $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); } /** * */ public function resource_calendar($attrib = array()) { $attrib += array('id' => 'calendar-resources-calendar'); $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']); return html::div($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); } /** * */ function event_invitebox($attrib = array()) { if ($this->cal->event) { return html::div($attrib, $this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation')) . $this->cal->invitestatus ); } return ''; } function event_rsvp_buttons($attrib = array()) { $actions = array('accepted','tentative','declined'); if ($attrib['delegate'] !== 'false') $actions[] = 'delegated'; return $this->cal->itip->itip_rsvp_buttons($attrib, $actions); } } diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php index 38ce9456..04f56d4f 100644 --- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php +++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php @@ -1,340 +1,340 @@ * * 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 kolab_addressbook_ui { private $plugin; private $rc; /** * Class constructor * * @param kolab_addressbook $plugin Plugin object */ public function __construct($plugin) { $this->rc = rcube::get_instance(); $this->plugin = $plugin; $this->init_ui(); } /** * Adds folders management functionality to Addressbook UI */ private function init_ui() { if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action) && $this->rc->action != 'show') { return; } // Include script $this->plugin->include_script('kolab_addressbook.js'); if (empty($this->rc->action)) { // Include stylesheet (for directorylist) $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); // include kolab folderlist widget if available if (in_array('libkolab', $this->plugin->api->loaded_plugins())) { $this->plugin->api->include_script('libkolab/js/folderlist.js'); } // Add actions on address books $options = array('book-create', 'book-edit', 'book-delete', 'book-remove'); $idx = 0; if ($this->rc->config->get('kolab_addressbook_carddav_url')) { $options[] = 'book-showurl'; $this->rc->output->set_env('kolab_addressbook_carddav_url', true); } foreach ($options as $command) { $content = html::tag('li', $idx ? null : array('class' => 'separator_above'), $this->plugin->api->output->button(array( 'label' => 'kolab_addressbook.'.str_replace('-', '', $command), 'domain' => $this->ID, 'classact' => 'active', 'command' => $command ))); $this->plugin->api->add_content($content, 'groupoptions'); $idx++; } // Link to Settings/Folders $content = html::tag('li', array('class' => 'separator_above'), $this->plugin->api->output->button(array( 'label' => 'managefolders', 'type' => 'link', 'classact' => 'active', 'command' => 'folders', 'task' => 'settings', ))); $this->plugin->api->add_content($content, 'groupoptions'); $this->rc->output->add_label('kolab_addressbook.bookdeleteconfirm', 'kolab_addressbook.bookdeleting', 'kolab_addressbook.bookshowurl', 'kolab_addressbook.carddavurldescription', 'kolab_addressbook.bookedit', 'kolab_addressbook.bookdelete', 'kolab_addressbook.bookshowurl', 'kolab_addressbook.findaddressbooks', 'kolab_addressbook.searchterms', 'kolab_addressbook.foldersearchform', 'kolab_addressbook.listsearchresults', 'kolab_addressbook.nraddressbooksfound', 'kolab_addressbook.noaddressbooksfound', 'kolab_addressbook.foldersubscribe', 'resetsearch'); if ($this->plugin->bonnie_api) { $this->rc->output->set_env('kolab_audit_trail', true); $this->plugin->api->include_script('libkolab/js/audittrail.js'); $this->rc->output->add_label( 'kolab_addressbook.showhistory', 'kolab_addressbook.objectchangelog', 'kolab_addressbook.objectdiff', 'kolab_addressbook.objectdiffnotavailable', 'kolab_addressbook.objectchangelognotavailable', 'kolab_addressbook.revisionrestoreconfirm' ); $this->plugin->add_hook('render_page', array($this, 'render_audittrail_page')); $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); } } // include stylesheet for audit trail else if ($this->rc->action == 'show' && $this->plugin->bonnie_api) { $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); $this->rc->output->add_label('kolab_addressbook.showhistory'); } // book create/edit form else { $this->rc->output->add_label('kolab_addressbook.nobooknamewarning', 'kolab_addressbook.booksaving'); } } /** * Handler for address book create/edit action */ public function book_edit() { $this->rc->output->add_handler('bookdetails', array($this, 'book_form')); $this->rc->output->send('kolab_addressbook.bookedit'); } /** * Handler for 'bookdetails' object returning form content for book create/edit * * @param array $attr Object attributes * * @return string HTML output */ public function book_form($attrib) { $action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC)); $folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true)); // UTF8 $hidden_fields[] = array('name' => '_source', 'value' => $folder); - $folder = rcube_charset::convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP'); + $folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP'); $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); if ($this->rc->action == 'plugin.book-save') { // save error $name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_GPC, true)); // UTF8 $old = trim(rcube_utils::get_input_value('_oldname', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP $path_imap = trim(rcube_utils::get_input_value('_parent', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP $hidden_fields[] = array('name' => '_oldname', 'value' => $old); $folder = $old; } else if ($action == 'edit') { $path_imap = explode($delim, $folder); $name = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP'); $path_imap = implode($path_imap, $delim); } else { // create $path_imap = $folder; $name = ''; $folder = ''; } // Store old name, get folder options if (strlen($folder)) { $hidden_fields[] = array('name' => '_oldname', 'value' => $folder); $options = $storage->folder_info($folder); } $form = array(); // General tab $form['props'] = array( 'name' => $this->rc->gettext('properties'), ); if (!empty($options) && ($options['norename'] || $options['protected'])) { $foldername = Q(str_replace($delim, ' » ', kolab_storage::object_name($folder))); } else { $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30)); $foldername = $foldername->show($name); } $form['props']['fieldsets']['location'] = array( 'name' => $this->rc->gettext('location'), 'content' => array( 'name' => array( 'label' => $this->plugin->gettext('bookname'), 'value' => $foldername, ), ), ); 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('contact', array('name' => '_parent'), $folder); $form['props']['fieldsets']['location']['content']['path'] = array( 'label' => $this->plugin->gettext('parentbook'), 'value' => $select->show(strlen($folder) ? $path_imap : ''), ); } // Allow plugins to modify address book form content (e.g. with ACL form) $plugin = $this->rc->plugins->exec_hook('addressbook_form', array('form' => $form, 'options' => $options, 'name' => $folder)); $form = $plugin['form']; // Set form tags and hidden fields list($form_start, $form_end) = $this->get_form_tags($attrib, 'plugin.book-save', null, $hidden_fields); unset($attrib['form']); // return the complete edit form as table $out = "$form_start\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) { $out .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n"; } } $out .= "\n$form_end"; return $out; } /** * */ public function render_audittrail_page($p) { // append audit trail UI elements to contact page if ($p['template'] === 'addressbook' && !$p['kolab-audittrail']) { $this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.audittrail', false, false)); $p['kolab-audittrail'] = true; } return $p; } private function get_form_part($form) { $content = ''; if (is_array($form['content']) && !empty($form['content'])) { $table = new html_table(array('cols' => 2, 'class' => 'propform')); 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; } private function get_form_tags($attrib, $action, $id = null, $hidden = null) { $form_start = $form_end = ''; $request_key = $action . (isset($id) ? '.'.$id : ''); $form_start = $this->rc->output->request_form(array( 'name' => 'form', 'method' => 'post', 'task' => $this->rc->task, 'action' => $action, 'request' => $request_key, 'noclose' => true, ) + $attrib); if (is_array($hidden)) { foreach ($hidden as $field) { $hiddenfield = new html_hiddenfield($field); $form_start .= $hiddenfield->show(); } } $form_end = !strlen($attrib['form']) ? '' : ''; $EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form'; $this->rc->output->add_gui_object('editform', $EDIT_FORM); return array($form_start, $form_end); } } diff --git a/plugins/kolab_files/lib/kolab_files_engine.php b/plugins/kolab_files/lib/kolab_files_engine.php index 36f93abf..f2a28d31 100644 --- a/plugins/kolab_files/lib/kolab_files_engine.php +++ b/plugins/kolab_files/lib/kolab_files_engine.php @@ -1,1180 +1,1185 @@ * * Copyright (C) 2013, 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 kolab_files_engine { private $plugin; private $rc; private $timeout = 600; private $sort_cols = array('name', 'mtime', 'size'); const API_VERSION = 2; /** * Class constructor */ public function __construct($plugin, $url) { $this->url = rcube_utils::resolve_url($url); $this->plugin = $plugin; $this->rc = $plugin->rc; $this->timeout = $this->rc->config->get('session_lifetime') * 60; } /** * User interface initialization */ public function ui() { $this->plugin->add_texts('localization/'); // set templates of Files UI and widgets if ($this->rc->task == 'mail') { if ($this->rc->action == 'compose') { $template = 'compose_plugin'; } else if (in_array($this->rc->action, array('show', 'preview', 'get'))) { $template = 'message_plugin'; if ($this->rc->action == 'get') { // add "Save as" button into attachment toolbar $this->plugin->add_button(array( 'id' => 'saveas', 'name' => 'saveas', 'type' => 'link', 'onclick' => 'kolab_directory_selector_dialog()', 'class' => 'button buttonPas saveas', 'classact' => 'button saveas', 'label' => 'kolab_files.save', 'title' => 'kolab_files.saveto', ), 'toolbar'); } else { // add "Save as" button into attachment menu $this->plugin->add_button(array( 'id' => 'attachmenusaveas', 'name' => 'attachmenusaveas', 'type' => 'link', 'wrapper' => 'li', 'onclick' => 'return false', 'class' => 'icon active saveas', 'classact' => 'icon active saveas', 'innerclass' => 'icon active saveas', 'label' => 'kolab_files.saveto', ), 'attachmentmenu'); } } $this->plugin->add_label('save', 'cancel', 'saveto', 'saveall', 'fromcloud', 'attachsel', 'selectfiles', 'attaching', 'collection_audio', 'collection_video', 'collection_image', 'collection_document', 'folderauthtitle', 'authenticating' ); } else if ($this->rc->task == 'files') { $template = 'files'; // get list of external sources $this->get_external_storage_drivers(); } // add taskbar button if (empty($_REQUEST['framed'])) { $this->plugin->add_button(array( 'command' => 'files', 'class' => 'button-files', 'classsel' => 'button-files button-selected', 'innerclass' => 'button-inner', 'label' => 'kolab_files.files', ), 'taskbar'); } $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/style.css'); if (!empty($template)) { $this->plugin->include_script($this->url . '/js/files_api.js'); $this->plugin->include_script('kolab_files.js'); $this->rc->output->set_env('files_url', $this->url . '/api/'); $this->rc->output->set_env('files_token', $this->get_api_token()); // register template objects for dialogs (and main interface) $this->rc->output->add_handlers(array( 'folder-create-form' => array($this, 'folder_create_form'), 'folder-edit-form' => array($this, 'folder_edit_form'), 'folder-mount-form' => array($this, 'folder_mount_form'), 'folder-auth-options'=> array($this, 'folder_auth_options'), 'file-search-form' => array($this, 'file_search_form'), 'file-edit-form' => array($this, 'file_edit_form'), 'filelist' => array($this, 'file_list'), 'filequotadisplay' => array($this, 'quota_display'), )); if ($this->rc->task != 'files') { // add dialog content at the end of page body $this->rc->output->add_footer( $this->rc->output->parse('kolab_files.' . $template, false, false)); } } } /** * Engine actions handler */ public function actions() { if ($this->rc->task == 'files' && $this->rc->action) { $action = $this->rc->action; } else if ($this->rc->task != 'files' && $_POST['act']) { $action = $_POST['act']; } else { $action = 'index'; } $method = 'action_' . str_replace('-', '_', $action); if (method_exists($this, $method)) { $this->plugin->add_texts('localization/'); $this->{$method}(); } } /** * Template object for folder creation form */ public function folder_create_form($attrib) { $attrib['name'] = 'folder-create-form'; if (empty($attrib['id'])) { $attrib['id'] = 'folder-create-form'; } $input_name = new html_inputfield(array('id' => 'folder-name', 'name' => 'name', 'size' => 30)); $select_parent = new html_select(array('id' => 'folder-parent', 'name' => 'parent')); $table = new html_table(array('cols' => 2, 'class' => 'propform')); $table->add('title', html::label('folder-name', rcube::Q($this->plugin->gettext('foldername')))); $table->add(null, $input_name->show()); $table->add('title', html::label('folder-parent', rcube::Q($this->plugin->gettext('folderinside')))); $table->add(null, $select_parent->show()); $out = $table->show(); // add form tag around text field if (empty($attrib['form'])) { $out = $this->rc->output->form_tag($attrib, $out); } $this->plugin->add_label('foldercreating', 'foldercreatenotice', 'create', 'foldercreate', 'cancel'); $this->rc->output->add_gui_object('folder-create-form', $attrib['id']); return $out; } /** * Template object for folder editing form */ public function folder_edit_form($attrib) { $attrib['name'] = 'folder-edit-form'; if (empty($attrib['id'])) { $attrib['id'] = 'folder-edit-form'; } $input_name = new html_inputfield(array('id' => 'folder-edit-name', 'name' => 'name', 'size' => 30)); $select_parent = new html_select(array('id' => 'folder-edit-parent', 'name' => 'parent')); $table = new html_table(array('cols' => 2, 'class' => 'propform')); $table->add('title', html::label('folder-name', rcube::Q($this->plugin->gettext('foldername')))); $table->add(null, $input_name->show()); $table->add('title', html::label('folder-parent', rcube::Q($this->plugin->gettext('folderinside')))); $table->add(null, $select_parent->show()); $out = $table->show(); // add form tag around text field if (empty($attrib['form'])) { $out = $this->rc->output->form_tag($attrib, $out); } $this->plugin->add_label('folderupdating', 'folderupdatenotice', 'save', 'folderedit', 'cancel'); $this->rc->output->add_gui_object('folder-edit-form', $attrib['id']); return $out; } /** * Template object for folder mounting form */ public function folder_mount_form($attrib) { $sources = $this->rc->output->get_env('external_sources'); if (empty($sources) || !is_array($sources)) { return ''; } $attrib['name'] = 'folder-mount-form'; if (empty($attrib['id'])) { $attrib['id'] = 'folder-mount-form'; } // build form content $table = new html_table(array('cols' => 2, 'class' => 'propform')); $input_name = new html_inputfield(array('id' => 'folder-mount-name', 'name' => 'name', 'size' => 30)); $input_driver = new html_radiobutton(array('name' => 'driver', 'size' => 30)); $table->add('title', html::label('folder-mount-name', rcube::Q($this->plugin->gettext('name')))); $table->add(null, $input_name->show()); foreach ($sources as $key => $source) { $id = 'source-' . $key; $form = new html_table(array('cols' => 2, 'class' => 'propform driverform')); foreach ((array) $source['form'] as $idx => $label) { $iid = $id . '-' . $idx; $type = stripos($idx, 'pass') !== false ? 'html_passwordfield' : 'html_inputfield'; $input = new $type(array('size' => 30)); $form->add('title', html::label($iid, rcube::Q($label))); $form->add(null, $input->show('', array( 'id' => $iid, 'name' => $key . '[' . $idx . ']' ))); } $row = $input_driver->show(null, array('value' => $key)) . html::img(array('src' => $source['image'], 'alt' => $key, 'title' => $source['name'])) . html::div(null, html::span('name', rcube::Q($source['name'])) . html::br() . html::span('description', rcube::Q($source['description'])) . $form->show() ); $table->add(array('id' => $id, 'colspan' => 2, 'class' => 'source'), $row); } $out = $table->show() . $this->folder_auth_options(array('suffix' => '-form')); // add form tag around text field if (empty($attrib['form'])) { $out = $this->rc->output->form_tag($attrib, $out); } $this->plugin->add_label('foldermounting', 'foldermountnotice', 'foldermount', 'save', 'cancel', 'folderauthtitle', 'authenticating' ); $this->rc->output->add_gui_object('folder-mount-form', $attrib['id']); return $out; } /** * Template object for folder authentication options */ public function folder_auth_options($attrib) { $checkbox = new html_checkbox(array( 'name' => 'store_passwords', 'value' => '1', 'id' => 'auth-pass-checkbox' . $attrib['suffix'], )); return html::div('auth-options', $checkbox->show(). ' ' . html::label('auth-pass-checkbox' . $attrib['suffix'], $this->plugin->gettext('storepasswords')) . html::span('description', $this->plugin->gettext('storepasswordsdesc')) ); } /** * Template object for file_edit form */ public function file_edit_form($attrib) { $attrib['name'] = 'file-edit-form'; if (empty($attrib['id'])) { $attrib['id'] = 'file-edit-form'; } $input_name = new html_inputfield(array('id' => 'file-name', 'name' => 'name', 'size' => 30)); $table = new html_table(array('cols' => 2, 'class' => 'propform')); $table->add('title', html::label('file-name', rcube::Q($this->plugin->gettext('filename')))); $table->add(null, $input_name->show()); $out = $table->show(); // add form tag around text field if (empty($attrib['form'])) { $out = $this->rc->output->form_tag($attrib, $out); } $this->plugin->add_label('save', 'cancel', 'fileupdating', 'fileedit'); $this->rc->output->add_gui_object('file-edit-form', $attrib['id']); return $out; } /** * Template object for file search form in "From cloud" dialog */ public function file_search_form($attrib) { $attrib['name'] = '_q'; if (empty($attrib['id'])) { $attrib['id'] = 'filesearchbox'; } if ($attrib['type'] == 'search' && !$this->rc->output->browser->khtml) { unset($attrib['type'], $attrib['results']); } $input_q = new html_inputfield($attrib); $out = $input_q->show(); // add some labels to client $this->rc->output->add_label('searching'); $this->rc->output->add_gui_object('filesearchbox', $attrib['id']); // add form tag around text field if (empty($attrib['form'])) { $out = $this->rc->output->form_tag(array( 'action' => '?_task=files', 'name' => "filesearchform", 'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('files-search'); return false", ), $out); } return $out; } /** * Template object for files list */ public function file_list($attrib) { // define list of cols to be displayed based on parameter or config if (empty($attrib['columns'])) { $list_cols = $this->rc->config->get('kolab_files_list_cols'); $dont_override = $this->rc->config->get('dont_override'); $a_show_cols = is_array($list_cols) ? $list_cols : array('name'); $this->rc->output->set_env('col_movable', !in_array('kolab_files_list_cols', (array)$dont_override)); } else { $a_show_cols = preg_split('/[\s,;]+/', strip_quotes($attrib['columns'])); } // make sure 'name' and 'options' column is present if (!in_array('name', $a_show_cols)) { array_unshift($a_show_cols, 'name'); } if (!in_array('options', $a_show_cols)) { array_unshift($a_show_cols, 'options'); } $attrib['columns'] = $a_show_cols; // save some variables for use in ajax list $_SESSION['kolab_files_list_attrib'] = $attrib; // For list in dialog(s) remove all option-like columns if ($this->rc->task != 'files') { $a_show_cols = array_intersect($a_show_cols, $this->sort_cols); } // set default sort col/order to session if (!isset($_SESSION['kolab_files_sort_col'])) $_SESSION['kolab_files_sort_col'] = $this->rc->config->get('kolab_files_sort_col') ?: 'name'; if (!isset($_SESSION['kolab_files_sort_order'])) $_SESSION['kolab_files_sort_order'] = strtoupper($this->rc->config->get('kolab_files_sort_order') ?: 'asc'); // set client env $this->rc->output->add_gui_object('filelist', $attrib['id']); $this->rc->output->set_env('sort_col', $_SESSION['kolab_files_sort_col']); $this->rc->output->set_env('sort_order', $_SESSION['kolab_files_sort_order']); $this->rc->output->set_env('coltypes', $a_show_cols); $this->rc->output->set_env('search_threads', $this->rc->config->get('kolab_files_search_threads')); $this->rc->output->include_script('list.js'); // attach css rules for mimetype icons $this->plugin->include_stylesheet($this->url . '/skins/default/images/mimetypes/style.css'); $thead = ''; foreach ($this->file_list_head($attrib, $a_show_cols) as $cell) { $thead .= html::tag('th', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']); } return html::tag('table', $attrib, html::tag('thead', null, html::tag('tr', null, $thead)) . html::tag('tbody', null, ''), array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary')); } /** * Creates for message list table */ protected function file_list_head($attrib, $a_show_cols) { $skin_path = $_SESSION['skin_path']; // $image_tag = html::img(array('src' => "%s%s", 'alt' => "%s")); // check to see if we have some settings for sorting $sort_col = $_SESSION['kolab_files_sort_col']; $sort_order = $_SESSION['kolab_files_sort_order']; $dont_override = (array)$this->rc->config->get('dont_override'); $disabled_sort = in_array('message_sort_col', $dont_override); $disabled_order = in_array('message_sort_order', $dont_override); $this->rc->output->set_env('disabled_sort_col', $disabled_sort); $this->rc->output->set_env('disabled_sort_order', $disabled_order); // define sortable columns if ($disabled_sort) $a_sort_cols = $sort_col && !$disabled_order ? array($sort_col) : array(); else $a_sort_cols = $this->sort_cols; if (!empty($attrib['optionsmenuicon'])) { - $onclick = 'return ' . JS_OBJECT_NAME . ".command('menu-open', 'filelistmenu', this, event)"; + $onclick = 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('menu-open', 'filelistmenu', this, event)"; $inner = $this->rc->gettext('listoptions'); if (is_string($attrib['optionsmenuicon']) && $attrib['optionsmenuicon'] != 'true') { $inner = html::img(array('src' => $skin_path . $attrib['optionsmenuicon'], 'alt' => $this->rc->gettext('listoptions'))); } $list_menu = html::a(array( 'href' => '#list-options', 'onclick' => $onclick, 'class' => 'listmenu', 'id' => 'listmenulink', 'title' => $this->rc->gettext('listoptions'), 'tabindex' => '0', ), $inner); } else { $list_menu = ''; } $cells = array(); foreach ($a_show_cols as $col) { // get column name switch ($col) { /* case 'status': $col_name = ' '; break; */ case 'options': $col_name = $list_menu; break; default: $col_name = rcube::Q($this->plugin->gettext($col)); } // make sort links - if (in_array($col, $a_sort_cols)) - $col_name = html::a(array('href'=>"#sort", 'onclick' => 'return '.JS_OBJECT_NAME.".command('files-sort','".$col."',this)", 'title' => rcube_label('sortby')), $col_name); + if (in_array($col, $a_sort_cols)) { + $col_name = html::a(array( + 'href' => "#sort", + 'onclick' => 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('files-sort','$col',this)", + 'title' => $this->plugin->gettext('sortby') + ), $col_name); + } else if ($col_name[0] != '<') $col_name = '' . $col_name . ''; $sort_class = $col == $sort_col && !$disabled_order ? " sorted$sort_order" : ''; $class_name = $col.$sort_class; // put it all together $cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name); } return $cells; } /** * Update files list object */ protected function file_list_update($prefs) { $attrib = $_SESSION['kolab_files_list_attrib']; if (!empty($prefs['kolab_files_list_cols'])) { $attrib['columns'] = $prefs['kolab_files_list_cols']; $_SESSION['kolab_files_list_attrib'] = $attrib; } $a_show_cols = $attrib['columns']; $head = ''; foreach ($this->file_list_head($attrib, $a_show_cols) as $cell) { $head .= html::tag('td', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']); } $head = html::tag('tr', null, $head); $this->rc->output->set_env('coltypes', $a_show_cols); $this->rc->output->command('files_list_update', $head); } /** * Template object for file info box */ public function file_info_box($attrib) { // print_r($this->file_data, true); $table = new html_table(array('cols' => 2, 'class' => $attrib['class'])); // file name $table->add('label', $this->plugin->gettext('name').':'); $table->add('data filename', $this->file_data['name']); // file type // @TODO: human-readable type name $table->add('label', $this->plugin->gettext('type').':'); $table->add('data filetype', $this->file_data['type']); // file size $table->add('label', $this->plugin->gettext('size').':'); $table->add('data filesize', $this->rc->show_bytes($this->file_data['size'])); // file modification time $table->add('label', $this->plugin->gettext('mtime').':'); $table->add('data filemtime', $this->file_data['mtime']); // @TODO: for images: width, height, color depth, etc. // @TODO: for text files: count of characters, lines, words return $table->show(); } /** * Template object for file preview frame */ public function file_preview_frame($attrib) { if (empty($attrib['id'])) { $attrib['id'] = 'filepreviewframe'; } if ($frame = $this->file_data['viewer']['frame']) { return $frame; } if ($href = $this->file_data['viewer']['href']) { // file href attribute must be an absolute URL (Bug #2063) if (!empty($href)) { if (!preg_match('|^https?://|', $href)) { $href = $this->url . '/api/' . $href; } } } else { $token = $this->get_api_token(); $href = $this->url . '/api/?method=file_get' . '&file=' . urlencode($this->file_data['filename']) . '&token=' . urlencode($token); } $this->rc->output->add_gui_object('preview_frame', $attrib['id']); $attrib['allowfullscreen'] = true; $attrib['src'] = $href; $attrib['onload'] = 'kolab_files_frame_load(this)'; return html::iframe($attrib); } /** * Template object for quota display */ public function quota_display($attrib) { if (!$attrib['id']) { $attrib['id'] = 'rcmquotadisplay'; } $quota_type = !empty($attrib['display']) ? $attrib['display'] : 'text'; $this->rc->output->add_gui_object('quotadisplay', $attrib['id']); $this->rc->output->set_env('quota_type', $quota_type); // get quota $token = $this->get_api_token(); $request = $this->get_request(array('method' => 'quota'), $token); // send request to the API try { $response = $request->send(); $status = $response->getStatus(); $body = @json_decode($response->getBody(), true); if ($status == 200 && $body['status'] == 'OK') { $quota = $body['result']; } else { throw new Exception($body['reason']); } } catch (Exception $e) { rcube::raise_error($e, true, false); $quota = array('total' => 0, 'percent' => 0); } $quota = rcube_output::json_serialize($quota); $this->rc->output->add_script(rcmail_output::JS_OBJECT_NAME . ".files_set_quota($quota);", 'docready'); return html::span($attrib, ''); } /** * Get API token for current user session, authenticate if needed */ public function get_api_token() { $token = $_SESSION['kolab_files_token']; $time = $_SESSION['kolab_files_time']; if ($token && time() - $this->timeout < $time) { if (time() - $time <= $this->timeout / 2) { return $token; } } $request = $this->get_request(array('method' => 'ping'), $token); try { $url = $request->getUrl(); // Send ping request if ($token) { $url->setQueryVariables(array('method' => 'ping')); $request->setUrl($url); $response = $request->send(); $status = $response->getStatus(); if ($status == 200 && ($body = json_decode($response->getBody(), true))) { if ($body['status'] == 'OK') { $_SESSION['kolab_files_time'] = time(); return $token; } } } // Go with authenticate request $url->setQueryVariables(array('method' => 'authenticate', 'version' => self::API_VERSION)); $request->setUrl($url); $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password'])); $response = $request->send(); $status = $response->getStatus(); if ($status == 200 && ($body = json_decode($response->getBody(), true))) { $token = $body['result']['token']; if ($token) { $_SESSION['kolab_files_token'] = $token; $_SESSION['kolab_files_time'] = time(); $_SESSION['kolab_files_caps'] = $body['result']['capabilities']; } } else { throw new Exception(sprintf("Authenticate error (Status: %d)", $status)); } } catch (Exception $e) { rcube::raise_error($e, true, false); } return $token; } /** * Initialize HTTP_Request object */ protected function get_request($get = null, $token = null) { $url = $this->url . '/api/'; if (!$this->request) { $config = array( 'store_body' => true, 'follow_redirects' => true, ); $this->request = libkolab::http_request($url, 'GET', $config); } else { // cleanup try { $this->request->setBody(''); $this->request->setUrl($url); $this->request->setMethod(HTTP_Request2::METHOD_GET); } catch (Exception $e) { rcube::raise_error($e, true, true); } } if ($token) { $this->request->setHeader('X-Session-Token', $token); } if (!empty($get)) { $url = $this->request->getUrl(); $url->setQueryVariables($get); $this->request->setUrl($url); } // some HTTP server configurations require this header $this->request->setHeader('accept', "application/json,text/javascript,*/*"); return $this->request; } /** * Handler for main files interface (Files task) */ protected function action_index() { $this->plugin->add_label( 'folderdeleting', 'folderdeleteconfirm', 'folderdeletenotice', 'uploading', 'attaching', 'uploadsizeerror', 'filedeleting', 'filedeletenotice', 'filedeleteconfirm', 'filemoving', 'filemovenotice', 'filemoveconfirm', 'filecopying', 'filecopynotice', 'collection_audio', 'collection_video', 'collection_image', 'collection_document', 'fileskip', 'fileskipall', 'fileoverwrite', 'fileoverwriteall' ); $this->rc->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B'); $this->rc->output->set_pagetitle($this->plugin->gettext('files')); $this->rc->output->set_env('file_mimetypes', $this->get_mimetypes()); $this->rc->output->set_env('files_quota', $_SESSION['kolab_files_caps']['QUOTA']); $this->rc->output->set_env('files_max_upload', $_SESSION['kolab_files_caps']['MAX_UPLOAD']); $this->rc->output->set_env('files_progress_name', $_SESSION['kolab_files_caps']['PROGRESS_NAME']); $this->rc->output->set_env('files_progress_time', $_SESSION['kolab_files_caps']['PROGRESS_TIME']); $this->rc->output->send('kolab_files.files'); } /** * Handler for preferences save action */ protected function action_prefs() { $dont_override = (array)$this->rc->config->get('dont_override'); $prefs = array(); $opts = array( 'kolab_files_sort_col' => true, 'kolab_files_sort_order' => true, 'kolab_files_list_cols' => false, ); foreach ($opts as $o => $sess) { if (isset($_POST[$o]) && !in_array($o, $dont_override)) { $prefs[$o] = rcube_utils::get_input_value($o, rcube_utils::INPUT_POST); if ($sess) { $_SESSION[$o] = $prefs[$o]; } if ($o == 'kolab_files_list_cols') { $update_list = true; } } } // save preference values if (!empty($prefs)) { $this->rc->user->save_prefs($prefs); } if (!empty($update_list)) { $this->file_list_update($prefs); } $this->rc->output->send(); } /** * Handler for file open action */ protected function action_open() { $file = rcube_utils::get_input_value('file', rcube_utils::INPUT_GET); // get file info $token = $this->get_api_token(); $request = $this->get_request(array( 'method' => 'file_info', 'file' => $file, 'viewer' => !empty($_GET['viewer']), ), $token); // send request to the API try { $response = $request->send(); $status = $response->getStatus(); $body = @json_decode($response->getBody(), true); if ($status == 200 && $body['status'] == 'OK') { $this->file_data = $body['result']; } else { throw new Exception($body['reason']); } } catch (Exception $e) { rcube::raise_error(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, true); } $this->file_data['filename'] = $file; $this->plugin->add_label('filedeleteconfirm', 'filedeleting', 'filedeletenotice'); // register template objects for dialogs (and main interface) $this->rc->output->add_handlers(array( 'fileinfobox' => array($this, 'file_info_box'), 'filepreviewframe' => array($this, 'file_preview_frame'), )); // this one is for styling purpose $this->rc->output->set_env('extwin', true); $this->rc->output->set_env('file', $file); $this->rc->output->set_env('file_data', $this->file_data); $this->rc->output->set_pagetitle(rcube::Q($file)); $this->rc->output->send('kolab_files.filepreview'); } /** * Handler for "save all attachments into cloud" action */ protected function action_save_file() { // $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); $dest = rcube_utils::get_input_value('dest', rcube_utils::INPUT_POST); $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST); $name = rcube_utils::get_input_value('name', rcube_utils::INPUT_POST); $temp_dir = unslashify($this->rc->config->get('temp_dir')); $message = new rcube_message($uid); $request = $this->get_request(); $url = $request->getUrl(); $files = array(); $errors = array(); $attachments = array(); $request->setMethod(HTTP_Request2::METHOD_POST); $request->setHeader('X-Session-Token', $this->get_api_token()); $url->setQueryVariables(array('method' => 'file_upload', 'folder' => $dest)); $request->setUrl($url); foreach ($message->attachments as $attach_prop) { if (empty($id) || $id == $attach_prop->mime_id) { $filename = strlen($name) ? $name : rcmail_attachment_name($attach_prop, true); $attachments[$filename] = $attach_prop; } } // @TODO: handle error // @TODO: implement file upload using file URI instead of body upload foreach ($attachments as $attach_name => $attach_prop) { $path = tempnam($temp_dir, 'rcmAttmnt'); // save attachment to file if ($fp = fopen($path, 'w+')) { $message->get_part_body($attach_prop->mime_id, false, 0, $fp); } else { $errors[] = true; rcube::raise_error(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => "Unable to save attachment into file $path"), true, false); continue; } fclose($fp); // send request to the API try { $request->setBody(''); $request->addUpload('file[]', $path, $attach_name, $attach_prop->mimetype); $response = $request->send(); $status = $response->getStatus(); $body = @json_decode($response->getBody(), true); if ($status == 200 && $body['status'] == 'OK') { $files[] = $attach_name; } else { throw new Exception($body['reason']); } } catch (Exception $e) { unlink($path); $errors[] = $e->getMessage(); rcube::raise_error(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, false); continue; } // clean up unlink($path); $request->setBody(''); } if ($count = count($files)) { $msg = $this->plugin->gettext(array('name' => 'saveallnotice', 'vars' => array('n' => $count))); $this->rc->output->show_message($msg, 'confirmation'); } if ($count = count($errors)) { $msg = $this->plugin->gettext(array('name' => 'saveallerror', 'vars' => array('n' => $count))); $this->rc->output->show_message($msg, 'error'); } // @TODO: update quota indicator, make this optional in case files aren't stored in IMAP $this->rc->output->send(); } /** * Handler for "add attachments from the cloud" action */ protected function action_attach_file() { $files = rcube_utils::get_input_value('files', rcube_utils::INPUT_POST); $uploadid = rcube_utils::get_input_value('uploadid', rcube_utils::INPUT_POST); $COMPOSE_ID = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST); $COMPOSE = null; $errors = array(); if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) { $COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID]; } if (!$COMPOSE) { die("Invalid session var!"); } // attachment upload action if (!is_array($COMPOSE['attachments'])) { $COMPOSE['attachments'] = array(); } // clear all stored output properties (like scripts and env vars) $this->rc->output->reset(); $temp_dir = unslashify($this->rc->config->get('temp_dir')); $request = $this->get_request(); $url = $request->getUrl(); // Use observer object to store HTTP response into a file require_once $this->plugin->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_files_observer.php'; $observer = new kolab_files_observer(); $request->setHeader('X-Session-Token', $this->get_api_token()); // download files from the API and attach them foreach ($files as $file) { // decode filename $file = urldecode($file); // get file information try { $url->setQueryVariables(array('method' => 'file_info', 'file' => $file)); $request->setUrl($url); $response = $request->send(); $status = $response->getStatus(); $body = @json_decode($response->getBody(), true); if ($status == 200 && $body['status'] == 'OK') { $file_params = $body['result']; } else { throw new Exception($body['reason']); } } catch (Exception $e) { $errors[] = $e->getMessage(); rcube::raise_error(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, false); continue; } // set location of downloaded file $path = tempnam($temp_dir, 'rcmAttmnt'); $observer->set_file($path); // download file try { $url->setQueryVariables(array('method' => 'file_get', 'file' => $file)); $request->setUrl($url); $request->attach($observer); $response = $request->send(); $status = $response->getStatus(); $response->getBody(); // returns nothing $request->detach($observer); if ($status != 200 || !file_exists($path)) { throw new Exception("Unable to save file"); } } catch (Exception $e) { $errors[] = $e->getMessage(); rcube::raise_error(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, false); continue; } $attachment = array( 'path' => $path, 'size' => $file_params['size'], 'name' => $file_params['name'], 'mimetype' => $file_params['type'], 'group' => $COMPOSE_ID, ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; // store new attachment in session unset($attachment['data'], $attachment['status'], $attachment['abort']); $COMPOSE['attachments'][$id] = $attachment; if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) { $button = html::img(array( 'src' => $icon, 'alt' => $this->rc->gettext('delete') )); } else { $button = rcube::Q($this->rc->gettext('delete')); } $content = html::a(array( 'href' => "#delete", - 'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", JS_OBJECT_NAME, $id), + 'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", rcmail_output::JS_OBJECT_NAME, $id), 'title' => $this->rc->gettext('delete'), 'class' => 'delete', ), $button); $content .= rcube::Q($attachment['name']); $this->rc->output->command('add2attachment_list', "rcmfile$id", array( 'html' => $content, 'name' => $attachment['name'], 'mimetype' => $attachment['mimetype'], 'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']), 'complete' => true), $uploadid); } else if ($attachment['error']) { $errors[] = $attachment['error']; } else { $errors[] = $this->plugin->gettext('attacherror'); } } if (!empty($errors)) { $this->rc->output->command('display_message', $this->plugin->gettext('attacherror'), 'error'); $this->rc->output->command('remove_from_attachment_list', $uploadid); } // send html page with JS calls as response $this->rc->output->command('auto_save_start', false); $this->rc->output->send(); } /** * Returns mimetypes supported by File API viewers */ protected function get_mimetypes() { $token = $this->get_api_token(); $request = $this->get_request(array('method' => 'mimetypes'), $token); // send request to the API try { $response = $request->send(); $status = $response->getStatus(); $body = @json_decode($response->getBody(), true); if ($status == 200 && $body['status'] == 'OK') { $mimetypes = $body['result']; } else { throw new Exception($body['reason']); } } catch (Exception $e) { rcube::raise_error(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, false); } return $mimetypes; } /** * Get list of available external storage drivers */ protected function get_external_storage_drivers() { // first get configured sources from Chwala $token = $this->get_api_token(); $request = $this->get_request(array('method' => 'folder_types'), $token); // send request to the API try { $response = $request->send(); $status = $response->getStatus(); $body = @json_decode($response->getBody(), true); if ($status == 200 && $body['status'] == 'OK') { $sources = $body['result']; } else { throw new Exception($body['reason']); } } catch (Exception $e) { rcube::raise_error($e, true, false); return; } $this->rc->output->set_env('external_sources', $sources); } } diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php index 080a586c..394d138a 100644 --- a/plugins/kolab_notes/kolab_notes_ui.php +++ b/plugins/kolab_notes/kolab_notes_ui.php @@ -1,439 +1,439 @@ plugin = $plugin; $this->rc = $plugin->rc; } /** * Calendar UI initialization and requests handlers */ public function init() { if ($this->ready) // already done return; // add taskbar button $this->plugin->add_button(array( 'command' => 'notes', 'class' => 'button-notes', 'classsel' => 'button-notes button-selected', 'innerclass' => 'button-inner', 'label' => 'kolab_notes.navtitle', ), 'taskbar'); $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/notes.css'); $this->plugin->register_action('print', array($this, 'print_template')); $this->plugin->register_action('folder-acl', array($this, 'folder_acl')); $this->ready = true; } /** * Register handler methods for the template engine */ public function init_templates() { $this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist')); $this->plugin->register_handler('plugin.notebooks', array($this, 'folders')); #$this->plugin->register_handler('plugin.folders_select', array($this, 'folders_select')); $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); $this->plugin->register_handler('plugin.listing', array($this, 'listing')); $this->plugin->register_handler('plugin.editform', array($this, 'editform')); $this->plugin->register_handler('plugin.notetitle', array($this, 'notetitle')); $this->plugin->register_handler('plugin.detailview', array($this, 'detailview')); $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list')); $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); $this->rc->output->include_script('list.js'); $this->rc->output->include_script('treelist.js'); $this->plugin->include_script('notes.js'); jqueryui::tagedit(); // include kolab folderlist widget if available if (in_array('libkolab', $this->plugin->api->loaded_plugins())) { $this->plugin->api->include_script('libkolab/js/folderlist.js'); $this->plugin->api->include_script('libkolab/js/audittrail.js'); } // load config options and user prefs relevant for the UI $settings = array( 'sort_col' => $this->rc->config->get('kolab_notes_sort_col', 'changed'), 'print_template' => $this->rc->url('print'), ); if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) { $settings['selected_list'] = $list; } if ($uid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC)) { $settings['selected_uid'] = $uid; } $lang_codes = array($_SESSION['language']); if ($pos = strpos($_SESSION['language'], '_')) { $lang_codes[] = substr($_SESSION['language'], 0, $pos); } foreach ($lang_codes as $code) { if (file_exists(INSTALL_PATH . "program/js/tinymce/langs/$code.js")) { $lang = $code; break; } } if (empty($lang)) { $lang = 'en'; } $settings['editor'] = array( 'lang' => $lang, 'spellcheck' => intval($this->rc->config->get('enable_spellcheck')), 'spelldict' => intval($this->rc->config->get('spellcheck_dictionary')) ); $this->rc->output->set_env('kolab_notes_settings', $settings); $this->rc->output->add_label('save','cancel','delete','close'); } public function folders($attrib) { $attrib += array('id' => 'rcmkolabnotebooks'); if ($attrib['type'] == 'select') { $attrib['is_escaped'] = true; $select = new html_select($attrib); } $tree = $attrib['type'] != 'select' ? true : null; $lists = $this->plugin->get_lists($tree); $jsenv = array(); if (is_object($tree)) { $html = $this->folder_tree_html($tree, $lists, $jsenv, $attrib); } else { $html = ''; foreach ($lists as $prop) { $id = $prop['id']; if (!$prop['virtual']) { unset($prop['user_id']); $jsenv[$id] = $prop; } if ($attrib['type'] == 'select') { if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) { $select->add($prop['name'], $prop['id']); } } else { $html .= html::tag('li', array('id' => 'rcmliknb' . rcube_utils::html_identifier($id), 'class' => $prop['group']), $this->folder_list_item($id, $prop, $jsenv) ); } } } $this->rc->output->set_env('kolab_notebooks', $jsenv); $this->rc->output->add_gui_object('notebooks', $attrib['id']); return $attrib['type'] == 'select' ? $select->show() : html::tag('ul', $attrib, $html, html::$common_attrib); } /** * Return html for a structured list
      for the folder tree */ public function folder_tree_html($node, $data, &$jsenv, $attrib) { $out = ''; foreach ($node->children as $folder) { $id = $folder->id; $prop = $data[$id]; $is_collapsed = false; // TODO: determine this somehow? $content = $this->folder_list_item($id, $prop, $jsenv); if (!empty($folder->children)) { $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $this->folder_tree_html($folder, $data, $jsenv, $attrib)); } if (strlen($content)) { $out .= html::tag('li', array( 'id' => 'rcmliknb' . rcube_utils::html_identifier($id), 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''), ), $content); } } return $out; } /** * Helper method to build a tasklist item (HTML content and js data) */ public function folder_list_item($id, $prop, &$jsenv, $checkbox = false) { if (!$prop['virtual']) { unset($prop['user_id']); $jsenv[$id] = $prop; } $classes = array('folder'); if ($prop['virtual']) { $classes[] = 'virtual'; } else if (!$prop['editable']) { $classes[] = 'readonly'; } if ($prop['subscribed']) { $classes[] = 'subscribed'; } if ($prop['class']) { $classes[] = $prop['class']; } $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ? - html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : ''); + html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : ''); $label_id = 'nl:' . $id; $attr = $prop['virtual'] ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id))); return html::div(join(' ', $classes), html::a($attr + array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) . ($prop['virtual'] ? '' : ($checkbox ? html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) : '' ) . html::span('handle', '') . html::span('actions', (!$prop['default'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '' ) . (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('foldersubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '' ) ) ) ); return ''; } public function listing($attrib) { $attrib += array('id' => 'rcmkolabnoteslist'); $this->rc->output->add_gui_object('noteslist', $attrib['id']); return html::tag('ul', $attrib, '', html::$common_attrib); } public function tagslist($attrib) { $attrib += array('id' => 'rcmkolabnotestagslist'); $this->rc->output->add_gui_object('notestagslist', $attrib['id']); return html::tag('ul', $attrib, '', html::$common_attrib); } public function editform($attrib) { $attrib += array('action' => '#', 'id' => 'rcmkolabnoteseditform'); $this->rc->output->add_gui_object('noteseditform', $attrib['id']); $this->rc->output->include_script('tinymce/tinymce.min.js'); $textarea = new html_textarea(array('name' => 'content', 'id' => 'notecontent', 'cols' => 60, 'rows' => 20, 'tabindex' => 0)); return html::tag('form', $attrib, $textarea->show(), array_merge(html::$common_attrib, array('action'))); } public function detailview($attrib) { $attrib += array('id' => 'rcmkolabnotesdetailview'); $this->rc->output->add_gui_object('notesdetailview', $attrib['id']); return html::div($attrib, ''); } public function notetitle($attrib) { $attrib += array('id' => 'rcmkolabnotestitle'); $this->rc->output->add_gui_object('noteviewtitle', $attrib['id']); $summary = new html_inputfield(array('name' => 'summary', 'class' => 'notetitle inline-edit', 'size' => 60, 'tabindex' => 0)); $html = $summary->show(); $html .= html::div(array('class' => 'tagline tagedit', 'style' => 'display:none'), ' '); $html .= html::div(array('class' => 'dates', 'style' => 'display:none'), html::label(array(), $this->plugin->gettext('created')) . html::span('notecreated', '') . html::label(array(), $this->plugin->gettext('changed')) . html::span('notechanged', '') ); return html::div($attrib, $html); } public function attachments_list($attrib) { $attrib += array('id' => 'rcmkolabnotesattachmentslist'); $this->rc->output->add_gui_object('notesattachmentslist', $attrib['id']); return html::tag('ul', $attrib, '', html::$common_attrib); } /** * Render edit for notes lists (folders) */ public function list_editform($action, $list, $folder) { if (is_object($folder)) { $folder_name = $folder->name; // UTF7 } else { $folder_name = ''; } $hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name); $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); $form = array(); if (strlen($folder_name)) { $options = $storage->folder_info($folder_name); $path_imap = explode($delim, $folder_name); array_pop($path_imap); // pop off name part $path_imap = implode($path_imap, $delim); } else { $path_imap = ''; $options = array(); } // General tab $form['properties'] = array( 'name' => $this->rc->gettext('properties'), 'fields' => array(), ); // folder name (default field) $input_name = new html_inputfield(array('name' => 'name', 'id' => 'noteslist-name', 'size' => 20)); $form['properties']['fields']['name'] = array( 'label' => $this->plugin->gettext('listname'), 'value' => $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected']))), 'id' => 'noteslist-name', ); // prevent user from moving folder if (!empty($options) && ($options['norename'] || $options['protected'])) { $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap); } else { $select = kolab_storage::folder_selector('note', array('name' => 'parent', 'id' => 'parent-folder'), $folder_name); $form['properties']['fields']['path'] = array( 'label' => $this->plugin->gettext('parentfolder'), 'value' => $select->show(strlen($folder_name) ? $path_imap : ''), 'id' => 'parent-folder', ); } // add folder ACL tab if ($action != 'form-new') { $form['sharing'] = array( 'name' => Q($this->plugin->gettext('tabsharing')), 'content' => html::tag('iframe', array( 'src' => $this->rc->url(array('_action' => 'folder-acl', '_folder' => $folder_name, 'framed' => 1)), 'width' => '100%', 'height' => 280, 'border' => 0, 'style' => 'border:0'), '') ); } $form_html = ''; if (is_array($hidden_fields)) { foreach ($hidden_fields as $field) { $hiddenfield = new html_hiddenfield($field); $form_html .= $hiddenfield->show() . "\n"; } } // create form output foreach ($form as $tab) { if (is_array($tab['fields']) && empty($tab['content'])) { $table = new html_table(array('cols' => 2)); foreach ($tab['fields'] as $col => $colprop) { $label = !empty($colprop['label']) ? $colprop['label'] : $this->plugin->gettext($col); $table->add('title', html::label($colprop['id'], Q($label))); $table->add(null, $colprop['value']); } $content = $table->show(); } else { $content = $tab['content']; } if (!empty($content)) { $form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) . "\n"; } } return html::tag('form', array('action' => "#", 'method' => "post", 'id' => "noteslistpropform"), $form_html); } /** * Handler to render ACL form for a notes folder */ public function folder_acl() { $this->plugin->require_plugin('acl'); $this->rc->output->add_handler('folderacl', array($this, 'folder_acl_form')); $this->rc->output->send('kolab_notes.kolabacl'); } /** * Handler for ACL form template object */ public function folder_acl_form() { $folder = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_GPC); if (strlen($folder)) { $storage = $this->rc->get_storage(); $options = $storage->folder_info($folder); // get sharing UI from acl plugin $acl = $this->rc->plugins->exec_hook('folder_form', array('form' => array(), 'options' => $options, 'name' => $folder)); } return $acl['form']['sharing']['content'] ?: html::div('hint', $this->plugin->gettext('aclnorights')); } /** * Render the template for printing with placeholders */ public function print_template() { header('Content-Type: text/html; charset=' . RCUBE_CHARSET); $this->rc->output->reset(true); echo $this->rc->output->parse('kolab_notes.print', false, false); exit; } } diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php index a67a380f..6a70d261 100644 --- a/plugins/libcalendaring/lib/libcalendaring_itip.php +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php @@ -1,817 +1,817 @@ * * Copyright (C) 2011-2014, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class libcalendaring_itip { protected $rc; protected $lib; protected $plugin; protected $sender; protected $domain; protected $itip_send = false; protected $rsvp_actions = array('accepted','tentative','declined','delegated'); protected $rsvp_status = array('accepted','tentative','declined','delegated'); function __construct($plugin, $domain = 'libcalendaring') { $this->plugin = $plugin; $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->domain = $domain; $hook = $this->rc->plugins->exec_hook('calendar_load_itip', array('identity' => $this->rc->user->list_emails(true))); $this->sender = $hook['identity']; $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook')); $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook')); } public function set_sender_email($email) { if (!empty($email)) $this->sender['email'] = $email; } public function set_rsvp_actions($actions) { $this->rsvp_actions = (array)$actions; $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated')); } public function set_rsvp_status($status) { $this->rsvp_status = $status; } /** * Wrapper for rcube_plugin::gettext() * Checking for a label in different domains * * @see rcube::gettext() */ public function gettext($p) { $label = is_array($p) ? $p['name'] : $p; $domain = $this->domain; if (!$this->rc->text_exists($label, $domain)) { $domain = 'libcalendaring'; } return $this->rc->gettext($p, $domain); } /** * Send an iTip mail message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @param array Hash array with recipient data (name, email) * @param string Mail subject * @param string Mail body text label * @param object Mail_mime object with message data * @param boolean Request RSVP * @return boolean True on success, false on failure */ public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true) { if (!$this->sender['name']) $this->sender['name'] = $this->sender['email']; if (!$message) { libcalendaring::identify_recurrence_instance($event); $message = $this->compose_itip_message($event, $method, $rsvp); } $mailto = rcube_idn_to_ascii($recipient['email']); $headers = $message->headers(); $headers['To'] = format_email_recipient($mailto, $recipient['name']); $headers['Subject'] = $this->gettext(array( 'name' => $subject, 'vars' => array( 'title' => $event['title'], 'name' => $this->sender['name'] ) )); // compose a list of all event attendees $attendees_list = array(); foreach ((array)$event['attendees'] as $attendee) { $attendees_list[] = ($attendee['name'] && $attendee['email']) ? $attendee['name'] . ' <' . $attendee['email'] . '>' : ($attendee['name'] ? $attendee['name'] : $attendee['email']); } $recurrence_info = ''; if (!empty($event['recurrence_id'])) { $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **'; } else if (!empty($event['recurrence'])) { $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence'])); } $mailbody = $this->gettext(array( 'name' => $bodytext, 'vars' => array( 'title' => $event['title'], 'date' => $this->lib->event_date_text($event, true) . $recurrence_info, 'attendees' => join(",\n ", $attendees_list), 'sender' => $this->sender['name'], 'organizer' => $this->sender['name'], ) )); // if (!empty($event['comment'])) { // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment']; // } // append links for direct invitation replies if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) { $mailbody .= "\n\n" . $this->gettext(array( 'name' => 'invitationattendlinks', 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))), )); } else if ($method == 'CANCEL' && $event['cancelled']) { $this->cancel_itip_invitation($event); } $message->headers($headers, true); $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79)); if ($this->rc->config->get('libcalendaring_itip_debug', false)) { console('iTip ' . $method, $message->txtHeaders() . "\n\r" . $message->get()); } // finally send the message $this->itip_send = true; $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error); $this->itip_send = false; return $sent; } /** * Plugin hook triggered by rcube::deliver_message() before delivering a message. * Here we can set the 'smtp_server' config option to '' in order to use * PHP's mail() function for unauthenticated email sending. */ public function before_send_hook($p) { if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') { $this->rc->config->set('smtp_server', ''); } return $p; } /** * Plugin hook to alter SMTP authentication. * This is used if iTip messages are to be sent from an unauthenticated session */ public function smtp_connect_hook($p) { // replace smtp auth settings if we're not in an authenticated session if ($this->itip_send && !$this->rc->user->ID) { foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) { $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]); } } return $p; } /** * Helper function to build a Mail_mime object to send an iTip message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @param boolean Request RSVP * @return object Mail_mime object with message data */ public function compose_itip_message($event, $method, $rsvp = true) { $from = rcube_idn_to_ascii($this->sender['email']); $from_utf = rcube_utils::idn_to_utf8($from); $sender = format_email_recipient($from, $this->sender['name']); // truncate list attendees down to the recipient of the iTip Reply. // constraints for a METHOD:REPLY according to RFC 5546 if ($method == 'REPLY') { $replying_attendee = null; $reply_attendees = array(); foreach ($event['attendees'] as $attendee) { if ($attendee['role'] == 'ORGANIZER') { $reply_attendees[] = $attendee; } else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) { $replying_attendee = $attendee; if ($attendee['status'] != 'DELEGATED') { unset($replying_attendee['rsvp']); // unset the RSVP attribute } } // include attendees relevant for delegation (RFC 5546, Section 4.2.5) else if ((!empty($attendee['delegated-to']) && (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) || (!empty($attendee['delegated-from']) && (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) { $reply_attendees[] = $attendee; } } if ($replying_attendee) { array_unshift($reply_attendees, $replying_attendee); $event['attendees'] = $reply_attendees; } if ($event['recurrence']) { unset($event['recurrence']['EXCEPTIONS']); } } // set RSVP for every attendee else if ($method == 'REQUEST') { foreach ($event['attendees'] as $i => $attendee) { if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) { $event['attendees'][$i]['rsvp']= (bool)$rsvp; } } } else if ($method == 'CANCEL') { if ($event['recurrence']) { unset($event['recurrence']['EXCEPTIONS']); } } // compose multipart message using PEAR:Mail_Mime $message = new Mail_mime("\r\n"); $message->setParam('text_encoding', 'quoted-printable'); $message->setParam('head_encoding', 'quoted-printable'); - $message->setParam('head_charset', RCMAIL_CHARSET); - $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed"); + $message->setParam('head_charset', RCUBE_CHARSET); + $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed"); $message->setContentType('multipart/alternative'); // compose common headers array $headers = array( 'From' => $sender, 'Date' => $this->rc->user_date(), 'Message-ID' => $this->rc->gen_message_id(), 'X-Sender' => $from, ); if ($agent = $this->rc->config->get('useragent')) { $headers['User-Agent'] = $agent; } $message->headers($headers); // attach ics file for this event $ical = libcalendaring::get_ical(); $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false); $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics'; - $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method); + $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method); return $message; } /** * Forward the given iTip event as delegation to another person * * @param array Event object to delegate * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto' * @param boolean The delegator's RSVP flag * @param array List with indexes of new/updated attendees * @return boolean True on success, False on failure */ public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array()) { if (is_string($delegate)) { $delegates = rcube_mime::decode_address_list($delegate, 1, false); if (count($delegates) > 0) { $delegate = reset($delegates); } } $emails = $this->lib->get_user_emails(); $me = $this->rc->user->list_emails(true); // find/create the delegate attendee $delegate_attendee = array( 'email' => $delegate['mailto'], 'name' => $delegate['name'], 'role' => 'REQ-PARTICIPANT', ); $delegate_index = count($event['attendees']); foreach ($event['attendees'] as $i => $attendee) { // set myself the DELEGATED-TO parameter if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $event['attendees'][$i]['delegated-to'] = $delegate['mailto']; $event['attendees'][$i]['status'] = 'DELEGATED'; $event['attendees'][$i]['role'] = 'NON-PARTICIPANT'; $event['attendees'][$i]['rsvp'] = $rsvp; $me['email'] = $attendee['email']; $delegate_attendee['role'] = $attendee['role']; } // the disired delegatee is already listed as an attendee else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') { $delegate_attendee = $attendee; $delegate_index = $i; break; } // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me) } // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter $delegate_attendee['rsvp'] = true; $delegate_attendee['status'] = 'NEEDS-ACTION'; $delegate_attendee['delegated-from'] = $me['email']; $event['attendees'][$delegate_index] = $delegate_attendee; $attendees[] = $delegate_index; $this->set_sender_email($me['email']); return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto'); } /** * Handler for calendar/itip-status requests */ public function get_itip_status($event, $existing = null) { $action = $event['rsvp'] ? 'rsvp' : ''; $status = $event['fallback']; $latest = false; $html = ''; if (is_numeric($event['changed'])) $event['changed'] = new DateTime('@'.$event['changed']); // check if the given itip object matches the last state if ($existing) { $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']); } // determine action for REQUEST if ($event['method'] == 'REQUEST') { $html = html::div('rsvp-status', $this->gettext('acceptinvitation')); if ($existing) { $rsvp = $event['rsvp']; $emails = $this->lib->get_user_emails(); foreach ($existing['attendees'] as $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $status = strtoupper($attendee['status']); break; } } } else { $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true); } $status_lc = strtolower($status); if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { $html = html::div('rsvp-status', $this->gettext('notanattendee')); $action = 'import'; } else if (in_array($status_lc, $this->rsvp_status)) { $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc); if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) { $action = ''; // nothing to do here, outdated invitation if ($status_lc == 'needs-action') $status_text = $this->gettext('outdatedinvitation'); } else if (!$existing && !$rsvp) { $action = 'import'; } else if ($latest && $status_lc != 'needs-action') { $action = 'update'; } $html = html::div('rsvp-status ' . $status_lc, $status_text); } } // determine action for REPLY else if ($event['method'] == 'REPLY') { // check whether the sender already is an attendee if ($existing) { $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : ''; $listed = false; foreach ($existing['attendees'] as $attendee) { if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) { $status_lc = strtolower($status); if (in_array($status_lc, $this->rsvp_status)) { $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array( 'name' => 'attendee' . $status_lc, 'vars' => array( 'delegatedto' => Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')), ) ))); } $action = $attendee['status'] == $status || !$latest ? '' : 'update'; $listed = true; break; } } if (!$listed) { $html = html::div('rsvp-status', $this->gettext('itipnewattendee')); } } else { $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); $action = ''; } } else if ($event['method'] == 'CANCEL') { if (!$existing) { $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); $action = ''; } } return array( 'uid' => $event['uid'], 'id' => asciiwords($event['uid'], true), 'existing' => $existing ? true : false, 'saved' => $existing ? true : false, 'latest' => $latest, 'status' => $status, 'action' => $action, 'html' => $html, ); } /** * Build inline UI elements for iTip messages */ public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null) { $buttons = array(); $dom_id = asciiwords($event['uid'], true); $rsvp_status = 'unknown'; // pass some metadata about the event and trigger the asynchronous status check $changed = is_object($event['changed']) ? $event['changed'] : $message_date; $metadata = array( 'uid' => $event['uid'], '_instance' => $event['_instance'], 'changed' => $changed ? $changed->format('U') : 0, 'sequence' => intval($event['sequence']), 'method' => $method, 'task' => $task, ); // create buttons to be activated from async request checking existence of this event in local calendars $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading')); // on iTip REPLY we have two options: if ($method == 'REPLY') { $title = $this->gettext('itipreply'); foreach ($event['attendees'] as $attendee) { if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER' && (empty($event['_sender']) || ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf']))) { $metadata['attendee'] = $attendee['email']; $rsvp_status = strtoupper($attendee['status']); if ($attendee['delegated-to']) $metadata['delegated-to'] = $attendee['delegated-to']; break; } } // 1. update the attendee status on our copy $update_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updateattendeestatus'), )); // 2. accept or decline a new or delegate attendee $accept_buttons = html::tag('input', array( 'type' => 'button', 'class' => "button accept", 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('acceptattendee'), )); $accept_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button decline", 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('declineattendee'), )); $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons); } // when receiving iTip REQUEST messages: else if ($method == 'REQUEST') { $emails = $this->lib->get_user_emails(); $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation'); $metadata['rsvp'] = true; $metadata['sensitivity'] = $event['sensitivity']; if (is_object($event['start'])) { $metadata['date'] = $event['start']->format('U'); } // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') { $this->rsvp_actions = array('accepted','declined'); $metadata['nosave'] = true; } // 1. display RSVP buttons (if the user was invited) foreach ($this->rsvp_actions as $method) { $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button $method", 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task', '$method', '$dom_id')", 'value' => $this->gettext('itip' . $method), )); } // add button to open calendar/preview if (!empty($preview_url)) { $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id; $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button preview", 'onclick' => "rcube_libcalendaring.open_itip_preview('" . JQ($preview_url) . "', '" . JQ($msgref) . "')", 'value' => $this->gettext('openpreview'), )); } // 2. update the local copy with minor changes $update_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updatemycopy'), )); // 3. Simply import the event without replying $import_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('importtocalendar'), )); // check my status foreach ($event['attendees'] as $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION'; break; } } // add itip reply message controls $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave'])); $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons); $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); // prepare autocompletion for delegation dialog if (in_array('delegated', $this->rsvp_actions)) { $this->rc->autocomplete_init(); } } // for CANCEL messages, we can: else if ($method == 'CANCEL') { $title = $this->gettext('itipcancellation'); $event_prop = array_filter(array( 'uid' => $event['uid'], '_instance' => $event['_instance'], '_savemode' => $event['_savemode'], )); // 1. remove the event from our calendar $button_remove = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . JQ($event['title']) . "')", 'value' => $this->gettext('removefromcalendar'), )); // 2. update our copy with status=cancelled $button_update = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updatemycopy'), )); $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update); $rsvp_status = 'CANCELLED'; $metadata['rsvp'] = true; } // append generic import button if ($import_button) { $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button); } // pass some metadata about the event and trigger the asynchronous status check $metadata['fallback'] = $rsvp_status; $metadata['rsvp'] = intval($metadata['rsvp']); $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . json_serialize($metadata) . ")", 'docready'); // get localized texts from the right domain foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee', 'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation', 'delegateto','delegatersvpme','delegateinvalidaddress') as $label) { $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); } // show event details with buttons return $this->itip_object_details_table($event, $title) . html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons)); } /** * Render an RSVP UI widget with buttons to respond on iTip invitations */ function itip_rsvp_buttons($attrib = array(), $actions = null) { $attrib += array('type' => 'button'); if (!$actions) $actions = $this->rsvp_actions; foreach ($actions as $method) { $buttons .= html::tag('input', array( 'type' => $attrib['type'], 'name' => $attrib['iname'], 'class' => 'button', 'rel' => $method, 'value' => $this->gettext('itip' . $method), )); } // add localized texts for the delegation dialog if (in_array('delegated', $actions)) { foreach (array('itipdelegated','itipcomment','delegateinvitation', 'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) { $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); } } foreach (array('all','current','future') as $mode) { $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode")); } $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode')); return html::div($attrib, html::div('label', $this->gettext('acceptinvitation')) . html::div('rsvp-buttons', $buttons . html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id'])) ) ); } /** * Render UI elements to control iTip reply message sending */ public function itip_rsvp_options_ui($dom_id, $disable = false) { $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3); // itip sending is entirely disabled if ($itip_sending === 0) { return ''; } // add checkbox to suppress itip reply message else if ($itip_sending >= 2) { $rsvp_additions = html::label(array('class' => 'noreply-toggle'), html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0)) . ' ' . $this->gettext('itipsuppressreply') ); } // add input field for reply comment $rsvp_additions .= html::a(array('href' => '#toggle', 'class' => 'reply-comment-toggle'), $this->gettext('itipeditresponse')); $rsvp_additions .= html::div('itip-reply-comment', html::tag('textarea', array('id' => 'reply-comment-'.$dom_id, 'name' => '_comment', 'cols' => 40, 'rows' => 6, 'style' => 'display:none', 'placeholder' => $this->gettext('itipcomment')), '') ); return $rsvp_additions; } /** * Render event/task details in a table */ function itip_object_details_table($event, $title) { $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); $table->add('ititle', $title); $table->add('title', Q($event['title'])); if ($event['start'] && $event['end']) { $table->add('label', $this->gettext('date')); $table->add('date', Q($this->lib->event_date_text($event))); } else if ($event['due'] && $event['_type'] == 'task') { $table->add('label', $this->gettext('date')); $table->add('date', Q($this->lib->event_date_text($event))); } if (!empty($event['recurrence_date'])) { $table->add('label', ''); $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence')); } else if (!empty($event['recurrence'])) { $table->add('label', $this->gettext('recurring')); $table->add('recurrence', $this->lib->recurrence_text($event['recurrence'])); } if ($event['location']) { $table->add('label', $this->gettext('location')); $table->add('location', Q($event['location'])); } if ($event['sensitivity'] && $event['sensitivity'] != 'public') { $table->add('label', $this->gettext('sensitivity')); $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!'); } if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') { $table->add('label', $this->gettext('status')); $table->add('status', $this->gettext('status-' . strtolower($event['status']))); } if ($event['comment']) { $table->add('label', $this->gettext('comment')); $table->add('location', Q($event['comment'])); } return $table->show(); } /** * Create iTIP invitation token for later replies via URL * * @param array Hash array with event properties * @param string Attendee email address * @return string Invitation token */ public function store_invitation($event, $attendee) { // empty stub return false; } /** * Mark invitations for the given event as cancelled * * @param array Hash array with event properties */ public function cancel_itip_invitation($event) { // empty stub return false; } /** * Utility function to get the value of a custom property */ public static function get_custom_property($event, $name) { $ret = false; if (is_array($event['x-custom'])) { array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) { if (strcasecmp($prop[0], $name) === 0) { $ret = $prop[1]; } }); } return $ret; } } diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index ddbebe43..7ca0b3ea 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -1,1650 +1,1650 @@ * * Copyright (C) 2012-2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class libcalendaring extends rcube_plugin { public $rc; public $timezone; public $gmt_offset; public $dst_active; public $timezone_offset; public $ical_parts = array(); public $ical_message; public $defaults = array( 'calendar_date_format' => "yyyy-MM-dd", 'calendar_date_short' => "M-d", 'calendar_date_long' => "MMM d yyyy", 'calendar_date_agenda' => "ddd MM-dd", 'calendar_time_format' => "HH:mm", 'calendar_first_day' => 1, 'calendar_first_hour' => 6, 'calendar_date_format_sets' => array( 'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'), 'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'), 'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'), 'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'), 'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'), 'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'), 'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'), ), ); private static $instance; private static $email_regex = '/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/'; private $mail_ical_parser; /** * Singleton getter to allow direct access from other plugins */ public static function get_instance() { return self::$instance; } /** * Required plugin startup method */ public function init() { self::$instance = $this; $this->rc = rcube::get_instance(); // set user's timezone try { $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT')); } catch (Exception $e) { $this->timezone = new DateTimeZone('GMT'); } $now = new DateTime('now', $this->timezone); $this->gmt_offset = $now->getOffset(); $this->dst_active = $now->format('I'); $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active; $this->add_texts('localization/', false); // include client scripts and styles if ($this->rc->output) { // add hook to display alarms $this->add_hook('refresh', array($this, 'refresh')); $this->register_action('plugin.alarms', array($this, 'alarms_action')); $this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group')); } // proceed initialization in startup hook $this->add_hook('startup', array($this, 'startup')); } /** * Startup hook */ public function startup($args) { if ($this->rc->output && $this->rc->output->type == 'html') { $this->rc->output->set_env('libcal_settings', $this->load_settings()); $this->include_script('libcalendaring.js'); $this->include_stylesheet($this->local_skin_path() . '/libcal.css'); } if ($args['task'] == 'mail') { if ($args['action'] == 'show' || $args['action'] == 'preview') { $this->add_hook('message_load', array($this, 'mail_message_load')); } } } /** * Load iCalendar functions */ public static function get_ical() { $self = self::get_instance(); require_once($self->home . '/libvcalendar.php'); return new libvcalendar(); } /** * Load iTip functions */ public static function get_itip($domain = 'libcalendaring') { $self = self::get_instance(); require_once($self->home . '/lib/libcalendaring_itip.php'); return new libcalendaring_itip($self, $domain); } /** * Load recurrence computation engine */ public static function get_recurrence() { $self = self::get_instance(); require_once($self->home . '/lib/libcalendaring_recurrence.php'); return new libcalendaring_recurrence($self); } /** * Shift dates into user's current timezone * * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp) * @return object DateTime object in user's timezone */ public function adjust_timezone($dt, $dateonly = false) { if (is_numeric($dt)) $dt = new DateTime('@'.$dt); else if (is_string($dt)) $dt = rcube_utils::anytodatetime($dt); if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) { $dt->setTimezone($this->timezone); } return $dt; } /** * */ public function load_settings() { $this->date_format_defaults(); $settings = array(); // configuration $settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']); $settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']); $settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']); $settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']); $settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '—' " . $settings['date_long'] . '}'; $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['timezone'] = $this->timezone_offset; $settings['dst'] = $this->dst_active; // localization $settings['days'] = array( $this->rc->gettext('sunday'), $this->rc->gettext('monday'), $this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'), $this->rc->gettext('thursday'), $this->rc->gettext('friday'), $this->rc->gettext('saturday') ); $settings['days_short'] = array( $this->rc->gettext('sun'), $this->rc->gettext('mon'), $this->rc->gettext('tue'), $this->rc->gettext('wed'), $this->rc->gettext('thu'), $this->rc->gettext('fri'), $this->rc->gettext('sat') ); $settings['months'] = array( $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'), $this->rc->gettext('longmar'), $this->rc->gettext('longapr'), $this->rc->gettext('longmay'), $this->rc->gettext('longjun'), $this->rc->gettext('longjul'), $this->rc->gettext('longaug'), $this->rc->gettext('longsep'), $this->rc->gettext('longoct'), $this->rc->gettext('longnov'), $this->rc->gettext('longdec') ); $settings['months_short'] = array( $this->rc->gettext('jan'), $this->rc->gettext('feb'), $this->rc->gettext('mar'), $this->rc->gettext('apr'), $this->rc->gettext('may'), $this->rc->gettext('jun'), $this->rc->gettext('jul'), $this->rc->gettext('aug'), $this->rc->gettext('sep'), $this->rc->gettext('oct'), $this->rc->gettext('nov'), $this->rc->gettext('dec') ); $settings['today'] = $this->rc->gettext('today'); // define list of file types which can be displayed inline // same as in program/steps/mail/show.inc $settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes'); return $settings; } /** * Helper function to set date/time format according to config and user preferences */ private function date_format_defaults() { static $defaults = array(); // nothing to be done if (isset($defaults['date_format'])) return; $defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format'))); $defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format'))); // override defaults if ($defaults['date_format']) $this->defaults['calendar_date_format'] = $defaults['date_format']; if ($defaults['time_format']) $this->defaults['calendar_time_format'] = $defaults['time_format']; // derive format variants from basic date format $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']); if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) { $this->defaults['calendar_date_long'] = $format_set[0]; $this->defaults['calendar_date_short'] = $format_set[1]; $this->defaults['calendar_date_agenda'] = $format_set[2]; } } /** * Compose a date string for the given event */ public function event_date_text($event, $tzinfo = false) { $fromto = '--'; // handle task objects if ($event['_type'] == 'task' && is_object($event['due'])) { $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null; $fromto = $this->rc->format_date($event['due'], $date_format, false); // add timezone information if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) { $fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; } return $fromto; } // abort if no valid event dates are given if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) { return $fromto; } $duration = $event['start']->diff($event['end'])->format('s'); $this->date_format_defaults(); $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])); $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])); if ($event['allday']) { $fromto = format_date($event['start'], $date_format); if (($todate = format_date($event['end'], $date_format)) != $fromto) $fromto .= ' - ' . $todate; } else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) { $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) . ' - ' . format_date($event['end'], $time_format); } else { $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) . ' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format); } // add timezone information if ($tzinfo && ($tzname = $this->timezone->getName())) { $fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; } return $fromto; } /** * Render HTML form for alarm configuration */ public function alarm_select($attrib, $alarm_types, $absolute_time = true) { unset($attrib['name']); $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3)); $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10)); $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6)); $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id'])); $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related')); $object_type = $attrib['_type'] ?: 'event'; $select_type->add($this->gettext('none'), ''); foreach ($alarm_types as $type) $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type); foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) $select_offset->add($this->gettext('trigger' . $trigger), $trigger); $select_offset->add($this->gettext('trigger0'), '0'); if ($absolute_time) $select_offset->add($this->gettext('trigger@'), '@'); $select_related->add($this->gettext('relatedstart'), 'start'); $select_related->add($this->gettext('relatedend' . $object_type), 'end'); // pre-set with default values from user settings $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); $hidden = array('style' => 'display:none'); $html = html::span('edit-alarm-set', $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' . html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'), $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]) . ' ' . $select_related->show() . ' ' . $input_date->show('', $hidden) . ' ' . $input_time->show('', $hidden) ) ); // TODO: support adding more alarms #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')), # $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)'); return $html; } /** * Get a list of email addresses of the given user (from login and identities) * * @param string User Email (default to current user) * @return array Email addresses related to the user */ public function get_user_emails($user = null) { static $_emails = array(); if (empty($user)) { $user = $this->rc->user->get_username(); } // return cached result if (is_array($_emails[$user])) { return $_emails[$user]; } $emails = array($user); $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails)); $emails = array_map('strtolower', $plugin['emails']); // add all emails from the current user's identities if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) { foreach ($this->rc->user->list_emails() as $identity) { $emails[] = strtolower($identity['email']); } } $_emails[$user] = array_unique($emails); return $_emails[$user]; } /** * Set the given participant status to the attendee matching the current user's identities * * @param array Hash array with event struct * @param string The PARTSTAT value to set * @return mixed Email address of the updated attendee or False if none matching found */ public function set_partstat(&$event, $status, $recursive = true) { $success = false; $emails = $this->get_user_emails(); foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $event['attendees'][$i]['status'] = strtoupper($status); $success = $attendee['email']; } } // apply partstat update to each existing exception if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) { foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) { $this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false); } // set link to top-level exceptions $event['exceptions'] = &$event['recurrence']['EXCEPTIONS']; } return $success; } /********* Alarms handling *********/ /** * Helper function to convert alarm trigger strings * into two-field values (e.g. "-45M" => 45, "-M") */ public static function parse_alarm_value($val) { if ($val[0] == '@') { return array(new DateTime($val)); } else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) { if ($m[1] == '') $m[1] = '+'; foreach ($m2 as $seg) { $prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT'; if ($seg[1] > 0) { // ignore zero values // convert seconds to minutes if ($seg[2] == 'S') { $seg[2] = 'M'; $seg[1] = max(1, round($seg[1]/60)); } return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]); } } // return zero value nevertheless return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]); } return false; } /** * Convert the alarms list items to be processed on the client */ public static function to_client_alarms($valarms) { return array_map(function($alarm){ if ($alarm['trigger'] instanceof DateTime) { $alarm['trigger'] = '@' . $alarm['trigger']->format('U'); } else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) { $alarm['trigger'] = $trigger[2]; } return $alarm; }, (array)$valarms); } /** * Process the alarms values submitted by the client */ public static function from_client_alarms($valarms) { return array_map(function($alarm){ if ($alarm['trigger'][0] == '@') { try { $alarm['trigger'] = new DateTime($alarm['trigger']); $alarm['trigger']->setTimezone(new DateTimeZone('UTC')); } catch (Exception $e) { /* handle this ? */ } } else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) { $alarm['trigger'] = $trigger[3]; } return $alarm; }, (array)$valarms); } /** * Render localized text for alarm settings */ public static function alarms_text($alarms) { if (is_array($alarms) && is_array($alarms[0])) { $texts = array(); foreach ($alarms as $alarm) { if ($text = self::alarm_text($alarm)) $texts[] = $text; } return join(', ', $texts); } else { return self::alarm_text($alarms); } } /** * Render localized text for a single alarm property */ public static function alarm_text($alarm) { if (is_string($alarm)) { list($trigger, $action) = explode(':', $alarm); } else { $trigger = $alarm['trigger']; $action = $alarm['action']; $related = $alarm['related']; } $text = ''; $rcube = rcube::get_instance(); switch ($action) { case 'EMAIL': $text = $rcube->gettext('libcalendaring.alarmemail'); break; case 'DISPLAY': $text = $rcube->gettext('libcalendaring.alarmdisplay'); break; case 'AUDIO': $text = $rcube->gettext('libcalendaring.alarmaudio'); break; } if ($trigger instanceof DateTime) { $text .= ' ' . $rcube->gettext(array( 'name' => 'libcalendaring.alarmat', 'vars' => array('datetime' => $rcube->format_date($trigger)) )); } else if (preg_match('/@(\d+)/', $trigger, $m)) { $text .= ' ' . $rcube->gettext(array( 'name' => 'libcalendaring.alarmat', 'vars' => array('datetime' => $rcube->format_date($m[1])) )); } else if ($val = self::parse_alarm_value($trigger)) { $r = strtoupper($related ?: 'start') == 'END' ? 'end' : ''; // TODO: for all-day events say 'on date of event at XX' ? if ($val[0] == 0) { $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r); } else { $label = 'libcalendaring.trigger' . $r . $val[1]; $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label); } } else { return false; } return $text; } /** * Get the next alarm (time & action) for the given event * * @param array Record data * @return array Hash array with alarm time/type or null if no alarms are configured */ public static function get_next_alarm($rec, $type = 'event') { if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED') return null; if ($type == 'task') { $timezone = self::get_instance()->timezone; if ($rec['startdate']) $rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone); if ($rec['date']) $rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone); } if (!$rec['end']) $rec['end'] = $rec['start']; // support legacy format if (!$rec['valarms']) { list($trigger, $action) = explode(':', $rec['alarms'], 2); if ($alarm = self::parse_alarm_value($trigger)) { $rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0])); } } $expires = new DateTime('now - 12 hours'); $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility // handle multiple alarms $notify_at = null; foreach ($rec['valarms'] as $alarm) { $notify_time = null; if ($alarm['trigger'] instanceof DateTime) { $notify_time = $alarm['trigger']; } else if (is_string($alarm['trigger'])) { $refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start']; // abort if no reference date is available to compute notification time if (!is_a($refdate, 'DateTime')) continue; // TODO: for all-day events, take start @ 00:00 as reference date ? try { $interval = new DateInterval(trim($alarm['trigger'], '+-')); $interval->invert = $alarm['trigger'][0] == '-'; $notify_time = clone $refdate; $notify_time->add($interval); } catch (Exception $e) { rcube::raise_error($e, true); continue; } } if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) { $notify_at = $notify_time; $action = $alarm['action']; $alarm_prop = $alarm; // generate a unique alarm ID if multiple alarms are set if (count($rec['valarms']) > 1) { $alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis'); } } } return !$notify_at ? null : array( 'time' => $notify_at->format('U'), 'action' => $action ? strtoupper($action) : 'DISPLAY', 'id' => $alarm_id, 'prop' => $alarm_prop, ); } /** * Handler for keep-alive requests * This will check for pending notifications and pass them to the client */ public function refresh($attr) { // collect pending alarms from all providers (e.g. calendar, tasks) $plugin = $this->rc->plugins->exec_hook('pending_alarms', array( 'time' => time(), 'alarms' => array(), )); if (!$plugin['abort'] && !empty($plugin['alarms'])) { // make sure texts and env vars are available on client $this->add_texts('localization/', true); $this->rc->output->add_label('close'); $this->rc->output->set_env('snooze_select', $this->snooze_select()); $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms'])); } } /** * Handler for alarm dismiss/snooze requests */ public function alarms_action() { // $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); $data['ids'] = explode(',', $data['id']); $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data); if ($plugin['success']) $this->rc->output->show_message('successfullysaved', 'confirmation'); else $this->rc->output->show_message('calendar.errorsaving', 'error'); } /** * Generate reduced and streamlined output for pending alarms */ private function _alarms_output($alarms) { $out = array(); foreach ($alarms as $alarm) { $out[] = array( 'id' => $alarm['id'], 'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '', 'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '', 'allDay' => ($alarm['allday'] == 1)?true:false, 'title' => $alarm['title'], 'location' => $alarm['location'], 'calendar' => $alarm['calendar'], ); } return $out; } /** * Render a dropdown menu to choose snooze time */ private function snooze_select($attrib = array()) { $steps = array( 5 => 'repeatinmin', 10 => 'repeatinmin', 15 => 'repeatinmin', 20 => 'repeatinmin', 30 => 'repeatinmin', 60 => 'repeatinhr', 120 => 'repeatinhrs', 1440 => 'repeattomorrow', 10080 => 'repeatinweek', ); $items = array(); foreach ($steps as $n => $label) { $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'), $this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60)))))); } return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib); } /********* Recurrence rules handling ********/ /** * Render localized text describing the recurrence rule of an event */ public function recurrence_text($rrule) { // derive missing FREQ and INTERVAL from RDATE list if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { $first = $rrule['RDATE'][0]; $second = $rrule['RDATE'][1]; $third = $rrule['RDATE'][2]; if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) { $diff = $first->diff($second); foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) { if ($diff->$k != 0) { $rrule['FREQ'] = $freq; $rrule['INTERVAL'] = $diff->$k; // verify interval with next item if (is_a($third, 'DateTime')) { $diff2 = $second->diff($third); if ($diff2->$k != $diff->$k) { unset($rrule['INTERVAL']); } } break; } } } if (!$rrule['INTERVAL']) { $rrule['FREQ'] = 'RDATE'; } $rrule['UNTIL'] = end($rrule['RDATE']); } $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']); $details = ''; switch ($rrule['FREQ']) { case 'DAILY': $freq .= $this->gettext('days'); break; case 'WEEKLY': $freq .= $this->gettext('weeks'); break; case 'MONTHLY': $freq .= $this->gettext('months'); break; case 'YEARLY': $freq .= $this->gettext('years'); break; } if ($rrule['INTERVAL'] <= 1) { $freq = $this->gettext(strtolower($rrule['FREQ'])); } if ($rrule['COUNT']) { $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); } else if ($rrule['UNTIL']) { $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']))); } else { $until = $this->gettext('forever'); } $except = ''; if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) { $format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])); $exdates = array_map( function($dt) use ($format) { return format_date($dt, $format); }, array_slice($rrule['EXDATE'], 0, 10) ); $except = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates); } return rtrim($freq . $details . ', ' . $until . $except); } /** * Generate the form for recurrence settings */ public function recurrence_form($attrib = array()) { switch ($attrib['part']) { // frequency selector case 'frequency': $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency')); $select->add($this->gettext('never'), ''); $select->add($this->gettext('daily'), 'DAILY'); $select->add($this->gettext('weekly'), 'WEEKLY'); $select->add($this->gettext('monthly'), 'MONTHLY'); $select->add($this->gettext('yearly'), 'YEARLY'); $select->add($this->gettext('rdate'), 'RDATE'); $html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show(''); break; // daily recurrence case 'daily': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily')); $html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days'))); break; // weekly recurrence form case 'weekly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly')); $html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks'))); // weekday selection $daymap = array('sun','mon','tue','wed','thu','fri','sat'); $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday')); $first = $this->rc->config->get('calendar_first_day', 1); for ($weekdays = '', $j = $first; $j <= $first+6; $j++) { $d = $j % 7; $weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->gettext($daymap[$d]) ) . ' '; } $html .= html::div($attrib, html::label(null, $this->gettext('bydays')) . $weekdays); break; // monthly recurrence form case 'monthly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly')); $html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months'))); $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday')); for ($monthdays = '', $d = 1; $d <= 31; $d++) { $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d); $monthdays .= $d % 7 ? ' ' : html::br(); } // rule selectors $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode')); $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each'))); $table->add(null, $monthdays); $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery'))); $table->add(null, $this->rrule_selectors($attrib['part'])); $html .= html::div($attrib, $table->show()); break; // annually recurrence form case 'yearly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly')); $html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years'))); // month selector $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'); $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth')); for ($months = '', $m = 1; $m <= 12; $m++) { $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m])); $months .= $m % 4 ? ' ' : html::br(); } $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months); // day rule selection $html .= html::div($attrib, html::label(null, $this->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---')); break; // end of recurrence form case 'until': $radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until')); $select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times')); $input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10")); $html = html::div('line first', html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' . $this->gettext('forever')) ); $forntimes = $this->gettext(array( 'name' => 'forntimes', 'vars' => array('nr' => '%s')) ); $html .= html::div('line', $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' . sprintf($forntimes, $select->show(1)) ); $html .= html::div('line', $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' . $this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate'))) ); $html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html); break; case 'rdate': $ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), ''); $input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10")); $button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate'))); $html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show())); break; } return $html; } /** * Input field for interval selection */ private function interval_selector($attrib) { $select = new html_select($attrib); $select->add(range(1,30), range(1,30)); return $select; } /** * Drop-down menus for recurrence rules like "each last sunday of" */ private function rrule_selectors($part, $noselect = null) { // rule selectors $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix")); if ($noselect) $select_prefix->add($noselect, ''); $select_prefix->add(array( $this->gettext('first'), $this->gettext('second'), $this->gettext('third'), $this->gettext('fourth'), $this->gettext('last') ), array(1, 2, 3, 4, -1)); $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday")); if ($noselect) $select_wday->add($noselect, ''); $daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday'); $first = $this->rc->config->get('calendar_first_day', 1); for ($j = $first; $j <= $first+6; $j++) { $d = $j % 7; $select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2))); } return $select_prefix->show() . ' ' . $select_wday->show(); } /** * Convert the recurrence settings to be processed on the client */ public function to_client_recurrence($recurrence, $allday = false) { if ($recurrence['UNTIL']) $recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c'); // format RDATE values if (is_array($recurrence['RDATE'])) { $libcal = $this; $recurrence['RDATE'] = array_map(function($rdate) use ($libcal) { return $libcal->adjust_timezone($rdate, true)->format('c'); }, $recurrence['RDATE']); } unset($recurrence['EXCEPTIONS']); return $recurrence; } /** * Process the alarms values submitted by the client */ public function from_client_recurrence($recurrence, $start = null) { if (is_array($recurrence) && !empty($recurrence['UNTIL'])) { $recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone); } if (is_array($recurrence) && is_array($recurrence['RDATE'])) { $tz = $this->timezone; $recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) { try { $dt = new DateTime($rdate, $tz); if (is_a($start, 'DateTime')) $dt->setTime($start->format('G'), $start->format('i')); return $dt; } catch (Exception $e) { return null; } }, $recurrence['RDATE']); } return $recurrence; } /********* Attachments handling *********/ /** * Handler for attachment uploads */ public function attachment_upload($session_key, $id_prefix = '') { // Upload progress update if (!empty($_GET['_progress'])) { $this->rc->upload_progress(); } $recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC); if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) { $_SESSION[$session_key] = array(); $_SESSION[$session_key]['id'] = $recid; $_SESSION[$session_key]['attachments'] = array(); } // clear all stored output properties (like scripts and env vars) $this->rc->output->reset(); if (is_array($_FILES['_attachments']['tmp_name'])) { foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) { // Process uploaded attachment if there is no error $err = $_FILES['_attachments']['error'][$i]; if (!$err) { $attachment = array( 'path' => $filepath, 'size' => $_FILES['_attachments']['size'][$i], 'name' => $_FILES['_attachments']['name'][$i], 'mimetype' => rcube_mime::file_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]), 'group' => $recid, ); $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment); } if (!$err && $attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; // store new attachment in session unset($attachment['status'], $attachment['abort']); $_SESSION[$session_key]['attachments'][$id] = $attachment; if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) { $button = html::img(array( 'src' => $icon, 'alt' => $this->rc->gettext('delete') )); } else { $button = Q($this->rc->gettext('delete')); } $content = html::a(array( 'href' => "#delete", 'class' => 'delete', - 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id), + 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id), 'title' => $this->rc->gettext('delete'), 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'], ), $button); $content .= Q($attachment['name']); $this->rc->output->command('add2attachment_list', "rcmfile$id", array( 'html' => $content, 'name' => $attachment['name'], 'mimetype' => $attachment['mimetype'], 'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']), 'complete' => true), $uploadid); } else { // upload failed if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array( 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else if ($attachment['error']) { $msg = $attachment['error']; } else { $msg = $this->rc->gettext('fileuploaderror'); } $this->rc->output->command('display_message', $msg, 'error'); $this->rc->output->command('remove_from_attachment_list', $uploadid); } } } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { // if filesize exceeds post_max_size then $_FILES array is empty, // show filesizeerror instead of fileuploaderror if ($maxsize = ini_get('post_max_size')) $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array( 'size' => show_bytes(parse_bytes($maxsize))))); else $msg = $this->rc->gettext('fileuploaderror'); $this->rc->output->command('display_message', $msg, 'error'); $this->rc->output->command('remove_from_attachment_list', $uploadid); } $this->rc->output->send('iframe'); } /** * Deliver an event/task attachment to the client * (similar as in Roundcube core program/steps/mail/get.inc) */ public function attachment_get($attachment) { ob_end_clean(); if ($attachment && $attachment['body']) { // allow post-processing of the attachment body $part = new rcube_message_part; $part->filename = $attachment['name']; $part->size = $attachment['size']; $part->mimetype = $attachment['mimetype']; $plugin = $this->rc->plugins->exec_hook('message_part_get', array( 'body' => $attachment['body'], 'mimetype' => strtolower($attachment['mimetype']), 'download' => !empty($_GET['_download']), 'part' => $part, )); if ($plugin['abort']) exit; $mimetype = $plugin['mimetype']; list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); $browser = $this->rc->output->browser; // send download headers if ($plugin['download']) { header("Content-Type: application/octet-stream"); if ($browser->ie) header("Content-Type: application/force-download"); } else if ($ctype_primary == 'text') { header("Content-Type: text/$ctype_secondary"); } else { header("Content-Type: $mimetype"); header("Content-Transfer-Encoding: binary"); } // display page, @TODO: support text/plain (and maybe some other text formats) if ($mimetype == 'text/html' && empty($_GET['_download'])) { - $OUTPUT = new rcube_html_page(); + $OUTPUT = new rcmail_html_page(); // @TODO: use washtml on $body $OUTPUT->write($plugin['body']); } else { // don't kill the connection if download takes more than 30 sec. @set_time_limit(0); $filename = $attachment['name']; $filename = preg_replace('[\r\n]', '', $filename); if ($browser->ie && $browser->ver < 7) $filename = rawurlencode(abbreviate_string($filename, 55)); else if ($browser->ie) $filename = rawurlencode($filename); else $filename = addcslashes($filename, '"'); $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline'; header("Content-Disposition: $disposition; filename=\"$filename\""); echo $plugin['body']; } exit; } // if we arrive here, the requested part was not found header('HTTP/1.1 404 Not Found'); exit; } /** * Show "loading..." page in attachment iframe */ public function attachment_loading_page() { $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']); $message = $this->rc->gettext('loadingdata'); header('Content-Type: text/html; charset=' . RCUBE_CHARSET); print "\n\n" . '' . "\n" . '' . "\n" . "\n\n$message\n\n"; exit; } /** * Template object for attachment display frame */ public function attachment_frame($attrib = array()) { $mimetype = strtolower($this->attachment['mimetype']); list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']); $this->rc->output->add_gui_object('attachmentframe', $attrib['id']); return html::iframe($attrib); } /** * */ public function attachment_header($attrib = array()) { $rcmail = rcmail::get_instance(); $dl_link = strtolower($attrib['downloadlink']) == 'true'; $dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET); $table = new html_table(array('cols' => $dl_link ? 3 : 2)); if (!empty($this->attachment['name'])) { $table->add('title', Q($this->rc->gettext('filename'))); $table->add('header', Q($this->attachment['name'])); if ($dl_link) { $table->add('download-link', html::a($dl_url, Q($this->rc->gettext('download')))); } } if (!empty($this->attachment['mimetype'])) { $table->add('title', Q($this->rc->gettext('type'))); $table->add('header', Q($this->attachment['mimetype'])); } if (!empty($this->attachment['size'])) { $table->add('title', Q($this->rc->gettext('filesize'))); $table->add('header', Q(show_bytes($this->attachment['size']))); } $this->rc->output->set_env('attachment_download_url', $dl_url); return $table->show($attrib); } /********* iTip message detection *********/ /** * Check mail message structure of there are .ics files attached */ public function mail_message_load($p) { $this->ical_message = $p['object']; $itip_part = null; // check all message parts for .ics files foreach ((array)$this->ical_message->mime_parts as $part) { if (self::part_is_vcalendar($part)) { if ($part->ctype_parameters['method']) $itip_part = $part->mime_id; else $this->ical_parts[] = $part->mime_id; } } // priorize part with method parameter if ($itip_part) { $this->ical_parts = array($itip_part); } } /** * Getter for the parsed iCal objects attached to the current email message * * @return object libvcalendar parser instance with the parsed objects */ public function get_mail_ical_objects() { // create parser and load ical objects if (!$this->mail_ical_parser) { $this->mail_ical_parser = $this->get_ical(); foreach ($this->ical_parts as $mime_id) { $part = $this->ical_message->mime_parts[$mime_id]; - $charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET; + $charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET; $this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset); // check if the parsed object is an instance of a recurring event/task array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance'); // stop on the part that has an iTip method specified if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) { $this->mail_ical_parser->message_date = $this->ical_message->headers->date; $this->mail_ical_parser->mime_id = $mime_id; // store the message's sender address for comparisons $this->mail_ical_parser->sender = preg_match(self::$email_regex, $this->ical_message->headers->from, $m) ? $m[1] : ''; if (!empty($this->mail_ical_parser->sender)) { foreach ($this->mail_ical_parser->objects as $i => $object) { $this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender; $this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender); } } break; } } } return $this->mail_ical_parser; } /** * Read the given mime message from IMAP and parse ical data * * @param string Mailbox name * @param string Message UID * @param string Message part ID and object index (e.g. '1.2:0') * @param string Object type filter (optional) * * @return array Hash array with the parsed iCal */ public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null) { - $charset = RCMAIL_CHARSET; + $charset = RCUBE_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); $headers = $imap->get_message_headers($uid); $parser = $this->get_ical(); if ($part->ctype_parameters['charset']) { $charset = $part->ctype_parameters['charset']; } if ($part) { $objects = $parser->import($part, $charset); } } // successfully parsed events/tasks? if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) { if ($parser->method) $object['_method'] = $parser->method; // store the message's sender address for comparisons $object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : ''; $object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']); // check if this is an instance of a recurring event/task self::identify_recurrence_instance($object); return $object; } return null; } /** * Checks if specified message part is a vcalendar data * * @param rcube_message_part Part object * @return boolean True if part is of type vcard */ public static function part_is_vcalendar($part) { return ( in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || // Apple sends files as application/x-any (!?) ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) ); } /** * Single occourrences of recurring events are identified by their RECURRENCE-ID property * in iCal which is represented as 'recurrence_date' in our internal data structure. * * Check if such a property exists and derive the '_instance' identifier and '_savemode' * attributes which are used in the storage backend to identify the nested exception item. */ public static function identify_recurrence_instance(&$object) { // for savemode=all, remove recurrence instance identifiers if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) { unset($object['_instance'], $object['recurrence_date']); } // set instance and 'savemode' according to recurrence-id else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) { $object['_instance'] = self::recurrence_instance_identifier($object); $object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current'; } else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) { if (strlen($object['_instance']) > 4) { $object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone()); } else { $object['recurrence_date'] = clone $object['start']; } } } /** * Return a date() format string to render identifiers for recurrence instances * * @param array Hash array with event properties * @return string Format string */ public static function recurrence_id_format($event) { return $event['allday'] ? 'Ymd' : 'Ymd\THis'; } /** * Return the identifer for the given instance of a recurring event * * @param array Hash array with event properties * @return mixed Format string or null if identifier cannot be generated */ public static function recurrence_instance_identifier($event) { $instance_date = $event['recurrence_date'] ?: $event['start']; if ($instance_date && is_a($instance_date, 'DateTime')) { $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis'; return $instance_date->format($recurrence_id_format); } return null; } /********* Attendee handling functions *********/ /** * Handler for attendee group expansion requests */ public function expand_attendee_group() { $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST); $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); $result = array('id' => $id, 'members' => array()); $maxnum = 500; // iterate over all autocomplete address books (we don't know the source of the group) foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) { if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) { foreach ($abook->list_groups($data['name'], 1) as $group) { // this is the matching group to expand if (in_array($data['email'], (array)$group['email'])) { $abook->set_pagesize($maxnum); $abook->set_group($group['ID']); // get all members $res = $abook->list_records($this->rc->config->get('contactlist_fields')); // handle errors (e.g. sizelimit, timelimit) if ($abook->get_error()) { $result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring'); $res = false; } // check for maximum number of members (we don't wanna bloat the UI too much) else if ($res->count > $maxnum) { $result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring'); $res = false; } while ($res && ($member = $res->iterate())) { $emails = (array)$abook->get_col_values('email', $member, true); if (!empty($emails) && ($email = array_shift($emails))) { $result['members'][] = array( 'email' => $email, 'name' => rcube_addressbook::compose_list_name($member), ); } } break 2; } } } } $this->rc->output->command('plugin.expand_attendee_callback', $result); } /********* Static utility functions *********/ /** * Convert the internal structured data into a vcalendar rrule 2.0 string */ public static function to_rrule($recurrence, $allday = false) { if (is_string($recurrence)) return $recurrence; $rrule = ''; foreach ((array)$recurrence as $k => $val) { $k = strtoupper($k); switch ($k) { case 'UNTIL': // convert to UTC according to RFC 5545 if (is_a($val, 'DateTime')) { if (!$allday && !$val->_dateonly) { $until = clone $val; $until->setTimezone(new DateTimeZone('UTC')); $val = $until->format('Ymd\THis\Z'); } else { $val = $val->format('Ymd'); } } break; case 'RDATE': case 'EXDATE': foreach ((array)$val as $i => $ex) { if (is_a($ex, 'DateTime')) $val[$i] = $ex->format('Ymd\THis'); } $val = join(',', (array)$val); break; case 'EXCEPTIONS': continue 2; } if (strlen($val)) $rrule .= $k . '=' . $val . ';'; } return rtrim($rrule, ';'); } /** * Convert from fullcalendar date format to PHP date() format string */ public static function to_php_date_format($from) { // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s" return strtr(strtr($from, array( 'yyyy' => 'Y', 'yy' => 'y', 'MMMM' => 'F', 'MMM' => 'M', 'MM' => 'm', 'M' => 'n', 'dddd' => 'l', 'ddd' => 'D', 'dd' => 'd', 'd' => 'j', 'HH' => '**', 'hh' => '%%', 'H' => 'G', 'h' => 'g', 'mm' => 'i', 'ss' => 's', 'TT' => 'A', 'tt' => 'a', 'T' => 'A', 't' => 'a', 'u' => 'c', )), array( '**' => 'H', '%%' => 'h', )); } /** * Convert from PHP date() format to fullcalendar format string */ public static function from_php_date_format($from) { // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss" return strtr($from, array( 'y' => 'yy', 'Y' => 'yyyy', 'M' => 'MMM', 'F' => 'MMMM', 'm' => 'MM', 'n' => 'M', 'j' => 'd', 'd' => 'dd', 'D' => 'ddd', 'l' => 'dddd', 'H' => 'HH', 'h' => 'hh', 'G' => 'H', 'g' => 'h', 'i' => 'mm', 's' => 'ss', 'A' => 'TT', 'a' => 'tt', 'c' => 'u', )); } } diff --git a/plugins/odfviewer/odfviewer.php b/plugins/odfviewer/odfviewer.php index 5fcde31c..42332e5a 100644 --- a/plugins/odfviewer/odfviewer.php +++ b/plugins/odfviewer/odfviewer.php @@ -1,109 +1,109 @@ * * Copyright (C) 2011-2013, 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 odfviewer extends rcube_plugin { public $task = 'mail|calendar|tasks'; private $odf_mimetypes = array( 'application/vnd.oasis.opendocument.chart', 'application/vnd.oasis.opendocument.chart-template', 'application/vnd.oasis.opendocument.formula', 'application/vnd.oasis.opendocument.formula-template', 'application/vnd.oasis.opendocument.graphics', 'application/vnd.oasis.opendocument.graphics-template', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.presentation-template', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-master', 'application/vnd.oasis.opendocument.text-template', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template', ); function init() { // webODF only supports IE9 or higher $ua = new rcube_browser; if ($ua->ie && $ua->ver < 9) { return; } // extend list of mimetypes that should open in preview $rcmail = rcube::get_instance(); if ($rcmail->action == 'preview' || $rcmail->action == 'show' || $rcmail->task == 'calendar' || $rcmail->task == 'tasks') { $mimetypes = (array)$rcmail->config->get('client_mimetypes'); $rcmail->config->set('client_mimetypes', array_merge($mimetypes, $this->odf_mimetypes)); } $this->add_hook('message_part_get', array($this, 'get_part')); } /** * Handler for message attachment download */ function get_part($args) { if (!$args['download'] && $args['mimetype'] && in_array($args['mimetype'], $this->odf_mimetypes)) { $rcmail = rcube::get_instance(); $params = array( 'documentUrl' => $_SERVER['REQUEST_URI'] . '&_download=1', 'filename' => $args['part']->filename ?: 'file.odt', 'type' => $args['mimetype'], ); // send webODF viewer page $html = file_get_contents($this->home . '/odf.html'); - header("Content-Type: text/html; charset=" . RCMAIL_CHARSET); + header("Content-Type: text/html; charset=" . RCUBE_CHARSET); echo strtr($html, array( '%%PARAMS%%' => rcube_output::json_serialize($params), '%%viewer.css%%' => $this->asset_path('viewer.css'), '%%viewer.js%%' => $this->asset_path('viewer.js'), '%%ODFViewerPlugin.js%%' => $this->asset_path('ODFViewerPlugin.js'), '%%webodf.js%%' => $this->asset_path('webodf.js'), )); $args['abort'] = true; } return $args; } private function asset_path($path) { $rcmail = rcube::get_instance(); $assets_dir = $rcmail->config->get('assets_dir'); $mtime = filemtime($this->home . '/' . $path); if (!$mtime && $assets_dir) { $mtime = filemtime($assets_dir . '/plugins/odfviewer/' . $path); } $path = $this->urlbase . $path . ($mtime ? '?s=' . $mtime : ''); return $rcmail->output->asset_url($path); } } diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index e569f218..2fb81c71 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -1,2277 +1,2277 @@ * * 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 tasklist extends rcube_plugin { const FILTER_MASK_TODAY = 1; const FILTER_MASK_TOMORROW = 2; const FILTER_MASK_WEEK = 4; const FILTER_MASK_LATER = 8; const FILTER_MASK_NODATE = 16; const FILTER_MASK_OVERDUE = 32; const FILTER_MASK_FLAGGED = 64; const FILTER_MASK_COMPLETE = 128; const FILTER_MASK_ASSIGNED = 256; const FILTER_MASK_MYTASKS = 512; const SESSION_KEY = 'tasklist_temp'; public static $filter_masks = array( 'today' => self::FILTER_MASK_TODAY, 'tomorrow' => self::FILTER_MASK_TOMORROW, 'week' => self::FILTER_MASK_WEEK, 'later' => self::FILTER_MASK_LATER, 'nodate' => self::FILTER_MASK_NODATE, 'overdue' => self::FILTER_MASK_OVERDUE, 'flagged' => self::FILTER_MASK_FLAGGED, 'complete' => self::FILTER_MASK_COMPLETE, 'assigned' => self::FILTER_MASK_ASSIGNED, 'mytasks' => self::FILTER_MASK_MYTASKS, ); public $task = '?(?!login|logout).*'; public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order'); public $rc; public $lib; public $driver; public $timezone; public $ui; public $home; // declare public to be used in other classes private $collapsed_tasks = array(); private $message_tasks = array(); private $itip; private $ical; /** * Plugin initialization. */ function init() { $this->require_plugin('libcalendaring'); $this->require_plugin('jqueryui'); $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->register_task('tasks', 'tasklist'); // load plugin configuration $this->load_config(); $this->timezone = $this->lib->timezone; // proceed initialization in startup hook $this->add_hook('startup', array($this, 'startup')); $this->add_hook('user_delete', array($this, 'user_delete')); } /** * Startup hook */ public function startup($args) { // the tasks module can be enabled/disabled by the kolab_auth plugin if ($this->rc->config->get('tasklist_disabled', false) || !$this->rc->config->get('tasklist_enabled', true)) return; // load localizations $this->add_texts('localization/', $args['task'] == 'tasks' && (!$args['action'] || $args['action'] == 'print')); $this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle'))); // add label for task title if ($args['task'] == 'tasks' && $args['action'] != 'save-pref') { $this->load_driver(); // register calendar actions $this->register_action('index', array($this, 'tasklist_view')); $this->register_action('task', array($this, 'task_action')); $this->register_action('tasklist', array($this, 'tasklist_action')); $this->register_action('counts', array($this, 'fetch_counts')); $this->register_action('fetch', array($this, 'fetch_tasks')); $this->register_action('print', array($this, 'print_tasks')); $this->register_action('inlineui', array($this, 'get_inline_ui')); $this->register_action('mail2task', array($this, 'mail_message2task')); $this->register_action('get-attachment', array($this, 'attachment_get')); $this->register_action('upload', array($this, 'attachment_upload')); $this->register_action('mailimportitip', array($this, 'mail_import_itip')); $this->register_action('mailimportattach', array($this, 'mail_import_attachment')); $this->register_action('itip-status', array($this, 'task_itip_status')); $this->register_action('itip-remove', array($this, 'task_itip_remove')); $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply')); $this->register_action('itip-delegate', array($this, 'mail_itip_delegate')); $this->add_hook('refresh', array($this, 'refresh')); $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', ''))); } else if ($args['task'] == 'mail') { if ($args['action'] == 'show' || $args['action'] == 'preview') { if ($this->rc->config->get('tasklist_mail_embed', true)) { $this->add_hook('message_load', array($this, 'mail_message_load')); } $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } // add 'Create event' item to message menu if ($this->api->output->type == 'html') { $this->api->add_content(html::tag('li', null, $this->api->output->button(array( 'command' => 'tasklist-create-from-mail', 'label' => 'tasklist.createfrommail', 'type' => 'link', 'classact' => 'icon taskaddlink active', 'class' => 'icon taskaddlink', 'innerclass' => 'icon taskadd', ))), 'messagemenu'); $this->api->output->add_label('tasklist.createfrommail'); } } if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) { $this->load_ui(); $this->ui->init(); } // add hooks for alarms handling $this->add_hook('pending_alarms', array($this, 'pending_alarms')); $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms')); } /** * */ private function load_ui() { if (!$this->ui) { require_once($this->home . '/tasklist_ui.php'); $this->ui = new tasklist_ui($this); } } /** * Helper method to load the backend driver according to local config */ private function load_driver() { if (is_object($this->driver)) return; $driver_name = $this->rc->config->get('tasklist_driver', 'database'); $driver_class = 'tasklist_' . $driver_name . '_driver'; require_once($this->home . '/drivers/tasklist_driver.php'); require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); switch ($driver_name) { case "kolab": $this->require_plugin('libkolab'); default: $this->driver = new $driver_class($this); break; } $this->rc->output->set_env('tasklist_driver', $driver_name); } /** * Dispatcher for task-related actions initiated by the client */ public function task_action() { $filter = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)); $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true); $oldrec = $rec; $success = $refresh = $got_msg = false; // force notify if hidden + active $itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3); if ($itip_send_option === 1 && empty($rec['_reportpartstat'])) $rec['_notify'] = 1; switch ($action) { case 'new': $oldrec = null; $rec = $this->prepare_task($rec); $rec['uid'] = $this->generate_uid(); $temp_id = $rec['tempid']; if ($success = $this->driver->create_task($rec)) { $refresh = $this->driver->get_task($rec); if ($temp_id) $refresh['tempid'] = $temp_id; $this->cleanup_task($rec); } break; case 'complete': $complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST)); if (!($rec = $this->driver->get_task($rec))) { break; } $oldrec = $rec; $rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION'); // sent itip notifications if enabled (no user interaction here) if (($itip_send_option & 1)) { if ($this->is_attendee($rec)) { $rec['_reportpartstat'] = $rec['status']; } else if ($this->is_organizer($rec)) { $rec['_notify'] = 1; } } case 'edit': $oldrec = $this->driver->get_task($rec); $rec = $this->prepare_task($rec); $clone = $this->handle_recurrence($rec, $this->driver->get_task($rec)); if ($success = $this->driver->edit_task($rec)) { $refresh[] = $this->driver->get_task($rec); $this->cleanup_task($rec); // add clone from recurring task if ($clone && $this->driver->create_task($clone)) { $refresh[] = $this->driver->get_task($clone); $this->driver->clear_alarms($rec['id']); } // move all childs if list assignment was changed if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) { foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) { $child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']); if ($this->driver->move_task($child)) { $r = $this->driver->get_task($child); if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) { $refresh[] = $r; } } } } } break; case 'move': foreach ((array)$rec['id'] as $id) { $r = $rec; $r['id'] = $id; if ($this->driver->move_task($r)) { $new_task = $this->driver->get_task($r); $new_task['tempid'] = $id; $refresh[] = $new_task; $success = true; // move all childs, too foreach ($this->driver->get_childs(array('id' => $id, 'list' => $rec['_fromlist']), true) as $cid) { $child = $rec; $child['id'] = $cid; if ($this->driver->move_task($child)) { $r = $this->driver->get_task($child); if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) { $r['tempid'] = $cid; $refresh[] = $r; } } } } } break; case 'delete': $mode = intval(rcube_utils::get_input_value('mode', rcube_utils::INPUT_POST)); $oldrec = $this->driver->get_task($rec); if ($success = $this->driver->delete_task($rec, false)) { // delete/modify all childs foreach ($this->driver->get_childs($rec, $mode) as $cid) { $child = array('id' => $cid, 'list' => $rec['list']); if ($mode == 1) { // delete all childs if ($this->driver->delete_task($child, false)) { if ($this->driver->undelete) $_SESSION['tasklist_undelete'][$rec['id']][] = $cid; } else $success = false; } else { $child['parent_id'] = strval($oldrec['parent_id']); $this->driver->edit_task($child); } } // update parent task to adjust list of children if (!empty($oldrec['parent_id'])) { $refresh[] = $this->driver->get_task(array('id' => $oldrec['parent_id'], 'list' => $rec['list'])); } } if (!$success) $this->rc->output->command('plugin.reload_data'); break; case 'undelete': if ($success = $this->driver->undelete_task($rec)) { $refresh[] = $this->driver->get_task($rec); foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) { if ($this->driver->undelete_task($rec)) { $refresh[] = $this->driver->get_task($rec); } } } break; case 'collapse': foreach (explode(',', $rec['id']) as $rec_id) { if (intval(rcube_utils::get_input_value('collapsed', rcube_utils::INPUT_GPC))) { $this->collapsed_tasks[] = $rec_id; } else { $i = array_search($rec_id, $this->collapsed_tasks); if ($i !== false) unset($this->collapsed_tasks[$i]); } } $this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks)))); return; // avoid further actions case 'rsvp': $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC); $noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action'; $task = $this->driver->get_task($rec); $task['attendees'] = $rec['attendees']; $task['_type'] = 'task'; // send invitation to delegatee + add it as attendee if ($status == 'delegated' && $rec['to']) { $itip = $this->load_itip(); if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) { $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); $refresh[] = $task; $noreply = false; } } $rec = $task; if ($success = $this->driver->edit_task($rec)) { if (!$noreply) { // let the reply clause further down send the iTip message $rec['_reportpartstat'] = $status; } } break; case 'changelog': $data = $this->driver->get_task_changelog($rec); if (is_array($data) && !empty($data)) { $lib = $this->lib; $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); array_walk($data, function(&$change) use ($lib, $dtformat) { if ($change['date']) { $dt = $lib->adjust_timezone($change['date']); if ($dt instanceof DateTime) { $change['date'] = $this->rc->format_date($dt, $dtformat, false); } } }); $this->rc->output->command('plugin.task_render_changelog', $data); } else { $this->rc->output->command('plugin.task_render_changelog', false); } $got_msg = true; break; case 'diff': $data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']); if (is_array($data)) { // convert some properties, similar to self::_client_event() $lib = $this->lib; $date_format = $this->rc->config->get('date_format', 'Y-m-d'); $time_format = $this->rc->config->get('time_format', 'H:i'); array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) { // convert date cols if (in_array($change['property'], array('date','start','created','changed'))) { if (!empty($change['old'])) { $dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format; $change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat); } if (!empty($change['new'])) { $dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format; $change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat); } } // create textual representation for alarms and recurrence if ($change['property'] == 'alarms') { if (is_array($change['old'])) $change['old_'] = libcalendaring::alarm_text($change['old']); if (is_array($change['new'])) $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'recurrence') { if (is_array($change['old'])) $change['old_'] = $lib->recurrence_text($change['old']); if (is_array($change['new'])) $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'complete') { $change['old_'] = intval($change['old']) . '%'; $change['new_'] = intval($change['new']) . '%'; } if ($change['property'] == 'attachments') { if (is_array($change['old'])) $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']); if (is_array($change['new'])) { $change['new'] = array_merge((array)$change['old'], $change['new']); $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']); } } // resolve parent_id to the refered task title for display if ($change['property'] == 'parent_id') { $change['property'] = 'parent-title'; if (!empty($change['old']) && ($old_parent = $this->driver->get_task(array('id' => $change['old'], 'list' => $rec['list'])))) { $change['old_'] = $old_parent['title']; } if (!empty($change['new']) && ($new_parent = $this->driver->get_task(array('id' => $change['new'], 'list' => $rec['list'])))) { $change['new_'] = $new_parent['title']; } } // compute a nice diff of description texts if ($change['property'] == 'description') { $change['diff_'] = libkolab::html_diff($change['old'], $change['new']); } }); $this->rc->output->command('plugin.task_show_diff', $data); } else { $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); } $got_msg = true; break; case 'show': if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) { $this->encode_task($rec); $rec['readonly'] = 1; $this->rc->output->command('plugin.task_show_revision', $rec); } else { $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error'); } $got_msg = true; break; case 'restore': if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) { $refresh = $this->driver->get_task($rec); $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation'); $this->rc->output->command('plugin.close_history_dialog'); } else { $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error'); } $got_msg = true; break; } if ($success) { $this->rc->output->show_message('successfullysaved', 'confirmation'); $this->update_counts($oldrec, $refresh); } else if (!$got_msg) { $this->rc->output->show_message('tasklist.errorsaving', 'error'); } // send out notifications if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) { // make sure we have the complete record $task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec); // only notify if data really changed (TODO: do diff check on client already) if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) { $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']); if ($sent > 0) $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); else if ($sent < 0) $this->rc->output->show_message('tasklist.errornotifying', 'error'); } } else if ($success && $rec['_reportpartstat'] && $rec['_reportpartstat'] != 'NEEDS-ACTION') { // get the full record after update $task = $this->driver->get_task($rec); // send iTip REPLY with the updated partstat if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) { $sender = $task['attendees'][$idx]; $status = strtolower($sender['status']); if (!empty($_POST['comment'])) $task['comment'] = rcube_utils::get_input_value('comment', rcube_utils::INPUT_POST); $itip = $this->load_itip(); $itip->set_sender_email($sender['email']); if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } } // unlock client $this->rc->output->command('plugin.unlock_saving'); if ($refresh) { if ($refresh['id']) { $this->encode_task($refresh); } else if (is_array($refresh)) { foreach ($refresh as $i => $r) $this->encode_task($refresh[$i]); } $this->rc->output->command('plugin.update_task', $refresh); } } /** * Load iTIP functions */ private function load_itip() { if (!$this->itip) { require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php'); $this->itip = new libcalendaring_itip($this, 'tasklist'); $this->itip->set_rsvp_actions(array('accepted','declined','delegated')); $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed')); } return $this->itip; } /** * repares new/edited task properties before save */ private function prepare_task($rec) { // try to be smart and extract date from raw input if ($rec['raw']) { foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) { $locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i'; $normwords[] = $word; $datewords[] = $word; } foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) { $locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i'; $normwords[] = $month; $datewords[] = $month; } foreach (array('on','this','next','at') as $word) { $fillwords[] = preg_quote(mb_strtolower($this->gettext($word))); $fillwords[] = $word; } $raw = trim($rec['raw']); $date_str = ''; // translate localized keywords $raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw); $raw = preg_replace($locwords, $normwords, $raw); // find date pattern $date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i'; if (preg_match($date_pattern, $raw, $m)) { $date_str .= $m[1] . $m[2] . $m[3]; $raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw); // add year to date string if ($m[1] && !$m[3]) $date_str .= date('Y'); } // find time pattern $time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i'; if (preg_match($time_pattern, $raw, $m)) { $has_time = true; $date_str .= ($date_str ? ' ' : 'today ') . $m[1]; $raw = preg_replace($time_pattern, '', $raw); } // yes, raw input matched a (valid) date if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) { $rec['date'] = $date->format('Y-m-d'); if ($has_time) $rec['time'] = $date->format('H:i'); $rec['title'] = $raw; } else $rec['title'] = $rec['raw']; } // normalize input from client if (isset($rec['complete'])) { $rec['complete'] = floatval($rec['complete']); if ($rec['complete'] > 1) $rec['complete'] /= 100; } if (isset($rec['flagged'])) $rec['flagged'] = intval($rec['flagged']); // fix for garbage input if ($rec['description'] == 'null') $rec['description'] = ''; foreach ($rec as $key => $val) { if ($val === 'null') $rec[$key] = null; } if (!empty($rec['date'])) { $this->normalize_dates($rec, 'date', 'time'); } if (!empty($rec['startdate'])) { $this->normalize_dates($rec, 'startdate', 'starttime'); } // convert tags to array, filter out empty entries if (isset($rec['tags']) && !is_array($rec['tags'])) { $rec['tags'] = array_filter((array)$rec['tags']); } // convert the submitted alarm values if ($rec['valarms']) { $valarms = array(); foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) { // alarms can only work with a date (either task start, due or absolute alarm date) if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate']) $valarms[] = $alarm; } $rec['valarms'] = $valarms; } // convert the submitted recurrence settings if (is_array($rec['recurrence'])) { $refdate = null; if (!empty($rec['date'])) { $refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone); } else if (!empty($rec['startdate'])) { $refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone); } if ($refdate) { $rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate); // translate count into an absolute end date. // why? because when shifting completed tasks to the next recurrence, // the initial start date to count from gets lost. if ($rec['recurrence']['COUNT']) { $engine = libcalendaring::get_recurrence(); $engine->init($rec['recurrence'], $refdate); if ($until = $engine->end()) { $rec['recurrence']['UNTIL'] = $until; unset($rec['recurrence']['COUNT']); } } } else { // recurrence requires a reference date $rec['recurrence'] = ''; } } $attachments = array(); $taskid = $rec['id']; if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) { if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) { foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) { if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) { $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment); unset($attachments[$id]['abort'], $attachments[$id]['group']); } } } } $rec['attachments'] = $attachments; // convert link references into simple URIs if (array_key_exists('links', $rec)) { $rec['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$rec['links']); } // convert invalid data if (isset($rec['attendees']) && !is_array($rec['attendees'])) $rec['attendees'] = array(); foreach ((array)$rec['attendees'] as $i => $attendee) { if (is_string($attendee['rsvp'])) { $rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1'; } } // copy the task status to my attendee partstat if (!empty($rec['_reportpartstat'])) { if (($idx = $this->is_attendee($rec)) !== false) { if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED')) $rec['attendees'][$idx]['status'] = $rec['_reportpartstat']; else unset($rec['_reportpartstat']); } } // set organizer from identity selector if ((isset($rec['_identity']) || (!empty($rec['attendees']) && empty($rec['organizer']))) && ($identity = $this->rc->user->get_identity($rec['_identity']))) { $rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']); } if (is_numeric($rec['id']) && $rec['id'] < 0) unset($rec['id']); return $rec; } /** * Utility method to convert a tasks date/time values into a normalized format */ private function normalize_dates(&$rec, $date_key, $time_key) { try { // parse date from user format (#2801) $date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d'); $date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone); // fall back to default strtotime logic if (empty($date)) { $date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone); } $rec[$date_key] = $date->format('Y-m-d'); if (!empty($rec[$time_key])) $rec[$time_key] = $date->format('H:i'); return true; } catch (Exception $e) { $rec[$date_key] = $rec[$time_key] = null; } return false; } /** * Releases some resources after successful save */ private function cleanup_task(&$rec) { // remove temp. attachment files if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) { $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid)); $this->rc->session->remove(self::SESSION_KEY); } } /** * When flagging a recurring task as complete, * clone it and shift dates to the next occurrence */ private function handle_recurrence(&$rec, $old) { $clone = null; if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) { $engine = libcalendaring::get_recurrence(); $rrule = $rec['recurrence']; $updates = array(); // compute the next occurrence of date attributes foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) { if (empty($rec[$date_key])) continue; $date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone); $engine->init($rrule, $date); if ($next = $engine->next()) { $updates[$date_key] = $next->format('Y-m-d'); if (!empty($rec[$time_key])) $updates[$time_key] = $next->format('H:i'); } } // shift absolute alarm dates if (!empty($updates) && is_array($rec['valarms'])) { $updates['valarms'] = array(); unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited foreach ($rec['valarms'] as $i => $alarm) { if ($alarm['trigger'] instanceof DateTime) { $engine->init($rrule, $alarm['trigger']); if ($next = $engine->next()) { $alarm['trigger'] = $next; } } $updates['valarms'][$i] = $alarm; } } if (!empty($updates)) { // clone task to save a completed copy $clone = $rec; $clone['uid'] = $this->generate_uid(); $clone['parent_id'] = $rec['id']; unset($clone['id'], $clone['recurrence'], $clone['attachments']); // update the task but unset completed flag $rec = array_merge($rec, $updates); $rec['complete'] = $old['complete']; $rec['status'] = $old['status']; } } return $clone; } /** * Send out an invitation/notification to all task attendees */ private function notify_attendees($task, $old, $action = 'edit', $comment = null) { if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) { $task['cancelled'] = true; $is_cancelled = true; } $itip = $this->load_itip(); $emails = $this->lib->get_user_emails(); $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3); // add comment to the iTip attachment $task['comment'] = $comment; // needed to generate VTODO instead of VEVENT entry $task['_type'] = 'task'; // compose multipart message using PEAR:Mail_Mime $method = $action == 'delete' ? 'CANCEL' : 'REQUEST'; $object = $this->to_libcal($task); $message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']); // list existing attendees from the $old task $old_attendees = array(); foreach ((array)$old['attendees'] as $attendee) { $old_attendees[] = $attendee['email']; } // send to every attendee $sent = 0; $current = array(); foreach ((array)$task['attendees'] as $attendee) { $current[] = strtolower($attendee['email']); // skip myself for obvious reasons if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) { continue; } // skip if notification is disabled for this attendee if ($attendee['noreply'] && $itip_notify & 2) { continue; } // skip if this attendee has delegated and set RSVP=FALSE if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) { continue; } // which template to use for mail text $is_new = !in_array($attendee['email'], $old_attendees); $is_rsvp = $is_new || $task['sequence'] > $old['sequence']; $bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody'); $subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty')); // finally send the message if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message, $is_rsvp)) $sent++; else $sent = -100; } // send CANCEL message to removed attendees foreach ((array)$old['attendees'] as $attendee) { if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) { continue; } $vtodo = $this->to_libcal($old); $vtodo['cancelled'] = $is_cancelled; $vtodo['attendees'] = array($attendee); $vtodo['comment'] = $comment; if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody')) $sent++; else $sent = -100; } return $sent; } /** * Compare two task objects and return differing properties * * @param array Event A * @param array Event B * @return array List of differing task properties */ public static function task_diff($a, $b) { $diff = array(); $ignore = array('changed' => 1, 'attachments' => 1); foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { if (!$ignore[$key] && $a[$key] != $b[$key]) $diff[] = $key; } // only compare number of attachments if (count($a['attachments']) != count($b['attachments'])) $diff[] = 'attachments'; return $diff; } /** * Dispatcher for tasklist actions initiated by the client */ public function tasklist_action() { $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $list = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true); $success = false; if (isset($list['showalarms'])) $list['showalarms'] = intval($list['showalarms']); switch ($action) { case 'form-new': case 'form-edit': echo $this->ui->tasklist_editform($action, $list); exit; case 'new': $list += array('showalarms' => true, 'active' => true, 'editable' => true); if ($insert_id = $this->driver->create_list($list)) { $list['id'] = $insert_id; if (!$list['_reload']) { $this->load_ui(); $list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv); $list += (array)$jsenv[$insert_id]; } $this->rc->output->command('plugin.insert_tasklist', $list); $success = true; } break; case 'edit': if ($newid = $this->driver->edit_list($list)) { $list['oldid'] = $list['id']; $list['id'] = $newid; $this->rc->output->command('plugin.update_tasklist', $list); $success = true; } break; case 'subscribe': $success = $this->driver->subscribe_list($list); break; case 'delete': if (($success = $this->driver->delete_list($list))) $this->rc->output->command('plugin.destroy_tasklist', $list); break; case 'search': $this->load_ui(); $results = array(); $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); foreach ((array)$this->driver->search_lists($query, $source) as $id => $prop) { $editname = $prop['editname']; unset($prop['editname']); // force full name to be displayed $prop['active'] = false; // let the UI generate HTML and CSS representation for this calendar $html = $this->ui->tasklist_list_item($id, $prop, $jsenv); $prop += (array)$jsenv[$id]; $prop['editname'] = $editname; $prop['html'] = $html; $results[] = $prop; } // report more results available if ($this->driver->search_more_results) { $this->rc->output->show_message('autocompletemore', 'info'); } $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC)); return; } if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else $this->rc->output->show_message('tasklist.errorsaving', 'error'); $this->rc->output->command('plugin.unlock_saving'); } /** * Get counts for active tasks divided into different selectors */ public function fetch_counts() { if (isset($_REQUEST['lists'])) { $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC); } else { foreach ($this->driver->get_lists() as $list) { if ($list['active']) $lists[] = $list['id']; } } $counts = $this->driver->count_tasks($lists); $this->rc->output->command('plugin.update_counts', $counts); } /** * Adjust the cached counts after changing a task */ public function update_counts($oldrec, $newrec) { // rebuild counts until this function is finally implemented $this->fetch_counts(); // $this->rc->output->command('plugin.update_counts', $counts); } /** * */ public function fetch_tasks() { $f = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)); $search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC); $filter = array('mask' => $f, 'search' => $search); /* // convert magic date filters into a real date range switch ($f) { case self::FILTER_MASK_TODAY: $today = new DateTime('now', $this->timezone); $filter['from'] = $filter['to'] = $today->format('Y-m-d'); break; case self::FILTER_MASK_TOMORROW: $tomorrow = new DateTime('now + 1 day', $this->timezone); $filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d'); break; case self::FILTER_MASK_OVERDUE: $yesterday = new DateTime('yesterday', $this->timezone); $filter['to'] = $yesterday->format('Y-m-d'); break; case self::FILTER_MASK_WEEK: $today = new DateTime('now', $this->timezone); $filter['from'] = $today->format('Y-m-d'); $weekend = new DateTime('now + 7 days', $this->timezone); $filter['to'] = $weekend->format('Y-m-d'); break; case self::FILTER_MASK_LATER: $date = new DateTime('now + 8 days', $this->timezone); $filter['from'] = $date->format('Y-m-d'); break; } */ $data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f); $this->rc->output->command('plugin.data_ready', array( 'filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => $this->driver->get_tags(), )); } /** * Handler for printing calendars */ public function print_tasks() { // Add CSS stylesheets to the page header $skin_path = $this->local_skin_path(); $this->include_stylesheet($skin_path . '/print.css'); $this->include_script('tasklist.js'); $this->rc->output->add_handlers(array( 'plugin.tasklist_print' => array($this, 'print_tasks_list'), )); $this->rc->output->set_pagetitle($this->gettext('print')); $this->rc->output->send('tasklist.print'); } /** * Handler for printing calendars */ public function print_tasks_list($attrib) { $f = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)); $search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC); $filter = array('mask' => $f, 'search' => $search); $data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f); // we'll build the tasks table in javascript on page load // where we have sorting methods, etc. $this->rc->output->set_env('tasks', $data); return $this->ui->tasks_resultview($attrib); } /** * Prepare and sort the given task records to be sent to the client */ private function tasks_data($records, $f) { $data = $this->task_tree = $this->task_titles = array(); foreach ($records as $rec) { if ($rec['parent_id']) { $this->task_tree[$rec['id']] = $rec['parent_id']; } $this->encode_task($rec); // apply filter; don't trust the driver on this :-) if ((!$f && !$this->driver->is_complete($rec)) || ($rec['mask'] & $f)) $data[] = $rec; } // assign hierarchy level indicators for later sorting array_walk($data, array($this, 'task_walk_tree')); return $data; } /** * Prepare the given task record before sending it to the client */ private function encode_task(&$rec) { $rec['mask'] = $this->filter_mask($rec); $rec['flagged'] = intval($rec['flagged']); $rec['complete'] = floatval($rec['complete']); if (is_object($rec['created'])) { $rec['created_'] = $this->rc->format_date($rec['created']); $rec['created'] = $rec['created']->format('U'); } if (is_object($rec['changed'])) { $rec['changed_'] = $this->rc->format_date($rec['changed']); $rec['changed'] = $rec['changed']->format('U'); } else { $rec['changed'] = null; } if ($rec['date']) { try { $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone); $rec['datetime'] = intval($date->format('U')); $rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d')); $rec['_hasdate'] = 1; } catch (Exception $e) { $rec['date'] = $rec['datetime'] = null; } } else { $rec['date'] = $rec['datetime'] = null; $rec['_hasdate'] = 0; } if ($rec['startdate']) { try { $date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone); $rec['startdatetime'] = intval($date->format('U')); $rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d')); } catch (Exception $e) { $rec['startdate'] = $rec['startdatetime'] = null; } } if ($rec['valarms']) { $rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']); $rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']); } if ($rec['recurrence']) { $rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']); $rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']); } foreach ((array)$rec['attachments'] as $k => $attachment) { $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); } // convert link URIs references into structs if (array_key_exists('links', $rec)) { foreach ((array)$rec['links'] as $i => $link) { if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) { $rec['links'][$i] = $msgref; } } } // Convert HTML description into plain text if ($this->is_html($rec)) { $h2t = new rcube_html2text($rec['description'], false, true, 0); $rec['description'] = $h2t->get_text(); } if (!is_array($rec['tags'])) $rec['tags'] = (array)$rec['tags']; sort($rec['tags'], SORT_LOCALE_STRING); if (in_array($rec['id'], $this->collapsed_tasks)) $rec['collapsed'] = true; if (empty($rec['parent_id'])) $rec['parent_id'] = null; $this->task_titles[$rec['id']] = $rec['title']; } /** * Determine whether the given task description is HTML formatted */ private function is_html($task) { // check for opening and closing or tags return (preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m) && strpos($task['description'], '') > 0); } /** * Callback function for array_walk over all tasks. * Sets tree depth and parent titles */ private function task_walk_tree(&$rec) { $rec['_depth'] = 0; $parent_titles = array(); $parent_id = $this->task_tree[$rec['id']]; while ($parent_id) { $rec['_depth']++; array_unshift($parent_titles, $this->task_titles[$parent_id]); $parent_id = $this->task_tree[$parent_id]; } if (count($parent_titles)) { $rec['parent_title'] = join(' ยป ', array_filter($parent_titles)); } } /** * Compute the filter mask of the given task * * @param array Hash array with Task record properties * @return int Filter mask */ public function filter_mask($rec) { static $today, $tomorrow, $weeklimit; if (!$today) { $today_date = new DateTime('now', $this->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); $week_date = new DateTime('now + 7 days', $this->timezone); $weeklimit = $week_date->format('Y-m-d'); } $mask = 0; $start = $rec['startdate'] ?: '1900-00-00'; $duedate = $rec['date'] ?: '3000-00-00'; if ($rec['flagged']) $mask |= self::FILTER_MASK_FLAGGED; if ($this->driver->is_complete($rec)) $mask |= self::FILTER_MASK_COMPLETE; if (empty($rec['date'])) $mask |= self::FILTER_MASK_NODATE; else if ($rec['date'] < $today) $mask |= self::FILTER_MASK_OVERDUE; if ($duedate <= $today || ($rec['startdate'] && $start <= $today)) $mask |= self::FILTER_MASK_TODAY; if ($duedate <= $tomorrow || ($rec['startdate'] && $start <= $tomorrow)) $mask |= self::FILTER_MASK_TOMORROW; if (($start > $tomorrow && $start <= $weeklimit) || ($duedate > $tomorrow && $duedate <= $weeklimit)) $mask |= self::FILTER_MASK_WEEK; else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit)) $mask |= self::FILTER_MASK_LATER; // add masks for assigned tasks if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false) $mask |= self::FILTER_MASK_ASSIGNED; else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false) $mask |= self::FILTER_MASK_MYTASKS; return $mask; } /** * Determine whether the current user is an attendee of the given task */ public function is_attendee($task) { $emails = $this->lib->get_user_emails(); foreach ((array)$task['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { return $i; } } return false; } /** * Determine whether the current user is the organizer of the given task */ public function is_organizer($task) { $emails = $this->lib->get_user_emails(); return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails)); } /******* UI functions ********/ /** * Render main view of the tasklist task */ public function tasklist_view() { $this->ui->init(); $this->ui->init_templates(); // set autocompletion env $this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0)); $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15)); $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length')); $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'close', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata'); $this->rc->output->set_pagetitle($this->gettext('navtitle')); $this->rc->output->send('tasklist.mainview'); } /** * */ public function get_inline_ui() { foreach (array('save','cancel','savingdata') as $label) $texts['tasklist.'.$label] = $this->gettext($label); $texts['tasklist.newtask'] = $this->gettext('createfrommail'); // collect env variables $env = array( 'tasklists' => array(), 'tasklist_settings' => $this->ui->load_settings(), ); $this->ui->init_templates(); echo $this->api->output->parse('tasklist.taskedit', false, false); $script_add = ''; foreach ($this->ui->get_gui_objects() as $obj => $id) { $script_add .= rcmail_output::JS_OBJECT_NAME . ".gui_object('$obj', '$id');\n"; } echo html::tag('script', array('type' => 'text/javascript'), rcmail_output::JS_OBJECT_NAME . ".set_env(" . json_encode($env) . ");\n". rcmail_output::JS_OBJECT_NAME . ".add_label(" . json_encode($texts) . ");\n". $script_add ); exit; } /** * Handler for keep-alive requests * This will check for updated data in active lists and sync them to the client */ public function refresh($attr) { // refresh the entire list every 10th time to also sync deleted items if (rand(0,10) == 10) { $this->rc->output->command('plugin.reload_data'); return; } $filter = array( 'since' => $attr['last'], 'search' => rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), 'mask' => intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)) & self::FILTER_MASK_COMPLETE, ); $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);; $updates = $this->driver->list_tasks($filter, $lists); if (!empty($updates)) { $this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates, 255), true); // update counts $counts = $this->driver->count_tasks($lists); $this->rc->output->command('plugin.update_counts', $counts); } } /** * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. * This will check for pending notifications and pass them to the client */ public function pending_alarms($p) { $this->load_driver(); if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) { foreach ($alarms as $alarm) { // encode alarm object to suit the expectations of the calendaring code if ($alarm['date']) $alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone); $alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task: $alarm['allday'] = empty($alarm['time']) ? 1 : 0; $p['alarms'][] = $alarm; } } return $p; } /** * Handler for alarm dismiss hook triggered by the calendar module */ public function dismiss_alarms($p) { $this->load_driver(); foreach ((array)$p['ids'] as $id) { if (strpos($id, 'task:') === 0) $p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']); } return $p; } /******* Attachment handling *******/ /** * Handler for attachments upload */ public function attachment_upload() { $this->lib->attachment_upload(self::SESSION_KEY); } /** * Handler for attachments download/displaying */ public function attachment_get() { // show loading page if (!empty($_GET['_preload'])) { return $this->lib->attachment_loading_page(); } $task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC); $list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC); $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC); $task = array('id' => $task, 'list' => $list, 'rev' => $rev); $attachment = $this->driver->get_attachment($id, $task); // show part page if (!empty($_GET['_frame'])) { $this->lib->attachment = $attachment; $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame')); $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header')); $this->rc->output->send('tasklist.attachment'); } // deliver attachment content else if ($attachment) { $attachment['body'] = $this->driver->get_attachment_body($id, $task); $this->lib->attachment_get($attachment); } // if we arrive here, the requested part was not found header('HTTP/1.1 404 Not Found'); exit; } /******* Email related function *******/ public function mail_message2task() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $task = array(); // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); $message = new rcube_message($uid); if ($message->headers) { $task['title'] = trim($message->subject); $task['description'] = trim($message->first_text_part()); $task['id'] = -$uid; $this->load_driver(); // add a reference to the email message if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) { $task['links'] = array($msgref); } // copy mail attachments to task else if ($message->attachments && $this->driver->attachments) { if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) { $_SESSION[self::SESSION_KEY] = array(); $_SESSION[self::SESSION_KEY]['id'] = $task['id']; $_SESSION[self::SESSION_KEY]['attachments'] = array(); } foreach ((array)$message->attachments as $part) { $attachment = array( 'data' => $imap->get_message_part($uid, $part->mime_id, $part), 'size' => $part->size, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'group' => $task['id'], ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); // store new attachment in session unset($attachment['status'], $attachment['abort'], $attachment['data']); $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' $task['attachments'][] = $attachment; } } } $this->rc->output->command('plugin.mail2taskdialog', $task); } else { $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); } $this->rc->output->send(); } /** * Add UI element to copy task invitations or updates to the tasklist */ public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) if (!empty($this->lib->ical_parts)) { $this->get_ical(); $this->load_itip(); } $html = ''; $has_tasks = false; $ical_objects = $this->lib->get_mail_ical_objects(); // show a box for every task in the file foreach ($ical_objects as $idx => $task) { if ($task['_type'] != 'task') { continue; } $has_tasks = true; // get prepared inline UI for this event object if ($ical_objects->method) { $html .= html::div('tasklist-invitebox', $this->itip->mail_itip_inline_ui( $task, $ical_objects->method, $ical_objects->mime_id . ':' . $idx, 'tasks', rcube_utils::anytodatetime($ical_objects->message_date) ) ); } // limit listing if ($idx >= 3) { break; } } // list linked tasks $links = array(); foreach ($this->message_tasks as $task) { $checkbox = new html_checkbox(array( 'name' => 'completed', 'class' => 'complete', 'title' => $this->gettext('complete'), 'data-list' => $task['list'], )); $complete = $this->driver->is_complete($task); $links[] = html::tag('li', 'messagetaskref' . ($complete ? ' complete' : ''), $checkbox->show($complete ? $task['id'] : null, array('value' => $task['id'])) . ' ' . html::a(array( 'href' => $this->rc->url(array( 'task' => 'tasks', 'list' => $task['list'], 'id' => $task['id'], )), 'class' => 'messagetasklink', 'rel' => $task['id'] . '@' . $task['list'], 'target' => '_blank', ), Q($task['title'])) ); } if (count($links)) { $html .= html::div('messagetasklinks', html::tag('ul', 'tasklist', join("\n", $links))); } // prepend iTip/relation boxes to message body if ($html) { $this->load_ui(); $this->ui->init(); $p['content'] = $html . $p['content']; $this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm'); } // add "Save to tasks" button into attachment menu if ($has_tasks) { $this->add_button(array( 'id' => 'attachmentsavetask', 'name' => 'attachmentsavetask', 'type' => 'link', 'wrapper' => 'li', 'command' => 'attachment-save-task', 'class' => 'icon tasklistlink', 'classact' => 'icon tasklistlink active', 'innerclass' => 'icon taskadd', 'label' => 'tasklist.savetotasklist', ), 'attachmentmenu'); } return $p; } /** * Lookup backend storage and find notes associated with the given message */ public function mail_message_load($p) { if (!$p['object']->headers->others['x-kolab-type']) { $this->load_driver(); $this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder); // sort message tasks by completeness and due date $driver = $this->driver; array_walk($this->message_tasks, array($this, 'encode_task')); usort($this->message_tasks, function($a, $b) use ($driver) { $a_complete = intval($driver->is_complete($a)); $b_complete = intval($driver->is_complete($b)); $d = $a_complete - $b_complete; if (!$d) $d = $b['_hasdate'] - $a['_hasdate']; if (!$d) $d = $a['datetime'] - $b['datetime']; return $d; }); } } /** * Load iCalendar functions */ public function get_ical() { if (!$this->ical) { $this->ical = libcalendaring::get_ical(); } return $this->ical; } /** * Get properties of the tasklist this user has specified as default */ public function get_default_tasklist($sensitivity = null) { $lists = $this->driver->get_lists(); $list = null; foreach ($lists as $l) { if ($sensitivity && $l['subtype'] == $sensitivity) { $list = $l; break; } if ($l['default']) { $list = $l; } if ($l['editable']) { $first = $l; } } return $list ?: $first; } /** * Import the full payload from a mail message attachment */ public function mail_import_attachment() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); - $charset = RCMAIL_CHARSET; + $charset = RCUBE_CHARSET; // establish imap connection $imap = $this->rc->get_storage(); $imap->set_mailbox($mbox); if ($uid && $mime_id) { $part = $imap->get_message_part($uid, $mime_id); // $headers = $imap->get_message_headers($uid); if ($part->ctype_parameters['charset']) { $charset = $part->ctype_parameters['charset']; } if ($part) { $tasks = $this->get_ical()->import($part, $charset); } } $success = $existing = 0; if (!empty($tasks)) { // find writeable tasklist to store task $cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null; $lists = $this->driver->get_lists(); foreach ($tasks as $task) { // save to tasklist $list = $lists[$cal_id] ?: $this->get_default_tasklist($task['sensitivity']); if ($list && $list['editable'] && $task['_type'] == 'task') { $task = $this->from_ical($task); $task['list'] = $list['id']; if (!$this->driver->get_task($task['uid'])) { $success += (bool) $this->driver->create_task($task); } else { $existing++; } } } } if ($success) { $this->rc->output->command('display_message', $this->gettext(array( 'name' => 'importsuccess', 'vars' => array('nr' => $success), )), 'confirmation'); } else if ($existing) { $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning'); } else { $this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error'); } } /** * Handler for POST request to import an event attached to a mail message */ public function mail_import_itip() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST); $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST)); $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action'; $error_msg = $this->gettext('errorimportingtask'); $success = false; $delegate = null; if ($status == 'delegated') { $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false); $delegate = reset($delegates); if (empty($delegate) || empty($delegate['mailto'])) { $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error'); return; } } // successfully parsed tasks? if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) { $task = $this->from_ical($task); // forward iTip request to delegatee if ($delegate) { $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST)); $itip = $this->load_itip(); if ($itip->delegate_to($task, $delegate, $rsvpme ? true : false)) { $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); } else { $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } } // find writeable list to store the task $list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null; $lists = $this->driver->get_lists(); $list = $lists[$list_id]; $dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST'); // select default list except user explicitly selected 'none' if (!$list && !$dontsave) { $list = $this->get_default_tasklist($task['sensitivity']); } $metadata = array( 'uid' => $task['uid'], 'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0, 'sequence' => intval($task['sequence']), 'fallback' => strtoupper($status), 'method' => $task['_method'], 'task' => 'tasks', ); // update my attendee status according to submitted method if (!empty($status)) { $organizer = $task['organizer']; $emails = $this->lib->get_user_emails(); foreach ($task['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $reply_sender = $attendee['email']; $task['attendees'][$i]['status'] = strtoupper($status); if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) { $task['attendees'][$i]['rsvp'] = false; // unset RSVP attribute } } } // add attendee with this user's default identity if not listed if (!$reply_sender) { $sender_identity = $this->rc->user->list_emails(true); $task['attendees'][] = array( 'name' => $sender_identity['name'], 'email' => $sender_identity['email'], 'role' => 'OPT-PARTICIPANT', 'status' => strtoupper($status), ); $metadata['attendee'] = $sender_identity['email']; } } // save to tasklist if ($list && $list['editable']) { $task['list'] = $list['id']; // check for existing task with the same UID $existing = $this->driver->get_task($task['uid']); if ($existing) { // only update attendee status if ($task['_method'] == 'REPLY') { // try to identify the attendee using the email sender address $existing_attendee = -1; $existing_attendee_emails = array(); foreach ($existing['attendees'] as $i => $attendee) { $existing_attendee_emails[] = $attendee['email']; if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) { $existing_attendee = $i; } } $task_attendee = null; foreach ($task['attendees'] as $attendee) { if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) { $task_attendee = $attendee; $metadata['fallback'] = $attendee['status']; $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; if ($attendee['status'] != 'DELEGATED') { break; } } // also copy delegate attendee else if (!empty($attendee['delegated-from']) && (stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) && (!in_array($attendee['email'], $existing_attendee_emails))) { $existing['attendees'][] = $attendee; } } // if delegatee has declined, set delegator's RSVP=True if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) { foreach ($existing['attendees'] as $i => $attendee) { if ($attendee['email'] == $task_attendee['delegated-from']) { $existing['attendees'][$i]['rsvp'] = true; break; } } } // found matching attendee entry in both existing and new events if ($existing_attendee >= 0 && $task_attendee) { $existing['attendees'][$existing_attendee] = $task_attendee; $success = $this->driver->edit_task($existing); } // update the entire attendees block else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) { $existing['attendees'][] = $task_attendee; $success = $this->driver->edit_task($existing); } else { $error_msg = $this->gettext('newerversionexists'); } } // delete the task when declined else if ($status == 'declined' && $delete) { $deleted = $this->driver->delete_task($existing, true); $success = true; } // import the (newer) task else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) { $task['id'] = $existing['id']; $task['list'] = $existing['list']; // preserve my participant status for regular updates if (empty($status)) { $emails = $this->lib->get_user_emails(); foreach ($task['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { foreach ($existing['attendees'] as $j => $_attendee) { if ($attendee['email'] == $_attendee['email']) { $task['attendees'][$i] = $existing['attendees'][$j]; break; } } } } } // set status=CANCELLED on CANCEL messages if ($task['_method'] == 'CANCEL') { $task['status'] = 'CANCELLED'; } // show me as free when declined (#1670) if ($status == 'declined' || $task['status'] == 'CANCELLED') { $task['free_busy'] = 'free'; } $success = $this->driver->edit_task($task); } else if (!empty($status)) { $existing['attendees'] = $task['attendees']; if ($status == 'declined') { // show me as free when declined (#1670) $existing['free_busy'] = 'free'; } $success = $this->driver->edit_event($existing); } else { $error_msg = $this->gettext('newerversionexists'); } } else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) { $success = $this->driver->create_task($task); } else if ($status == 'declined') { $error_msg = null; } } else if ($status == 'declined' || $dontsave) { $error_msg = null; } else { $error_msg = $this->gettext('nowritetasklistfound'); } } if ($success || $dontsave) { if ($success) { $message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully')); $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation'); } $metadata['rsvp'] = intval($metadata['rsvp']); $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0); $this->rc->output->command('plugin.itip_message_processed', $metadata); $error_msg = null; } else if ($error_msg) { $this->rc->output->command('display_message', $error_msg, 'error'); } // send iTip reply if ($task['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) { $task['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } $this->rc->output->send(); } /**** Task invitation plugin hooks ****/ /** * Handler for task/itip-delegate requests */ function mail_itip_delegate() { // forward request to mail_import_itip() with the right status $_POST['_status'] = $_REQUEST['_status'] = 'delegated'; $this->mail_import_itip(); } /** * Handler for task/itip-status requests */ public function task_itip_status() { $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); // find local copy of the referenced task $existing = $this->driver->get_task($data); $itip = $this->load_itip(); $response = $itip->get_itip_status($data, $existing); // get a list of writeable lists to save new tasks to if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') { $lists = $this->driver->get_lists(); $select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true)); $select->add('--', ''); foreach ($lists as $list) { if ($list['editable']) { $select->add($list['name'], $list['id']); } } } if ($select) { $default_list = $this->get_default_tasklist($data['sensitivity']); $response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' . $select->show($default_list['id'])); } $this->rc->output->command('plugin.update_itip_object_status', $response); } /** * Handler for task/itip-remove requests */ public function task_itip_remove() { $success = false; $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); // search for event if only UID is given if ($task = $this->driver->get_task($uid)) { $success = $this->driver->delete_task($task, true); } if ($success) { $this->rc->output->show_message('tasklist.successremoval', 'confirmation'); } else { $this->rc->output->show_message('tasklist.errorsaving', 'error'); } } /******* Utility functions *******/ /** * Generate a unique identifier for an event */ public function generate_uid() { return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); } /** * Map task properties for ical exprort using libcalendaring */ public function to_libcal($task) { $object = $task; $object['_type'] = 'task'; $object['categories'] = (array)$task['tags']; // convert to datetime objects if (!empty($task['date'])) { $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone); if (empty($task['time'])) $object['due']->_dateonly = true; unset($object['date']); } if (!empty($task['startdate'])) { $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone); if (empty($task['starttime'])) $object['start']->_dateonly = true; unset($object['startdate']); } $object['complete'] = $task['complete'] * 100; if ($task['complete'] == 1.0 && empty($task['complete'])) { $object['status'] = 'COMPLETED'; } if ($task['flagged']) { $object['priority'] = 1; } else if (!$task['priority']) { $object['priority'] = 0; } return $object; } /** * Convert task properties from ical parser to the internal format */ public function from_ical($vtodo) { $task = $vtodo; $task['tags'] = array_filter((array)$vtodo['categories']); $task['flagged'] = $vtodo['priority'] == 1; $task['complete'] = floatval($vtodo['complete'] / 100); // convert from DateTime to internal date format if (is_a($vtodo['due'], 'DateTime')) { $due = $this->lib->adjust_timezone($vtodo['due']); $task['date'] = $due->format('Y-m-d'); if (!$vtodo['due']->_dateonly) $task['time'] = $due->format('H:i'); } // convert from DateTime to internal date format if (is_a($vtodo['start'], 'DateTime')) { $start = $this->lib->adjust_timezone($vtodo['start']); $task['startdate'] = $start->format('Y-m-d'); if (!$vtodo['start']->_dateonly) $task['starttime'] = $start->format('H:i'); } if (is_a($vtodo['dtstamp'], 'DateTime')) { $task['changed'] = $vtodo['dtstamp']; } unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']); return $task; } /** * Handler for user_delete plugin hook */ public function user_delete($args) { $this->load_driver(); return $this->driver->user_delete($args); } /** * Magic getter for public access to protected members */ public function __get($name) { switch ($name) { case 'ical': return $this->get_ical(); case 'itip': return $this->load_itip(); case 'driver': $this->load_driver(); return $this->driver; } return null; } } diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index 0315d8f8..d89defc4 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -1,541 +1,541 @@ * * Copyright (C) 2012-2015, 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 tasklist_ui { private $rc; private $plugin; private $ready = false; private $gui_objects = array(); function __construct($plugin) { $this->plugin = $plugin; $this->rc = $plugin->rc; } /** * Calendar UI initialization and requests handlers */ public function init() { if ($this->ready) { return; } // add taskbar button $this->plugin->add_button(array( 'command' => 'tasks', 'class' => 'button-tasklist', 'classsel' => 'button-tasklist button-selected', 'innerclass' => 'button-inner', 'label' => 'tasklist.navtitle', ), 'taskbar'); $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tasklist.css'); if ($this->rc->task == 'mail' || $this->rc->task == 'tasks') { jqueryui::tagedit(); $this->plugin->include_script('tasklist_base.js'); // copy config to client $this->rc->output->set_env('tasklist_settings', $this->load_settings()); // initialize attendees autocompletion $this->rc->autocomplete_init(); } $this->ready = true; } /** * */ function load_settings() { $settings = array(); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0); $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', 3); $settings['sort_col'] = $this->rc->config->get('tasklist_sort_col', ''); $settings['sort_order'] = $this->rc->config->get('tasklist_sort_order', 'asc'); // get user identity to create default attendee foreach ($this->rc->user->list_emails() as $rec) { if (!$identity) $identity = $rec; $identity['emails'][] = $rec['email']; $settings['identities'][$rec['identity_id']] = $rec['email']; } $identity['emails'][] = $this->rc->user->get_username(); $settings['identity'] = array( 'name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])) ); if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) { $settings['selected_list'] = $list; } if ($list && ($id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC))) { $settings['selected_id'] = $id; // check if the referenced task is completed $task = $this->plugin->driver->get_task(array('id' => $id, 'list' => $list)); if ($task && $this->plugin->driver->is_complete($task)) { $settings['selected_filter'] = 'complete'; } } else if ($filter = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GPC)) { $settings['selected_filter'] = $filter; } return $settings; } /** * 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_emails(); foreach ($identities as $ident) { $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']); } return $select->show(null); } /** * Register handler methods for the template engine */ public function init_templates() { $this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists')); $this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select')); $this->plugin->register_handler('plugin.status_select', array($this, 'status_select')); $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); $this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form')); $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview')); $this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist')); $this->plugin->register_handler('plugin.tags_editline', array($this, 'tags_editline')); $this->plugin->register_handler('plugin.alarm_select', array($this, 'alarm_select')); $this->plugin->register_handler('plugin.recurrence_form', array($this->plugin->lib, 'recurrence_form')); $this->plugin->register_handler('plugin.attachments_form', array($this, 'attachments_form')); $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list')); $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area')); $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list')); $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form')); $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select')); $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify')); $this->plugin->register_handler('plugin.task_rsvp_buttons', array($this->plugin->itip, 'itip_rsvp_buttons')); $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); jqueryui::tagedit(); $this->plugin->include_script('tasklist.js'); $this->rc->output->include_script('treelist.js'); // include kolab folderlist widget if available if (in_array('libkolab', $this->plugin->api->loaded_plugins())) { $this->plugin->api->include_script('libkolab/js/folderlist.js'); $this->plugin->api->include_script('libkolab/js/audittrail.js'); } } /** * */ public function tasklists($attrib = array()) { $tree = true; $jsenv = array(); $lists = $this->plugin->driver->get_lists($tree); // walk folder tree if (is_object($tree)) { $html = $this->list_tree_html($tree, $lists, $jsenv, $attrib); } else { // fall-back to flat folder listing $attrib['class'] .= ' flat'; $html = ''; foreach ((array)$lists as $id => $prop) { if ($attrib['activeonly'] && !$prop['active']) continue; $html .= html::tag('li', array( 'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id), 'class' => $prop['group'], ), $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']) ); } } $this->rc->output->set_env('tasklists', $jsenv); $this->register_gui_object('tasklistslist', $attrib['id']); return html::tag('ul', $attrib, $html, html::$common_attrib); } /** * Return html for a structured list
        for the folder tree */ public function list_tree_html($node, $data, &$jsenv, $attrib) { $out = ''; foreach ($node->children as $folder) { $id = $folder->id; $prop = $data[$id]; $is_collapsed = false; // TODO: determine this somehow? $content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']); if (!empty($folder->children)) { $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $this->list_tree_html($folder, $data, $jsenv, $attrib)); } if (strlen($content)) { $out .= html::tag('li', array( 'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id), 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''), ), $content); } } return $out; } /** * Helper method to build a tasklist item (HTML content and js data) */ public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false) { // enrich list properties with settings from the driver if (!$prop['virtual']) { unset($prop['user_id']); $prop['alarms'] = $this->plugin->driver->alarms; $prop['undelete'] = $this->plugin->driver->undelete; $prop['sortable'] = $this->plugin->driver->sortable; $prop['attachments'] = $this->plugin->driver->attachments; $prop['attendees'] = $this->plugin->driver->attendees; $prop['caldavurl'] = $this->plugin->driver->tasklist_caldav_url($prop); $jsenv[$id] = $prop; } $classes = array('tasklist'); $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ? - html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : ''); + html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : ''); if ($prop['virtual']) $classes[] = 'virtual'; else if (!$prop['editable']) $classes[] = 'readonly'; if ($prop['subscribed']) $classes[] = 'subscribed'; if ($prop['class']) $classes[] = $prop['class']; if (!$activeonly || $prop['active']) { $label_id = 'tl:' . $id; return html::div(join(' ', $classes), html::span(array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) . ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) . html::span('actions', ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '') . html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'), ' ') . (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '') ) ) ); } return ''; } /** * Render HTML form for task status selector */ function status_select($attrib = array()) { $attrib['name'] = 'status'; $select = new html_select($attrib); $select->add('---', ''); $select->add($this->plugin->gettext('status-needs-action'), 'NEEDS-ACTION'); $select->add($this->plugin->gettext('status-in-process'), 'IN-PROCESS'); $select->add($this->plugin->gettext('status-completed'), 'COMPLETED'); $select->add($this->plugin->gettext('status-cancelled'), 'CANCELLED'); return $select->show(null); } /** * Render a HTML select box for list selection */ function tasklist_select($attrib = array()) { $attrib['name'] = 'list'; $attrib['is_escaped'] = true; $select = new html_select($attrib); $default = null; foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) { if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) { $select->add($prop['name'], $id); if (!$default || $prop['default']) $default = $id; } } return $select->show($default); } function tasklist_editform($action, $list = array()) { $fields = array( 'name' => array( 'id' => 'taskedit-tasklistame', 'label' => $this->plugin->gettext('listname'), 'value' => html::tag('input', array('id' => 'taskedit-tasklistame', 'name' => 'name', 'type' => 'text', 'class' => 'text', 'size' => 40)), ), /* 'color' => array( 'id' => 'taskedit-color', 'label' => $this->plugin->gettext('color'), 'value' => html::tag('input', array('id' => 'taskedit-color', 'name' => 'color', 'type' => 'text', 'class' => 'text colorpicker', 'size' => 6)), ), */ 'showalarms' => array( 'id' => 'taskedit-showalarms', 'label' => $this->plugin->gettext('showalarms'), 'value' => html::tag('input', array('id' => 'taskedit-showalarms', 'name' => 'color', 'type' => 'checkbox')), ), ); return html::tag('form', array('action' => "#", 'method' => "post", 'id' => 'tasklisteditform'), $this->plugin->driver->tasklist_edit_form($action, $list, $fields) ); } /** * Render HTML form for alarm configuration */ function alarm_select($attrib = array()) { $attrib['_type'] = 'task'; return $this->plugin->lib->alarm_select($attrib, $this->plugin->driver->alarm_types, $this->plugin->driver->alarm_absolute); } /** * */ function quickadd_form($attrib) { $attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform'); $label = html::label(array('for' => 'quickaddinput', 'class' => 'voice'), $this->plugin->gettext('quickaddinput')); $input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput')); $button = html::tag('input', array('type' => 'submit', 'value' => '+', 'title' => $this->plugin->gettext('createtask'), 'class' => 'button mainaction')); $this->register_gui_object('quickaddform', $attrib['id']); return html::tag('form', $attrib, $label . $input->show() . $button); } /** * The result view */ function tasks_resultview($attrib) { $attrib += array('id' => 'rcmtaskslist'); $this->register_gui_object('resultlist', $attrib['id']); unset($attrib['name']); return html::tag('ul', $attrib, ''); } /** * Container for a tags cloud */ function tagslist($attrib) { $attrib += array('id' => 'rcmtasktagslist'); unset($attrib['name']); $this->register_gui_object('tagslist', $attrib['id']); return html::tag('ul', $attrib, ''); } /** * Interactive UI element to add/remove tags */ function tags_editline($attrib) { $attrib += array('id' => 'rcmtasktagsedit'); $this->register_gui_object('edittagline', $attrib['id']); $input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex'])); unset($attrib['tabindex']); return html::div($attrib, $input->show('')); } /** * Generate HTML element for attachments list */ function attachments_list($attrib = array()) { if (!$attrib['id']) $attrib['id'] = 'rcmtaskattachmentlist'; $this->register_gui_object('attachmentlist', $attrib['id']); return html::tag('ul', $attrib, '', html::$common_attrib); } /** * Generate the form for event attachments upload */ function attachments_form($attrib = array()) { // add ID if not given if (!$attrib['id']) $attrib['id'] = 'rcmtaskuploadform'; // 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)))) + html::div('formbuttons', $button->show($this->rc->gettext('upload'), array('class' => 'button mainaction', + 'onclick' => rcmail_output::JS_OBJECT_NAME . ".upload_file(this.form)"))) . + html::div('hint', $this->rc->gettext(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->register_gui_object('filedrop', $attrib['id']); $this->rc->output->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments')); } } /** * */ function attendees_list($attrib = array()) { // add "noreply" checkbox to attendees table only $invitations = strpos($attrib['id'], 'attend') !== false; $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite')); $table = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); // $table->add_header('role', $this->plugin->gettext('role')); $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee')); $table->add_header('confirmstate', $this->plugin->gettext('confirmstate')); if ($invitations) { $table->add_header(array('class' => 'invite', 'title' => $this->plugin->gettext('sendinvitations')), $invite->show(1) . html::label('edit-attendees-invite', $this->plugin->gettext('sendinvitations'))); } $table->add_header('options', ''); // hide invite column if disabled by config $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3); if ($invitations && !($itip_notify & 2)) { $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']); $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css)); } return $table->show($attrib); } /** * */ function attendees_form($attrib = array()) { $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30)); $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment', 'rows' => 4, 'cols' => 55, 'title' => $this->plugin->gettext('itipcommenttitle'))); return html::div($attrib, html::div(null, $input->show() . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->plugin->gettext('addattendee'))) // . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->plugin->gettext('scheduletime').'...')) ) . html::p('attendees-commentbox', html::label(null, $this->plugin->gettext('itipcomment') . $textarea->show())) ); } /** * */ 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->plugin->gettext('sendnotifications'))); } /** * Wrapper for rcube_output_html::add_gui_object() */ function register_gui_object($name, $id) { $this->gui_objects[$name] = $id; $this->rc->output->add_gui_object($name, $id); } /** * Getter for registered gui objects. * (for manual registration when loading the inline UI) */ function get_gui_objects() { return $this->gui_objects; } }