diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index c4bda126..8d859804 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 $this->rc->autocomplete_init(); $this->rc->output->set_env('timezone', $this->timezone->getName()); $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false); $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver')); $this->rc->output->set_env('mscolors', jqueryui::get_color_values()); $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer')))); $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC); if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) $this->rc->output->set_env('view', $view); if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) $this->rc->output->set_env('date', $date); if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC)) $this->rc->output->set_env('itip_events', $this->itip_events($msgref)); $this->rc->output->send("calendar.calendar"); } /** * Handler for preferences_sections_list hook. * Adds Calendar settings sections into preferences sections list. * * @param array Original parameters * @return array Modified parameters */ function preferences_sections_list($p) { $p['list']['calendar'] = array( 'id' => 'calendar', 'section' => $this->gettext('calendar'), ); return $p; } /** * Handler for preferences_list hook. * Adds options blocks into Calendar settings sections in Preferences. * * @param array Original parameters * @return array Modified parameters */ function preferences_list($p) { if ($p['section'] != 'calendar') { return $p; } $no_override = array_flip((array)$this->rc->config->get('dont_override')); $p['blocks']['view']['name'] = $this->gettext('mainoptions'); if (!isset($no_override['calendar_default_view'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_default_view'; $select = new html_select(array('name' => '_default_view', 'id' => $field_id)); $select->add($this->gettext('day'), "agendaDay"); $select->add($this->gettext('week'), "agendaWeek"); $select->add($this->gettext('month'), "month"); $select->add($this->gettext('agenda'), "table"); $p['blocks']['view']['options']['default_view'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('default_view'))), 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])), ); } if (!isset($no_override['calendar_timeslots'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_timeslot'; $choices = array('1', '2', '3', '4', '6'); $select = new html_select(array('name' => '_timeslots', 'id' => $field_id)); $select->add($choices); $p['blocks']['view']['options']['timeslots'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('timeslots'))), 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))), ); } if (!isset($no_override['calendar_first_day'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_firstday'; $select = new html_select(array('name' => '_first_day', 'id' => $field_id)); $select->add($this->gettext('sunday'), '0'); $select->add($this->gettext('monday'), '1'); $select->add($this->gettext('tuesday'), '2'); $select->add($this->gettext('wednesday'), '3'); $select->add($this->gettext('thursday'), '4'); $select->add($this->gettext('friday'), '5'); $select->add($this->gettext('saturday'), '6'); $p['blocks']['view']['options']['first_day'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('first_day'))), 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))), ); } if (!isset($no_override['calendar_first_hour'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']))); $select_hours = new html_select(); for ($h = 0; $h < 24; $h++) $select_hours->add(date($time_format, mktime($h, 0, 0)), $h); $field_id = 'rcmfd_firsthour'; $p['blocks']['view']['options']['first_hour'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('first_hour'))), 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)), ); } if (!isset($no_override['calendar_work_start'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_workstart'; $p['blocks']['view']['options']['workinghours'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('workinghours'))), 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) . ' — ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)), ); } if (!isset($no_override['calendar_event_coloring'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } $field_id = 'rcmfd_coloring'; $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id)); $select_colors->add($this->gettext('coloringmode0'), 0); $select_colors->add($this->gettext('coloringmode1'), 1); $select_colors->add($this->gettext('coloringmode2'), 2); $select_colors->add($this->gettext('coloringmode3'), 3); $p['blocks']['view']['options']['eventcolors'] = array( 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('eventcoloring'))), 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])), ); } // loading driver is expensive, don't do it if not needed $this->load_driver(); if (!isset($no_override['calendar_default_alarm_type'])) { 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($this->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type); $p['blocks']['view']['options']['alarmtype'] = array( 'title' => html::label($field_id, rcube::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($this->gettext('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', rcube::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', rcube::Q($this->gettext('defaultcalendar'))), 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)), ); } $p['blocks']['itip']['name'] = $this->gettext('itipoptions'); // Invitations handling if (!isset($no_override['calendar_itip_after_action'])) { if (!$p['current']) { $p['blocks']['itip']['content'] = true; return $p; } $field_id = 'rcmfd_after_action'; $select = new html_select(array('name' => '_after_action', 'id' => $field_id, 'onchange' => "\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()")); $select->add($this->gettext('afternothing'), ''); $select->add($this->gettext('aftertrash'), 1); $select->add($this->gettext('afterdelete'), 2); $select->add($this->gettext('afterflagdeleted'), 3); $select->add($this->gettext('aftermoveto'), 4); $val = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']); if ($val !== null && $val !== '' && !is_int($val)) { $folder = $val; $val = 4; } $folders = $this->rc->folder_selector(array( 'id' => $field_id . '_select', 'name' => '_after_action_folder', 'maxlength' => 30, 'folder_filter' => 'mail', 'folder_rights' => 'w', 'style' => $val !== 4 ? 'display:none' : '', )); $p['blocks']['itip']['options']['after_action'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('afteraction'))), 'content' => $select->show($val) . $folders->show($folder), ); } // category definitions if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) { $p['blocks']['categories']['name'] = $this->gettext('categories'); if (!$p['current']) { $p['blocks']['categories']['content'] = true; return $p; } $categories = (array) $this->driver->list_categories(); $categories_list = ''; foreach ($categories as $name => $color) { $key = md5($name); $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name); $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category'))); $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable)); $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6)); $hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : ''; $categories_list .= html::div(null, $hidden . $category_name->show($name) . ' ' . $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($this->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type); } $input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib); $select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib); foreach (array('-M','-H','-D') as $trigger) $select_offset->add($this->gettext('trigger' . $trigger, 'libcalendaring'), $trigger); $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D')); $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array( 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))), 'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } return $p; } /** * Handler for preferences_save hook. * Executed on Calendar settings form submit. * * @param array Original parameters * @return array Modified parameters */ function preferences_save($p) { if ($p['section'] == 'calendar') { $this->load_driver(); // compose default alarm preset value $alarm_offset = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST); $alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST); $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1]; $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST); $birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST); $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1]; $p['prefs'] = array( 'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST), 'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)), 'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)), 'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)), 'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)), 'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)), 'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)), 'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST), 'calendar_default_alarm_offset' => $default_alarm, 'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST), 'calendar_date_format' => null, // clear previously saved values 'calendar_time_format' => null, 'calendar_contact_birthdays' => rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST) ? true : false, 'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST), 'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST), 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null, 'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)), ); if ($p['prefs']['calendar_itip_after_action'] == 4) { $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true); } // categories if (!$this->driver->nocategories) { $old_categories = $new_categories = array(); foreach ($this->driver->list_categories() as $name => $color) { $old_categories[md5($name)] = $name; } $categories = (array) rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST); $colors = (array) rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST); foreach ($categories as $key => $name) { $color = preg_replace('/^#/', '', strval($colors[$key])); // rename categories in existing events -> driver's job if ($oldname = $old_categories[$key]) { $this->driver->replace_category($oldname, $name, $color); unset($old_categories[$key]); } else $this->driver->add_category($name, $color); $new_categories[$name] = $color; } // these old categories have been removed, alter events accordingly -> driver's job foreach ((array)$old_categories[$key] as $key => $name) { $this->driver->remove_category($name); } $p['prefs']['calendar_categories'] = $new_categories; } } return $p; } /** * Dispatcher for calendar actions initiated by the client */ function calendar_action() { $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $cal = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC); $success = $reload = false; if (isset($cal['showalarms'])) $cal['showalarms'] = intval($cal['showalarms']); switch ($action) { case "form-new": case "form-edit": echo $this->ui->calendar_editform($action, $cal); exit; case "new": $success = $this->driver->create_calendar($cal); $reload = true; break; case "edit": $success = $this->driver->edit_calendar($cal); $reload = true; break; case "delete": if ($success = $this->driver->delete_calendar($cal)) $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id'])); break; case "subscribe": if (!$this->driver->subscribe_calendar($cal)) $this->rc->output->show_message($this->gettext('errorsaving'), 'error'); return; case "search": $results = array(); $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); foreach ((array) $this->driver->search_calendars($query, $source) as $id => $prop) { $editname = $prop['editname']; unset($prop['editname']); // force full name to be displayed $prop['active'] = false; // let the UI generate HTML and CSS representation for this calendar $html = $this->ui->calendar_list_item($id, $prop, $jsenv); $cal = $jsenv[$id]; $cal['editname'] = $editname; $cal['html'] = $html; if (!empty($prop['color'])) $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode); $results[] = $cal; } // report more results available if ($this->driver->search_more_results) $this->rc->output->show_message('autocompletemore', 'info'); $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC)); return; } if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else { $error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :''); $this->rc->output->show_message($error_msg, 'error'); } $this->rc->output->command('plugin.unlock_saving'); if ($success && $reload) $this->rc->output->command('plugin.reload_view'); } /** * Dispatcher for event actions initiated by the client */ function event_action() { $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true); $success = $reload = $got_msg = false; // force notify if hidden + active if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1) $event['_notify'] = 1; // read old event data in order to find changes if (($event['_notify'] || $event['_decline']) && $action != 'new') { $old = $this->driver->get_event($event); // load main event if savemode is 'all' or if deleting 'future' events if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline'])) && $old['recurrence_id']) { $old['id'] = $old['recurrence_id']; $old = $this->driver->get_event($old); } } switch ($action) { case "new": // create UID for new event $event['uid'] = $this->generate_uid(); $this->write_preprocess($event, $action); if ($success = $this->driver->new_event($event)) { $event['id'] = $event['uid']; $event['_savemode'] = 'all'; $this->cleanup_event($event); $this->event_save_success($event, null, $action, true); } $reload = $success && $event['recurrence'] ? 2 : 1; break; case "edit": $this->write_preprocess($event, $action); if ($success = $this->driver->edit_event($event)) { $this->cleanup_event($event); $this->event_save_success($event, $old, $action, $success); } $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; break; case "resize": $this->write_preprocess($event, $action); if ($success = $this->driver->resize_event($event)) { $this->event_save_success($event, $old, $action, $success); } $reload = $event['_savemode'] ? 2 : 1; break; case "move": $this->write_preprocess($event, $action); if ($success = $this->driver->move_event($event)) { $this->event_save_success($event, $old, $action, $success); } $reload = $success && $event['_savemode'] ? 2 : 1; break; case "remove": // remove previous deletes $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0; $this->rc->session->remove('calendar_event_undo'); // search for event if only UID is given if (!isset($event['calendar']) && $event['uid']) { if (!($event = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) { break; } $undo_time = 0; } $success = $this->driver->remove_event($event, $undo_time < 1); $reload = (!$success || $event['_savemode']) ? 2 : 1; if ($undo_time > 0 && $success) { $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event); // display message with Undo link. $msg = html::span(null, $this->gettext('successremoval')) . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", rcmail_output::JS_OBJECT_NAME, rcmail_output::JS_OBJECT_NAME)), $this->gettext('undo')); $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time); $got_msg = true; } else if ($success) { $this->rc->output->show_message('calendar.successremoval', 'confirmation'); $got_msg = true; } // send cancellation for the main event if ($event['_savemode'] == 'all') { unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']); } // send an update for the main event's recurrence rule instead of a cancellation message else if ($event['_savemode'] == 'future' && $success !== false && $success !== true) { $event['_savemode'] = 'all'; // force event_save_success() to load master event $action = 'edit'; $success = true; } // send iTIP reply that participant has declined the event if ($success && $event['_decline']) { $emails = $this->get_user_emails(); foreach ($old['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $attendee; else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $old['attendees'][$i]['status'] = 'DECLINED'; $reply_sender = $attendee['email']; } } if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) { $old['thisandfuture'] = true; } $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } else if ($success) { $this->event_save_success($event, $old, $action, $success); } break; case "undo": // Restore deleted event $event = $_SESSION['calendar_event_undo']['data']; if ($event) $success = $this->driver->restore_event($event); if ($success) { $this->rc->session->remove('calendar_event_undo'); $this->rc->output->show_message('calendar.successrestore', 'confirmation'); $got_msg = true; $reload = 2; } break; case "rsvp": $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST); $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST); $reply_comment = $event['comment']; $this->write_preprocess($event, 'edit'); $ev = $this->driver->get_event($event); $ev['attendees'] = $event['attendees']; $ev['free_busy'] = $event['free_busy']; $ev['_savemode'] = $event['_savemode']; // send invitation to delegatee + add it as attendee if ($status == 'delegated' && $event['to']) { $itip = $this->load_itip(); if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) { $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); $noreply = false; } } $event = $ev; // compose a list of attendees affected by this change $updated_attendees = array_filter(array_map(function($j) use ($event) { return $event['attendees'][$j]; }, $attendees)); if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) { $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC); $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0; $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1; $organizer = null; $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $reply_sender = $attendee['email']; } } if (!$noreply) { $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); $event['comment'] = $reply_comment; $event['thisandfuture'] = $event['_savemode'] == 'future'; if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } // refresh all calendars if ($event['calendar'] != $ev['calendar']) { $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true)); $reload = 0; } } break; case "dismiss": $event['ids'] = explode(',', $event['id']); $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event); $success = $plugin['success']; foreach ($event['ids'] as $id) { if (strpos($id, 'cal:') === 0) $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']); } break; case "changelog": $data = $this->driver->get_event_changelog($event); if (is_array($data) && !empty($data)) { $lib = $this->lib; $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); array_walk($data, function(&$change) use ($lib, $dtformat) { if ($change['date']) { $dt = $lib->adjust_timezone($change['date']); if ($dt instanceof DateTime) $change['date'] = $this->rc->format_date($dt, $dtformat, false); } }); $this->rc->output->command('plugin.render_event_changelog', $data); } else { $this->rc->output->command('plugin.render_event_changelog', false); } $got_msg = true; $reload = false; break; case "diff": $data = $this->driver->get_event_diff($event, $event['rev1'], $event['rev2']); if (is_array($data)) { // convert some properties, similar to self::_client_event() $lib = $this->lib; array_walk($data['changes'], function(&$change, $i) use ($event, $lib) { // convert date cols foreach (array('start','end','created','changed') as $col) { if ($change['property'] == $col) { $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c'); $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c'); } } // create textual representation for alarms and recurrence if ($change['property'] == 'alarms') { if (is_array($change['old'])) $change['old_'] = libcalendaring::alarm_text($change['old']); if (is_array($change['new'])) $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'recurrence') { if (is_array($change['old'])) $change['old_'] = $lib->recurrence_text($change['old']); if (is_array($change['new'])) $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new'])); } if ($change['property'] == 'attachments') { if (is_array($change['old'])) $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']); if (is_array($change['new'])) $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']); } // compute a nice diff of description texts if ($change['property'] == 'description') { $change['diff_'] = libkolab::html_diff($change['old'], $change['new']); } }); $this->rc->output->command('plugin.event_show_diff', $data); } else { $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); } $got_msg = true; $reload = false; break; case "show": if ($event = $this->driver->get_event_revison($event, $event['rev'])) { $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event)); } else { $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error'); } $got_msg = true; $reload = false; break; case "restore": if ($success = $this->driver->restore_event_revision($event, $event['rev'])) { $_event = $this->driver->get_event($event); $reload = $_event['recurrence'] ? 2 : 1; $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation'); $this->rc->output->command('plugin.close_history_dialog'); } else { $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error'); $reload = 0; } $got_msg = true; break; } // show confirmation/error message if (!$got_msg) { if ($success) $this->rc->output->show_message('successfullysaved', 'confirmation'); else $this->rc->output->show_message('calendar.errorsaving', 'error'); } // unlock client $this->rc->output->command('plugin.unlock_saving'); // update event object on the client or trigger a complete refretch if too complicated if ($reload) { $args = array('source' => $event['calendar']); if ($reload > 1) $args['refetch'] = true; else if ($success && $action != 'remove') $args['update'] = $this->_client_event($this->driver->get_event($event), true); $this->rc->output->command('plugin.refresh_calendar', $args); } } /** * Helper method sending iTip notifications after successful event updates */ private function event_save_success(&$event, $old, $action, $success) { // $success is a new event ID if ($success !== true) { // send update notification on the main event if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) { $master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']), 0, true); unset($master['_instance'], $master['recurrence_date']); $sent = $this->notify_attendees($master, null, $action, $event['_comment'], false); if ($sent < 0) $this->rc->output->show_message('calendar.errornotifying', 'error'); $event['attendees'] = $master['attendees']; // this tricks us into the next if clause } // delete old reference if saved as new if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') { $old = null; } $event['id'] = $success; $event['_savemode'] = 'all'; } // send out notifications if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) { $_savemode = $event['_savemode']; // send notification for the main event when savemode is 'all' if ($action != 'remove' && $_savemode == 'all' && ($event['recurrence_id'] || $old['recurrence_id'] || ($old && $old['id'] != $event['id']))) { $event['id'] = $event['recurrence_id'] ?: ($old['recurrence_id'] ?: $old['id']); $event = $this->driver->get_event($event, 0, true); unset($event['_instance'], $event['recurrence_date']); } else { // make sure we have the complete record $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true); } $event['_savemode'] = $_savemode; if ($old) { $old['thisandfuture'] = $_savemode == 'future'; } // only notify if data really changed (TODO: do diff check on client already) if (!$old || $action == 'remove' || self::event_diff($event, $old)) { $sent = $this->notify_attendees($event, $old, $action, $event['_comment']); if ($sent > 0) $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); else if ($sent < 0) $this->rc->output->show_message('calendar.errornotifying', 'error'); } } } /** * Handler for load-requests from fullcalendar * This will return pure JSON formatted output */ function load_events() { $events = $this->driver->load_events( rcube_utils::get_input_value('start', rcube_utils::INPUT_GET), rcube_utils::get_input_value('end', rcube_utils::INPUT_GET), ($query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET)), rcube_utils::get_input_value('source', rcube_utils::INPUT_GET) ); echo $this->encode($events, !empty($query)); exit; } /** * Handler for requests fetching event counts for calendars */ public function count_events() { // don't update session on these requests (avoiding race conditions) $this->rc->session->nowrite = true; $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); if (!$start) { $start = new DateTime('today 00:00:00', $this->timezone); $start = $start->format('U'); } $counts = $this->driver->count_events( rcube_utils::get_input_value('source', rcube_utils::INPUT_GET), $start, rcube_utils::get_input_value('end', rcube_utils::INPUT_GET) ); $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); } /** * Load event data from an iTip message attachment */ public function itip_events($msgref) { $path = explode('/', $msgref); $msg = array_pop($path); $mbox = join('/', $path); list($uid, $mime_id) = explode('#', $msg); $events = array(); if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { $partstat = 'NEEDS-ACTION'; /* $user_emails = $this->lib->get_user_emails(); foreach ($event['attendees'] as $attendee) { if (in_array($attendee['email'], $user_emails)) { $partstat = $attendee['status']; break; } } */ $event['id'] = $event['uid']; $event['temporary'] = true; $event['readonly'] = true; $event['calendar'] = '--invitation--itip'; $event['className'] = 'fc-invitation-' . strtolower($partstat); $event['_mbox'] = $mbox; $event['_uid'] = $uid; $event['_part'] = $mime_id; $events[] = $this->_client_event($event, true); // add recurring instances if (!empty($event['recurrence'])) { foreach ($this->driver->get_recurring_events($event, $event['start']) as $recurring) { $recurring['temporary'] = true; $recurring['readonly'] = true; $recurring['calendar'] = '--invitation--itip'; $events[] = $this->_client_event($recurring, true); } } } return $events; } /** * Handler for keep-alive requests * This will check for updated data in active calendars and sync them to the client */ public function refresh($attr) { // refresh the entire calendar every 10th time to also sync deleted events if (rand(0,10) == 10) { $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true)); return; } $counts = array(); foreach ($this->driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) { $events = $this->driver->load_events( rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), $cal['id'], 1, $attr['last'] ); foreach ($events as $event) { $this->rc->output->command('plugin.refresh_calendar', array('source' => $cal['id'], 'update' => $this->_client_event($event))); } // refresh count for this calendar if ($cal['counts']) { $today = new DateTime('today 00:00:00', $this->timezone); $counts += $this->driver->count_events($cal['id'], $today->format('U')); } } if (!empty($counts)) { $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); } } /** * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. * This will check for pending notifications and pass them to the client */ public function pending_alarms($p) { $this->load_driver(); $time = $p['time'] ?: time(); if ($alarms = $this->driver->pending_alarms($time)) { foreach ($alarms as $alarm) { $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal: $p['alarms'][] = $alarm; } } // get alarms for birthdays calendar if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') { $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db'); foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) { $alarm = libcalendaring::get_next_alarm($e); // overwrite alarm time with snooze value (or null if dismissed) if ($dismissed = $cache->get($e['id'])) $alarm['time'] = $dismissed['notifyat']; // add to list if alarm is set if ($alarm && $alarm['time'] && $alarm['time'] <= $time) { $e['id'] = 'cal:bday:' . $e['id']; $e['notifyat'] = $alarm['time']; $p['alarms'][] = $e; } } } return $p; } /** * Handler for alarm dismiss hook triggered by libcalendaring */ public function dismiss_alarms($p) { $this->load_driver(); foreach ((array)$p['ids'] as $id) { if (strpos($id, 'cal:bday:') === 0) { $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']); } else if (strpos($id, 'cal:') === 0) { $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']); } } return $p; } /** - * Handler for check-recent requests which are accidentally sent to calendar taks + * Handler for check-recent requests which are accidentally sent to calendar */ 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 = $this->gettext(array('name' => 'filesizeerror', 'vars' => array( 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else { $msg = $this->gettext('fileuploaderror'); } $this->rc->output->command('plugin.import_error', array('message' => $msg)); } $this->rc->output->send('iframe'); } /** * Helper function to parse and import a single .ics file */ private function import_from_file($filepath, $calendar, $rangestart, &$errors) { $user_email = $this->rc->user->get_username(); $ical = $this->get_ical(); $errors = !$ical->fopen($filepath); $count = $i = 0; foreach ($ical as $event) { // keep the browser connection alive on long import jobs if (++$i > 100 && $i % 100 == 0) { echo ""; ob_flush(); } // TODO: correctly handle recurring events which start before $rangestart if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) continue; $event['_owner'] = $user_email; $event['calendar'] = $calendar; if ($this->driver->new_event($event)) { $count++; } else { $errors++; } } return $count; } /** * Construct the ics file for exporting events to iCalendar format; */ function export_events($terminate = true) { $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET); if (!isset($start)) $start = 'today -1 year'; if (!is_numeric($start)) $start = strtotime($start . ' 00:00:00'); if (!$end) $end = 'today +10 years'; if (!is_numeric($end)) $end = strtotime($end . ' 23:59:59'); $event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET); $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET); $calid = $filename = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET); $calendars = $this->driver->list_calendars(); $events = array(); if ($calendars[$calid]) { $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid; $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii if (!empty($event_id)) { if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id), 0, true)) { if ($event['recurrence_id']) { $event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']), 0, true); } $events = array($event); $filename = asciiwords($event['title']); if (empty($filename)) $filename = 'event'; } } else { $events = $this->driver->load_events($start, $end, null, $calid, 0); if (empty($filename)) $filename = $calid; } } header("Content-Type: text/calendar"); header("Content-Disposition: inline; filename=".$filename.'.ics'); $this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null); if ($terminate) exit; } /** * Handler for iCal feed requests */ function ical_feed_export() { $session_exists = !empty($_SESSION['user_id']); // process HTTP auth info if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host() $auth = $this->rc->plugins->exec_hook('authenticate', array( 'host' => $this->rc->autoselect_host(), 'user' => trim($_SERVER['PHP_AUTH_USER']), 'pass' => $_SERVER['PHP_AUTH_PW'], 'cookiecheck' => true, 'valid' => true, )); if ($auth['valid'] && !$auth['abort']) $this->rc->login($auth['user'], $auth['pass'], $auth['host']); } // require HTTP auth if (empty($_SESSION['user_id'])) { header('WWW-Authenticate: Basic realm="Roundcube Calendar"'); header('HTTP/1.0 401 Unauthorized'); exit; } // decode calendar feed hash $format = 'ics'; $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET); if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) { $format = strtolower($m[1]); $calhash = preg_replace($suff_regex, '', $calhash); } if (!strpos($calhash, ':')) $calhash = base64_decode($calhash); list($user, $_GET['source']) = explode(':', $calhash, 2); // sanity check user if ($this->rc->user->get_username() == $user) { $this->load_driver(); $this->export_events(false); } else { header('HTTP/1.0 404 Not Found'); } // don't save session data if (!$session_exists) session_destroy(); exit; } /** * */ function load_settings() { $this->lib->load_settings(); $this->defaults += $this->lib->defaults; $settings = array(); // configuration $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar'); $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']); $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']); $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']); $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']); $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']); $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']); $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']); $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false); $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); // get user identity to create default attendee if ($this->ui->screen == 'calendar') { foreach ($this->rc->user->list_emails() as $rec) { if (!$identity) $identity = $rec; $identity['emails'][] = $rec['email']; $settings['identities'][$rec['identity_id']] = $rec['email']; } $identity['emails'][] = $this->rc->user->get_username(); $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails']))); } return $settings; } /** * Encode events as JSON * * @param array Events as array * @param boolean Add CSS class names according to calendar and categories * @return string JSON encoded events */ function encode($events, $addcss = false) { $json = array(); foreach ($events as $event) { $json[] = $this->_client_event($event, $addcss); } return json_encode($json); } /** * Convert an event object to be used on the client */ private function _client_event($event, $addcss = false) { // compose a human readable strings for alarms_text and recurrence_text if ($event['valarms']) { $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']); $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']); } if ($event['recurrence']) { $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']); $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']); unset($event['recurrence_date']); } foreach ((array)$event['attachments'] as $k => $attachment) { $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); } // convert link URIs references into structs if (array_key_exists('links', $event)) { foreach ((array)$event['links'] as $i => $link) { if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) { $event['links'][$i] = $msgref; } } } // check for organizer in attendees list $organizer = null; foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) { $event['attendees'][$i]['noreply'] = true; } else { unset($event['attendees'][$i]['noreply']); } } if ($organizer === null && !empty($event['organizer'])) { $organizer = $event['organizer']; $organizer['role'] = 'ORGANIZER'; if (!is_array($event['attendees'])) $event['attendees'] = array(); array_unshift($event['attendees'], $organizer); } // Convert HTML description into plain text if ($this->is_html($event)) { $h2t = new rcube_html2text($event['description'], false, true, 0); $event['description'] = trim($h2t->get_text()); } // mapping url => vurl because of the fullcalendar client script $event['vurl'] = $event['url']; unset($event['url']); return array( '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'), 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'), // 'changed' might be empty for event recurrences (Bug #2185) 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null, 'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null, 'title' => strval($event['title']), 'description' => strval($event['description']), 'location' => strval($event['location']), 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true) . rtrim(' ' . $event['className']), 'allDay' => ($event['allday'] == 1), ) + $event; } /** * Generate a unique identifier for an event */ public function generate_uid() { return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); } /** * TEMPORARY: generate random event data for testing * Create events by opening http:///?_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', rcube::Q($event['title'])) ); } /** * */ public function mail_messages_list($p) { if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) { foreach ($p['messages'] as $header) { $part = new StdClass; $part->mimetype = $header->ctype; if (libcalendaring::part_is_vcalendar($part)) { $header->list_flags['attachmentClass'] = 'ical'; } else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) { // TODO: fetch bodystructure and search for ical parts. Maybe too expensive? if (!empty($header->structure) && is_array($header->structure->parts)) { foreach ($header->structure->parts as $part) { if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) { $header->list_flags['attachmentClass'] = 'ical'; break; } } } } } } } /** * Add UI element to copy event invitations or updates to the calendar */ public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) if (!empty($this->lib->ical_parts)) { $this->get_ical(); $this->load_itip(); } $html = ''; $has_events = false; $ical_objects = $this->lib->get_mail_ical_objects(); // show a box for every event in the file foreach ($ical_objects as $idx => $event) { if ($event['_type'] != 'event') // skip non-event objects (#2928) continue; $has_events = true; // get prepared inline UI for this event object if ($ical_objects->method) { $append = ''; // prepare a small agenda preview to be filled with actual event data on async request if ($ical_objects->method == 'REQUEST') { $append = html::div('calendar-agenda-preview', html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' . html::span('date', $this->rc->format_date($event['start'], $this->rc->config->get('date_format'))) ) . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%'); } $html .= html::div('calendar-invitebox', $this->itip->mail_itip_inline_ui( $event, $ical_objects->method, $ical_objects->mime_id . ':' . $idx, 'calendar', rcube_utils::anytodatetime($ical_objects->message_date), $this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $event['start']->format('U') ) . $append ); } // limit listing if ($idx >= 3) break; } // prepend event boxes to message body if ($html) { $this->ui->init(); $p['content'] = $html . $p['content']; $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm'); } // add "Save to calendar" button into attachment menu if ($has_events) { $this->add_button(array( 'id' => 'attachmentsavecal', 'name' => 'attachmentsavecal', 'type' => 'link', 'wrapper' => 'li', 'command' => 'attachment-save-calendar', 'class' => 'icon calendarlink', 'classact' => 'icon calendarlink active', 'innerclass' => 'icon calendar', 'label' => 'calendar.savetocalendar', ), 'attachmentmenu'); } return $p; } /** * Handler for POST request to import an event attached to a mail message */ public function mail_import_itip() { $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST); $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST)); $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)); $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0; $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST); $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST); $error_msg = $this->gettext('errorimportingevent'); $success = false; $delegate = null; if ($status == 'delegated') { $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false); $delegate = reset($delegates); if (empty($delegate) || empty($delegate['mailto'])) { $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error'); return; } } // successfully parsed events? if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { // forward iTip request to delegatee if ($delegate) { $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST)); $itip = $this->load_itip(); if ($itip->delegate_to($event, $delegate, $rsvpme ? true : false)) { $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); } else { $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } // the delegator is set to non-participant, thus save as non-blocking $event['free_busy'] = 'free'; } // find writeable calendar to store event $cal_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null; $dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST'); $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL); $calendar = $calendars[$cal_id]; // select default calendar except user explicitly selected 'none' if (!$calendar && !$dontsave) $calendar = $this->get_default_calendar($event['sensitivity']); $metadata = array( 'uid' => $event['uid'], '_instance' => $event['_instance'], 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0, 'sequence' => intval($event['sequence']), 'fallback' => strtoupper($status), 'method' => $event['_method'], 'task' => 'calendar', ); // update my attendee status according to submitted method if (!empty($status)) { $organizer = null; $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') { $organizer = $attendee; } else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $event['attendees'][$i]['status'] = strtoupper($status); if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $reply_sender = $attendee['email']; $event_attendee = $attendee; } } // add attendee with this user's default identity if not listed if (!$reply_sender) { $sender_identity = $this->rc->user->list_emails(true); $event['attendees'][] = array( 'name' => $sender_identity['name'], 'email' => $sender_identity['email'], 'role' => 'OPT-PARTICIPANT', 'status' => strtoupper($status), ); $metadata['attendee'] = $sender_identity['email']; } } // save to calendar if ($calendar && $calendar['editable']) { // check for existing event with the same UID $existing = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL); if ($existing) { // forward savemode for correct updates of recurring events $existing['_savemode'] = $savemode ?: $event['_savemode']; // only update attendee status if ($event['_method'] == 'REPLY') { // try to identify the attendee using the email sender address $existing_attendee = -1; $existing_attendee_emails = array(); foreach ($existing['attendees'] as $i => $attendee) { $existing_attendee_emails[] = $attendee['email']; if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { $existing_attendee = $i; } } $event_attendee = null; $update_attendees = array(); foreach ($event['attendees'] as $attendee) { if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { $event_attendee = $attendee; $update_attendees[] = $attendee; $metadata['fallback'] = $attendee['status']; $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; if ($attendee['status'] != 'DELEGATED') { break; } } // also copy delegate attendee else if (!empty($attendee['delegated-from']) && (stripos($attendee['delegated-from'], $event['_sender']) !== false || stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) { $update_attendees[] = $attendee; if (!in_array($attendee['email'], $existing_attendee_emails)) { $existing['attendees'][] = $attendee; } } } // if delegatee has declined, set delegator's RSVP=True if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) { foreach ($existing['attendees'] as $i => $attendee) { if ($attendee['email'] == $event_attendee['delegated-from']) { $existing['attendees'][$i]['rsvp'] = true; break; } } } // found matching attendee entry in both existing and new events if ($existing_attendee >= 0 && $event_attendee) { $existing['attendees'][$existing_attendee] = $event_attendee; $success = $this->driver->update_attendees($existing, $update_attendees); } // update the entire attendees block else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) { $existing['attendees'][] = $event_attendee; $success = $this->driver->update_attendees($existing, $update_attendees); } else { $error_msg = $this->gettext('newerversionexists'); } } // delete the event when declined (#1670) else if ($status == 'declined' && $delete) { $deleted = $this->driver->remove_event($existing, true); $success = true; } // import the (newer) event else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) { $event['id'] = $existing['id']; $event['calendar'] = $existing['calendar']; // preserve my participant status for regular updates if (empty($status)) { $emails = $this->get_user_emails(); foreach ($event['attendees'] as $i => $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { foreach ($existing['attendees'] as $j => $_attendee) { if ($attendee['email'] == $_attendee['email']) { $event['attendees'][$i] = $existing['attendees'][$j]; break; } } } } } // set status=CANCELLED on CANCEL messages if ($event['_method'] == 'CANCEL') $event['status'] = 'CANCELLED'; // show me as free when declined (#1670) if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') $event['free_busy'] = 'free'; $success = $this->driver->edit_event($event); } else if (!empty($status)) { $existing['attendees'] = $event['attendees']; if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // show me as free when declined (#1670) $existing['free_busy'] = 'free'; $success = $this->driver->edit_event($existing); } else $error_msg = $this->gettext('newerversionexists'); } else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) { if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') { $event['free_busy'] = 'free'; } // if the RSVP reply only refers to a single instance: // store unmodified master event with current instance as exception if (!empty($instance) && !empty($savemode) && $savemode != 'all') { $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event'); if ($master['recurrence'] && !$master['_instance']) { // compute recurring events until this instance's date if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) { $recurrence_date->setTime(23,59,59); foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) { if ($recurring['_instance'] == $instance) { // copy attendees block with my partstat to exception $recurring['attendees'] = $event['attendees']; $master['recurrence']['EXCEPTIONS'][] = $recurring; $event = $recurring; // set reference for iTip reply break; } } $master['calendar'] = $event['calendar'] = $calendar['id']; $success = $this->driver->new_event($master); } else { $master = null; } } else { $master = null; } } // save to the selected/default calendar if (!$master) { $event['calendar'] = $calendar['id']; $success = $this->driver->new_event($event); } } else if ($status == 'declined') $error_msg = null; } else if ($status == 'declined' || $dontsave) $error_msg = null; else $error_msg = $this->gettext('nowritecalendarfound'); } if ($success) { $message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully')); $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); } if ($success || $dontsave) { $metadata['calendar'] = $event['calendar']; $metadata['nosave'] = $dontsave; $metadata['rsvp'] = intval($metadata['rsvp']); $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']); $this->rc->output->command('plugin.itip_message_processed', $metadata); $error_msg = null; } else if ($error_msg) { $this->rc->output->command('display_message', $error_msg, 'error'); } // send iTip reply if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) { $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); $itip = $this->load_itip(); $itip->set_sender_email($reply_sender); if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } $this->rc->output->send(); } /** * Handler for calendar/itip-remove requests */ function mail_itip_decline_reply() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') { $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); foreach ($event['attendees'] as $_attendee) { if ($_attendee['role'] != 'ORGANIZER') { $attendee = $_attendee; break; } } $itip = $this->load_itip(); if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel')) $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } else { $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } } /** * Handler for calendar/itip-delegate requests */ function mail_itip_delegate() { // forward request to mail_import_itip() with the right status $_POST['_status'] = $_REQUEST['_status'] = 'delegated'; $this->mail_import_itip(); } /** * Import the full payload from a mail message attachment */ public function mail_import_attachment() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); $charset = RCUBE_CHARSET; // establish imap connection $imap = $this->rc->get_storage(); $imap->set_folder($mbox); if ($uid && $mime_id) { $part = $imap->get_message_part($uid, $mime_id); if ($part->ctype_parameters['charset']) $charset = $part->ctype_parameters['charset']; // $headers = $imap->get_message_headers($uid); if ($part) { $events = $this->get_ical()->import($part, $charset); } } $success = $existing = 0; if (!empty($events)) { // find writeable calendar to store event $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null; $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL); foreach ($events as $event) { // save to calendar $calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']); if ($calendar && $calendar['editable'] && $event['_type'] == 'event') { $event['calendar'] = $calendar['id']; if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) { $success += (bool)$this->driver->new_event($event); } else { $existing++; } } } } if ($success) { $this->rc->output->command('display_message', $this->gettext(array( 'name' => 'importsuccess', 'vars' => array('nr' => $success), )), 'confirmation'); } else if ($existing) { $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning'); } else { $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error'); } } /** * Read email message and return contents for a new event based on that message */ public function mail_message2event() { $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); $event = array(); // establish imap connection $imap = $this->rc->get_storage(); $imap->set_folder($mbox); $message = new rcube_message($uid); if ($message->headers) { $event['title'] = trim($message->subject); $event['description'] = trim($message->first_text_part()); $this->load_driver(); // add a reference to the email message if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) { $event['links'] = array($msgref); } // copy mail attachments to event else if ($message->attachments) { $eventid = 'cal-'; if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) { $_SESSION[self::SESSION_KEY] = array(); $_SESSION[self::SESSION_KEY]['id'] = $eventid; $_SESSION[self::SESSION_KEY]['attachments'] = array(); } foreach ((array)$message->attachments as $part) { $attachment = array( 'data' => $imap->get_message_part($uid, $part->mime_id, $part), 'size' => $part->size, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'group' => $eventid, ); $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status'] && !$attachment['abort']) { $id = $attachment['id']; $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); // store new attachment in session unset($attachment['status'], $attachment['abort'], $attachment['data']); $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' $event['attachments'][] = $attachment; } } } $this->rc->output->command('plugin.mail2event_dialog', $event); } else { $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); } $this->rc->output->send(); } /** * Handler for the 'message_compose' plugin hook. This will check for * a compose parameter 'calendar_event' and create an attachment with the * referenced event in iCal format */ public function mail_message_compose($args) { // set the submitted event ID as attachment if (!empty($args['param']['calendar_event'])) { $this->load_driver(); list($cal, $id) = explode(':', $args['param']['calendar_event'], 2); if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) { $filename = asciiwords($event['title']); if (empty($filename)) $filename = 'event'; // save ics to a temp file and register as attachment $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal'); file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body'))); $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar'); $args['param']['subject'] = $event['title']; } } return $args; } /** * Get a list of email addresses of the current user (from login and identities) */ public function get_user_emails() { return $this->lib->get_user_emails(); } /** * Build an absolute URL with the given parameters */ public function get_url($param = array()) { $param += array('task' => 'calendar'); return $this->rc->url($param, true, true); } public function ical_feed_hash($source) { return base64_encode($this->rc->user->get_username() . ':' . $source); } /** * Handler for user_delete plugin hook */ public function user_delete($args) { // delete itipinvitations entries related to this user $db = $this->rc->get_dbh(); $table_itipinvitations = $db->table_name('itipinvitations', true); $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID); $this->load_driver(); return $this->driver->user_delete($args); } /** * Magic getter for public access to protected members */ public function __get($name) { switch ($name) { case 'ical': return $this->get_ical(); case 'itip': return $this->load_itip(); case 'driver': $this->load_driver(); return $this->driver; } return null; } } diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index 023b6dec..ab81965f 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -1,839 +1,839 @@ * * 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_database_driver extends tasklist_driver { const IS_COMPLETE_SQL = "(status='COMPLETED' OR (complete=1 AND status=''))"; public $undelete = true; // yes, we can public $sortable = false; public $alarm_types = array('DISPLAY'); private $rc; private $plugin; private $lists = array(); private $list_ids = ''; private $tags = array(); private $db_tasks = 'tasks'; private $db_lists = 'tasklists'; /** * Default constructor */ public function __construct($plugin) { $this->rc = $plugin->rc; $this->plugin = $plugin; // read database config $db = $this->rc->get_dbh(); $this->db_lists = $this->rc->config->get('db_table_lists', $db->table_name($this->db_lists)); $this->db_tasks = $this->rc->config->get('db_table_tasks', $db->table_name($this->db_tasks)); $this->_read_lists(); } /** * Read available calendars for the current user and store them internally */ private function _read_lists() { $hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', ''))); if (!empty($this->rc->user->ID)) { $list_ids = array(); $result = $this->rc->db->query( "SELECT *, tasklist_id AS id FROM " . $this->db_lists . " WHERE user_id=? ORDER BY CASE WHEN name='INBOX' THEN 0 ELSE 1 END, name", $this->rc->user->ID ); while ($result && ($arr = $this->rc->db->fetch_assoc($result))) { $arr['showalarms'] = intval($arr['showalarms']); $arr['active'] = !in_array($arr['id'], $hidden); $arr['name'] = html::quote($arr['name']); $arr['listname'] = html::quote($arr['name']); $arr['editable'] = true; $arr['rights'] = 'lrswikxtea'; $this->lists[$arr['id']] = $arr; $list_ids[] = $this->rc->db->quote($arr['id']); } $this->list_ids = join(',', $list_ids); } } /** * Get a list of available tasks lists from this source */ public function get_lists() { // attempt to create a default list for this user if (empty($this->lists)) { $prop = array('name' => 'Default', 'color' => '000000'); if ($this->create_list($prop)) $this->_read_lists(); } return $this->lists; } /** * Create a new list assigned to the current user * * @param array Hash array with list properties * @return mixed ID of the new list on success, False on error * @see tasklist_driver::create_list() */ public function create_list(&$prop) { $result = $this->rc->db->query( "INSERT INTO " . $this->db_lists . " (user_id, name, color, showalarms) VALUES (?, ?, ?, ?)", $this->rc->user->ID, strval($prop['name']), strval($prop['color']), $prop['showalarms']?1:0 ); if ($result) return $this->rc->db->insert_id($this->db_lists); return false; } /** * Update properties of an existing tasklist * * @param array Hash array with list properties * @return boolean True on success, Fales on failure * @see tasklist_driver::edit_list() */ public function edit_list(&$prop) { $query = $this->rc->db->query( "UPDATE " . $this->db_lists . " SET name=?, color=?, showalarms=? WHERE tasklist_id=? AND user_id=?", $prop['name'], $prop['color'], $prop['showalarms']?1:0, $prop['id'], $this->rc->user->ID ); return $this->rc->db->affected_rows($query); } /** * Set active/subscribed state of a list * * @param array Hash array with list properties * @return boolean True on success, Fales on failure * @see tasklist_driver::subscribe_list() */ public function subscribe_list($prop) { $hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', ''))); if ($prop['active']) unset($hidden[$prop['id']]); else $hidden[$prop['id']] = 1; return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden)))); } /** * Delete the given list with all its contents * * @param array Hash array with list properties * @return boolean True on success, Fales on failure * @see tasklist_driver::delete_list() */ public function delete_list($prop) { $list_id = $prop['id']; if ($this->lists[$list_id]) { // delete all tasks linked with this list $this->rc->db->query( "DELETE FROM " . $this->db_tasks . " WHERE tasklist_id=?", $list_id ); // delete list record $query = $this->rc->db->query( "DELETE FROM " . $this->db_lists . " WHERE tasklist_id=? AND user_id=?", $list_id, $this->rc->user->ID ); return $this->rc->db->affected_rows($query); } return false; } /** * Search for shared or otherwise not listed tasklists the user has access * * @param string Search string * @param string Section/source to search * @return array List of tasklists */ public function search_lists($query, $source) { return array(); } /** * Get a list of tags to assign tasks to * * @return array List of tags */ public function get_tags() { return array_values(array_unique($this->tags, SORT_STRING)); } /** * Get number of tasks matching the given filter * * @param array List of lists to count tasks of * @return array Hash array with counts grouped by status (all|flagged|today|tomorrow|overdue|nodate) * @see tasklist_driver::count_tasks() */ function count_tasks($lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); // only allow to select from lists of this user $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists))); $today_date = new DateTime('now', $this->plugin->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); $result = $this->rc->db->query(sprintf( "SELECT task_id, flagged, date FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) AND del=0 AND NOT " . self::IS_COMPLETE_SQL, join(',', $list_ids) )); $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $counts['all']++; if ($rec['flagged']) $counts['flagged']++; if (empty($rec['date'])) $counts['nodate']++; else if ($rec['date'] == $today) $counts['today']++; else if ($rec['date'] == $tomorrow) $counts['tomorrow']++; else if ($rec['date'] < $today) $counts['overdue']++; } return $counts; } /** - * Get all taks records matching the given filter + * Get all task records matching the given filter * * @param array Hash array wiht filter criterias * @param array List of lists to get tasks from * @return array List of tasks records matchin the criteria * @see tasklist_driver::list_tasks() */ function list_tasks($filter, $lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); // only allow to select from lists of this user $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists))); $sql_add = ''; // add filter criteria if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) { $sql_add .= ' AND (date IS NULL OR date >= ?)'; $datefrom = $filter['from']; } if ($filter['to']) { if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE) $sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')'; else $sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')'; } // special case 'today': also show all events with date before today if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) { $datefrom = date('Y-m-d', 0); } if ($filter['mask'] & tasklist::FILTER_MASK_NODATE) $sql_add = ' AND date IS NULL'; if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) $sql_add .= ' AND ' . self::IS_COMPLETE_SQL; else if (empty($filter['since'])) // don't show complete tasks by default $sql_add .= ' AND NOT ' . self::IS_COMPLETE_SQL; if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED) $sql_add .= ' AND flagged=1'; // compose (slow) SQL query for searching // FIXME: improve searching using a dedicated col and normalized values if ($filter['search']) { $sql_query = array(); foreach (array('title','description','organizer','attendees') as $col) $sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%'); $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; } if ($filter['since'] && is_numeric($filter['since'])) { $sql_add .= ' AND changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since'])); } $tasks = array(); if (!empty($list_ids)) { $result = $this->rc->db->query(sprintf( "SELECT * FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) AND del=0 %s ORDER BY parent_id, task_id ASC", join(',', $list_ids), $sql_add ), $datefrom ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $tasks[] = $this->_read_postprocess($rec); } } return $tasks; } /** * Return data of a specific task * * @param mixed Hash array with task properties or task UID * @return array Hash array with task properties or false if not found */ public function get_task($prop) { if (is_string($prop)) $prop['uid'] = $prop; $query_col = $prop['id'] ? 'task_id' : 'uid'; $result = $this->rc->db->query(sprintf( "SELECT * FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) AND %s=? AND del=0", $this->list_ids, $query_col ), $prop['id'] ? $prop['id'] : $prop['uid'] ); if ($result && ($rec = $this->rc->db->fetch_assoc($result))) { return $this->_read_postprocess($rec); } return false; } /** * Get all decendents of the given task record * * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched * @return array List of all child task IDs */ public function get_childs($prop, $recursive = false) { // resolve UID first if (is_string($prop)) { $result = $this->rc->db->query(sprintf( "SELECT task_id AS id, tasklist_id AS list FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) AND uid=?", $this->list_ids ), $prop); $prop = $this->rc->db->fetch_assoc($result); } $childs = array(); $task_ids = array($prop['id']); // query for childs (recursively) while (!empty($task_ids)) { $result = $this->rc->db->query(sprintf( "SELECT task_id AS id FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) AND parent_id IN (%s) AND del=0", $this->list_ids, join(',', array_map(array($this->rc->db, 'quote'), $task_ids)) )); $task_ids = array(); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $childs[] = $rec['id']; $task_ids[] = $rec['id']; } if (!$recursive) break; } return $childs; } /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); // only allow to select from calendars with activated alarms $list_ids = array(); foreach ($lists as $lid) { if ($this->lists[$lid] && $this->lists[$lid]['showalarms']) $list_ids[] = $lid; } $list_ids = array_map(array($this->rc->db, 'quote'), $list_ids); $alarms = array(); if (!empty($list_ids)) { $result = $this->rc->db->query(sprintf( "SELECT * FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) AND notify <= %s AND NOT " . self::IS_COMPLETE_SQL, join(',', $list_ids), $this->rc->db->fromunixtime($time) )); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) $alarms[] = $this->_read_postprocess($rec); } return $alarms; } /** * Feedback after showing/sending an alarm notification * * @see tasklist_driver::dismiss_alarm() */ public function dismiss_alarm($task_id, $snooze = 0) { // set new notifyat time or unset if not snoozed $notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; $query = $this->rc->db->query(sprintf( "UPDATE " . $this->db_tasks . " SET changed=%s, notify=? WHERE task_id=? AND tasklist_id IN (" . $this->list_ids . ")", $this->rc->db->now()), $notify_at, $task_id ); return $this->rc->db->affected_rows($query); } /** * Remove alarm dismissal or snooze state * * @param string Task identifier */ public function clear_alarms($id) { // Nothing to do here. Alarms are reset in edit_task() } /** * Map some internal database values to match the generic "API" */ private function _read_postprocess($rec) { $rec['id'] = $rec['task_id']; $rec['list'] = $rec['tasklist_id']; $rec['changed'] = new DateTime($rec['changed']); $rec['tags'] = array_filter(explode(',', $rec['tags'])); if (!$rec['parent_id']) unset($rec['parent_id']); // decode serialized alarms if ($rec['alarms']) { $rec['valarms'] = $this->unserialize_alarms($rec['alarms']); unset($rec['alarms']); } // decode serialze recurrence rules if ($rec['recurrence']) { $rec['recurrence'] = $this->unserialize_recurrence($rec['recurrence']); } if (!empty($rec['tags'])) { $this->tags = array_merge($this->tags, (array)$rec['tags']); } unset($rec['task_id'], $rec['tasklist_id'], $rec['created']); return $rec; } /** * Add a single task to the database * * @param array Hash array with task properties (see header of this file) * @return mixed New event ID on success, False on error * @see tasklist_driver::create_task() */ public function create_task($prop) { // check list permissions $list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists)); if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly']) return false; if (is_array($prop['valarms'])) { $prop['alarms'] = $this->serialize_alarms($prop['valarms']); } if (is_array($prop['recurrence'])) { $prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']); } foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { if (empty($prop[$col])) $prop[$col] = null; } $notify_at = $this->_get_notification($prop); $result = $this->rc->db->query(sprintf( "INSERT INTO " . $this->db_tasks . " (tasklist_id, uid, parent_id, created, changed, title, date, time, startdate, starttime, description, tags, flagged, complete, status, alarms, recurrence, notify) VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $this->rc->db->now(), $this->rc->db->now() ), $list_id, $prop['uid'], $prop['parent_id'], $prop['title'], $prop['date'], $prop['time'], $prop['startdate'], $prop['starttime'], strval($prop['description']), join(',', (array)$prop['tags']), $prop['flagged'] ? 1 : 0, intval($prop['complete']), $prop['status'], $prop['alarms'], $prop['recurrence'], $notify_at ); if ($result) return $this->rc->db->insert_id($this->db_tasks); return false; } /** * Update an task entry with the given data * * @param array Hash array with task properties * @return boolean True on success, False on error * @see tasklist_driver::edit_task() */ public function edit_task($prop) { if (is_array($prop['valarms'])) { $prop['alarms'] = $this->serialize_alarms($prop['valarms']); } if (is_array($prop['recurrence'])) { $prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']); } $sql_set = array(); foreach (array('title', 'description', 'flagged', 'complete') as $col) { if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]); } foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col])); } if (isset($prop['tags'])) $sql_set[] = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop['tags'])); if (isset($prop['date']) || isset($prop['time']) || isset($prop['alarms'])) { $notify_at = $this->_get_notification($prop); $sql_set[] = $this->rc->db->quote_identifier('notify') . '=' . (empty($notify_at) ? 'NULL' : $this->rc->db->quote($notify_at)); } // moved from another list if ($prop['_fromlist'] && ($newlist = $prop['list'])) { $sql_set[] = 'tasklist_id=' . $this->rc->db->quote($newlist); } $query = $this->rc->db->query(sprintf( "UPDATE " . $this->db_tasks . " SET changed=%s %s WHERE task_id=? AND tasklist_id IN (%s)", $this->rc->db->now(), ($sql_set ? ', ' . join(', ', $sql_set) : ''), $this->list_ids ), $prop['id'] ); return $this->rc->db->affected_rows($query); } /** * Move a single task to another list * * @param array Hash array with task properties: * @return boolean True on success, False on error * @see tasklist_driver::move_task() */ public function move_task($prop) { return $this->edit_task($prop); } /** * Remove a single task from the database * * @param array Hash array with task properties * @param boolean Remove record irreversible * @return boolean True on success, False on error * @see tasklist_driver::delete_task() */ public function delete_task($prop, $force = true) { $task_id = $prop['id']; if ($task_id && $force) { $query = $this->rc->db->query( "DELETE FROM " . $this->db_tasks . " WHERE task_id=? AND tasklist_id IN (" . $this->list_ids . ")", $task_id ); } else if ($task_id) { $query = $this->rc->db->query(sprintf( "UPDATE " . $this->db_tasks . " SET changed=%s, del=1 WHERE task_id=? AND tasklist_id IN (%s)", $this->rc->db->now(), $this->list_ids ), $task_id ); } return $this->rc->db->affected_rows($query); } /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties * @return boolean True on success, False on error * @see tasklist_driver::undelete_task() */ public function undelete_task($prop) { $query = $this->rc->db->query(sprintf( "UPDATE " . $this->db_tasks . " SET changed=%s, del=0 WHERE task_id=? AND tasklist_id IN (%s)", $this->rc->db->now(), $this->list_ids ), $prop['id'] ); return $this->rc->db->affected_rows($query); } /** * Compute absolute time to notify the user */ private function _get_notification($task) { if ($task['valarms'] && !$this->is_complete($task)) { $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) return date('Y-m-d H:i:s', $alarm['time']); } return null; } /** * Helper method to serialize the list of alarms into a string */ private function serialize_alarms($valarms) { foreach ((array)$valarms as $i => $alarm) { if ($alarm['trigger'] instanceof DateTime) { $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c'); } } return $valarms ? json_encode($valarms) : null; } /** * Helper method to decode a serialized list of alarms */ private function unserialize_alarms($alarms) { // decode json serialized alarms if ($alarms && $alarms[0] == '[') { $valarms = json_decode($alarms, true); foreach ($valarms as $i => $alarm) { if ($alarm['trigger'][0] == '@') { try { $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1)); } catch (Exception $e) { unset($valarms[$i]); } } } } // convert legacy alarms data else if (strlen($alarms)) { list($trigger, $action) = explode(':', $alarms, 2); if ($trigger = libcalendaring::parse_alarm_value($trigger)) { $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0])); } } return $valarms; } /** * Helper method to serialize task recurrence properties */ private function serialize_recurrence($recurrence) { foreach ((array)$recurrence as $k => $val) { if ($val instanceof DateTime) { $recurrence[$k] = '@' . $val->format('c'); } } return $recurrence ? json_encode($recurrence) : null; } /** * Helper method to decode a serialized task recurrence struct */ private function unserialize_recurrence($ser) { if (strlen($ser)) { $recurrence = json_decode($ser, true); foreach ((array)$recurrence as $k => $val) { if ($val[0] == '@') { try { $recurrence[$k] = new DateTime(substr($val, 1)); } catch (Exception $e) { unset($recurrence[$k]); } } } } else { $recurrence = ''; } return $recurrence; } /** * Handler for user_delete plugin hook */ public function user_delete($args) { $db = $this->rc->db; $list_ids = array(); $lists = $db->query("SELECT tasklist_id FROM " . $this->db_lists . " WHERE user_id=?", $args['user']->ID); while ($row = $db->fetch_assoc($lists)) { $list_ids[] = $row['tasklist_id']; } if (!empty($list_ids)) { foreach (array($this->db_tasks, $this->db_lists) as $table) { $db->query(sprintf("DELETE FROM $table WHERE tasklist_id IN (%s)", join(',', $list_ids))); } } } } diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 614799bc..042b9989 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -1,1731 +1,1731 @@ * * 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_kolab_driver extends tasklist_driver { // features supported by the backend public $alarms = false; public $attachments = true; public $attendees = true; public $undelete = false; // task undelete action public $alarm_types = array('DISPLAY','AUDIO'); public $search_more_results; private $rc; private $plugin; private $lists; private $folders = array(); private $tasks = array(); private $tags = array(); private $bonnie_api = false; /** * Default constructor */ public function __construct($plugin) { $this->rc = $plugin->rc; $this->plugin = $plugin; if (kolab_storage::$version == '2.0') { $this->alarm_absolute = false; } // tasklist use fully encoded identifiers kolab_storage::$encode_ids = true; // get configuration for the Bonnie API $this->bonnie_api = libkolab::get_bonnie_api(); $this->_read_lists(); $this->plugin->register_action('folder-acl', array($this, 'folder_acl')); } /** * Read available calendars for the current user and store them internally */ private function _read_lists($force = false) { // already read sources if (isset($this->lists) && !$force) return $this->lists; // get all folders that have type "task" $folders = kolab_storage::sort_folders(kolab_storage::get_folders('task')); $this->lists = $this->folders = array(); $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); // find default folder $default_index = 0; foreach ($folders as $i => $folder) { if ($folder->default && strpos($folder->name, $delim) === false) $default_index = $i; } // put default folder (aka INBOX) on top of the list if ($default_index > 0) { $default_folder = $folders[$default_index]; unset($folders[$default_index]); array_unshift($folders, $default_folder); } $prefs = $this->rc->config->get('kolab_tasklists', array()); foreach ($folders as $folder) { $tasklist = $this->folder_props($folder, $prefs); $this->lists[$tasklist['id']] = $tasklist; $this->folders[$tasklist['id']] = $folder; $this->folders[$folder->name] = $folder; } } /** * Derive list properties from the given kolab_storage_folder object */ protected function folder_props($folder, $prefs) { if ($folder->get_namespace() == 'personal') { $norename = false; $editable = true; $rights = 'lrswikxtea'; $alarms = true; } else { $alarms = false; $rights = 'lr'; $editable = false; if (($myrights = $folder->get_myrights()) && !PEAR::isError($myrights)) { $rights = $myrights; if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) $editable = strpos($rights, 'i'); } $info = $folder->get_folder_info(); $norename = $readonly || $info['norename'] || $info['protected']; } $list_id = $folder->id; #kolab_storage::folder_id($folder->name); $old_id = kolab_storage::folder_id($folder->name, false); if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) { $prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms']; } return array( 'id' => $list_id, 'name' => $folder->get_name(), 'listname' => $folder->get_foldername(), 'editname' => $folder->get_foldername(), 'color' => $folder->get_color('0000CC'), 'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms, 'editable' => $editable, 'rights' => $rights, 'norename' => $norename, 'active' => $folder->is_active(), 'parentfolder' => $folder->get_parent(), 'default' => $folder->default, 'virtual' => $folder->virtual, 'children' => true, // TODO: determine if that folder indeed has child folders 'subscribed' => (bool)$folder->is_subscribed(), 'removable' => !$folder->default, 'subtype' => $folder->subtype, 'group' => $folder->default ? 'default' : $folder->get_namespace(), 'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')), 'caldavuid' => $folder->get_uid(), 'history' => !empty($this->bonnie_api), ); } /** * Get a list of available task lists from this source */ public function get_lists(&$tree = null) { // attempt to create a default list for this user if (empty($this->lists) && !isset($this->search_more_results)) { $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true); if ($this->create_list($prop)) $this->_read_lists(true); } $folders = array(); foreach ($this->lists as $id => $list) { if (!empty($this->folders[$id])) { $folders[] = $this->folders[$id]; } } // include virtual folders for a full folder tree if (!is_null($tree)) { $folders = kolab_storage::folder_hierarchy($folders, $tree); } $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); $prefs = $this->rc->config->get('kolab_tasklists', array()); $lists = array(); foreach ($folders as $folder) { $list_id = $folder->id; // kolab_storage::folder_id($folder->name); $imap_path = explode($delim, $folder->name); // find parent do { array_pop($imap_path); $parent_id = kolab_storage::folder_id(join($delim, $imap_path)); } while (count($imap_path) > 1 && !$this->folders[$parent_id]); // restore "real" parent ID if ($parent_id && !$this->folders[$parent_id]) { $parent_id = kolab_storage::folder_id($folder->get_parent()); } $fullname = $folder->get_name(); $listname = $folder->get_foldername(); // special handling for virtual folders if ($folder instanceof kolab_storage_folder_user) { $lists[$list_id] = array( 'id' => $list_id, 'name' => $folder->get_name(), 'listname' => $listname, 'title' => $folder->get_title(), 'virtual' => true, 'editable' => false, 'rights' => 'l', 'group' => 'other virtual', 'class' => 'user', 'parent' => $parent_id, ); } else if ($folder->virtual) { $lists[$list_id] = array( 'id' => $list_id, 'name' => kolab_storage::object_name($fullname), 'listname' => $listname, 'virtual' => true, 'editable' => false, 'rights' => 'l', 'group' => $folder->get_namespace(), 'class' => 'folder', 'parent' => $parent_id, ); } else { if (!$this->lists[$list_id]) { $this->lists[$list_id] = $this->folder_props($folder, $prefs); $this->folders[$list_id] = $folder; } $this->lists[$list_id]['parent'] = $parent_id; $lists[$list_id] = $this->lists[$list_id]; } } return $lists; } /** * Get the kolab_calendar instance for the given calendar ID * * @param string List identifier (encoded imap folder name) * @return object kolab_storage_folder Object nor null if list doesn't exist */ protected function get_folder($id) { // create list and folder instance if necesary if (!$this->lists[$id]) { $folder = kolab_storage::get_folder(kolab_storage::id_decode($id)); if ($folder->type) { $this->folders[$id] = $folder; $this->lists[$id] = $this->folder_props($folder, $this->rc->config->get('kolab_tasklists', array())); } } return $this->folders[$id]; } /** * Create a new list assigned to the current user * * @param array Hash array with list properties * name: List name * color: The color of the list * showalarms: True if alarms are enabled * @return mixed ID of the new list on success, False on error */ public function create_list(&$prop) { $prop['type'] = 'task' . ($prop['default'] ? '.default' : ''); $prop['active'] = true; // activate folder by default $prop['subscribed'] = true; $folder = kolab_storage::folder_update($prop); if ($folder === false) { $this->last_error = kolab_storage::$last_error; return false; } // create ID $id = kolab_storage::folder_id($folder); $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); if (isset($prop['showalarms'])) $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_tasklists'][$id]) $this->rc->user->save_prefs($prefs); // force page reload to properly render folder hierarchy if (!empty($prop['parent'])) { $prop['_reload'] = true; } else { $folder = kolab_storage::get_folder($folder); $prop += $this->folder_props($folder, array()); } return $id; } /** * Update properties of an existing tasklist * * @param array Hash array with list properties * id: List Identifier * name: List name * color: The color of the list * showalarms: True if alarms are enabled (if supported) * @return boolean True on success, Fales on failure */ public function edit_list(&$prop) { if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) { $prop['oldname'] = $folder->name; $prop['type'] = 'task'; $newfolder = kolab_storage::folder_update($prop); if ($newfolder === false) { $this->last_error = kolab_storage::$last_error; return false; } // create ID $id = kolab_storage::folder_id($newfolder); // fallback to local prefs $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); unset($prefs['kolab_tasklists'][$prop['id']]); if (isset($prop['showalarms'])) $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_tasklists'][$id]) $this->rc->user->save_prefs($prefs); // force page reload if folder name/hierarchy changed if ($newfolder != $prop['oldname']) $prop['_reload'] = true; return $id; } return false; } /** * Set active/subscribed state of a list * * @param array Hash array with list properties * id: List Identifier * active: True if list is active, false if not * permanent: True if list is to be subscribed permanently * @return boolean True on success, Fales on failure */ public function subscribe_list($prop) { if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) { $ret = false; if (isset($prop['permanent'])) $ret |= $folder->subscribe(intval($prop['permanent'])); if (isset($prop['active'])) $ret |= $folder->activate(intval($prop['active'])); // apply to child folders, too if ($prop['recursive']) { foreach ((array)kolab_storage::list_folders($folder->name, '*', 'task') as $subfolder) { if (isset($prop['permanent'])) ($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder)); if (isset($prop['active'])) ($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder)); } } return $ret; } return false; } /** * Delete the given list with all its contents * * @param array Hash array with list properties * id: list Identifier * @return boolean True on success, Fales on failure */ public function delete_list($prop) { if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) { if (kolab_storage::folder_delete($folder->name)) return true; else $this->last_error = kolab_storage::$last_error; } return false; } /** * Search for shared or otherwise not listed tasklists the user has access * * @param string Search string * @param string Section/source to search * @return array List of tasklists */ public function search_lists($query, $source) { if (!kolab_storage::setup()) { return array(); } $this->search_more_results = false; $this->lists = $this->folders = array(); // find unsubscribed IMAP folders that have "event" type if ($source == 'folders') { foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) { $this->folders[$folder->id] = $folder; $this->lists[$folder->id] = $this->folder_props($folder, array()); } } // search other user's namespace via LDAP else if ($source == 'users') { $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) { $folders = array(); // search for tasks folders shared by this user foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) { $folders[] = new kolab_storage_folder($foldername, 'task'); } if (count($folders)) { $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user); $this->folders[$userfolder->id] = $userfolder; $this->lists[$userfolder->id] = $this->folder_props($userfolder, array()); foreach ($folders as $folder) { $this->folders[$folder->id] = $folder; $this->lists[$folder->id] = $this->folder_props($folder, array()); $count++; } } if ($count >= $limit) { $this->search_more_results = true; break; } } } return $this->get_lists(); } /** * Get a list of tags to assign tasks to * * @return array List of tags */ public function get_tags() { $config = kolab_storage_config::get_instance(); $tags = $config->get_tags(); $backend_tags = array_map(function($v) { return $v['name']; }, $tags); return array_values(array_unique(array_merge($this->tags, $backend_tags))); } /** * Get number of tasks matching the given filter * * @param array List of lists to count tasks of * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) */ public function count_tasks($lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); $today_date = new DateTime('now', $this->plugin->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0, 'mytasks' => 0); foreach ($lists as $list_id) { if (!$folder = $this->get_folder($list_id)) { continue; } foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) { $rec = $this->_to_rcube_task($record, $list_id, false); if ($this->is_complete($rec)) // don't count complete tasks continue; $counts['all']++; if ($rec['flagged']) $counts['flagged']++; if (empty($rec['date'])) $counts['nodate']++; else if ($rec['date'] == $today) $counts['today']++; else if ($rec['date'] == $tomorrow) $counts['tomorrow']++; else if ($rec['date'] < $today) $counts['overdue']++; if ($this->plugin->is_attendee($rec) !== false) $counts['mytasks']++; } } // avoid session race conditions that will loose temporary subscriptions $this->plugin->rc->session->nowrite = true; return $counts; } /** - * Get all taks records matching the given filter + * Get all task records matching the given filter * * @param array Hash array with filter criterias: * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) * - from: Date range start as string (Y-m-d) * - to: Date range end as string (Y-m-d) * - search: Search query string * @param array List of lists to get tasks from * @return array List of tasks records matchin the criteria */ public function list_tasks($filter, $lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); $results = array(); // query Kolab storage $query = array(); if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) $query[] = array('tags','~','x-complete'); else if (empty($filter['since'])) $query[] = array('tags','!~','x-complete'); // full text search (only works with cache enabled) if ($filter['search']) { $search = mb_strtolower($filter['search']); foreach (rcube_utils::normalize_string($search, true) as $word) { $query[] = array('words', '~', $word); } } if ($filter['since']) { $query[] = array('changed', '>=', $filter['since']); } // load all tags into memory first kolab_storage_config::get_instance()->get_tags(); foreach ($lists as $list_id) { if (!$folder = $this->get_folder($list_id)) { continue; } foreach ($folder->select($query) as $record) { $this->load_tags($record); $task = $this->_to_rcube_task($record, $list_id); // TODO: post-filter tasks returned from storage $results[] = $task; } } // avoid session race conditions that will loose temporary subscriptions $this->plugin->rc->session->nowrite = true; return $results; } /** * Return data of a specific task * * @param mixed Hash array with task properties or task UID * @return array Hash array with task properties or false if not found */ public function get_task($prop) { $this->_parse_id($prop); $id = $prop['uid']; $list_id = $prop['list']; $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders; // find task in the available folders foreach ($folders as $list_id => $folder) { if (is_numeric($list_id) || !$folder) continue; if (!$this->tasks[$id] && ($object = $folder->get_object($id))) { $this->load_tags($object); $this->tasks[$id] = $this->_to_rcube_task($object, $list_id); break; } } return $this->tasks[$id]; } /** * Get all decendents of the given task record * * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched * @return array List of all child task IDs */ public function get_childs($prop, $recursive = false) { if (is_string($prop)) { $task = $this->get_task($prop); $prop = array('uid' => $task['uid'], 'list' => $task['list']); } else { $this->_parse_id($prop); } $childs = array(); $list_id = $prop['list']; $task_ids = array($prop['uid']); $folder = $this->get_folder($list_id); // query for childs (recursively) while ($folder && !empty($task_ids)) { $query_ids = array(); foreach ($task_ids as $task_id) { $query = array(array('tags','=','x-parent:' . $task_id)); foreach ($folder->select($query) as $record) { // don't rely on kolab_storage_folder filtering if ($record['parent_id'] == $task_id) { $childs[] = $list_id . ':' . $record['uid']; $query_ids[] = $record['uid']; } } } if (!$recursive) break; $task_ids = $query_ids; } return $childs; } /** * Provide a list of revisions for the given task * * @param array $task Hash array with task properties * @return array List of changes, each as a hash array * @see tasklist_driver::get_task_changelog() */ public function get_task_changelog($prop) { if (empty($this->bonnie_api)) { return false; } list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null; if (is_array($result) && $result['uid'] == $uid) { return $result['changes']; } return false; } /** * Return full data of a specific revision of an event * * @param mixed $task UID string or hash array with task properties * @param mixed $rev Revision number * * @return array Task object as hash array * @see tasklist_driver::get_task_revision() */ public function get_task_revison($prop, $rev) { if (empty($this->bonnie_api)) { return false; } $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); // call Bonnie API $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid); if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) { $format = kolab_format::factory('task'); $format->load($result['xml']); $rec = $format->to_array(); $format->get_attachments($rec, true); if ($format->is_valid()) { $rec = self::_to_rcube_task($rec, $list_id, false); $rec['rev'] = $result['rev']; return $rec; } } return false; } /** * Command the backend to restore a certain revision of a task. * This shall replace the current object with an older version. * * @param mixed $task UID string or hash array with task properties * @param mixed $rev Revision number * * @return boolean True on success, False on failure * @see tasklist_driver::restore_task_revision() */ public function restore_task_revision($prop, $rev) { if (empty($this->bonnie_api)) { return false; } $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); $folder = $this->get_folder($list_id); $success = false; if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) { $imap = $this->rc->get_storage(); // insert $raw_msg as new message if ($imap->save_message($folder->name, $raw_msg, null, false)) { $success = true; // delete old revision from imap and cache $imap->delete_message($msguid, $folder->name); $folder->cache->set($msguid, false); } } return $success; } /** * Get a list of property changes beteen two revisions of a task object * * @param array $task Hash array with task properties * @param mixed $rev Revisions: "from:to" * * @return array List of property changes, each as a hash array * @see tasklist_driver::get_task_diff() */ public function get_task_diff($prop, $rev1, $rev2) { $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop); // call Bonnie API $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id); if (is_array($result) && $result['uid'] == $uid) { $result['rev1'] = $rev1; $result['rev2'] = $rev2; $keymap = array( 'start' => 'start', 'due' => 'date', 'dstamp' => 'changed', 'summary' => 'title', 'alarm' => 'alarms', 'attendee' => 'attendees', 'attach' => 'attachments', 'rrule' => 'recurrence', 'related-to' => 'parent_id', 'percent-complete' => 'complete', 'lastmodified-date' => 'changed', ); $prop_keymaps = array( 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'), 'attendees' => array('partstat' => 'status'), ); $special_changes = array(); // map kolab event properties to keys the client expects array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) { if (array_key_exists($change['property'], $keymap)) { $change['property'] = $keymap[$change['property']]; } if ($change['property'] == 'priority') { $change['property'] = 'flagged'; $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null; $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null; } // map alarms trigger value if ($change['property'] == 'alarms') { if (is_array($change['old']) && is_array($change['old']['trigger'])) $change['old']['trigger'] = $change['old']['trigger']['value']; if (is_array($change['new']) && is_array($change['new']['trigger'])) $change['new']['trigger'] = $change['new']['trigger']['value']; } // make all property keys uppercase if ($change['property'] == 'recurrence') { $special_changes['recurrence'] = $i; foreach (array('old','new') as $m) { if (is_array($change[$m])) { $props = array(); foreach ($change[$m] as $k => $v) { $props[strtoupper($k)] = $v; } $change[$m] = $props; } } } // map property keys names if (is_array($prop_keymaps[$change['property']])) { foreach ($prop_keymaps[$change['property']] as $k => $dest) { if (is_array($change['old']) && array_key_exists($k, $change['old'])) { $change['old'][$dest] = $change['old'][$k]; unset($change['old'][$k]); } if (is_array($change['new']) && array_key_exists($k, $change['new'])) { $change['new'][$dest] = $change['new'][$k]; unset($change['new'][$k]); } } } if ($change['property'] == 'exdate') { $special_changes['exdate'] = $i; } else if ($change['property'] == 'rdate') { $special_changes['rdate'] = $i; } }); // merge some recurrence changes foreach (array('exdate','rdate') as $prop) { if (array_key_exists($prop, $special_changes)) { $exdate = $result['changes'][$special_changes[$prop]]; if (array_key_exists('recurrence', $special_changes)) { $recurrence = &$result['changes'][$special_changes['recurrence']]; } else { $i = count($result['changes']); $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array()); $recurrence = &$result['changes'][$i]['recurrence']; } $key = strtoupper($prop); $recurrence['old'][$key] = $exdate['old']; $recurrence['new'][$key] = $exdate['new']; unset($result['changes'][$special_changes[$prop]]); } } return $result; } return false; } /** * Helper method to resolved the given task identifier into uid and folder * * @return array (uid,folder,msguid) tuple */ private function _resolve_task_identity($prop) { $mailbox = $msguid = null; $this->_parse_id($prop); $uid = $prop['uid']; $list_id = $prop['list']; if ($folder = $this->get_folder($list_id)) { $mailbox = $folder->get_mailbox_id(); // get task object from storage in order to get the real object uid an msguid if ($rec = $folder->get_object($uid)) { $msguid = $rec['_msguid']; $uid = $rec['uid']; } } return array($uid, $mailbox, $msguid); } /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->config->get('refresh_interval', 0)); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) return array(); if ($lists && is_string($lists)) $lists = explode(',', $lists); $time = $slot + $interval; $candidates = array(); $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled if (!$list['showalarms'] || ($lists && !in_array($lid, $lists))) continue; $folder = $this->get_folder($lid); foreach ($folder->select($query) as $record) { if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-) continue; $task = $this->_to_rcube_task($record, $lid, false); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) { $id = $alarm['id']; // use alarm-id as primary identifier $candidates[$id] = array( 'id' => $id, 'title' => $task['title'], 'date' => $task['date'], 'time' => $task['time'], 'notifyat' => $alarm['time'], 'action' => $alarm['action'], ); } } } // get alarm information stored in local database if (!empty($candidates)) { $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates)); $result = $this->rc->db->query("SELECT *" . " FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")" . " AND `user_id` = ?", $this->rc->user->ID ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $dbdata[$rec['alarm_id']] = $rec; } } $alarms = array(); foreach ($candidates as $id => $task) { // skip dismissed if ($dbdata[$id]['dismissed']) continue; // snooze function may have shifted alarm time $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; if ($notifyat <= $time) $alarms[] = $task; } return $alarms; } /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string Task identifier * @param integer Suspend the alarm for this number of seconds */ public function dismiss_alarm($id, $snooze = 0) { // delete old alarm entry $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` = ? AND `user_id` = ?", $id, $this->rc->user->ID ); // set new notifyat time or unset if not snoozed $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; $query = $this->rc->db->query( "INSERT INTO " . $this->rc->db->table_name('kolab_alarms', true) . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`) VALUES (?, ?, ?, ?)", $id, $this->rc->user->ID, $snooze > 0 ? 0 : 1, $notifyat ); return $this->rc->db->affected_rows($query); } /** * Remove alarm dismissal or snooze state * * @param string Task identifier */ public function clear_alarms($id) { // delete alarm entry $this->rc->db->query( "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . " WHERE `alarm_id` = ? AND `user_id` = ?", $id, $this->rc->user->ID ); return true; } /** * Get task tags */ private function load_tags(&$object) { // this task hasn't been migrated yet if (!empty($object['categories'])) { // OPTIONAL: call kolab_storage_config::apply_tags() to migrate the object $object['tags'] = (array)$object['categories']; if (!empty($object['tags'])) { $this->tags = array_merge($this->tags, $object['tags']); } } else { $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($object['uid']); $object['tags'] = array_map(function($v) { return $v['name']; }, $tags); } } /** * Update task tags */ private function save_tags($uid, $tags) { $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } /** * Find messages linked with a task record */ private function get_links($uid) { $config = kolab_storage_config::get_instance(); return $config->get_object_links($uid); } /** * */ private function save_links($uid, $links) { // make sure we have a valid array if (empty($links)) { $links = array(); } $config = kolab_storage_config::get_instance(); $remove = array_diff($config->get_object_links($uid), $links); return $config->save_object_links($uid, $links, $remove); } /** * Extract uid + list identifiers from the given input * * @param mixed array or string with task identifier(s) */ private function _parse_id(&$prop) { $id_ = null; if (is_array($prop)) { // 'uid' + 'list' available, nothing to be done if (!empty($prop['uid']) && !empty($prop['list'])) { return; } // 'id' is given if (!empty($prop['id'])) { if (!empty($prop['list'])) { $list_id = $prop['_fromlist'] ?: $prop['list']; if (strpos($prop['id'], $list_id.':') === 0) { $prop['uid'] = substr($prop['id'], strlen($list_id)+1); } else { $prop['uid'] = $prop['id']; } } else { $id_ = $prop['id']; } } } else { $id_ = strval($prop); $prop = array(); } // split 'id' into list + uid if (!empty($id_)) { list($list, $uid) = explode(':', $id_, 2); if (!empty($uid)) { $prop['uid'] = $uid; $prop['list'] = $list; } else { $prop['uid'] = $id_; } } } /** * Convert from Kolab_Format to internal representation */ private function _to_rcube_task($record, $list_id, $all = true) { $id_prefix = $list_id . ':'; $task = array( 'id' => $id_prefix . $record['uid'], 'uid' => $record['uid'], 'title' => $record['title'], // 'location' => $record['location'], 'description' => $record['description'], 'flagged' => $record['priority'] == 1, 'complete' => floatval($record['complete'] / 100), 'status' => $record['status'], 'parent_id' => $record['parent_id'] ? $id_prefix . $record['parent_id'] : null, 'recurrence' => $record['recurrence'], 'attendees' => $record['attendees'], 'organizer' => $record['organizer'], 'sequence' => $record['sequence'], 'tags' => $record['tags'], 'list' => $list_id, ); // we can sometimes skip this expensive operation if ($all) { $task['links'] = $this->get_links($task['uid']); } // convert from DateTime to internal date format if (is_a($record['due'], 'DateTime')) { $due = $this->plugin->lib->adjust_timezone($record['due']); $task['date'] = $due->format('Y-m-d'); if (!$record['due']->_dateonly) $task['time'] = $due->format('H:i'); } // convert from DateTime to internal date format if (is_a($record['start'], 'DateTime')) { $start = $this->plugin->lib->adjust_timezone($record['start']); $task['startdate'] = $start->format('Y-m-d'); if (!$record['start']->_dateonly) $task['starttime'] = $start->format('H:i'); } if (is_a($record['changed'], 'DateTime')) { $task['changed'] = $record['changed']; } if (is_a($record['created'], 'DateTime')) { $task['created'] = $record['created']; } if ($record['valarms']) { $task['valarms'] = $record['valarms']; } else if ($record['alarms']) { $task['alarms'] = $record['alarms']; } if (!empty($task['attendees'])) { foreach ((array)$task['attendees'] as $i => $attendee) { if (is_array($attendee['delegated-from'])) { $task['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']); } if (is_array($attendee['delegated-to'])) { $task['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']); } } } if (!empty($record['_attachments'])) { foreach ($record['_attachments'] as $key => $attachment) { if ($attachment !== false) { if (!$attachment['name']) $attachment['name'] = $key; $attachments[] = $attachment; } } $task['attachments'] = $attachments; } return $task; } /** * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving * (opposite of self::_to_rcube_event()) */ private function _from_rcube_task($task, $old = array()) { $object = $task; $id_prefix = $task['list'] . ':'; if (!empty($task['date'])) { $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->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->plugin->timezone); if (empty($task['starttime'])) $object['start']->_dateonly = true; unset($object['startdate']); } // as per RFC (and the Kolab schema validation), start and due dates need to be of the same type (#3614) // this should be catched in the client already but just make sure we don't write invalid objects if (!empty($object['start']) && !empty($object['due']) && $object['due']->_dateonly != $object['start']->_dateonly) { $object['start']->_dateonly = true; $object['due']->_dateonly = true; } $object['complete'] = $task['complete'] * 100; if ($task['complete'] == 1.0 && empty($task['complete'])) $object['status'] = 'COMPLETED'; if ($task['flagged']) $object['priority'] = 1; else $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0; // remove list: prefix from parent_id if (!empty($task['parent_id']) && strpos($task['parent_id'], $id_prefix) === 0) { $object['parent_id'] = substr($task['parent_id'], strlen($id_prefix)); } // copy meta data (starting with _) from old object foreach ((array)$old as $key => $val) { if (!isset($object[$key]) && $key[0] == '_') $object[$key] = $val; } // copy recurrence rules if the client didn't submit it (#2713) if (!array_key_exists('recurrence', $object) && $old['recurrence']) { $object['recurrence'] = $old['recurrence']; } // delete existing attachment(s) if (!empty($task['deleted_attachments'])) { foreach ($task['deleted_attachments'] as $attachment) { if (is_array($object['_attachments'])) { foreach ($object['_attachments'] as $idx => $att) { if ($att['id'] == $attachment) $object['_attachments'][$idx] = false; } } } unset($task['deleted_attachments']); } // in kolab_storage attachments are indexed by content-id if (is_array($task['attachments'])) { foreach ($task['attachments'] as $idx => $attachment) { $key = null; // Roundcube ID has nothing to do with the storage ID, remove it if ($attachment['content'] || $attachment['path']) { unset($attachment['id']); } else { foreach ((array)$old['_attachments'] as $cid => $oldatt) { if ($oldatt && $attachment['id'] == $oldatt['id']) $key = $cid; } } // replace existing entry if ($key) { $object['_attachments'][$key] = $attachment; } // append as new attachment else { $object['_attachments'][] = $attachment; } } unset($object['attachments']); } // allow sequence increments if I'm the organizer if ($this->plugin->is_organizer($object) && empty($object['_method'])) { unset($object['sequence']); } else if (isset($old['sequence']) && empty($object['_method'])) { $object['sequence'] = $old['sequence']; } unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']); return $object; } /** * Add a single task to the database * * @param array Hash array with task properties (see header of tasklist_driver.php) * @return mixed New task ID on success, False on error */ public function create_task($task) { return $this->edit_task($task); } /** * Update an task entry with the given data * * @param array Hash array with task properties (see header of tasklist_driver.php) * @return boolean True on success, False on error */ public function edit_task($task) { $this->_parse_id($task); $list_id = $task['list']; if (!$list_id || !($folder = $this->get_folder($list_id))) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Invalid list identifer to save taks: " . var_dump($list_id, true)), + 'message' => "Invalid list identifer to save task: " . var_dump($list_id, true)), true, false); return false; } // email links and tags are stored separately $links = $task['links']; $tags = $task['tags']; unset($task['tags'], $task['links']); // moved from another folder if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) { if (!$fromfolder->move($task['uid'], $folder)) return false; unset($task['_fromlist']); } // load previous version of this task to merge if ($task['id']) { $old = $folder->get_object($task['uid']); if (!$old || PEAR::isError($old)) return false; // merge existing properties if the update isn't complete if (!isset($task['title']) || !isset($task['complete'])) $task += $this->_to_rcube_task($old, $list_id); } // generate new task object from RC input $object = $this->_from_rcube_task($task, $old); $saved = $folder->save($object, 'task', $task['uid']); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving task object to Kolab server"), true, false); $saved = false; } else { // save links in configuration.relation object $this->save_links($object['uid'], $links); // save tags in configuration.relation object $this->save_tags($object['uid'], $tags); $task = $this->_to_rcube_task($object, $list_id); $task['tags'] = (array) $tags; $this->tasks[$task['uid']] = $task; } return $saved; } /** * Move a single task to another list * * @param array Hash array with task properties: * @return boolean True on success, False on error * @see tasklist_driver::move_task() */ public function move_task($task) { $this->_parse_id($task); $list_id = $task['list']; if (!$list_id || !($folder = $this->get_folder($list_id))) return false; // execute move command if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) { return $fromfolder->move($task['uid'], $folder); } return false; } /** * Remove a single task from the database * * @param array Hash array with task properties: * id: Task identifier * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) * @return boolean True on success, False on error */ public function delete_task($task, $force = true) { $this->_parse_id($task); $list_id = $task['list']; if (!$list_id || !($folder = $this->get_folder($list_id))) return false; $status = $folder->delete($task['uid']); if ($status) { // remove tag assignments // @TODO: don't do this when undelete feature will be implemented $this->save_tags($task['uid'], null); } return $status; } /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties: * id: Task identifier * @return boolean True on success, False on error */ public function undelete_task($prop) { // TODO: implement this return false; } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return array Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $task) { // get old revision of the object if ($task['rev']) { $task = $this->get_task_revison($task, $task['rev']); } else { $task = $this->get_task($task); } if ($task && !empty($task['attachments'])) { foreach ($task['attachments'] as $att) { if ($att['id'] == $id) return $att; } } return null; } /** * Get attachment body * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return string Attachment body */ public function get_attachment_body($id, $task) { $this->_parse_id($task); // get old revision of event if ($task['rev']) { if (empty($this->bonnie_api)) { return false; } $cid = substr($id, 4); // call Bonnie API and get the raw mime message list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task); if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) { // parse the message and find the part with the matching content-id $message = rcube_mime::parse_message($msg_raw); foreach ((array)$message->parts as $part) { if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) { return $part->body; } } } return false; } if ($storage = $this->get_folder($task['list'])) { return $storage->get_attachment($task['uid'], $id); } return false; } /** * Build a struct representing the given message reference * * @see tasklist_driver::get_message_reference() */ public function get_message_reference($uri_or_headers, $folder = null) { if (is_object($uri_or_headers)) { $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder); } if (is_string($uri_or_headers)) { return kolab_storage_config::get_message_reference($uri_or_headers, 'task'); } return false; } /** * Find tasks assigned to a specified message * * @see tasklist_driver::get_message_related_tasks() */ public function get_message_related_tasks($headers, $folder) { $config = kolab_storage_config::get_instance(); $result = $config->get_message_relations($headers, $folder, 'task'); foreach ($result as $idx => $rec) { $result[$idx] = $this->_to_rcube_task($rec, kolab_storage::folder_id($rec['_mailbox'])); } return $result; } /** * */ public function tasklist_edit_form($action, $list, $fieldprop) { if ($list['id'] && ($list = $this->lists[$list['id']])) { $folder_name = $this->get_folder($list['id'])->name; // UTF7 } else { $folder_name = ''; } $storage = $this->rc->get_storage(); $delim = $storage->get_hierarchy_delimiter(); $form = array(); if (strlen($folder_name)) { $path_imap = explode($delim, $folder_name); array_pop($path_imap); // pop off name part $path_imap = implode($path_imap, $delim); $options = $storage->folder_info($folder_name); } else { $path_imap = ''; } $hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name); // folder name (default field) $input_name = new html_inputfield(array('name' => 'name', 'id' => 'taskedit-tasklistame', 'size' => 20)); $fieldprop['name']['value'] = $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected']))); // 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('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), $folder_name); $fieldprop['parent'] = array( 'id' => 'taskedit-parentfolder', 'label' => $this->plugin->gettext('parentfolder'), 'value' => $select->show($path_imap), ); } // General tab $form['properties'] = array( 'name' => $this->rc->gettext('properties'), 'fields' => array(), ); foreach (array('name','parent','showalarms') as $f) { $form['properties']['fields'][$f] = $fieldprop[$f]; } // add folder ACL tab if ($action != 'form-new') { $form['sharing'] = array( 'name' => rcube::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'], rcube::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, rcube::Q($tab['name'])) . $content) . "\n"; } } return $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('tasklist.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')); } } diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index be823447..a0df271a 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -1,454 +1,454 @@ * * 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 . */ /** * Struct of an internal task object how it is passed from/to the driver classes: * * $task = array( * 'id' => 'Task ID used for editing', // must be unique for the current user * 'parent_id' => 'ID of parent task', // null if top-level task * 'uid' => 'Unique identifier of this task', * 'list' => 'Task list identifier to add the task to or where the task is stored', * 'changed' => , // Last modification date/time of the record * 'title' => 'Event title/summary', * 'description' => 'Event description', * 'tags' => array(), // List of tags for this task * 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set * 'time' => 'Due time', // as string of format hh::ii or null if no due time is set * 'startdate' => 'Start date' // Delay start of the task until that date * 'starttime' => 'Start time' // ...and time * 'categories' => 'Task category', * 'flagged' => 'Boolean value whether this record is flagged', * 'complete' => 'Float value representing the completeness state (range 0..1)', * 'status' => 'Task status string according to (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) RFC 2445', * 'valarms' => array( // List of reminders (new format), each represented as a hash array: * array( * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object * 'action' => 'DISPLAY|EMAIL|AUDIO', * 'duration' => 'PT15M', // ISO 8601 period string * 'repeat' => 0, // number of repetitions * 'description' => '', // text to display for DISPLAY actions * 'summary' => '', // message text for EMAIL actions * 'attendees' => array(), // list of email addresses to receive alarm messages * ), * ), * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY', * 'INTERVAL' => 1...n, * 'UNTIL' => DateTime, * 'COUNT' => 1..n, // number of times * 'RDATE' => array(), // complete list of DateTime objects denoting individual repeat dates * ), * '_fromlist' => 'List identifier where the task was stored before', * ); */ /** * Driver interface for the Tasklist plugin */ abstract class tasklist_driver { // features supported by the backend public $alarms = false; public $attachments = false; public $attendees = false; public $undelete = false; // task undelete action public $sortable = false; public $alarm_types = array('DISPLAY'); public $alarm_absolute = true; public $last_error; /** * Get a list of available task lists from this source */ abstract function get_lists(); /** * Create a new list assigned to the current user * * @param array Hash array with list properties * name: List name * color: The color of the list * showalarms: True if alarms are enabled * @return mixed ID of the new list on success, False on error */ abstract function create_list(&$prop); /** * Update properties of an existing tasklist * * @param array Hash array with list properties * id: List Identifier * name: List name * color: The color of the list * showalarms: True if alarms are enabled (if supported) * @return boolean True on success, Fales on failure */ abstract function edit_list(&$prop); /** * Set active/subscribed state of a list * * @param array Hash array with list properties * id: List Identifier * active: True if list is active, false if not * @return boolean True on success, Fales on failure */ abstract function subscribe_list($prop); /** * Delete the given list with all its contents * * @param array Hash array with list properties * id: list Identifier * @return boolean True on success, Fales on failure */ abstract function delete_list($prop); /** * Search for shared or otherwise not listed tasklists the user has access * * @param string Search string * @param string Section/source to search * @return array List of tasklists */ abstract function search_lists($query, $source); /** * Get number of tasks matching the given filter * * @param array List of lists to count tasks of * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) */ abstract function count_tasks($lists = null); /** - * Get all taks records matching the given filter + * Get all task records matching the given filter * * @param array Hash array with filter criterias: * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) * - from: Date range start as string (Y-m-d) * - to: Date range end as string (Y-m-d) * - search: Search query string * @param array List of lists to get tasks from * @return array List of tasks records matchin the criteria */ abstract function list_tasks($filter, $lists = null); /** * Get a list of tags to assign tasks to * * @return array List of tags */ abstract function get_tags(); /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * id: Task identifier * uid: Unique identifier of this task * date: Task due date * time: Task due time * title: Task title/summary */ abstract function pending_alarms($time, $lists = null); /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string Task identifier * @param integer Suspend the alarm for this number of seconds */ abstract function dismiss_alarm($id, $snooze = 0); /** * Remove alarm dismissal or snooze state * * @param string Task identifier */ abstract public function clear_alarms($id); /** * Return data of a specific task * * @param mixed Hash array with task properties or task UID * @return array Hash array with task properties or false if not found */ abstract public function get_task($prop); /** * Get decendents of the given task record * * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched * @return array List of all child task IDs */ abstract public function get_childs($prop, $recursive = false); /** * Add a single task to the database * * @param array Hash array with task properties (see header of this file) * @return mixed New event ID on success, False on error */ abstract function create_task($prop); /** * Update an task entry with the given data * * @param array Hash array with task properties (see header of this file) * @return boolean True on success, False on error */ abstract function edit_task($prop); /** * Move a single task to another list * * @param array Hash array with task properties: * id: Task identifier * list: New list identifier to move to * _fromlist: Previous list identifier * @return boolean True on success, False on error */ abstract function move_task($prop); /** * Remove a single task from the database * * @param array Hash array with task properties: * id: Task identifier * list: Tasklist identifer * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) * @return boolean True on success, False on error */ abstract function delete_task($prop, $force = true); /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties: * id: Task identifier * @return boolean True on success, False on error */ public function undelete_task($prop) { return false; } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return array Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $task) { } /** * Get attachment body * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * rev: Revision (optional) * * @return string Attachment body */ public function get_attachment_body($id, $task) { } /** * Build a struct representing the given message reference * * @param object|string $uri_or_headers rcube_message_header instance holding the message headers * or an URI from a stored link referencing a mail message. * @param string $folder IMAP folder the message resides in * * @return array An struct referencing the given IMAP message */ public function get_message_reference($uri_or_headers, $folder = null) { // to be implemented by the derived classes return false; } /** * Find tasks assigned to a specified message * * @param object $message rcube_message_header instance * @param string $folder IMAP folder the message resides in * * @param array List of linked task objects */ public function get_message_related_tasks($headers, $folder) { // to be implemented by the derived classes return array(); } /** * Helper method to determine whether the given task is considered "complete" * * @param array $task Hash array with event properties * @return boolean True if complete, False otherwiese */ public function is_complete($task) { return ($task['complete'] >= 1.0 && empty($task['status'])) || $task['status'] === 'COMPLETED'; } /** * Provide a list of revisions for the given task * * @param array $task Hash array with task properties: * id: Task identifier * list: List identifier * * @return array List of changes, each as a hash array: * rev: Revision number * type: Type of the change (create, update, move, delete) * date: Change date * user: The user who executed the change * ip: Client IP * mailbox: Destination list for 'move' type */ public function get_task_changelog($task) { return false; } /** * Get a list of property changes beteen two revisions of a task object * * @param array $task Hash array with task properties: * id: Task identifier * list: List identifier * @param mixed $rev1 Old Revision * @param mixed $rev2 New Revision * * @return array List of property changes, each as a hash array: * property: Revision number * old: Old property value * new: Updated property value */ public function get_task_diff($task, $rev1, $rev2) { return false; } /** * Return full data of a specific revision of an event * * @param mixed $task UID string or hash array with task properties: * id: Task identifier * list: List identifier * @param mixed $rev Revision number * * @return array Task object as hash array * @see self::get_task() */ public function get_task_revison($task, $rev) { return false; } /** * Command the backend to restore a certain revision of a task. * This shall replace the current object with an older version. * * @param mixed $task UID string or hash array with task properties: * id: Task identifier * list: List identifier * @param mixed $rev Revision number * * @return boolean True on success, False on failure */ public function restore_task_revision($task, $rev) { return false; } /** * Build the edit/create form for lists. * This gives the drivers the opportunity to add more list properties * * @param string The action called this form * @param array Tasklist properties * @param array List with form fields to be rendered * @return string HTML content of the form */ public function tasklist_edit_form($action, $list, $formfields) { $html = ''; foreach ($formfields as $field) { $html .= html::div('form-section', html::label($field['id'], $field['label']) . $field['value']); } return $html; } /** * Compose an URL for CalDAV access to the given list (if configured) */ public function tasklist_caldav_url($list) { $rcmail = rcube::get_instance(); if (!empty($list['caldavuid']) && ($template = $rcmail->config->get('calendar_caldav_url', null))) { return strtr($template, array( '%h' => $_SERVER['HTTP_HOST'], '%u' => urlencode($rcmail->get_user_name()), '%i' => urlencode($list['caldavuid']), '%n' => urlencode($list['editname']), )); } return null; } /** * Handler for user_delete plugin hook * * @param array Hash array with hook arguments * @return array Return arguments for plugin hooks */ public function user_delete($args) { // TO BE OVERRIDDEN return $args; } }