4 * Calendar plugin for Roundcube webmail
6 * @version @package_version@
7 * @author Lazlo Westerhof <hello@lazlo.me>
8 * @author Thomas Bruederli <bruederli@kolabsys.com>
10 * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
11 * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License as
15 * published by the Free Software Foundation, either version 3 of the
16 * License, or (at your option) any later version.
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU Affero General Public License for more details.
23 * You should have received a copy of the GNU Affero General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27 class calendar
extends rcube_plugin
29 const FREEBUSY_UNKNOWN
= 0;
30 const FREEBUSY_FREE
= 1;
31 const FREEBUSY_BUSY
= 2;
32 const FREEBUSY_TENTATIVE
= 3;
33 const FREEBUSY_OOF
= 4;
35 const SESSION_KEY
= 'calendar_temp';
37 public $task = '?(?!logout).*';
41 public $home; // declare public to be used in other classes
44 public $timezone_offset;
50 public $defaults = array(
51 'calendar_default_view' => "agendaWeek",
52 'calendar_timeslots' => 2,
53 'calendar_work_start' => 6,
54 'calendar_work_end' => 18,
55 'calendar_agenda_range' => 60,
56 'calendar_agenda_sections' => 'smart',
57 'calendar_event_coloring' => 0,
58 'calendar_time_indicator' => true,
59 'calendar_allow_invite_shared' => false,
62 private $ics_parts = array();
66 * Plugin initialization.
70 $this->require_plugin('libcalendaring');
72 $this->rc
= rcube
::get_instance();
73 $this->lib
= libcalendaring
::get_instance();
75 $this->register_task('calendar', 'calendar');
77 // load calendar configuration
81 $this->add_texts('localization/', $this->rc
->task
== 'calendar' && (!$this->rc
->action ||
$this->rc
->action
== 'print'));
83 $this->timezone
= $this->lib
->timezone
;
84 $this->gmt_offset
= $this->lib
->gmt_offset
;
85 $this->dst_active
= $this->lib
->dst_active
;
86 $this->timezone_offset
= $this->gmt_offset
/ 3600 - $this->dst_active
;
88 require($this->home
. '/lib/calendar_ui.php');
89 $this->ui
= new calendar_ui($this);
91 // catch iTIP confirmation requests that don're require a valid session
92 if ($this->rc
->action
== 'attend' && !empty($_REQUEST['_t'])) {
93 $this->add_hook('startup', array($this, 'itip_attend_response'));
95 else if ($this->rc
->action
== 'feed' && !empty($_REQUEST['_cal'])) {
96 $this->add_hook('startup', array($this, 'ical_feed_export'));
99 // default startup routine
100 $this->add_hook('startup', array($this, 'startup'));
107 public function startup($args)
109 // the calendar module can be enabled/disabled by the kolab_auth plugin
110 if ($this->rc
->config
->get('calendar_disabled', false) ||
!$this->rc
->config
->get('calendar_enabled', true))
113 // load Calendar user interface
114 if (!$this->rc
->output
->ajax_call
&& !$this->rc
->output
->env
['framed']) {
117 // settings are required in (almost) every GUI step
118 if ($args['action'] != 'attend')
119 $this->rc
->output
->set_env('calendar_settings', $this->load_settings());
122 if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
123 if ($args['action'] != 'upload') {
124 $this->load_driver();
127 // register calendar actions
128 $this->register_action('index', array($this, 'calendar_view'));
129 $this->register_action('event', array($this, 'event_action'));
130 $this->register_action('calendar', array($this, 'calendar_action'));
131 $this->register_action('load_events', array($this, 'load_events'));
132 $this->register_action('export_events', array($this, 'export_events'));
133 $this->register_action('import_events', array($this, 'import_events'));
134 $this->register_action('upload', array($this, 'attachment_upload'));
135 $this->register_action('get-attachment', array($this, 'attachment_get'));
136 $this->register_action('freebusy-status', array($this, 'freebusy_status'));
137 $this->register_action('freebusy-times', array($this, 'freebusy_times'));
138 $this->register_action('randomdata', array($this, 'generate_randomdata'));
139 $this->register_action('print', array($this,'print_view'));
140 $this->register_action('mailimportevent', array($this, 'mail_import_event'));
141 $this->register_action('mailtoevent', array($this, 'mail_message2event'));
142 $this->register_action('inlineui', array($this, 'get_inline_ui'));
143 $this->register_action('check-recent', array($this, 'check_recent'));
144 $this->add_hook('refresh', array($this, 'refresh'));
146 // remove undo information...
147 if ($undo = $_SESSION['calendar_event_undo']) {
149 $undo_time = $this->rc
->config
->get('undo_timeout', 0);
150 if ($undo['ts'] < time() - $undo_time) {
151 $this->rc
->session
->remove('calendar_event_undo');
152 // @TODO: do EXPUNGE on kolab objects?
156 else if ($args['task'] == 'settings') {
157 // add hooks for Calendar settings
158 $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
159 $this->add_hook('preferences_list', array($this, 'preferences_list'));
160 $this->add_hook('preferences_save', array($this, 'preferences_save'));
162 else if ($args['task'] == 'mail') {
163 // hooks to catch event invitations on incoming mails
164 if ($args['action'] == 'show' ||
$args['action'] == 'preview') {
165 $this->add_hook('message_load', array($this, 'mail_message_load'));
166 $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
169 // add 'Create event' item to message menu
170 if ($this->api
->output
->type
== 'html') {
171 $this->api
->add_content(html
::tag('li', null,
172 $this->api
->output
->button(array(
173 'command' => 'calendar-create-from-mail',
174 'label' => 'calendar.createfrommail',
176 'classact' => 'icon calendarlink active',
177 'class' => 'icon calendarlink',
178 'innerclass' => 'icon calendar',
182 $this->api
->output
->add_label('calendar.createfrommail');
186 // add hooks to display alarms
187 $this->add_hook('pending_alarms', array($this, 'pending_alarms'));
188 $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
192 * Helper method to load the backend driver according to local config
194 private function load_driver()
196 if (is_object($this->driver
))
199 $driver_name = $this->rc
->config
->get('calendar_driver', 'database');
200 $driver_class = $driver_name . '_driver';
202 require_once($this->home
. '/drivers/calendar_driver.php');
203 require_once($this->home
. '/drivers/' . $driver_name . '/' . $driver_class . '.php');
205 switch ($driver_name) {
207 $this->require_plugin('libkolab');
209 $this->driver
= new $driver_class($this);
213 if ($this->driver
->undelete
)
214 $this->driver
->undelete
= $this->rc
->config
->get('undo_timeout', 0) > 0;
218 * Load iTIP functions
220 private function load_itip()
223 require_once($this->home
. '/lib/calendar_itip.php');
225 $plugin = $this->rc
->plugins
->exec_hook('calendar_load_itip',
226 array('identity' => null));
228 $this->itip
= new calendar_itip($this, $plugin['identity']);
235 * Load iCalendar functions
237 public function get_ical()
240 $this->ical
= libcalendaring
::get_ical();
247 * Get properties of the calendar this user has specified as default
249 public function get_default_calendar($writeable = false)
251 $default_id = $this->rc
->config
->get('calendar_default_calendar');
252 $calendars = $this->driver
->list_calendars(false, true);
253 $calendar = $calendars[$default_id] ?
: null;
254 if (!$calendar ||
($writeable && $calendar['readonly'])) {
255 foreach ($calendars as $cal) {
256 if ($cal['default']) {
260 if (!$writeable ||
!$cal['readonly']) {
266 return $calendar ?
: $first;
271 * Render the main calendar view from skin template
273 function calendar_view()
275 $this->rc
->output
->set_pagetitle($this->gettext('calendar'));
277 // Add CSS stylesheets to the page header
280 // Add JS files to the page header
283 $this->ui
->init_templates();
284 $this->rc
->output
->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning');
286 // initialize attendees autocompletion
287 rcube_autocomplete_init();
289 $this->rc
->output
->set_env('timezone', $this->timezone
->getName());
290 $this->rc
->output
->set_env('calendar_driver', $this->rc
->config
->get('calendar_driver'), false);
291 $this->rc
->output
->set_env('mscolors', $this->driver
->get_color_values());
292 $this->rc
->output
->set_env('identities-selector', $this->ui
->identity_select(array('id' => 'edit-identities-list')));
294 $view = get_input_value('view', RCUBE_INPUT_GPC
);
295 if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
296 $this->rc
->output
->set_env('view', $view);
298 if ($date = get_input_value('date', RCUBE_INPUT_GPC
))
299 $this->rc
->output
->set_env('date', $date);
301 $this->rc
->output
->send("calendar.calendar");
305 * Handler for preferences_sections_list hook.
306 * Adds Calendar settings sections into preferences sections list.
308 * @param array Original parameters
309 * @return array Modified parameters
311 function preferences_sections_list($p)
313 $p['list']['calendar'] = array(
314 'id' => 'calendar', 'section' => $this->gettext('calendar'),
321 * Handler for preferences_list hook.
322 * Adds options blocks into Calendar settings sections in Preferences.
324 * @param array Original parameters
325 * @return array Modified parameters
327 function preferences_list($p)
329 if ($p['section'] != 'calendar') {
333 $no_override = array_flip((array)$this->rc
->config
->get('dont_override'));
335 $p['blocks']['view']['name'] = $this->gettext('mainoptions');
337 if (!isset($no_override['calendar_default_view'])) {
338 if (!$p['current']) {
339 $p['blocks']['view']['content'] = true;
343 $field_id = 'rcmfd_default_view';
344 $select = new html_select(array('name' => '_default_view', 'id' => $field_id));
345 $select->add($this->gettext('day'), "agendaDay");
346 $select->add($this->gettext('week'), "agendaWeek");
347 $select->add($this->gettext('month'), "month");
348 $select->add($this->gettext('agenda'), "table");
349 $p['blocks']['view']['options']['default_view'] = array(
350 'title' => html
::label($field_id, Q($this->gettext('default_view'))),
351 'content' => $select->show($this->rc
->config
->get('calendar_default_view', $this->defaults
['calendar_default_view'])),
355 if (!isset($no_override['calendar_timeslots'])) {
356 if (!$p['current']) {
357 $p['blocks']['view']['content'] = true;
361 $field_id = 'rcmfd_timeslot';
362 $choices = array('1', '2', '3', '4', '6');
363 $select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
364 $select->add($choices);
365 $p['blocks']['view']['options']['timeslots'] = array(
366 'title' => html
::label($field_id, Q($this->gettext('timeslots'))),
367 'content' => $select->show(strval($this->rc
->config
->get('calendar_timeslots', $this->defaults
['calendar_timeslots']))),
371 if (!isset($no_override['calendar_first_day'])) {
372 if (!$p['current']) {
373 $p['blocks']['view']['content'] = true;
377 $field_id = 'rcmfd_firstday';
378 $select = new html_select(array('name' => '_first_day', 'id' => $field_id));
379 $select->add(rcube_label('sunday'), '0');
380 $select->add(rcube_label('monday'), '1');
381 $select->add(rcube_label('tuesday'), '2');
382 $select->add(rcube_label('wednesday'), '3');
383 $select->add(rcube_label('thursday'), '4');
384 $select->add(rcube_label('friday'), '5');
385 $select->add(rcube_label('saturday'), '6');
386 $p['blocks']['view']['options']['first_day'] = array(
387 'title' => html
::label($field_id, Q($this->gettext('first_day'))),
388 'content' => $select->show(strval($this->rc
->config
->get('calendar_first_day', $this->defaults
['calendar_first_day']))),
392 if (!isset($no_override['calendar_first_hour'])) {
393 if (!$p['current']) {
394 $p['blocks']['view']['content'] = true;
398 $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'])));
399 $select_hours = new html_select();
400 for ($h = 0; $h < 24; $h++
)
401 $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
403 $field_id = 'rcmfd_firsthour';
404 $p['blocks']['view']['options']['first_hour'] = array(
405 'title' => html
::label($field_id, Q($this->gettext('first_hour'))),
406 'content' => $select_hours->show($this->rc
->config
->get('calendar_first_hour', $this->defaults
['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
410 if (!isset($no_override['calendar_work_start'])) {
411 if (!$p['current']) {
412 $p['blocks']['view']['content'] = true;
416 $field_id = 'rcmfd_workstart';
417 $p['blocks']['view']['options']['workinghours'] = array(
418 'title' => html
::label($field_id, Q($this->gettext('workinghours'))),
419 'content' => $select_hours->show($this->rc
->config
->get('calendar_work_start', $this->defaults
['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
420 ' — ' . $select_hours->show($this->rc
->config
->get('calendar_work_end', $this->defaults
['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
424 if (!isset($no_override['calendar_event_coloring'])) {
425 if (!$p['current']) {
426 $p['blocks']['view']['content'] = true;
430 $field_id = 'rcmfd_coloring';
431 $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
432 $select_colors->add($this->gettext('coloringmode0'), 0);
433 $select_colors->add($this->gettext('coloringmode1'), 1);
434 $select_colors->add($this->gettext('coloringmode2'), 2);
435 $select_colors->add($this->gettext('coloringmode3'), 3);
437 $p['blocks']['view']['options']['eventcolors'] = array(
438 'title' => html
::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
439 'content' => $select_colors->show($this->rc
->config
->get('calendar_event_coloring', $this->defaults
['calendar_event_coloring'])),
443 // loading driver is expensive, don't do it if not needed
444 $this->load_driver();
446 if (!isset($no_override['calendar_default_alarm_type'])) {
447 if (!$p['current']) {
448 $p['blocks']['view']['content'] = true;
452 $field_id = 'rcmfd_alarm';
453 $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
454 $select_type->add($this->gettext('none'), '');
455 foreach ($this->driver
->alarm_types
as $type)
456 $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
458 $p['blocks']['view']['options']['alarmtype'] = array(
459 'title' => html
::label($field_id, Q($this->gettext('defaultalarmtype'))),
460 'content' => $select_type->show($this->rc
->config
->get('calendar_default_alarm_type', '')),
464 if (!isset($no_override['calendar_default_alarm_offset'])) {
465 if (!$p['current']) {
466 $p['blocks']['view']['content'] = true;
470 $field_id = 'rcmfd_alarm';
471 $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
472 $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
473 foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
474 $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
476 $preset = libcalendaring
::parse_alaram_value($this->rc
->config
->get('calendar_default_alarm_offset', '-15M'));
477 $p['blocks']['view']['options']['alarmoffset'] = array(
478 'title' => html
::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
479 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
483 if (!isset($no_override['calendar_default_calendar'])) {
484 if (!$p['current']) {
485 $p['blocks']['view']['content'] = true;
488 // default calendar selection
489 $field_id = 'rcmfd_default_calendar';
490 $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
491 foreach ((array)$this->driver
->list_calendars(false, true) as $id => $prop) {
492 $select_cal->add($prop['name'], strval($id));
493 if ($prop['default'])
494 $default_calendar = $id;
496 $p['blocks']['view']['options']['defaultcalendar'] = array(
497 'title' => html
::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
498 'content' => $select_cal->show($this->rc
->config
->get('calendar_default_calendar', $default_calendar)),
502 // category definitions
503 if (!$this->driver
->nocategories
&& !isset($no_override['calendar_categories'])) {
504 $p['blocks']['categories']['name'] = $this->gettext('categories');
506 if (!$p['current']) {
507 $p['blocks']['categories']['content'] = true;
511 $categories = (array) $this->driver
->list_categories();
512 $categories_list = '';
513 foreach ($categories as $name => $color) {
515 $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
516 $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
517 $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver
->categoriesimmutable
));
518 $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
519 $hidden = $this->driver
->categoriesimmutable ? html
::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
520 $categories_list .= html
::div(null, $hidden . $category_name->show($name) . ' ' . $category_color->show($color) . ' ' . $category_remove->show());
523 $p['blocks']['categories']['options']['category_' . $name] = array(
524 'content' => html
::div(array('id' => 'calendarcategories'), $categories_list),
527 $field_id = 'rcmfd_new_category';
528 $new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
529 $add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
530 $p['blocks']['categories']['options']['categories'] = array(
531 'content' => $new_category->show('') . ' ' . $add_category->show(),
534 $this->rc
->output
->add_script('function rcube_calendar_add_category(){
535 var name = $("#rcmfd_new_category").val();
537 var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
538 var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
539 var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
540 $("<div>").append(input).append(" ").append(color).append(" ").append(button).appendTo("#calendarcategories");
541 color.miniColors({ colorValues:(rcmail.env.mscolors || []) });
542 $("#rcmfd_new_category").val("");
546 $this->rc
->output
->add_script('$("#rcmfd_new_category").keypress(function(event){
547 if (event.which == 13) {
548 rcube_calendar_add_category();
549 event.preventDefault();
554 // include color picker
555 $this->include_script('lib/js/jquery.miniColors.min.js');
556 $this->include_stylesheet($this->local_skin_path() . '/jquery.miniColors.css');
557 $this->rc
->output
->set_env('mscolors', $this->driver
->get_color_values());
558 $this->rc
->output
->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready');
565 * Handler for preferences_save hook.
566 * Executed on Calendar settings form submit.
568 * @param array Original parameters
569 * @return array Modified parameters
571 function preferences_save($p)
573 if ($p['section'] == 'calendar') {
574 $this->load_driver();
576 // compose default alarm preset value
577 $alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST
);
578 $default_alarm = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST
)) . $alarm_offset[1];
581 'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST
),
582 'calendar_timeslots' => intval(get_input_value('_timeslots', RCUBE_INPUT_POST
)),
583 'calendar_first_day' => intval(get_input_value('_first_day', RCUBE_INPUT_POST
)),
584 'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST
)),
585 'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST
)),
586 'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST
)),
587 'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST
)),
588 'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST
),
589 'calendar_default_alarm_offset' => $default_alarm,
590 'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST
),
591 'calendar_date_format' => null, // clear previously saved values
592 'calendar_time_format' => null,
596 if (!$this->driver
->nocategories
) {
597 $old_categories = $new_categories = array();
598 foreach ($this->driver
->list_categories() as $name => $color) {
599 $old_categories[md5($name)] = $name;
602 $categories = (array) get_input_value('_categories', RCUBE_INPUT_POST
);
603 $colors = (array) get_input_value('_colors', RCUBE_INPUT_POST
);
605 foreach ($categories as $key => $name) {
606 $color = preg_replace('/^#/', '', strval($colors[$key]));
608 // rename categories in existing events -> driver's job
609 if ($oldname = $old_categories[$key]) {
610 $this->driver
->replace_category($oldname, $name, $color);
611 unset($old_categories[$key]);
614 $this->driver
->add_category($name, $color);
616 $new_categories[$name] = $color;
619 // these old categories have been removed, alter events accordingly -> driver's job
620 foreach ((array)$old_categories[$key] as $key => $name) {
621 $this->driver
->remove_category($name);
624 $p['prefs']['calendar_categories'] = $new_categories;
632 * Dispatcher for calendar actions initiated by the client
634 function calendar_action()
636 $action = get_input_value('action', RCUBE_INPUT_GPC
);
637 $cal = get_input_value('c', RCUBE_INPUT_GPC
);
638 $success = $reload = false;
640 if (isset($cal['showalarms']))
641 $cal['showalarms'] = intval($cal['showalarms']);
646 echo $this->ui
->calendar_editform($action, $cal);
649 $success = $this->driver
->create_calendar($cal);
653 $success = $this->driver
->edit_calendar($cal);
657 if ($success = $this->driver
->remove_calendar($cal))
658 $this->rc
->output
->command('plugin.destroy_source', array('id' => $cal['id']));
661 if (!$this->driver
->subscribe_calendar($cal))
662 $this->rc
->output
->show_message($this->gettext('errorsaving'), 'error');
667 $this->rc
->output
->show_message('successfullysaved', 'confirmation');
669 $error_msg = $this->gettext('errorsaving') . ($this->driver
->last_error ?
': ' . $this->driver
->last_error
:'');
670 $this->rc
->output
->show_message($error_msg, 'error');
673 $this->rc
->output
->command('plugin.unlock_saving');
675 // TODO: keep view and date selection
676 if ($success && $reload)
677 $this->rc
->output
->redirect('');
682 * Dispatcher for event actions initiated by the client
684 function event_action()
686 $action = get_input_value('action', RCUBE_INPUT_GPC
);
687 $event = get_input_value('e', RCUBE_INPUT_POST
, true);
688 $success = $reload = $got_msg = false;
690 // don't notify if modifying a recurring instance (really?)
691 if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify'])
692 unset($event['_notify']);
694 // read old event data in order to find changes
695 if (($event['_notify'] ||
$event['decline']) && $action != 'new')
696 $old = $this->driver
->get_event($event);
700 // create UID for new event
701 $event['uid'] = $this->generate_uid();
702 $this->prepare_event($event, $action);
703 if ($success = $this->driver
->new_event($event)) {
704 $event['id'] = $event['uid'];
705 $this->cleanup_event($event);
707 $reload = $success && $event['recurrence'] ?
2 : 1;
711 $this->prepare_event($event, $action);
712 if ($success = $this->driver
->edit_event($event))
713 $this->cleanup_event($event);
714 $reload = $success && ($event['recurrence'] ||
$event['_savemode'] ||
$event['_fromcalendar']) ?
2 : 1;
718 $this->prepare_event($event, $action);
719 $success = $this->driver
->resize_event($event);
720 $reload = $event['_savemode'] ?
2 : 1;
724 $this->prepare_event($event, $action);
725 $success = $this->driver
->move_event($event);
726 $reload = $success && $event['_savemode'] ?
2 : 1;
730 // remove previous deletes
731 $undo_time = $this->driver
->undelete ?
$this->rc
->config
->get('undo_timeout', 0) : 0;
732 $this->rc
->session
->remove('calendar_event_undo');
734 // search for event if only UID is given
735 if (!isset($event['calendar']) && $event['uid']) {
736 if (!($event = $this->driver
->get_event($event, true))) {
742 $success = $this->driver
->remove_event($event, $undo_time < 1);
743 $reload = (!$success ||
$event['_savemode']) ?
2 : 1;
745 if ($undo_time > 0 && $success) {
746 $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
747 // display message with Undo link.
748 $msg = html
::span(null, $this->gettext('successremoval'))
749 . ' ' . html
::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
750 JS_OBJECT_NAME
, JS_OBJECT_NAME
)), rcube_label('undo'));
751 $this->rc
->output
->show_message($msg, 'confirmation', null, true, $undo_time);
755 $this->rc
->output
->show_message('calendar.successremoval', 'confirmation');
759 // send iTIP reply that participant has declined the event
760 if ($success && $event['decline']) {
761 $emails = $this->get_user_emails();
762 foreach ($old['attendees'] as $i => $attendee) {
763 if ($attendee['role'] == 'ORGANIZER')
764 $organizer = $attendee;
765 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
766 $old['attendees'][$i]['status'] = 'DECLINED';
767 $reply_sender = $attendee['email'];
771 $itip = $this->load_itip();
772 $itip->set_sender_email($reply_sender);
773 if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
774 $this->rc
->output
->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?
$organizer['name'] : $organizer['email']))), 'confirmation');
776 $this->rc
->output
->command('display_message', $this->gettext('itipresponseerror'), 'error');
781 // Restore deleted event
782 $event = $_SESSION['calendar_event_undo']['data'];
785 $success = $this->driver
->restore_event($event);
788 $this->rc
->session
->remove('calendar_event_undo');
789 $this->rc
->output
->show_message('calendar.successrestore', 'confirmation');
798 $status = $event['fallback'];
800 $html = html
::div('rsvp-status', $status != 'CANCELLED' ?
$this->gettext('acceptinvitation') : '');
801 if (is_numeric($event['changed']))
802 $event['changed'] = new DateTime('@'.$event['changed']);
803 $this->load_driver();
804 if ($existing = $this->driver
->get_event($event, true, false, true)) {
805 $latest = ($event['sequence'] && $existing['sequence'] == $event['sequence']) ||
(!$event['sequence'] && $existing['changed'] && $existing['changed'] >= $event['changed']);
806 $emails = $this->get_user_emails();
807 foreach ($existing['attendees'] as $i => $attendee) {
808 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
809 $status = $attendee['status'];
815 // get a list of writeable calendars
816 $calendars = $this->driver
->list_calendars(false, true);
817 $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto', 'is_escaped' => true));
819 foreach ($calendars as $calendar) {
820 if (!$calendar['readonly']) {
821 $calendar_select->add($calendar['name'], $calendar['id']);
826 $calendar_select = null;
829 if ($status == 'unknown') {
830 $html = html
::div('rsvp-status', $this->gettext('notanattendee'));
833 else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) {
834 $html = html
::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
835 if ($existing['sequence'] > $event['sequence'] ||
(!$event['sequence'] && $existing['changed'] && $existing['changed'] > $event['changed'])) {
836 $action = ''; // nothing to do here, outdated invitation
840 $default_calendar = $calendar_select ?
$this->get_default_calendar(true) : null;
841 $this->rc
->output
->command('plugin.update_event_rsvp_status', array(
842 'uid' => $event['uid'],
843 'id' => asciiwords($event['uid'], true),
844 'saved' => $existing ?
true : false,
849 'select' => $calendar_select ? html
::span('calendar-select', $this->gettext('saveincalendar') . ' ' . $calendar_select->show($this->rc
->config
->get('calendar_default_calendar', $default_calendar['id']))) : '',
854 $ev = $this->driver
->get_event($event);
855 $ev['attendees'] = $event['attendees'];
858 if ($success = $this->driver
->edit_event($event)) {
859 $status = get_input_value('status', RCUBE_INPUT_GPC
);
861 foreach ($event['attendees'] as $i => $attendee) {
862 if ($attendee['role'] == 'ORGANIZER') {
863 $organizer = $attendee;
867 $itip = $this->load_itip();
868 if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
869 $this->rc
->output
->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?
$organizer['name'] : $organizer['email']))), 'confirmation');
871 $this->rc
->output
->command('display_message', $this->gettext('itipresponseerror'), 'error');
876 $event['ids'] = explode(',', $event['id']);
877 $plugin = $this->rc
->plugins
->exec_hook('dismiss_alarms', $event);
878 $success = $plugin['success'];
879 foreach ($event['ids'] as $id) {
880 if (strpos($id, 'cal:') === 0)
881 $success |
= $this->driver
->dismiss_alarm(substr($id, 4), $event['snooze']);
886 // show confirmation/error message
889 $this->rc
->output
->show_message('successfullysaved', 'confirmation');
891 $this->rc
->output
->show_message('calendar.errorsaving', 'error');
894 // send out notifications
895 if ($success && $event['_notify'] && ($event['attendees'] ||
$old['attendees'])) {
896 // make sure we have the complete record
897 $event = $action == 'remove' ?
$old : $this->driver
->get_event($event);
899 // only notify if data really changed (TODO: do diff check on client already)
900 if (!$old ||
$action == 'remove' || self
::event_diff($event, $old)) {
901 $sent = $this->notify_attendees($event, $old, $action);
903 $this->rc
->output
->show_message('calendar.itipsendsuccess', 'confirmation');
905 $this->rc
->output
->show_message('calendar.errornotifying', 'error');
910 $this->rc
->output
->command('plugin.unlock_saving');
912 // update event object on the client or trigger a complete refretch if too complicated
914 $args = array('source' => $event['calendar']);
916 $args['refetch'] = true;
917 else if ($success && $action != 'remove')
918 $args['update'] = $this->_client_event($this->driver
->get_event($event));
919 $this->rc
->output
->command('plugin.refresh_calendar', $args);
924 * Handler for load-requests from fullcalendar
925 * This will return pure JSON formatted output
927 function load_events()
929 $events = $this->driver
->load_events(
930 get_input_value('start', RCUBE_INPUT_GET
),
931 get_input_value('end', RCUBE_INPUT_GET
),
932 ($query = get_input_value('q', RCUBE_INPUT_GET
)),
933 get_input_value('source', RCUBE_INPUT_GET
)
935 echo $this->encode($events, !empty($query));
940 * Handler for keep-alive requests
941 * This will check for updated data in active calendars and sync them to the client
943 public function refresh($attr)
945 // refresh the entire calendar every 10th time to also sync deleted events
946 if (rand(0,10) == 10) {
947 $this->rc
->output
->command('plugin.refresh_calendar', array('refetch' => true));
951 foreach ($this->driver
->list_calendars(true) as $cal) {
952 $events = $this->driver
->load_events(
953 get_input_value('start', RCUBE_INPUT_GET
),
954 get_input_value('end', RCUBE_INPUT_GET
),
955 get_input_value('q', RCUBE_INPUT_GET
),
961 foreach ($events as $event) {
962 $this->rc
->output
->command('plugin.refresh_calendar',
963 array('source' => $cal['id'], 'update' => $this->_client_event($event)));
969 * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
970 * This will check for pending notifications and pass them to the client
972 public function pending_alarms($p)
974 $this->load_driver();
975 if ($alarms = $this->driver
->pending_alarms($p['time'] ?
: time())) {
976 foreach ($alarms as $alarm) {
977 $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
978 $p['alarms'][] = $alarm;
986 * Handler for alarm dismiss hook triggered by libcalendaring
988 public function dismiss_alarms($p)
990 $this->load_driver();
991 foreach ((array)$p['ids'] as $id) {
992 if (strpos($id, 'cal:') === 0)
993 $p['success'] |
= $this->driver
->dismiss_alarm(substr($id, 4), $p['snooze']);
1000 * Handler for check-recent requests which are accidentally sent to calendar taks
1002 function check_recent()
1005 $this->rc
->output
->send();
1011 function import_events()
1013 // Upload progress update
1014 if (!empty($_GET['_progress'])) {
1015 rcube_upload_progress();
1020 // process uploaded file if there is no error
1021 $err = $_FILES['_data']['error'];
1023 if (!$err && $_FILES['_data']['tmp_name']) {
1024 $calendar = get_input_value('calendar', RCUBE_INPUT_GPC
);
1025 $rangestart = $_REQUEST['_range'] ?
date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
1026 $user_email = $this->rc
->user
->get_username();
1028 $ical = $this->get_ical();
1029 $errors = !$ical->fopen($_FILES['_data']['tmp_name']);
1031 foreach ($ical as $event) {
1032 // keep the browser connection alive on long import jobs
1033 if (++
$i > 100 && $i %
100 == 0) {
1038 // TODO: correctly handle recurring events which start before $rangestart
1039 if ($event['end'] < $rangestart && (!$event['recurrence'] ||
($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
1042 $event['_owner'] = $user_email;
1043 $event['calendar'] = $calendar;
1044 if ($this->driver
->new_event($event)) {
1052 $this->rc
->output
->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
1053 $this->rc
->output
->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
1055 else if (!$errors) {
1056 $this->rc
->output
->command('display_message', $this->gettext('importnone'), 'notice');
1057 $this->rc
->output
->command('plugin.import_success', array('source' => $calendar));
1060 $this->rc
->output
->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ?
': ' . $msg : '')));
1064 if ($err == UPLOAD_ERR_INI_SIZE ||
$err == UPLOAD_ERR_FORM_SIZE
) {
1065 $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
1066 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
1069 $msg = rcube_label('fileuploaderror');
1072 $this->rc
->output
->command('plugin.import_error', array('message' => $msg));
1073 $this->rc
->output
->command('plugin.unlock_saving', false);
1076 $this->rc
->output
->send('iframe');
1080 * Construct the ics file for exporting events to iCalendar format;
1082 function export_events($terminate = true)
1084 $start = get_input_value('start', RCUBE_INPUT_GET
);
1085 $end = get_input_value('end', RCUBE_INPUT_GET
);
1086 if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
1087 if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+
10);
1088 $calid = $calname = get_input_value('source', RCUBE_INPUT_GET
);
1089 $calendars = $this->driver
->list_calendars(true);
1091 if ($calendars[$calid]) {
1092 $calname = $calendars[$calid]['name'] ?
$calendars[$calid]['name'] : $calid;
1093 $calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
1094 if (empty($calname)) $calname = $calid;
1095 $events = $this->driver
->load_events($start, $end, null, $calid, 0);
1100 header("Content-Type: text/calendar");
1101 header("Content-Disposition: inline; filename=".$calname.'.ics');
1103 $this->get_ical()->export($events, '', true, array($this->driver
, 'get_attachment_body'));
1111 * Handler for iCal feed requests
1113 function ical_feed_export()
1115 // process HTTP auth info
1116 if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
1117 $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
1118 $auth = $this->rc
->plugins
->exec_hook('authenticate', array(
1119 'host' => $this->rc
->autoselect_host(),
1120 'user' => trim($_SERVER['PHP_AUTH_USER']),
1121 'pass' => $_SERVER['PHP_AUTH_PW'],
1122 'cookiecheck' => true,
1125 if ($auth['valid'] && !$auth['abort'])
1126 $this->rc
->login($auth['user'], $auth['pass'], $auth['host']);
1129 // require HTTP auth
1130 if (empty($_SESSION['user_id'])) {
1131 header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
1132 header('HTTP/1.0 401 Unauthorized');
1136 // decode calendar feed hash
1138 $calhash = get_input_value('_cal', RCUBE_INPUT_GET
);
1139 if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
1140 $format = strtolower($m[1]);
1141 $calhash = preg_replace($suff_regex, '', $calhash);
1144 if (!strpos($calhash, ':'))
1145 $calhash = base64_decode($calhash);
1147 list($user, $_GET['source']) = explode(':', $calhash, 2);
1149 // sanity check user
1150 if ($this->rc
->user
->get_username() == $user) {
1151 $this->load_driver();
1152 $this->export_events(false);
1155 header('HTTP/1.0 404 Not Found');
1158 // don't save session data
1167 function load_settings()
1169 $this->lib
->load_settings();
1170 $this->defaults +
= $this->lib
->defaults
;
1172 $settings = array();
1175 $settings['default_calendar'] = $this->rc
->config
->get('calendar_default_calendar');
1176 $settings['default_view'] = (string)$this->rc
->config
->get('calendar_default_view', $this->defaults
['calendar_default_view']);
1177 $settings['date_agenda'] = (string)$this->rc
->config
->get('calendar_date_agenda', $this->defaults
['calendar_date_agenda']);
1179 $settings['timeslots'] = (int)$this->rc
->config
->get('calendar_timeslots', $this->defaults
['calendar_timeslots']);
1180 $settings['first_day'] = (int)$this->rc
->config
->get('calendar_first_day', $this->defaults
['calendar_first_day']);
1181 $settings['first_hour'] = (int)$this->rc
->config
->get('calendar_first_hour', $this->defaults
['calendar_first_hour']);
1182 $settings['work_start'] = (int)$this->rc
->config
->get('calendar_work_start', $this->defaults
['calendar_work_start']);
1183 $settings['work_end'] = (int)$this->rc
->config
->get('calendar_work_end', $this->defaults
['calendar_work_end']);
1184 $settings['agenda_range'] = (int)$this->rc
->config
->get('calendar_agenda_range', $this->defaults
['calendar_agenda_range']);
1185 $settings['agenda_sections'] = $this->rc
->config
->get('calendar_agenda_sections', $this->defaults
['calendar_agenda_sections']);
1186 $settings['event_coloring'] = (int)$this->rc
->config
->get('calendar_event_coloring', $this->defaults
['calendar_event_coloring']);
1187 $settings['time_indicator'] = (int)$this->rc
->config
->get('calendar_time_indicator', $this->defaults
['calendar_time_indicator']);
1188 $settings['invite_shared'] = (int)$this->rc
->config
->get('calendar_allow_invite_shared', $this->defaults
['calendar_allow_invite_shared']);
1190 // get user identity to create default attendee
1191 if ($this->ui
->screen
== 'calendar') {
1192 foreach ($this->rc
->user
->list_identities() as $rec) {
1195 $identity['emails'][] = $rec['email'];
1196 $settings['identities'][$rec['identity_id']] = $rec['email'];
1198 $identity['emails'][] = $this->rc
->user
->get_username();
1199 $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])));
1206 * Encode events as JSON
1208 * @param array Events as array
1209 * @param boolean Add CSS class names according to calendar and categories
1210 * @return string JSON encoded events
1212 function encode($events, $addcss = false)
1215 foreach ($events as $event) {
1216 $json[] = $this->_client_event($event, $addcss);
1218 return json_encode($json);
1222 * Convert an event object to be used on the client
1224 private function _client_event($event, $addcss = false)
1226 // compose a human readable strings for alarms_text and recurrence_text
1227 if ($event['alarms'])
1228 $event['alarms_text'] = libcalendaring
::alarms_text($event['alarms']);
1229 if ($event['recurrence']) {
1230 $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
1231 if ($event['recurrence']['UNTIL'])
1232 $event['recurrence']['UNTIL'] = $this->lib
->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c');
1233 unset($event['recurrence']['EXCEPTIONS']);
1236 foreach ((array)$event['attachments'] as $k => $attachment) {
1237 $event['attachments'][$k]['classname'] = rcube_utils
::file2class($attachment['mimetype'], $attachment['name']);
1240 // check for organizer in attendees list
1242 foreach ((array)$event['attendees'] as $i => $attendee) {
1243 if ($attendee['role'] == 'ORGANIZER') {
1244 $organizer = $attendee;
1249 if ($organizer === null && !empty($event['organizer'])) {
1250 $organizer = $event['organizer'];
1251 $organizer['role'] = 'ORGANIZER';
1252 if (!is_array($event['attendees']))
1253 $event['attendees'] = array();
1254 array_unshift($event['attendees'], $organizer);
1257 // mapping url => vurl because of the fullcalendar client script
1258 $event['vurl'] = $event['url'];
1259 unset($event['url']);
1262 '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
1263 'start' => $this->lib
->adjust_timezone($event['start'], $event['allday'])->format('c'),
1264 'end' => $this->lib
->adjust_timezone($event['end'], $event['allday'])->format('c'),
1265 // 'changed' might be empty for event recurrences (Bug #2185)
1266 'changed' => $event['changed'] ?
$this->lib
->adjust_timezone($event['changed'])->format('c') : null,
1267 'title' => strval($event['title']),
1268 'description' => strval($event['description']),
1269 'location' => strval($event['location']),
1270 'className' => ($addcss ?
'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true),
1271 'allDay' => ($event['allday'] == 1),
1277 * Render localized text describing the recurrence rule of an event
1279 private function _recurrence_text($rrule)
1281 // TODO: finish this
1282 $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
1284 switch ($rrule['FREQ']) {
1286 $freq .= $this->gettext('days');
1289 $freq .= $this->gettext('weeks');
1292 $freq .= $this->gettext('months');
1295 $freq .= $this->gettext('years');
1299 if ($rrule['INTERVAL'] <= 1)
1300 $freq = $this->gettext(strtolower($rrule['FREQ']));
1302 if ($rrule['COUNT'])
1303 $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
1304 else if ($rrule['UNTIL'])
1305 $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring
::to_php_date_format($this->rc
->config
->get('calendar_date_format', $this->defaults
['calendar_date_format'])));
1307 $until = $this->gettext('forever');
1309 return rtrim($freq . $details . ', ' . $until);
1313 * Generate a unique identifier for an event
1315 public function generate_uid()
1317 return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc
->user
->get_username()), 0, 16));
1322 * TEMPORARY: generate random event data for testing
1323 * Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500
1325 public function generate_randomdata()
1327 $num = $_REQUEST['_num'] ?
intval($_REQUEST['_num']) : 100;
1328 $cats = array_keys($this->driver
->list_categories());
1329 $cals = $this->driver
->list_calendars(true);
1332 while ($count++
< $num) {
1333 $start = round((time() +
rand(-2600, 2600) * 1000) / 300) * 300;
1334 $duration = round(rand(30, 360) / 30) * 30 * 60;
1335 $allday = rand(0,20) > 18;
1336 $alarm = rand(-30,12) * 5;
1339 if (date('G', $start) > 23)
1343 $start = strtotime(date('Y-m-d 00:00:00', $start));
1349 $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.");
1350 // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
1351 for ($i = 0; $i < $len; $i++
)
1352 $title .= $words[rand(0,count($words)-1)] . " ";
1354 $this->driver
->new_event(array(
1355 'uid' => $this->generate_uid(),
1356 'start' => new DateTime('@'.$start),
1357 'end' => new DateTime('@'.($start +
$duration)),
1358 'allday' => $allday,
1359 'title' => rtrim($title),
1360 'free_busy' => $fb == 2 ?
'outofoffice' : ($fb ?
'busy' : 'free'),
1361 'categories' => $cats[array_rand($cats)],
1362 'calendar' => array_rand($cals),
1363 'alarms' => $alarm > 0 ?
"-{$alarm}M:DISPLAY" : '',
1364 'priority' => rand(0,9),
1368 $this->rc
->output
->redirect('');
1372 * Handler for attachments upload
1374 public function attachment_upload()
1376 $this->lib
->attachment_upload(self
::SESSION_KEY
, 'cal:');
1380 * Handler for attachments download/displaying
1382 public function attachment_get()
1384 // show loading page
1385 if (!empty($_GET['_preload'])) {
1386 return $this->lib
->attachment_loading_page();
1389 $event_id = get_input_value('_event', RCUBE_INPUT_GPC
);
1390 $calendar = get_input_value('_cal', RCUBE_INPUT_GPC
);
1391 $id = get_input_value('_id', RCUBE_INPUT_GPC
);
1393 $event = array('id' => $event_id, 'calendar' => $calendar);
1394 $attachment = $this->driver
->get_attachment($id, $event);
1397 if (!empty($_GET['_frame'])) {
1398 $this->lib
->attachment
= $attachment;
1399 $this->register_handler('plugin.attachmentframe', array($this->lib
, 'attachment_frame'));
1400 $this->register_handler('plugin.attachmentcontrols', array($this->lib
, 'attachment_header'));
1401 $this->rc
->output
->send('calendar.attachment');
1403 // deliver attachment content
1404 else if ($attachment) {
1405 $attachment['body'] = $this->driver
->get_attachment_body($id, $event);
1406 $this->lib
->attachment_get($attachment);
1409 // if we arrive here, the requested part was not found
1410 header('HTTP/1.1 404 Not Found');
1416 * Prepares new/edited event properties before save
1418 private function prepare_event(&$event, $action)
1420 // convert dates into DateTime objects in user's current timezone
1421 $event['start'] = new DateTime($event['start'], $this->timezone
);
1422 $event['end'] = new DateTime($event['end'], $this->timezone
);
1424 // start/end is all we need for 'move' action (#1480)
1425 if ($action == 'move') {
1429 if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL']))
1430 $event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone
);
1432 $attachments = array();
1433 $eventid = 'cal:'.$event['id'];
1434 if (is_array($_SESSION[self
::SESSION_KEY
]) && $_SESSION[self
::SESSION_KEY
]['id'] == $eventid) {
1435 if (!empty($_SESSION[self
::SESSION_KEY
]['attachments'])) {
1436 foreach ($_SESSION[self
::SESSION_KEY
]['attachments'] as $id => $attachment) {
1437 if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
1438 $attachments[$id] = $this->rc
->plugins
->exec_hook('attachment_get', $attachment);
1444 $event['attachments'] = $attachments;
1446 // check for organizer in attendees
1447 if ($action == 'new' ||
$action == 'edit') {
1448 if (!$event['attendees'])
1449 $event['attendees'] = array();
1451 $emails = $this->get_user_emails();
1452 $organizer = $owner = false;
1453 foreach ((array)$event['attendees'] as $i => $attendee) {
1454 if ($attendee['role'] == 'ORGANIZER')
1456 if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails))
1458 else if (!isset($attendee['rsvp']))
1459 $event['attendees'][$i]['rsvp'] = true;
1462 // set new organizer identity
1463 if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc
->user
->get_identity($event['_identity']))) {
1464 $event['attendees'][$organizer]['name'] = $identity['name'];
1465 $event['attendees'][$organizer]['email'] = $identity['email'];
1468 // set owner as organizer if yet missing
1469 if ($organizer === false && $owner !== false) {
1470 $event['attendees'][$owner]['role'] = 'ORGANIZER';
1471 unset($event['attendees'][$owner]['rsvp']);
1473 else if ($organizer === false && $action == 'new' && ($identity = $this->rc
->user
->get_identity($event['_identity'])) && $identity['email']) {
1474 array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
1478 // mapping url => vurl because of the fullcalendar client script
1479 $event['url'] = $event['vurl'];
1480 unset($event['vurl']);
1484 * Releases some resources after successful event save
1486 private function cleanup_event(&$event)
1488 // remove temp. attachment files
1489 if (!empty($_SESSION[self
::SESSION_KEY
]) && ($eventid = $_SESSION[self
::SESSION_KEY
]['id'])) {
1490 $this->rc
->plugins
->exec_hook('attachments_cleanup', array('group' => $eventid));
1491 $this->rc
->session
->remove(self
::SESSION_KEY
);
1496 * Send out an invitation/notification to all event attendees
1498 private function notify_attendees($event, $old, $action = 'edit')
1500 if ($action == 'remove') {
1501 $event['cancelled'] = true;
1502 $is_cancelled = true;
1505 $itip = $this->load_itip();
1506 $emails = $this->get_user_emails();
1508 // compose multipart message using PEAR:Mail_Mime
1509 $method = $action == 'remove' ?
'CANCEL' : 'REQUEST';
1510 $message = $itip->compose_itip_message($event, $method);
1512 // list existing attendees from $old event
1513 $old_attendees = array();
1514 foreach ((array)$old['attendees'] as $attendee) {
1515 $old_attendees[] = $attendee['email'];
1518 // send to every attendee
1520 foreach ((array)$event['attendees'] as $attendee) {
1521 // skip myself for obvious reasons
1522 if (!$attendee['email'] ||
in_array(strtolower($attendee['email']), $emails))
1525 // which template to use for mail text
1526 $is_new = !in_array($attendee['email'], $old_attendees);
1527 $bodytext = $is_cancelled ?
'eventcancelmailbody' : ($is_new ?
'invitationmailbody' : 'eventupdatemailbody');
1528 $subject = $is_cancelled ?
'eventcancelsubject' : ($is_new ?
'invitationsubject' : ($event['title'] ?
'eventupdatesubject':'eventupdatesubjectempty'));
1530 // finally send the message
1531 if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
1541 * Echo simple free/busy status text for the given user and time range
1543 public function freebusy_status()
1545 $email = get_input_value('email', RCUBE_INPUT_GPC
);
1546 $start = get_input_value('start', RCUBE_INPUT_GPC
);
1547 $end = get_input_value('end', RCUBE_INPUT_GPC
);
1549 // convert dates into unix timestamps
1550 if (!empty($start) && !is_numeric($start)) {
1551 $dts = new DateTime($start, $this->timezone
);
1552 $start = $dts->format('U');
1554 if (!empty($end) && !is_numeric($end)) {
1555 $dte = new DateTime($end, $this->timezone
);
1556 $end = $dte->format('U');
1559 if (!$start) $start = time();
1560 if (!$end) $end = $start +
3600;
1562 $fbtypemap = array(calendar
::FREEBUSY_UNKNOWN
=> 'UNKNOWN', calendar
::FREEBUSY_FREE
=> 'FREE', calendar
::FREEBUSY_BUSY
=> 'BUSY', calendar
::FREEBUSY_TENTATIVE
=> 'TENTATIVE', calendar
::FREEBUSY_OOF
=> 'OUT-OF-OFFICE');
1563 $status = 'UNKNOWN';
1565 // if the backend has free-busy information
1566 $fblist = $this->driver
->get_freebusy_list($email, $start, $end);
1567 if (is_array($fblist)) {
1570 foreach ($fblist as $slot) {
1571 list($from, $to, $type) = $slot;
1572 if ($from < $end && $to > $start) {
1573 $status = isset($type) && $fbtypemap[$type] ?
$fbtypemap[$type] : 'BUSY';
1579 // let this information be cached for 5min
1580 send_future_expire_header(300);
1587 * Return a list of free/busy time slots within the given period
1588 * Echo data in JSON encoding
1590 public function freebusy_times()
1592 $email = get_input_value('email', RCUBE_INPUT_GPC
);
1593 $start = get_input_value('start', RCUBE_INPUT_GPC
);
1594 $end = get_input_value('end', RCUBE_INPUT_GPC
);
1595 $interval = intval(get_input_value('interval', RCUBE_INPUT_GPC
));
1596 $strformat = $interval > 60 ?
'Ymd' : 'YmdHis';
1598 // convert dates into unix timestamps
1599 if (!empty($start) && !is_numeric($start)) {
1600 $dts = new DateTime($start, $this->timezone
);
1601 $start = $dts->format('U');
1603 if (!empty($end) && !is_numeric($end)) {
1604 $dte = new DateTime($end, $this->timezone
);
1605 $end = $dte->format('U');
1608 if (!$start) $start = time();
1609 if (!$end) $end = $start +
86400 * 30;
1610 if (!$interval) $interval = 60; // 1 hour
1613 $dts = new DateTime('@'.$start);
1614 $dts->setTimezone($this->timezone
);
1617 $fblist = $this->driver
->get_freebusy_list($email, $start, $end);
1620 // build a list from $start till $end with blocks representing the fb-status
1621 for ($s = 0, $t = $start; $t <= $end; $s++
) {
1622 $status = self
::FREEBUSY_UNKNOWN
;
1623 $t_end = $t +
$interval * 60;
1624 $dt = new DateTime('@'.$t);
1625 $dt->setTimezone($this->timezone
);
1627 // determine attendee's status
1628 if (is_array($fblist)) {
1629 $status = self
::FREEBUSY_FREE
;
1630 foreach ($fblist as $slot) {
1631 list($from, $to, $type) = $slot;
1632 if ($from < $t_end && $to > $t) {
1633 $status = isset($type) ?
$type : self
::FREEBUSY_BUSY
;
1634 if ($status == self
::FREEBUSY_BUSY
) // can't get any worse :-)
1640 $slots[$s] = $status;
1641 $times[$s] = intval($dt->format($strformat));
1645 $dte = new DateTime('@'.$t_end);
1646 $dte->setTimezone($this->timezone
);
1648 // let this information be cached for 5min
1649 send_future_expire_header(300);
1651 echo json_encode(array(
1653 'start' => $dts->format('c'),
1654 'end' => $dte->format('c'),
1655 'interval' => $interval,
1663 * Handler for printing calendars
1665 public function print_view()
1667 $title = $this->gettext('print');
1669 $view = get_input_value('view', RCUBE_INPUT_GPC
);
1670 if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
1671 $view = 'agendaDay';
1673 $this->rc
->output
->set_env('view',$view);
1675 if ($date = get_input_value('date', RCUBE_INPUT_GPC
))
1676 $this->rc
->output
->set_env('date', $date);
1678 if ($range = get_input_value('range', RCUBE_INPUT_GPC
))
1679 $this->rc
->output
->set_env('listRange', intval($range));
1681 if (isset($_REQUEST['sections']))
1682 $this->rc
->output
->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC
));
1684 if ($search = get_input_value('search', RCUBE_INPUT_GPC
)) {
1685 $this->rc
->output
->set_env('search', $search);
1686 $title .= ' "' . $search . '"';
1689 // Add CSS stylesheets to the page header
1690 $skin_path = $this->local_skin_path();
1691 $this->include_stylesheet($skin_path . '/fullcalendar.css');
1692 $this->include_stylesheet($skin_path . '/print.css');
1694 // Add JS files to the page header
1695 $this->include_script('print.js');
1696 $this->include_script('lib/js/fullcalendar.js');
1698 $this->register_handler('plugin.calendar_css', array($this->ui
, 'calendar_css'));
1699 $this->register_handler('plugin.calendar_list', array($this->ui
, 'calendar_list'));
1701 $this->rc
->output
->set_pagetitle($title);
1702 $this->rc
->output
->send("calendar.print");
1708 public function get_inline_ui()
1710 foreach (array('save','cancel','savingdata') as $label)
1711 $texts['calendar.'.$label] = $this->gettext($label);
1713 $texts['calendar.new_event'] = $this->gettext('createfrommail');
1715 $this->ui
->init_templates();
1716 $this->ui
->calendar_list(); # set env['calendars']
1717 echo $this->api
->output
->parse('calendar.eventedit', false, false);
1718 echo html
::tag('script', array('type' => 'text/javascript'),
1719 "rcmail.set_env('calendars', " . json_encode($this->api
->output
->env
['calendars']) . ");\n".
1720 "rcmail.set_env('deleteicon', '" . $this->api
->output
->env
['deleteicon'] . "');\n".
1721 "rcmail.set_env('cancelicon', '" . $this->api
->output
->env
['cancelicon'] . "');\n".
1722 "rcmail.set_env('loadingicon', '" . $this->api
->output
->env
['loadingicon'] . "');\n".
1723 "rcmail.gui_object('attachmentlist', '" . $this->ui
->attachmentlist_id
. "');\n".
1724 "rcmail.add_label(" . json_encode($texts) . ");\n"
1730 * Compare two event objects and return differing properties
1732 * @param array Event A
1733 * @param array Event B
1734 * @return array List of differing event properties
1736 public static function event_diff($a, $b)
1739 $ignore = array('changed' => 1, 'attachments' => 1);
1740 foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
1741 if (!$ignore[$key] && $a[$key] != $b[$key])
1745 // only compare number of attachments
1746 if (count($a['attachments']) != count($b['attachments']))
1747 $diff[] = 'attachments';
1753 /**** Event invitation plugin hooks ****/
1756 * Handler for URLs that allow an invitee to respond on his invitation mail
1758 public function itip_attend_response($p)
1760 if ($p['action'] == 'attend') {
1761 $this->rc
->output
->set_env('task', 'calendar'); // override some env vars
1762 $this->rc
->output
->set_env('refresh_interval', 0);
1763 $this->rc
->output
->set_pagetitle($this->gettext('calendar'));
1765 $itip = $this->load_itip();
1766 $token = get_input_value('_t', RCUBE_INPUT_GPC
);
1768 // read event info stored under the given token
1769 if ($invitation = $itip->get_invitation($token)) {
1770 $this->token
= $token;
1771 $this->event
= $invitation['event'];
1773 // show message about cancellation
1774 if ($invitation['cancelled']) {
1775 $this->invitestatus
= html
::div('rsvp-status declined', $this->gettext('eventcancelled'));
1777 // save submitted RSVP status
1778 else if (!empty($_POST['rsvp'])) {
1780 foreach (array('accepted','tentative','declined') as $method) {
1781 if ($_POST['rsvp'] == $this->gettext('itip' . $method)) {
1787 // send itip reply to organizer
1788 if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
1789 $this->invitestatus
= html
::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
1792 $this->rc
->output
->command('display_message', $this->gettext('errorsaving'), 'error', -1);
1794 // if user is logged in...
1795 if ($this->rc
->user
->ID
) {
1796 $this->load_driver();
1797 $invitation = $itip->get_invitation($token);
1799 // save the event to his/her default calendar if not yet present
1800 if (!$this->driver
->get_event($this->event
) && ($calendar = $this->get_default_calendar(true))) {
1801 $invitation['event']['calendar'] = $calendar['id'];
1802 if ($this->driver
->new_event($invitation['event']))
1803 $this->rc
->output
->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
1808 $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
1809 $this->register_handler('plugin.event_invitebox', array($this->ui
, 'event_invitebox'));
1811 if (!$this->invitestatus
)
1812 $this->register_handler('plugin.event_rsvp_buttons', array($this->ui
, 'event_rsvp_buttons'));
1814 $this->rc
->output
->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event
['title']);
1817 $this->rc
->output
->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
1819 $this->rc
->output
->send('calendar.itipattend');
1826 public function itip_event_inviteform($attrib)
1828 $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token
));
1829 return html
::tag('form', array('action' => $this->rc
->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) +
$attrib) . $hidden->show();
1833 * Check mail message structure of there are .ics files attached
1835 public function mail_message_load($p)
1837 $this->message
= $p['object'];
1840 // check all message parts for .ics files
1841 foreach ((array)$this->message
->mime_parts
as $part) {
1842 if ($this->is_vcalendar($part)) {
1843 if ($part->ctype_parameters
['method'])
1844 $itip_part = $part->mime_id
;
1846 $this->ics_parts
[] = $part->mime_id
;
1850 // priorize part with method parameter
1852 $this->ics_parts
= array($itip_part);
1856 * Add UI element to copy event invitations or updates to the calendar
1858 public function mail_messagebody_html($p)
1860 // load iCalendar functions (if necessary)
1861 if (!empty($this->ics_parts
)) {
1866 foreach ($this->ics_parts
as $mime_id) {
1867 $part = $this->message
->mime_parts
[$mime_id];
1868 $charset = $part->ctype_parameters
['charset'] ?
$part->ctype_parameters
['charset'] : RCMAIL_CHARSET
;
1869 $events = $this->ical
->import($this->message
->get_part_content($mime_id), $charset);
1870 $title = $this->gettext('title');
1871 $date = rcube_utils
::anytodatetime($this->message
->headers
->date
);
1873 // successfully parsed events?
1877 // show a box for every event in the file
1878 foreach ($events as $idx => $event) {
1879 // define buttons according to method
1880 if ($this->ical
->method
== 'REPLY') {
1881 $title = $this->gettext('itipreply');
1882 $buttons = html
::tag('input', array(
1884 'class' => 'button',
1885 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1886 'value' => $this->gettext('updateattendeestatus'),
1889 else if ($this->ical
->method
== 'REQUEST') {
1890 $emails = $this->get_user_emails();
1891 $title = $event['sequence'] > 0 ?
$this->gettext('itipupdate') : $this->gettext('itipinvitation');
1893 // add (hidden) buttons and activate them from asyncronous request
1894 foreach (array('accepted','tentative','declined') as $method) {
1895 $rsvp_buttons .= html
::tag('input', array(
1897 'class' => "button $method",
1898 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')",
1899 'value' => $this->gettext('itip' . $method),
1902 $import_button = html
::tag('input', array(
1904 'class' => 'button',
1905 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1906 'value' => $this->gettext('importtocalendar'),
1910 $status = 'unknown';
1911 foreach ($event['attendees'] as $attendee) {
1912 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
1913 $status = strtoupper($attendee['status']);
1918 $dom_id = asciiwords($event['uid'], true);
1919 $buttons = html
::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
1920 $buttons .= html
::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
1921 $buttons_pre = html
::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
1922 $changed = is_object($event['changed']) ?
$event['changed'] : $date;
1924 $script = json_serialize(array(
1925 'uid' => $event['uid'],
1926 'changed' => $changed ?
$changed->format('U') : 0,
1927 'sequence' => intval($event['sequence']),
1928 'fallback' => $status,
1931 $this->rc
->output
->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
1933 else if ($this->ical
->method
== 'CANCEL') {
1934 $title = $this->gettext('itipcancellation');
1936 // create buttons to be activated from async request checking existence of this event in local calendars
1937 $button_import = html
::tag('input', array(
1939 'class' => 'button',
1940 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1941 'value' => $this->gettext('importtocalendar'),
1943 $button_remove = html
::tag('input', array(
1945 'class' => 'button',
1946 'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
1947 'value' => $this->gettext('removefromcalendar'),
1950 $dom_id = asciiwords($event['uid'], true);
1951 $buttons = html
::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
1952 $buttons .= html
::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
1953 $buttons_pre = html
::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
1954 $changed = is_object($event['changed']) ?
$event['changed'] : $date;
1956 $script = json_serialize(array(
1957 'uid' => $event['uid'],
1958 'changed' => $changed ?
$changed->format('U') : 0,
1959 'sequence' => intval($event['sequence']),
1960 'fallback' => 'CANCELLED',
1963 $this->rc
->output
->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
1966 $buttons = html
::tag('input', array(
1968 'class' => 'button',
1969 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1970 'value' => $this->gettext('importtocalendar'),
1974 // show event details with buttons
1975 $html .= html
::div('calendar-invitebox', $this->ui
->event_details_table($event, $title) . $buttons_pre . html
::div('rsvp-buttons', $buttons));
1983 // prepend event boxes to message body
1986 $p['content'] = $html . $p['content'];
1987 $this->rc
->output
->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
1995 * Handler for POST request to import an event attached to a mail message
1997 public function mail_import_event()
1999 $uid = get_input_value('_uid', RCUBE_INPUT_POST
);
2000 $mbox = get_input_value('_mbox', RCUBE_INPUT_POST
);
2001 $mime_id = get_input_value('_part', RCUBE_INPUT_POST
);
2002 $status = get_input_value('_status', RCUBE_INPUT_POST
);
2003 $delete = intval(get_input_value('_del', RCUBE_INPUT_POST
));
2004 $charset = RCMAIL_CHARSET
;
2006 // establish imap connection
2007 $imap = $this->rc
->get_storage();
2008 $imap->set_mailbox($mbox);
2010 if ($uid && $mime_id) {
2011 list($mime_id, $index) = explode(':', $mime_id);
2012 $part = $imap->get_message_part($uid, $mime_id);
2013 if ($part->ctype_parameters
['charset'])
2014 $charset = $part->ctype_parameters
['charset'];
2015 $headers = $imap->get_message_headers($uid);
2018 $events = $this->get_ical()->import($part, $charset);
2020 $error_msg = $this->gettext('errorimportingevent');
2023 // successfully parsed events?
2024 if (!empty($events) && ($event = $events[$index])) {
2025 // find writeable calendar to store event
2026 $cal_id = !empty($_REQUEST['_calendar']) ?
get_input_value('_calendar', RCUBE_INPUT_POST
) : null;
2027 $calendars = $this->driver
->list_calendars(false, true);
2028 $calendar = $calendars[$cal_id] ?
: $this->get_default_calendar(true);
2030 // update my attendee status according to submitted method
2031 if (!empty($status)) {
2033 $emails = $this->get_user_emails();
2034 foreach ($event['attendees'] as $i => $attendee) {
2035 if ($attendee['role'] == 'ORGANIZER') {
2036 $organizer = $attendee;
2038 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
2039 $event['attendees'][$i]['status'] = strtoupper($status);
2040 $reply_sender = $attendee['email'];
2046 if ($calendar && !$calendar['readonly']) {
2047 $event['calendar'] = $calendar['id'];
2049 // check for existing event with the same UID
2050 $existing = $this->driver
->get_event($event['uid'], true, false, true);
2053 // only update attendee status
2054 if ($this->ical
->method
== 'REPLY') {
2055 // try to identify the attendee using the email sender address
2056 $sender = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from
, $m) ?
$m[1] : '';
2057 $sender_utf = rcube_idn_to_utf8($sender);
2059 $existing_attendee = -1;
2060 foreach ($existing['attendees'] as $i => $attendee) {
2061 if ($sender && ($attendee['email'] == $sender ||
$attendee['email'] == $sender_utf)) {
2062 $existing_attendee = $i;
2066 $event_attendee = null;
2067 foreach ($event['attendees'] as $attendee) {
2068 if ($sender && ($attendee['email'] == $sender ||
$attendee['email'] == $sender_utf)) {
2069 $event_attendee = $attendee;
2074 // found matching attendee entry in both existing and new events
2075 if ($existing_attendee >= 0 && $event_attendee) {
2076 $existing['attendees'][$existing_attendee] = $event_attendee;
2077 $success = $this->driver
->edit_event($existing);
2079 // update the entire attendees block
2080 else if ($event['changed'] >= $existing['changed'] && $event['attendees']) {
2081 $existing['attendees'] = $event['attendees'];
2082 $success = $this->driver
->edit_event($existing);
2085 $error_msg = $this->gettext('newerversionexists');
2088 // delete the event when declined (#1670)
2089 else if ($status == 'declined' && $delete) {
2090 $deleted = $this->driver
->remove_event($existing, true);
2093 // import the (newer) event
2094 else if ($event['sequence'] >= $existing['sequence'] ||
$event['changed'] >= $existing['changed']) {
2095 $event['id'] = $existing['id'];
2096 $event['calendar'] = $existing['calendar'];
2097 if ($status == 'declined') // show me as free when declined (#1670)
2098 $event['free_busy'] = 'free';
2099 $success = $this->driver
->edit_event($event);
2101 else if (!empty($status)) {
2102 $existing['attendees'] = $event['attendees'];
2103 if ($status == 'declined') // show me as free when declined (#1670)
2104 $existing['free_busy'] = 'free';
2105 $success = $this->driver
->edit_event($existing);
2108 $error_msg = $this->gettext('newerversionexists');
2110 else if (!$existing && $status != 'declined') {
2111 $success = $this->driver
->new_event($event);
2113 else if ($status == 'declined')
2116 else if ($status == 'declined')
2119 $error_msg = $this->gettext('nowritecalendarfound');
2123 $message = $this->ical
->method
== 'REPLY' ?
'attendeupdateesuccess' : ($deleted ?
'successremoval' : 'importedsuccessfully');
2124 $this->rc
->output
->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
2125 $this->rc
->output
->command('plugin.fetch_event_rsvp_status', array('uid' => $event['uid'], 'changed' => $event['changed']->format('U'), 'sequence' => intval($event['sequence']), 'fallback' => strtoupper($status)));
2128 else if ($error_msg)
2129 $this->rc
->output
->command('display_message', $error_msg, 'error');
2133 if ($this->ical
->method
== 'REQUEST' && $organizer && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
2134 $itip = $this->load_itip();
2135 $itip->set_sender_email($reply_sender);
2136 if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
2137 $this->rc
->output
->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?
$organizer['name'] : $organizer['email']))), 'confirmation');
2139 $this->rc
->output
->command('display_message', $this->gettext('itipresponseerror'), 'error');
2142 $this->rc
->output
->send();
2147 * Read email message and return contents for a new event based on that message
2149 public function mail_message2event()
2151 $uid = get_input_value('_uid', RCUBE_INPUT_POST
);
2152 $mbox = get_input_value('_mbox', RCUBE_INPUT_POST
);
2155 // establish imap connection
2156 $imap = $this->rc
->get_storage();
2157 $imap->set_mailbox($mbox);
2158 $message = new rcube_message($uid);
2160 if ($message->headers
) {
2161 $event['title'] = trim($message->subject
);
2162 $event['description'] = trim($message->first_text_part());
2164 // copy mail attachments to event
2165 if ($message->attachments
) {
2167 if (!is_array($_SESSION[self
::SESSION_KEY
]) ||
$_SESSION[self
::SESSION_KEY
]['id'] != $eventid) {
2168 $_SESSION[self
::SESSION_KEY
] = array();
2169 $_SESSION[self
::SESSION_KEY
]['id'] = $eventid;
2170 $_SESSION[self
::SESSION_KEY
]['attachments'] = array();
2173 foreach ((array)$message->attachments
as $part) {
2174 $attachment = array(
2175 'data' => $imap->get_message_part($uid, $part->mime_id
, $part),
2176 'size' => $part->size
,
2177 'name' => $part->filename
,
2178 'mimetype' => $part->mimetype
,
2179 'group' => $eventid,
2182 $attachment = $this->rc
->plugins
->exec_hook('attachment_save', $attachment);
2184 if ($attachment['status'] && !$attachment['abort']) {
2185 $id = $attachment['id'];
2186 $attachment['classname'] = rcube_utils
::file2class($attachment['mimetype'], $attachment['name']);
2188 // store new attachment in session
2189 unset($attachment['status'], $attachment['abort'], $attachment['data']);
2190 $_SESSION[self
::SESSION_KEY
]['attachments'][$id] = $attachment;
2192 $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
2193 $event['attachments'][] = $attachment;
2198 $this->rc
->output
->command('plugin.mail2event_dialog', $event);
2201 $this->rc
->output
->command('display_message', $this->gettext('messageopenerror'), 'error');
2204 $this->rc
->output
->send();
2209 * Checks if specified message part is a vcalendar data
2211 * @param rcube_message_part Part object
2212 * @return boolean True if part is of type vcard
2214 private function is_vcalendar($part)
2217 in_array($part->mimetype
, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
2218 // Apple sends files as application/x-any (!?)
2219 ($part->mimetype
== 'application/x-any' && $part->filename
&& preg_match('/\.ics$/i', $part->filename
))
2225 * Get a list of email addresses of the current user (from login and identities)
2227 private function get_user_emails()
2230 $plugin = $this->rc
->plugins
->exec_hook('calendar_user_emails', array('emails' => $emails));
2231 $emails = array_map('strtolower', $plugin['emails']);
2233 if ($plugin['abort']) {
2237 $emails[] = $this->rc
->user
->get_username();
2238 foreach ($this->rc
->user
->list_identities() as $identity)
2239 $emails[] = strtolower($identity['email']);
2241 return array_unique($emails);
2246 * Build an absolute URL with the given parameters
2248 public function get_url($param = array())
2250 $param +
= array('task' => 'calendar');
2254 if (rcube_https_check()) {
2256 $default_port = 443;
2258 $url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
2259 if ($_SERVER['SERVER_PORT'] != $default_port)
2260 $url .= ':' . $_SERVER['SERVER_PORT'];
2261 if (dirname($_SERVER['SCRIPT_NAME']) != '/')
2262 $url .= dirname($_SERVER['SCRIPT_NAME']);
2263 $url .= preg_replace('!^\./!', '/', $this->rc
->url($param));
2269 public function ical_feed_hash($source)
2271 return base64_encode($this->rc
->user
->get_username() . ':' . $source);