comparison plugins/calendar/calendar.php @ 3:f6fe4b6ae66a

calendar plugin nearly as distributed
author Charlie Root
date Sat, 13 Jan 2018 08:56:12 -0500
parents
children 3bd5fe8166b8
comparison
equal deleted inserted replaced
2:c828b0fd4a6e 3:f6fe4b6ae66a
1 <?php
2
3 /**
4 * Calendar plugin for Roundcube webmail
5 *
6 * @author Lazlo Westerhof <hello@lazlo.me>
7 * @author Thomas Bruederli <bruederli@kolabsys.com>
8 *
9 * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
10 * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
11 *
12 * This program is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU Affero General Public License as
14 * published by the Free Software Foundation, either version 3 of the
15 * License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU Affero General Public License for more details.
21 *
22 * You should have received a copy of the GNU Affero General Public License
23 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 */
25
26 class calendar extends rcube_plugin
27 {
28 const FREEBUSY_UNKNOWN = 0;
29 const FREEBUSY_FREE = 1;
30 const FREEBUSY_BUSY = 2;
31 const FREEBUSY_TENTATIVE = 3;
32 const FREEBUSY_OOF = 4;
33
34 const SESSION_KEY = 'calendar_temp';
35
36 public $task = '?(?!logout).*';
37 public $rc;
38 public $lib;
39 public $resources_dir;
40 public $home; // declare public to be used in other classes
41 public $urlbase;
42 public $timezone;
43 public $timezone_offset;
44 public $gmt_offset;
45 public $ui;
46
47 public $defaults = array(
48 'calendar_default_view' => "agendaWeek",
49 'calendar_timeslots' => 2,
50 'calendar_work_start' => 6,
51 'calendar_work_end' => 18,
52 'calendar_agenda_range' => 60,
53 'calendar_agenda_sections' => 'smart',
54 'calendar_event_coloring' => 0,
55 'calendar_time_indicator' => true,
56 'calendar_allow_invite_shared' => false,
57 'calendar_itip_send_option' => 3,
58 'calendar_itip_after_action' => 0,
59 );
60
61 // These are implemented with __get()
62 // private $ical;
63 // private $itip;
64 // private $driver;
65
66
67 /**
68 * Plugin initialization.
69 */
70 function init()
71 {
72 $this->rc = rcube::get_instance();
73
74 $this->register_task('calendar', 'calendar');
75
76 // load calendar configuration
77 $this->load_config();
78
79 // catch iTIP confirmation requests that don're require a valid session
80 if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
81 $this->add_hook('startup', array($this, 'itip_attend_response'));
82 }
83 else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
84 $this->add_hook('startup', array($this, 'ical_feed_export'));
85 }
86 else if ($this->rc->task != 'login') {
87 // default startup routine
88 $this->add_hook('startup', array($this, 'startup'));
89 }
90
91 $this->add_hook('user_delete', array($this, 'user_delete'));
92 }
93
94 /**
95 * Setup basic plugin environment and UI
96 */
97 protected function setup()
98 {
99 $this->require_plugin('libcalendaring');
100
101 $this->lib = libcalendaring::get_instance();
102 $this->timezone = $this->lib->timezone;
103 $this->gmt_offset = $this->lib->gmt_offset;
104 $this->dst_active = $this->lib->dst_active;
105 $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
106
107 // load localizations
108 $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
109
110 require($this->home . '/lib/calendar_ui.php');
111 $this->ui = new calendar_ui($this);
112 }
113
114 /**
115 * Startup hook
116 */
117 public function startup($args)
118 {
119 // the calendar module can be enabled/disabled by the kolab_auth plugin
120 if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true))
121 return;
122
123 $this->setup();
124
125 // load Calendar user interface
126 if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'preview')) {
127 $this->ui->init();
128
129 // settings are required in (almost) every GUI step
130 if ($args['action'] != 'attend')
131 $this->rc->output->set_env('calendar_settings', $this->load_settings());
132 }
133
134 if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
135 if ($args['action'] != 'upload') {
136 $this->load_driver();
137 }
138
139 // register calendar actions
140 $this->register_action('index', array($this, 'calendar_view'));
141 $this->register_action('event', array($this, 'event_action'));
142 $this->register_action('calendar', array($this, 'calendar_action'));
143 $this->register_action('count', array($this, 'count_events'));
144 $this->register_action('load_events', array($this, 'load_events'));
145 $this->register_action('export_events', array($this, 'export_events'));
146 $this->register_action('import_events', array($this, 'import_events'));
147 $this->register_action('upload', array($this, 'attachment_upload'));
148 $this->register_action('get-attachment', array($this, 'attachment_get'));
149 $this->register_action('freebusy-status', array($this, 'freebusy_status'));
150 $this->register_action('freebusy-times', array($this, 'freebusy_times'));
151 $this->register_action('randomdata', array($this, 'generate_randomdata'));
152 $this->register_action('print', array($this,'print_view'));
153 $this->register_action('mailimportitip', array($this, 'mail_import_itip'));
154 $this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
155 $this->register_action('mailtoevent', array($this, 'mail_message2event'));
156 $this->register_action('inlineui', array($this, 'get_inline_ui'));
157 $this->register_action('check-recent', array($this, 'check_recent'));
158 $this->register_action('itip-status', array($this, 'event_itip_status'));
159 $this->register_action('itip-remove', array($this, 'event_itip_remove'));
160 $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
161 $this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
162 $this->register_action('resources-list', array($this, 'resources_list'));
163 $this->register_action('resources-owner', array($this, 'resources_owner'));
164 $this->register_action('resources-calendar', array($this, 'resources_calendar'));
165 $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
166 $this->add_hook('refresh', array($this, 'refresh'));
167
168 // remove undo information...
169 if ($undo = $_SESSION['calendar_event_undo']) {
170 // ...after timeout
171 $undo_time = $this->rc->config->get('undo_timeout', 0);
172 if ($undo['ts'] < time() - $undo_time) {
173 $this->rc->session->remove('calendar_event_undo');
174 // @TODO: do EXPUNGE on kolab objects?
175 }
176 }
177 }
178 else if ($args['task'] == 'settings') {
179 // add hooks for Calendar settings
180 $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
181 $this->add_hook('preferences_list', array($this, 'preferences_list'));
182 $this->add_hook('preferences_save', array($this, 'preferences_save'));
183 }
184 else if ($args['task'] == 'mail') {
185 // hooks to catch event invitations on incoming mails
186 if ($args['action'] == 'show' || $args['action'] == 'preview') {
187 $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
188 }
189
190 // add 'Create event' item to message menu
191 if ($this->api->output->type == 'html') {
192 $this->api->add_content(html::tag('li', null,
193 $this->api->output->button(array(
194 'command' => 'calendar-create-from-mail',
195 'label' => 'calendar.createfrommail',
196 'type' => 'link',
197 'classact' => 'icon calendarlink active',
198 'class' => 'icon calendarlink',
199 'innerclass' => 'icon calendar',
200 ))),
201 'messagemenu');
202
203 $this->api->output->add_label('calendar.createfrommail');
204 }
205
206 $this->add_hook('messages_list', array($this, 'mail_messages_list'));
207 $this->add_hook('message_compose', array($this, 'mail_message_compose'));
208 }
209 else if ($args['task'] == 'addressbook') {
210 if ($this->rc->config->get('calendar_contact_birthdays')) {
211 $this->add_hook('contact_update', array($this, 'contact_update'));
212 $this->add_hook('contact_create', array($this, 'contact_update'));
213 }
214 }
215
216 // add hooks to display alarms
217 $this->add_hook('pending_alarms', array($this, 'pending_alarms'));
218 $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
219 }
220
221 /**
222 * Helper method to load the backend driver according to local config
223 */
224 private function load_driver()
225 {
226 if (is_object($this->driver))
227 return;
228
229 $driver_name = $this->rc->config->get('calendar_driver', 'database');
230 $driver_class = $driver_name . '_driver';
231
232 require_once($this->home . '/drivers/calendar_driver.php');
233 require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
234
235 $this->driver = new $driver_class($this);
236
237 if ($this->driver->undelete)
238 $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
239 }
240
241 /**
242 * Load iTIP functions
243 */
244 private function load_itip()
245 {
246 if (!$this->itip) {
247 require_once($this->home . '/lib/calendar_itip.php');
248 $this->itip = new calendar_itip($this);
249
250 if ($this->rc->config->get('kolab_invitation_calendars'))
251 $this->itip->set_rsvp_actions(array('accepted','tentative','declined','delegated','needs-action'));
252 }
253
254 return $this->itip;
255 }
256
257 /**
258 * Load iCalendar functions
259 */
260 public function get_ical()
261 {
262 if (!$this->ical) {
263 $this->ical = libcalendaring::get_ical();
264 }
265
266 return $this->ical;
267 }
268
269 /**
270 * Get properties of the calendar this user has specified as default
271 */
272 public function get_default_calendar($sensitivity = null, $calendars = null)
273 {
274 if ($calendars === null) {
275 $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_WRITEABLE);
276 }
277
278 $default_id = $this->rc->config->get('calendar_default_calendar');
279 $calendar = $calendars[$default_id] ?: null;
280
281 if (!$calendar || $sensitivity) {
282 foreach ($calendars as $cal) {
283 if ($sensitivity && $cal['subtype'] == $sensitivity) {
284 $calendar = $cal;
285 break;
286 }
287 if ($cal['default'] && $cal['editable']) {
288 $calendar = $cal;
289 }
290 if ($cal['editable']) {
291 $first = $cal;
292 }
293 }
294 }
295
296 return $calendar ?: $first;
297 }
298
299
300 /**
301 * Render the main calendar view from skin template
302 */
303 function calendar_view()
304 {
305 $this->rc->output->set_pagetitle($this->gettext('calendar'));
306
307 // Add CSS stylesheets to the page header
308 $this->ui->addCSS();
309
310 // Add JS files to the page header
311 $this->ui->addJS();
312
313 $this->ui->init_templates();
314 $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close');
315
316 // initialize attendees autocompletion
317 $this->rc->autocomplete_init();
318
319 $this->rc->output->set_env('timezone', $this->timezone->getName());
320 $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
321 $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
322 $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer'))));
323
324 $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
325 if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
326 $this->rc->output->set_env('view', $view);
327
328 if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC))
329 $this->rc->output->set_env('date', $date);
330
331 if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC))
332 $this->rc->output->set_env('itip_events', $this->itip_events($msgref));
333
334 $this->rc->output->send("calendar.calendar");
335 }
336
337 /**
338 * Handler for preferences_sections_list hook.
339 * Adds Calendar settings sections into preferences sections list.
340 *
341 * @param array Original parameters
342 * @return array Modified parameters
343 */
344 function preferences_sections_list($p)
345 {
346 $p['list']['calendar'] = array(
347 'id' => 'calendar', 'section' => $this->gettext('calendar'),
348 );
349
350 return $p;
351 }
352
353 /**
354 * Handler for preferences_list hook.
355 * Adds options blocks into Calendar settings sections in Preferences.
356 *
357 * @param array Original parameters
358 * @return array Modified parameters
359 */
360 function preferences_list($p)
361 {
362 if ($p['section'] != 'calendar') {
363 return $p;
364 }
365
366 $no_override = array_flip((array)$this->rc->config->get('dont_override'));
367
368 $p['blocks']['view']['name'] = $this->gettext('mainoptions');
369
370 if (!isset($no_override['calendar_default_view'])) {
371 if (!$p['current']) {
372 $p['blocks']['view']['content'] = true;
373 return $p;
374 }
375
376 $field_id = 'rcmfd_default_view';
377 $select = new html_select(array('name' => '_default_view', 'id' => $field_id));
378 $select->add($this->gettext('day'), "agendaDay");
379 $select->add($this->gettext('week'), "agendaWeek");
380 $select->add($this->gettext('month'), "month");
381 $select->add($this->gettext('agenda'), "table");
382 $p['blocks']['view']['options']['default_view'] = array(
383 'title' => html::label($field_id, rcube::Q($this->gettext('default_view'))),
384 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
385 );
386 }
387
388 if (!isset($no_override['calendar_timeslots'])) {
389 if (!$p['current']) {
390 $p['blocks']['view']['content'] = true;
391 return $p;
392 }
393
394 $field_id = 'rcmfd_timeslot';
395 $choices = array('1', '2', '3', '4', '6');
396 $select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
397 $select->add($choices);
398 $p['blocks']['view']['options']['timeslots'] = array(
399 'title' => html::label($field_id, rcube::Q($this->gettext('timeslots'))),
400 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))),
401 );
402 }
403
404 if (!isset($no_override['calendar_first_day'])) {
405 if (!$p['current']) {
406 $p['blocks']['view']['content'] = true;
407 return $p;
408 }
409
410 $field_id = 'rcmfd_firstday';
411 $select = new html_select(array('name' => '_first_day', 'id' => $field_id));
412 $select->add($this->gettext('sunday'), '0');
413 $select->add($this->gettext('monday'), '1');
414 $select->add($this->gettext('tuesday'), '2');
415 $select->add($this->gettext('wednesday'), '3');
416 $select->add($this->gettext('thursday'), '4');
417 $select->add($this->gettext('friday'), '5');
418 $select->add($this->gettext('saturday'), '6');
419 $p['blocks']['view']['options']['first_day'] = array(
420 'title' => html::label($field_id, rcube::Q($this->gettext('first_day'))),
421 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
422 );
423 }
424
425 if (!isset($no_override['calendar_first_hour'])) {
426 if (!$p['current']) {
427 $p['blocks']['view']['content'] = true;
428 return $p;
429 }
430
431 $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'])));
432 $select_hours = new html_select();
433 for ($h = 0; $h < 24; $h++)
434 $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
435
436 $field_id = 'rcmfd_firsthour';
437 $p['blocks']['view']['options']['first_hour'] = array(
438 'title' => html::label($field_id, rcube::Q($this->gettext('first_hour'))),
439 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
440 );
441 }
442
443 if (!isset($no_override['calendar_work_start'])) {
444 if (!$p['current']) {
445 $p['blocks']['view']['content'] = true;
446 return $p;
447 }
448
449 $field_id = 'rcmfd_workstart';
450 $p['blocks']['view']['options']['workinghours'] = array(
451 'title' => html::label($field_id, rcube::Q($this->gettext('workinghours'))),
452 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
453 ' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
454 );
455 }
456
457 if (!isset($no_override['calendar_event_coloring'])) {
458 if (!$p['current']) {
459 $p['blocks']['view']['content'] = true;
460 return $p;
461 }
462
463 $field_id = 'rcmfd_coloring';
464 $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
465 $select_colors->add($this->gettext('coloringmode0'), 0);
466 $select_colors->add($this->gettext('coloringmode1'), 1);
467 $select_colors->add($this->gettext('coloringmode2'), 2);
468 $select_colors->add($this->gettext('coloringmode3'), 3);
469
470 $p['blocks']['view']['options']['eventcolors'] = array(
471 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('eventcoloring'))),
472 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
473 );
474 }
475
476 // loading driver is expensive, don't do it if not needed
477 $this->load_driver();
478
479 if (!isset($no_override['calendar_default_alarm_type']) || !isset($no_override['calendar_default_alarm_offset'])) {
480 if (!$p['current']) {
481 $p['blocks']['view']['content'] = true;
482 return $p;
483 }
484
485 $alarm_type = $alarm_offset = '';
486
487 if (!isset($no_override['calendar_default_alarm_type'])) {
488 $field_id = 'rcmfd_alarm';
489 $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
490 $select_type->add($this->gettext('none'), '');
491
492 foreach ($this->driver->alarm_types as $type) {
493 $select_type->add($this->rc->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
494 }
495
496 $alarm_type = $select_type->show($this->rc->config->get('calendar_default_alarm_type', ''));
497 }
498
499 if (!isset($no_override['calendar_default_alarm_offset'])) {
500 $field_id = 'rcmfd_alarm';
501 $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
502 $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
503
504 foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) {
505 $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);
506 }
507
508 $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
509 $alarm_offset = $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]);
510 }
511
512 $p['blocks']['view']['options']['alarmtype'] = array(
513 'title' => html::label($field_id, rcube::Q($this->gettext('defaultalarmtype'))),
514 'content' => $alarm_type . ' ' . $alarm_offset,
515 );
516 }
517
518 if (!isset($no_override['calendar_default_calendar'])) {
519 if (!$p['current']) {
520 $p['blocks']['view']['content'] = true;
521 return $p;
522 }
523 // default calendar selection
524 $field_id = 'rcmfd_default_calendar';
525 $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
526 foreach ((array)$this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_ACTIVE) as $id => $prop) {
527 $select_cal->add($prop['name'], strval($id));
528 if ($prop['default'])
529 $default_calendar = $id;
530 }
531 $p['blocks']['view']['options']['defaultcalendar'] = array(
532 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('defaultcalendar'))),
533 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),
534 );
535 }
536
537 $p['blocks']['itip']['name'] = $this->gettext('itipoptions');
538
539 // Invitations handling
540 if (!isset($no_override['calendar_itip_after_action'])) {
541 if (!$p['current']) {
542 $p['blocks']['itip']['content'] = true;
543 return $p;
544 }
545
546 $field_id = 'rcmfd_after_action';
547 $select = new html_select(array('name' => '_after_action', 'id' => $field_id,
548 'onchange' => "\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()"));
549
550 $select->add($this->gettext('afternothing'), '');
551 $select->add($this->gettext('aftertrash'), 1);
552 $select->add($this->gettext('afterdelete'), 2);
553 $select->add($this->gettext('afterflagdeleted'), 3);
554 $select->add($this->gettext('aftermoveto'), 4);
555
556 $val = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
557 if ($val !== null && $val !== '' && !is_int($val)) {
558 $folder = $val;
559 $val = 4;
560 }
561
562 $folders = $this->rc->folder_selector(array(
563 'id' => $field_id . '_select',
564 'name' => '_after_action_folder',
565 'maxlength' => 30,
566 'folder_filter' => 'mail',
567 'folder_rights' => 'w',
568 'style' => $val !== 4 ? 'display:none' : '',
569 ));
570
571 $p['blocks']['itip']['options']['after_action'] = array(
572 'title' => html::label($field_id, rcube::Q($this->gettext('afteraction'))),
573 'content' => $select->show($val) . $folders->show($folder),
574 );
575 }
576
577 // category definitions
578 if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) {
579 $p['blocks']['categories']['name'] = $this->gettext('categories');
580
581 if (!$p['current']) {
582 $p['blocks']['categories']['content'] = true;
583 return $p;
584 }
585
586 $categories = (array) $this->driver->list_categories();
587 $categories_list = '';
588 foreach ($categories as $name => $color) {
589 $key = md5($name);
590 $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
591 $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
592 $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable));
593 $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
594 $hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
595 $categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
596 }
597
598 $p['blocks']['categories']['options']['category_' . $name] = array(
599 'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
600 );
601
602 $field_id = 'rcmfd_new_category';
603 $new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
604 $add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
605 $p['blocks']['categories']['options']['categories'] = array(
606 'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
607 );
608
609 $this->rc->output->add_script('function rcube_calendar_add_category(){
610 var name = $("#rcmfd_new_category").val();
611 if (name.length) {
612 var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
613 var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
614 var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
615 $("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
616 color.miniColors({ colorValues:(rcmail.env.mscolors || []) });
617 $("#rcmfd_new_category").val("");
618 }
619 }');
620
621 $this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){
622 if (event.which == 13) {
623 rcube_calendar_add_category();
624 event.preventDefault();
625 }
626 });
627 ', 'docready');
628
629 // load miniColors js/css files
630 jqueryui::miniColors();
631 }
632
633 // virtual birthdays calendar
634 if (!isset($no_override['calendar_contact_birthdays'])) {
635 $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar');
636
637 if (!$p['current']) {
638 $p['blocks']['birthdays']['content'] = true;
639 return $p;
640 }
641
642 $field_id = 'rcmfd_contact_birthdays';
643 $input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)'));
644
645 $p['blocks']['birthdays']['options']['contact_birthdays'] = array(
646 'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')),
647 'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0),
648 );
649
650 $input_attrib = array(
651 'class' => 'calendar_birthday_props',
652 'disabled' => !$this->rc->config->get('calendar_contact_birthdays'),
653 );
654
655 $sources = array();
656 $checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib);
657 foreach ($this->rc->get_address_sources(false, true) as $source) {
658 $active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : '';
659 $sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . '&nbsp;' . rcube::Q($source['realname'] ?: $source['name']));
660 }
661
662 $p['blocks']['birthdays']['options']['birthday_adressbooks'] = array(
663 'title' => rcube::Q($this->gettext('birthdayscalendarsources')),
664 'content' => join(html::br(), $sources),
665 );
666
667 $field_id = 'rcmfd_birthdays_alarm';
668 $select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib);
669 $select_type->add($this->gettext('none'), '');
670 foreach ($this->driver->alarm_types as $type) {
671 $select_type->add($this->rc->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
672 }
673
674 $input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib);
675 $select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib);
676 foreach (array('-M','-H','-D') as $trigger)
677 $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);
678
679 $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D'));
680 $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array(
681 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))),
682 'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . '&nbsp;' . $select_offset->show($preset[1]),
683 );
684 }
685
686 return $p;
687 }
688
689 /**
690 * Handler for preferences_save hook.
691 * Executed on Calendar settings form submit.
692 *
693 * @param array Original parameters
694 * @return array Modified parameters
695 */
696 function preferences_save($p)
697 {
698 if ($p['section'] == 'calendar') {
699 $this->load_driver();
700
701 // compose default alarm preset value
702 $alarm_offset = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST);
703 $alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST);
704 $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1];
705
706 $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST);
707 $birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST);
708 $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1];
709
710 $p['prefs'] = array(
711 'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST),
712 'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)),
713 'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)),
714 'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)),
715 'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)),
716 'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)),
717 'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)),
718 'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST),
719 'calendar_default_alarm_offset' => $default_alarm,
720 'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST),
721 'calendar_date_format' => null, // clear previously saved values
722 'calendar_time_format' => null,
723 'calendar_contact_birthdays' => rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST) ? true : false,
724 'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST),
725 'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST),
726 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,
727 'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)),
728 );
729
730 if ($p['prefs']['calendar_itip_after_action'] == 4) {
731 $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true);
732 }
733
734 // categories
735 if (!$this->driver->nocategories) {
736 $old_categories = $new_categories = array();
737 foreach ($this->driver->list_categories() as $name => $color) {
738 $old_categories[md5($name)] = $name;
739 }
740
741 $categories = (array) rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST);
742 $colors = (array) rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST);
743
744 foreach ($categories as $key => $name) {
745 $color = preg_replace('/^#/', '', strval($colors[$key]));
746
747 // rename categories in existing events -> driver's job
748 if ($oldname = $old_categories[$key]) {
749 $this->driver->replace_category($oldname, $name, $color);
750 unset($old_categories[$key]);
751 }
752 else
753 $this->driver->add_category($name, $color);
754
755 $new_categories[$name] = $color;
756 }
757
758 // these old categories have been removed, alter events accordingly -> driver's job
759 foreach ((array)$old_categories[$key] as $key => $name) {
760 $this->driver->remove_category($name);
761 }
762
763 $p['prefs']['calendar_categories'] = $new_categories;
764 }
765 }
766
767 return $p;
768 }
769
770 /**
771 * Dispatcher for calendar actions initiated by the client
772 */
773 function calendar_action()
774 {
775 $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
776 $cal = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC);
777 $success = $reload = false;
778
779 if (isset($cal['showalarms']))
780 $cal['showalarms'] = intval($cal['showalarms']);
781
782 switch ($action) {
783 case "form-new":
784 case "form-edit":
785 echo $this->ui->calendar_editform($action, $cal);
786 exit;
787 case "new":
788 $success = $this->driver->create_calendar($cal);
789 $reload = true;
790 break;
791 case "edit":
792 $success = $this->driver->edit_calendar($cal);
793 $reload = true;
794 break;
795 case "delete":
796 if ($success = $this->driver->delete_calendar($cal))
797 $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
798 break;
799 case "subscribe":
800 if (!$this->driver->subscribe_calendar($cal))
801 $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
802 else {
803 $calendars = $this->driver->list_calendars();
804 $calendar = $calendars[$cal['id']];
805
806 // find parent folder and check if it's a "user calendar"
807 // if it's also activated we need to refresh it (#5340)
808 while ($calendar['parent']) {
809 if (isset($calendars[$calendar['parent']]))
810 $calendar = $calendars[$calendar['parent']];
811 else
812 break;
813 }
814
815 if ($calendar['id'] != $cal['id'] && $calendar['active'] && $calendar['group'] == "other user")
816 $this->rc->output->command('plugin.refresh_source', $calendar['id']);
817 }
818 return;
819 case "search":
820 $results = array();
821 $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
822 $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
823 $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
824
825 foreach ((array) $this->driver->search_calendars($query, $source) as $id => $prop) {
826 $editname = $prop['editname'];
827 unset($prop['editname']); // force full name to be displayed
828 $prop['active'] = false;
829
830 // let the UI generate HTML and CSS representation for this calendar
831 $html = $this->ui->calendar_list_item($id, $prop, $jsenv);
832 $cal = $jsenv[$id];
833 $cal['editname'] = $editname;
834 $cal['html'] = $html;
835 if (!empty($prop['color']))
836 $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);
837
838 $results[] = $cal;
839 }
840 // report more results available
841 if ($this->driver->search_more_results)
842 $this->rc->output->show_message('autocompletemore', 'info');
843
844 $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
845 return;
846 }
847
848 if ($success)
849 $this->rc->output->show_message('successfullysaved', 'confirmation');
850 else {
851 $error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :'');
852 $this->rc->output->show_message($error_msg, 'error');
853 }
854
855 $this->rc->output->command('plugin.unlock_saving');
856
857 if ($success && $reload)
858 $this->rc->output->command('plugin.reload_view');
859 }
860
861
862 /**
863 * Dispatcher for event actions initiated by the client
864 */
865 function event_action()
866 {
867 $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
868 $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
869 $success = $reload = $got_msg = false;
870
871 // force notify if hidden + active
872 if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
873 $event['_notify'] = 1;
874
875 // read old event data in order to find changes
876 if (($event['_notify'] || $event['_decline']) && $action != 'new') {
877 $old = $this->driver->get_event($event);
878
879 // load main event if savemode is 'all' or if deleting 'future' events
880 if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline'])) && $old['recurrence_id']) {
881 $old['id'] = $old['recurrence_id'];
882 $old = $this->driver->get_event($old);
883 }
884 }
885
886 switch ($action) {
887 case "new":
888 // create UID for new event
889 $event['uid'] = $this->generate_uid();
890 $this->write_preprocess($event, $action);
891 if ($success = $this->driver->new_event($event)) {
892 $event['id'] = $event['uid'];
893 $event['_savemode'] = 'all';
894 $this->cleanup_event($event);
895 $this->event_save_success($event, null, $action, true);
896 }
897 $reload = $success && $event['recurrence'] ? 2 : 1;
898 break;
899
900 case "edit":
901 $this->write_preprocess($event, $action);
902 if ($success = $this->driver->edit_event($event)) {
903 $this->cleanup_event($event);
904 $this->event_save_success($event, $old, $action, $success);
905 }
906 $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
907 break;
908
909 case "resize":
910 $this->write_preprocess($event, $action);
911 if ($success = $this->driver->resize_event($event)) {
912 $this->event_save_success($event, $old, $action, $success);
913 }
914 $reload = $event['_savemode'] ? 2 : 1;
915 break;
916
917 case "move":
918 $this->write_preprocess($event, $action);
919 if ($success = $this->driver->move_event($event)) {
920 $this->event_save_success($event, $old, $action, $success);
921 }
922 $reload = $success && $event['_savemode'] ? 2 : 1;
923 break;
924
925 case "remove":
926 // remove previous deletes
927 $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
928 $this->rc->session->remove('calendar_event_undo');
929
930 // search for event if only UID is given
931 if (!isset($event['calendar']) && $event['uid']) {
932 if (!($event = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) {
933 break;
934 }
935 $undo_time = 0;
936 }
937
938 $success = $this->driver->remove_event($event, $undo_time < 1);
939 $reload = (!$success || $event['_savemode']) ? 2 : 1;
940
941 if ($undo_time > 0 && $success) {
942 $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
943 // display message with Undo link.
944 $msg = html::span(null, $this->gettext('successremoval'))
945 . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
946 rcmail_output::JS_OBJECT_NAME, rcmail_output::JS_OBJECT_NAME)), $this->gettext('undo'));
947 $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
948 $got_msg = true;
949 }
950 else if ($success) {
951 $this->rc->output->show_message('calendar.successremoval', 'confirmation');
952 $got_msg = true;
953 }
954
955 // send cancellation for the main event
956 if ($event['_savemode'] == 'all') {
957 unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);
958 }
959 // send an update for the main event's recurrence rule instead of a cancellation message
960 else if ($event['_savemode'] == 'future' && $success !== false && $success !== true) {
961 $event['_savemode'] = 'all'; // force event_save_success() to load master event
962 $action = 'edit';
963 $success = true;
964 }
965
966 // send iTIP reply that participant has declined the event
967 if ($success && $event['_decline']) {
968 $emails = $this->get_user_emails();
969 foreach ($old['attendees'] as $i => $attendee) {
970 if ($attendee['role'] == 'ORGANIZER')
971 $organizer = $attendee;
972 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
973 $old['attendees'][$i]['status'] = 'DECLINED';
974 $reply_sender = $attendee['email'];
975 }
976 }
977
978 if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) {
979 $old['thisandfuture'] = true;
980 }
981
982 $itip = $this->load_itip();
983 $itip->set_sender_email($reply_sender);
984 if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
985 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
986 else
987 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
988 }
989 else if ($success) {
990 $this->event_save_success($event, $old, $action, $success);
991 }
992 break;
993
994 case "undo":
995 // Restore deleted event
996 $event = $_SESSION['calendar_event_undo']['data'];
997
998 if ($event)
999 $success = $this->driver->restore_event($event);
1000
1001 if ($success) {
1002 $this->rc->session->remove('calendar_event_undo');
1003 $this->rc->output->show_message('calendar.successrestore', 'confirmation');
1004 $got_msg = true;
1005 $reload = 2;
1006 }
1007
1008 break;
1009
1010 case "rsvp":
1011 $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
1012 $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);
1013 $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);
1014 $reply_comment = $event['comment'];
1015
1016 $this->write_preprocess($event, 'edit');
1017 $ev = $this->driver->get_event($event);
1018 $ev['attendees'] = $event['attendees'];
1019 $ev['free_busy'] = $event['free_busy'];
1020 $ev['_savemode'] = $event['_savemode'];
1021 $ev['comment'] = $reply_comment;
1022
1023 // send invitation to delegatee + add it as attendee
1024 if ($status == 'delegated' && $event['to']) {
1025 $itip = $this->load_itip();
1026 if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) {
1027 $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
1028 $noreply = false;
1029 }
1030 }
1031
1032 $event = $ev;
1033
1034 // compose a list of attendees affected by this change
1035 $updated_attendees = array_filter(array_map(function($j) use ($event) {
1036 return $event['attendees'][$j];
1037 }, $attendees));
1038
1039 if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) {
1040 $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
1041 $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
1042 $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
1043 $organizer = null;
1044 $emails = $this->get_user_emails();
1045
1046 foreach ($event['attendees'] as $i => $attendee) {
1047 if ($attendee['role'] == 'ORGANIZER') {
1048 $organizer = $attendee;
1049 }
1050 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
1051 $reply_sender = $attendee['email'];
1052 }
1053 }
1054
1055 if (!$noreply) {
1056 $itip = $this->load_itip();
1057 $itip->set_sender_email($reply_sender);
1058 $event['thisandfuture'] = $event['_savemode'] == 'future';
1059 if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
1060 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
1061 else
1062 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
1063 }
1064
1065 // refresh all calendars
1066 if ($event['calendar'] != $ev['calendar']) {
1067 $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true));
1068 $reload = 0;
1069 }
1070 }
1071 break;
1072
1073 case "dismiss":
1074 $event['ids'] = explode(',', $event['id']);
1075 $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event);
1076 $success = $plugin['success'];
1077 foreach ($event['ids'] as $id) {
1078 if (strpos($id, 'cal:') === 0)
1079 $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
1080 }
1081 break;
1082
1083 case "changelog":
1084 $data = $this->driver->get_event_changelog($event);
1085 if (is_array($data) && !empty($data)) {
1086 $lib = $this->lib;
1087 $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
1088 array_walk($data, function(&$change) use ($lib, $dtformat) {
1089 if ($change['date']) {
1090 $dt = $lib->adjust_timezone($change['date']);
1091 if ($dt instanceof DateTime)
1092 $change['date'] = $this->rc->format_date($dt, $dtformat, false);
1093 }
1094 });
1095 $this->rc->output->command('plugin.render_event_changelog', $data);
1096 }
1097 else {
1098 $this->rc->output->command('plugin.render_event_changelog', false);
1099 }
1100 $got_msg = true;
1101 $reload = false;
1102 break;
1103
1104 case "diff":
1105 $data = $this->driver->get_event_diff($event, $event['rev1'], $event['rev2']);
1106 if (is_array($data)) {
1107 // convert some properties, similar to self::_client_event()
1108 $lib = $this->lib;
1109 array_walk($data['changes'], function(&$change, $i) use ($event, $lib) {
1110 // convert date cols
1111 foreach (array('start','end','created','changed') as $col) {
1112 if ($change['property'] == $col) {
1113 $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
1114 $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
1115 }
1116 }
1117 // create textual representation for alarms and recurrence
1118 if ($change['property'] == 'alarms') {
1119 if (is_array($change['old']))
1120 $change['old_'] = libcalendaring::alarm_text($change['old']);
1121 if (is_array($change['new']))
1122 $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
1123 }
1124 if ($change['property'] == 'recurrence') {
1125 if (is_array($change['old']))
1126 $change['old_'] = $lib->recurrence_text($change['old']);
1127 if (is_array($change['new']))
1128 $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
1129 }
1130 if ($change['property'] == 'attachments') {
1131 if (is_array($change['old']))
1132 $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
1133 if (is_array($change['new']))
1134 $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
1135 }
1136 // compute a nice diff of description texts
1137 if ($change['property'] == 'description') {
1138 $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
1139 }
1140 });
1141 $this->rc->output->command('plugin.event_show_diff', $data);
1142 }
1143 else {
1144 $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
1145 }
1146 $got_msg = true;
1147 $reload = false;
1148 break;
1149
1150 case "show":
1151 if ($event = $this->driver->get_event_revison($event, $event['rev'])) {
1152 $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
1153 }
1154 else {
1155 $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
1156 }
1157 $got_msg = true;
1158 $reload = false;
1159 break;
1160
1161 case "restore":
1162 if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
1163 $_event = $this->driver->get_event($event);
1164 $reload = $_event['recurrence'] ? 2 : 1;
1165 $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation');
1166 $this->rc->output->command('plugin.close_history_dialog');
1167 }
1168 else {
1169 $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
1170 $reload = 0;
1171 }
1172 $got_msg = true;
1173 break;
1174 }
1175
1176 // show confirmation/error message
1177 if (!$got_msg) {
1178 if ($success)
1179 $this->rc->output->show_message('successfullysaved', 'confirmation');
1180 else
1181 $this->rc->output->show_message('calendar.errorsaving', 'error');
1182 }
1183
1184 // unlock client
1185 $this->rc->output->command('plugin.unlock_saving');
1186
1187 // update event object on the client or trigger a complete refretch if too complicated
1188 if ($reload) {
1189 $args = array('source' => $event['calendar']);
1190 if ($reload > 1)
1191 $args['refetch'] = true;
1192 else if ($success && $action != 'remove')
1193 $args['update'] = $this->_client_event($this->driver->get_event($event), true);
1194 $this->rc->output->command('plugin.refresh_calendar', $args);
1195 }
1196 }
1197
1198 /**
1199 * Helper method sending iTip notifications after successful event updates
1200 */
1201 private function event_save_success(&$event, $old, $action, $success)
1202 {
1203 // $success is a new event ID
1204 if ($success !== true) {
1205 // send update notification on the main event
1206 if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) {
1207 $master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']), 0, true);
1208 unset($master['_instance'], $master['recurrence_date']);
1209
1210 $sent = $this->notify_attendees($master, null, $action, $event['_comment'], false);
1211 if ($sent < 0)
1212 $this->rc->output->show_message('calendar.errornotifying', 'error');
1213
1214 $event['attendees'] = $master['attendees']; // this tricks us into the next if clause
1215 }
1216
1217 // delete old reference if saved as new
1218 if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') {
1219 $old = null;
1220 }
1221
1222 $event['id'] = $success;
1223 $event['_savemode'] = 'all';
1224 }
1225
1226 // send out notifications
1227 if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) {
1228 $_savemode = $event['_savemode'];
1229
1230 // send notification for the main event when savemode is 'all'
1231 if ($action != 'remove' && $_savemode == 'all' && ($event['recurrence_id'] || $old['recurrence_id'] || ($old && $old['id'] != $event['id']))) {
1232 $event['id'] = $event['recurrence_id'] ?: ($old['recurrence_id'] ?: $old['id']);
1233 $event = $this->driver->get_event($event, 0, true);
1234 unset($event['_instance'], $event['recurrence_date']);
1235 }
1236 else {
1237 // make sure we have the complete record
1238 $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true);
1239 }
1240
1241 $event['_savemode'] = $_savemode;
1242
1243 if ($old) {
1244 $old['thisandfuture'] = $_savemode == 'future';
1245 }
1246
1247 // only notify if data really changed (TODO: do diff check on client already)
1248 if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
1249 $sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
1250 if ($sent > 0)
1251 $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
1252 else if ($sent < 0)
1253 $this->rc->output->show_message('calendar.errornotifying', 'error');
1254 }
1255 }
1256 }
1257
1258 /**
1259 * Handler for load-requests from fullcalendar
1260 * This will return pure JSON formatted output
1261 */
1262 function load_events()
1263 {
1264 $events = $this->driver->load_events(
1265 rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
1266 rcube_utils::get_input_value('end', rcube_utils::INPUT_GET),
1267 ($query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET)),
1268 rcube_utils::get_input_value('source', rcube_utils::INPUT_GET)
1269 );
1270 echo $this->encode($events, !empty($query));
1271 exit;
1272 }
1273
1274 /**
1275 * Handler for requests fetching event counts for calendars
1276 */
1277 public function count_events()
1278 {
1279 // don't update session on these requests (avoiding race conditions)
1280 $this->rc->session->nowrite = true;
1281
1282 $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
1283 if (!$start) {
1284 $start = new DateTime('today 00:00:00', $this->timezone);
1285 $start = $start->format('U');
1286 }
1287
1288 $counts = $this->driver->count_events(
1289 rcube_utils::get_input_value('source', rcube_utils::INPUT_GET),
1290 $start,
1291 rcube_utils::get_input_value('end', rcube_utils::INPUT_GET)
1292 );
1293
1294 $this->rc->output->command('plugin.update_counts', array('counts' => $counts));
1295 }
1296
1297 /**
1298 * Load event data from an iTip message attachment
1299 */
1300 public function itip_events($msgref)
1301 {
1302 $path = explode('/', $msgref);
1303 $msg = array_pop($path);
1304 $mbox = join('/', $path);
1305 list($uid, $mime_id) = explode('#', $msg);
1306 $events = array();
1307
1308 if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
1309 $partstat = 'NEEDS-ACTION';
1310 /*
1311 $user_emails = $this->lib->get_user_emails();
1312 foreach ($event['attendees'] as $attendee) {
1313 if (in_array($attendee['email'], $user_emails)) {
1314 $partstat = $attendee['status'];
1315 break;
1316 }
1317 }
1318 */
1319 $event['id'] = $event['uid'];
1320 $event['temporary'] = true;
1321 $event['readonly'] = true;
1322 $event['calendar'] = '--invitation--itip';
1323 $event['className'] = 'fc-invitation-' . strtolower($partstat);
1324 $event['_mbox'] = $mbox;
1325 $event['_uid'] = $uid;
1326 $event['_part'] = $mime_id;
1327
1328 $events[] = $this->_client_event($event, true);
1329
1330 // add recurring instances
1331 if (!empty($event['recurrence'])) {
1332 foreach ($this->driver->get_recurring_events($event, $event['start']) as $recurring) {
1333 $recurring['temporary'] = true;
1334 $recurring['readonly'] = true;
1335 $recurring['calendar'] = '--invitation--itip';
1336 $events[] = $this->_client_event($recurring, true);
1337 }
1338 }
1339 }
1340
1341 return $events;
1342 }
1343
1344 /**
1345 * Handler for keep-alive requests
1346 * This will check for updated data in active calendars and sync them to the client
1347 */
1348 public function refresh($attr)
1349 {
1350 // refresh the entire calendar every 10th time to also sync deleted events
1351 if (rand(0,10) == 10) {
1352 $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true));
1353 return;
1354 }
1355
1356 $counts = array();
1357
1358 foreach ($this->driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) {
1359 $events = $this->driver->load_events(
1360 rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC),
1361 rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC),
1362 rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
1363 $cal['id'],
1364 1,
1365 $attr['last']
1366 );
1367
1368 foreach ($events as $event) {
1369 $this->rc->output->command('plugin.refresh_calendar',
1370 array('source' => $cal['id'], 'update' => $this->_client_event($event)));
1371 }
1372
1373 // refresh count for this calendar
1374 if ($cal['counts']) {
1375 $today = new DateTime('today 00:00:00', $this->timezone);
1376 $counts += $this->driver->count_events($cal['id'], $today->format('U'));
1377 }
1378 }
1379
1380 if (!empty($counts)) {
1381 $this->rc->output->command('plugin.update_counts', array('counts' => $counts));
1382 }
1383 }
1384
1385 /**
1386 * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
1387 * This will check for pending notifications and pass them to the client
1388 */
1389 public function pending_alarms($p)
1390 {
1391 $this->load_driver();
1392 $time = $p['time'] ?: time();
1393 if ($alarms = $this->driver->pending_alarms($time)) {
1394 foreach ($alarms as $alarm) {
1395 $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
1396 $p['alarms'][] = $alarm;
1397 }
1398 }
1399
1400 // get alarms for birthdays calendar
1401 if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') {
1402 $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db');
1403
1404 foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) {
1405 $alarm = libcalendaring::get_next_alarm($e);
1406
1407 // overwrite alarm time with snooze value (or null if dismissed)
1408 if ($dismissed = $cache->get($e['id']))
1409 $alarm['time'] = $dismissed['notifyat'];
1410
1411 // add to list if alarm is set
1412 if ($alarm && $alarm['time'] && $alarm['time'] <= $time) {
1413 $e['id'] = 'cal:bday:' . $e['id'];
1414 $e['notifyat'] = $alarm['time'];
1415 $p['alarms'][] = $e;
1416 }
1417 }
1418 }
1419
1420 return $p;
1421 }
1422
1423 /**
1424 * Handler for alarm dismiss hook triggered by libcalendaring
1425 */
1426 public function dismiss_alarms($p)
1427 {
1428 $this->load_driver();
1429 foreach ((array)$p['ids'] as $id) {
1430 if (strpos($id, 'cal:bday:') === 0) {
1431 $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']);
1432 }
1433 else if (strpos($id, 'cal:') === 0) {
1434 $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);
1435 }
1436 }
1437
1438 return $p;
1439 }
1440
1441 /**
1442 * Handler for check-recent requests which are accidentally sent to calendar
1443 */
1444 function check_recent()
1445 {
1446 // NOP
1447 $this->rc->output->send();
1448 }
1449
1450 /**
1451 * Hook triggered when a contact is saved
1452 */
1453 function contact_update($p)
1454 {
1455 // clear birthdays calendar cache
1456 if (!empty($p['record']['birthday'])) {
1457 $cache = $this->rc->get_cache('calendar.birthdays', 'db');
1458 $cache->remove();
1459 }
1460 }
1461
1462 /**
1463 *
1464 */
1465 function import_events()
1466 {
1467 // Upload progress update
1468 if (!empty($_GET['_progress'])) {
1469 $this->rc->upload_progress();
1470 }
1471
1472 @set_time_limit(0);
1473
1474 // process uploaded file if there is no error
1475 $err = $_FILES['_data']['error'];
1476
1477 if (!$err && $_FILES['_data']['tmp_name']) {
1478 $calendar = rcube_utils::get_input_value('calendar', rcube_utils::INPUT_GPC);
1479 $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
1480
1481 // extract zip file
1482 if ($_FILES['_data']['type'] == 'application/zip') {
1483 $count = 0;
1484 if (class_exists('ZipArchive', false)) {
1485 $zip = new ZipArchive();
1486 if ($zip->open($_FILES['_data']['tmp_name'])) {
1487 $randname = uniqid('zip-' . session_id(), true);
1488 $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;
1489 mkdir($tmpdir, 0700);
1490
1491 // extract each ical file from the archive and import it
1492 for ($i = 0; $i < $zip->numFiles; $i++) {
1493 $filename = $zip->getNameIndex($i);
1494 if (preg_match('/\.ics$/i', $filename)) {
1495 $tmpfile = $tmpdir . '/' . basename($filename);
1496 if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) {
1497 $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors);
1498 unlink($tmpfile);
1499 }
1500 }
1501 }
1502
1503 rmdir($tmpdir);
1504 $zip->close();
1505 }
1506 else {
1507 $errors = 1;
1508 $msg = 'Failed to open zip file.';
1509 }
1510 }
1511 else {
1512 $errors = 1;
1513 $msg = 'Zip files are not supported for import.';
1514 }
1515 }
1516 else {
1517 // attempt to import teh uploaded file directly
1518 $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors);
1519 }
1520
1521 if ($count) {
1522 $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
1523 $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
1524 }
1525 else if (!$errors) {
1526 $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
1527 $this->rc->output->command('plugin.import_success', array('source' => $calendar));
1528 }
1529 else {
1530 $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
1531 }
1532 }
1533 else {
1534 if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
1535 $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
1536 'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
1537 }
1538 else {
1539 $msg = $this->rc->gettext('fileuploaderror');
1540 }
1541
1542 $this->rc->output->command('plugin.import_error', array('message' => $msg));
1543 }
1544
1545 $this->rc->output->send('iframe');
1546 }
1547
1548 /**
1549 * Helper function to parse and import a single .ics file
1550 */
1551 private function import_from_file($filepath, $calendar, $rangestart, &$errors)
1552 {
1553 $user_email = $this->rc->user->get_username();
1554
1555 $ical = $this->get_ical();
1556 $errors = !$ical->fopen($filepath);
1557 $count = $i = 0;
1558 foreach ($ical as $event) {
1559 // keep the browser connection alive on long import jobs
1560 if (++$i > 100 && $i % 100 == 0) {
1561 echo "<!-- -->";
1562 ob_flush();
1563 }
1564
1565 // TODO: correctly handle recurring events which start before $rangestart
1566 if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
1567 continue;
1568
1569 $event['_owner'] = $user_email;
1570 $event['calendar'] = $calendar;
1571 if ($this->driver->new_event($event)) {
1572 $count++;
1573 }
1574 else {
1575 $errors++;
1576 }
1577 }
1578
1579 return $count;
1580 }
1581
1582
1583 /**
1584 * Construct the ics file for exporting events to iCalendar format;
1585 */
1586 function export_events($terminate = true)
1587 {
1588 $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
1589 $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);
1590
1591 if (!isset($start))
1592 $start = 'today -1 year';
1593 if (!is_numeric($start))
1594 $start = strtotime($start . ' 00:00:00');
1595 if (!$end)
1596 $end = 'today +10 years';
1597 if (!is_numeric($end))
1598 $end = strtotime($end . ' 23:59:59');
1599
1600 $event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
1601 $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET);
1602 $calid = $filename = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
1603
1604 $calendars = $this->driver->list_calendars();
1605 $events = array();
1606
1607 if ($calendars[$calid]) {
1608 $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
1609 $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
1610 if (!empty($event_id)) {
1611 if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id), 0, true)) {
1612 if ($event['recurrence_id']) {
1613 $event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']), 0, true);
1614 }
1615 $events = array($event);
1616 $filename = asciiwords($event['title']);
1617 if (empty($filename))
1618 $filename = 'event';
1619 }
1620 }
1621 else {
1622 $events = $this->driver->load_events($start, $end, null, $calid, 0);
1623 if (empty($filename))
1624 $filename = $calid;
1625 }
1626 }
1627
1628 header("Content-Type: text/calendar");
1629 header("Content-Disposition: inline; filename=".$filename.'.ics');
1630
1631 $this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
1632
1633 if ($terminate)
1634 exit;
1635 }
1636
1637
1638 /**
1639 * Handler for iCal feed requests
1640 */
1641 function ical_feed_export()
1642 {
1643 $session_exists = !empty($_SESSION['user_id']);
1644
1645 // process HTTP auth info
1646 if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
1647 $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
1648 $auth = $this->rc->plugins->exec_hook('authenticate', array(
1649 'host' => $this->rc->autoselect_host(),
1650 'user' => trim($_SERVER['PHP_AUTH_USER']),
1651 'pass' => $_SERVER['PHP_AUTH_PW'],
1652 'cookiecheck' => true,
1653 'valid' => true,
1654 ));
1655 if ($auth['valid'] && !$auth['abort'])
1656 $this->rc->login($auth['user'], $auth['pass'], $auth['host']);
1657 }
1658
1659 // require HTTP auth
1660 if (empty($_SESSION['user_id'])) {
1661 header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
1662 header('HTTP/1.0 401 Unauthorized');
1663 exit;
1664 }
1665
1666 // decode calendar feed hash
1667 $format = 'ics';
1668 $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET);
1669 if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
1670 $format = strtolower($m[1]);
1671 $calhash = preg_replace($suff_regex, '', $calhash);
1672 }
1673
1674 if (!strpos($calhash, ':'))
1675 $calhash = base64_decode($calhash);
1676
1677 list($user, $_GET['source']) = explode(':', $calhash, 2);
1678
1679 // sanity check user
1680 if ($this->rc->user->get_username() == $user) {
1681 $this->setup();
1682 $this->load_driver();
1683 $this->export_events(false);
1684 }
1685 else {
1686 header('HTTP/1.0 404 Not Found');
1687 }
1688
1689 // don't save session data
1690 if (!$session_exists)
1691 session_destroy();
1692 exit;
1693 }
1694
1695 /**
1696 *
1697 */
1698 function load_settings()
1699 {
1700 $this->lib->load_settings();
1701 $this->defaults += $this->lib->defaults;
1702
1703 $settings = array();
1704
1705 // configuration
1706 $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
1707 $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
1708 $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
1709
1710 $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
1711 $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
1712 $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
1713 $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
1714 $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
1715 $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
1716 $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
1717 $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
1718 $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
1719 $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
1720 $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false);
1721 $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
1722
1723 // get user identity to create default attendee
1724 if ($this->ui->screen == 'calendar') {
1725 foreach ($this->rc->user->list_emails() as $rec) {
1726 if (!$identity)
1727 $identity = $rec;
1728 $identity['emails'][] = $rec['email'];
1729 $settings['identities'][$rec['identity_id']] = $rec['email'];
1730 }
1731 $identity['emails'][] = $this->rc->user->get_username();
1732 $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])));
1733 }
1734
1735 return $settings;
1736 }
1737
1738 /**
1739 * Encode events as JSON
1740 *
1741 * @param array Events as array
1742 * @param boolean Add CSS class names according to calendar and categories
1743 * @return string JSON encoded events
1744 */
1745 function encode($events, $addcss = false)
1746 {
1747 $json = array();
1748 foreach ($events as $event) {
1749 $json[] = $this->_client_event($event, $addcss);
1750 }
1751 return rcube_output::json_serialize($json);
1752 }
1753
1754 /**
1755 * Convert an event object to be used on the client
1756 */
1757 private function _client_event($event, $addcss = false)
1758 {
1759 // compose a human readable strings for alarms_text and recurrence_text
1760 if ($event['valarms']) {
1761 $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']);
1762 $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']);
1763 }
1764 if ($event['recurrence']) {
1765 $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
1766 $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
1767 unset($event['recurrence_date']);
1768 }
1769
1770 foreach ((array)$event['attachments'] as $k => $attachment) {
1771 $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
1772
1773 unset($event['attachments'][$k]['data'], $event['attachments'][$k]['content']);
1774
1775 if (!$attachment['id']) {
1776 $event['attachments'][$k]['id'] = $k;
1777 }
1778 }
1779
1780 // convert link URIs references into structs
1781 if (array_key_exists('links', $event)) {
1782 foreach ((array) $event['links'] as $i => $link) {
1783 if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) {
1784 $event['links'][$i] = $msgref;
1785 }
1786 }
1787 }
1788
1789 // check for organizer in attendees list
1790 $organizer = null;
1791 foreach ((array)$event['attendees'] as $i => $attendee) {
1792 if ($attendee['role'] == 'ORGANIZER') {
1793 $organizer = $attendee;
1794 }
1795 if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
1796 $event['attendees'][$i]['noreply'] = true;
1797 }
1798 else {
1799 unset($event['attendees'][$i]['noreply']);
1800 }
1801 }
1802
1803 if ($organizer === null && !empty($event['organizer'])) {
1804 $organizer = $event['organizer'];
1805 $organizer['role'] = 'ORGANIZER';
1806 if (!is_array($event['attendees']))
1807 $event['attendees'] = array();
1808 array_unshift($event['attendees'], $organizer);
1809 }
1810
1811 // Convert HTML description into plain text
1812 if ($this->is_html($event)) {
1813 $h2t = new rcube_html2text($event['description'], false, true, 0);
1814 $event['description'] = trim($h2t->get_text());
1815 }
1816
1817 // mapping url => vurl because of the fullcalendar client script
1818 $event['vurl'] = $event['url'];
1819 unset($event['url']);
1820
1821 return array(
1822 '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
1823 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'),
1824 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
1825 // 'changed' might be empty for event recurrences (Bug #2185)
1826 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
1827 'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null,
1828 'title' => strval($event['title']),
1829 'description' => strval($event['description']),
1830 'location' => strval($event['location']),
1831 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') .
1832 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true) .
1833 rtrim(' ' . $event['className']),
1834 'allDay' => ($event['allday'] == 1),
1835 ) + $event;
1836 }
1837
1838
1839 /**
1840 * Generate a unique identifier for an event
1841 */
1842 public function generate_uid()
1843 {
1844 return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
1845 }
1846
1847
1848 /**
1849 * TEMPORARY: generate random event data for testing
1850 * Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500&_date=2014-08-01&_dev=120
1851 */
1852 public function generate_randomdata()
1853 {
1854 @set_time_limit(0);
1855
1856 $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
1857 $date = $_REQUEST['_date'] ?: 'now';
1858 $dev = $_REQUEST['_dev'] ?: 30;
1859 $cats = array_keys($this->driver->list_categories());
1860 $cals = $this->driver->list_calendars(calendar_driver::FILTER_ACTIVE);
1861 $count = 0;
1862
1863 while ($count++ < $num) {
1864 $spread = intval($dev) * 86400; // days
1865 $refdate = strtotime($date);
1866 $start = round(($refdate + rand(-$spread, $spread)) / 600) * 600;
1867 $duration = round(rand(30, 360) / 30) * 30 * 60;
1868 $allday = rand(0,20) > 18;
1869 $alarm = rand(-30,12) * 5;
1870 $fb = rand(0,2);
1871
1872 if (date('G', $start) > 23)
1873 $start -= 3600;
1874
1875 if ($allday) {
1876 $start = strtotime(date('Y-m-d 00:00:00', $start));
1877 $duration = 86399;
1878 }
1879
1880 $title = '';
1881 $len = rand(2, 12);
1882 $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.");
1883 // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
1884 for ($i = 0; $i < $len; $i++)
1885 $title .= $words[rand(0,count($words)-1)] . " ";
1886
1887 $this->driver->new_event(array(
1888 'uid' => $this->generate_uid(),
1889 'start' => new DateTime('@'.$start),
1890 'end' => new DateTime('@'.($start + $duration)),
1891 'allday' => $allday,
1892 'title' => rtrim($title),
1893 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
1894 'categories' => $cats[array_rand($cats)],
1895 'calendar' => array_rand($cals),
1896 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
1897 'priority' => rand(0,9),
1898 ));
1899 }
1900
1901 $this->rc->output->redirect('');
1902 }
1903
1904 /**
1905 * Handler for attachments upload
1906 */
1907 public function attachment_upload()
1908 {
1909 $this->lib->attachment_upload(self::SESSION_KEY, 'cal-');
1910 }
1911
1912 /**
1913 * Handler for attachments download/displaying
1914 */
1915 public function attachment_get()
1916 {
1917 // show loading page
1918 if (!empty($_GET['_preload'])) {
1919 return $this->lib->attachment_loading_page();
1920 }
1921
1922 $event_id = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC);
1923 $calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC);
1924 $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
1925 $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
1926
1927 $event = array('id' => $event_id, 'calendar' => $calendar, 'rev' => $rev);
1928
1929 if ($calendar == '--invitation--itip') {
1930 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GPC);
1931 $part = rcube_utils::get_input_value('_part', rcube_utils::INPUT_GPC);
1932 $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC);
1933
1934 $event = $this->lib->mail_get_itip_object($mbox, $uid, $part, 'event');
1935 $attachment = $event['attachments'][$id];
1936 $attachment['body'] = &$attachment['data'];
1937 }
1938 else {
1939 $attachment = $this->driver->get_attachment($id, $event);
1940 }
1941
1942 // show part page
1943 if (!empty($_GET['_frame'])) {
1944 $this->lib->attachment = $attachment;
1945 $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
1946 $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
1947 $this->rc->output->send('calendar.attachment');
1948 }
1949 // deliver attachment content
1950 else if ($attachment) {
1951 if ($calendar != '--invitation--itip') {
1952 $attachment['body'] = $this->driver->get_attachment_body($id, $event);
1953 }
1954
1955 $this->lib->attachment_get($attachment);
1956 }
1957
1958 // if we arrive here, the requested part was not found
1959 header('HTTP/1.1 404 Not Found');
1960 exit;
1961 }
1962
1963 /**
1964 * Determine whether the given event description is HTML formatted
1965 */
1966 private function is_html($event)
1967 {
1968 // check for opening and closing <html> or <body> tags
1969 return (preg_match('/<(html|body)(\s+[a-z]|>)/', $event['description'], $m) && strpos($event['description'], '</'.$m[1].'>') > 0);
1970 }
1971
1972 /**
1973 * Prepares new/edited event properties before save
1974 */
1975 private function write_preprocess(&$event, $action)
1976 {
1977 // convert dates into DateTime objects in user's current timezone
1978 $event['start'] = new DateTime($event['start'], $this->timezone);
1979 $event['end'] = new DateTime($event['end'], $this->timezone);
1980 $event['allday'] = (bool)$event['allday'];
1981
1982 // start/end is all we need for 'move' action (#1480)
1983 if ($action == 'move') {
1984 return;
1985 }
1986
1987 // convert the submitted recurrence settings
1988 if (is_array($event['recurrence'])) {
1989 $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);
1990 }
1991
1992 // convert the submitted alarm values
1993 if ($event['valarms']) {
1994 $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']);
1995 }
1996
1997 $attachments = array();
1998 $eventid = 'cal-'.$event['id'];
1999
2000 if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
2001 if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
2002 foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
2003 if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
2004 $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
2005 }
2006 }
2007 }
2008 }
2009
2010 $event['attachments'] = $attachments;
2011
2012 // convert link references into simple URIs
2013 if (array_key_exists('links', $event)) {
2014 $event['links'] = array_map(function($link) {
2015 return is_array($link) ? $link['uri'] : strval($link);
2016 }, (array)$event['links']);
2017 }
2018
2019 // check for organizer in attendees
2020 if ($action == 'new' || $action == 'edit') {
2021 if (!$event['attendees'])
2022 $event['attendees'] = array();
2023
2024 $emails = $this->get_user_emails();
2025 $organizer = $owner = false;
2026 foreach ((array)$event['attendees'] as $i => $attendee) {
2027 if ($attendee['role'] == 'ORGANIZER')
2028 $organizer = $i;
2029 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails))
2030 $owner = $i;
2031 if (!isset($attendee['rsvp']))
2032 $event['attendees'][$i]['rsvp'] = true;
2033 else if (is_string($attendee['rsvp']))
2034 $event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
2035 }
2036
2037 if (!empty($event['_identity'])) {
2038 $identity = $this->rc->user->get_identity($event['_identity']);
2039 }
2040
2041 // set new organizer identity
2042 if ($organizer !== false && $identity) {
2043 $event['attendees'][$organizer]['name'] = $identity['name'];
2044 $event['attendees'][$organizer]['email'] = $identity['email'];
2045 }
2046 // set owner as organizer if yet missing
2047 else if ($organizer === false && $owner !== false) {
2048 $event['attendees'][$owner]['role'] = 'ORGANIZER';
2049 unset($event['attendees'][$owner]['rsvp']);
2050 }
2051 // fallback to the selected identity
2052 else if ($organizer === false && $identity) {
2053 $event['attendees'][] = array(
2054 'role' => 'ORGANIZER',
2055 'name' => $identity['name'],
2056 'email' => $identity['email'],
2057 );
2058 }
2059 }
2060
2061 // mapping url => vurl because of the fullcalendar client script
2062 if (array_key_exists('vurl', $event)) {
2063 $event['url'] = $event['vurl'];
2064 unset($event['vurl']);
2065 }
2066 }
2067
2068 /**
2069 * Releases some resources after successful event save
2070 */
2071 private function cleanup_event(&$event)
2072 {
2073 // remove temp. attachment files
2074 if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
2075 $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
2076 $this->rc->session->remove(self::SESSION_KEY);
2077 }
2078 }
2079
2080 /**
2081 * Send out an invitation/notification to all event attendees
2082 */
2083 private function notify_attendees($event, $old, $action = 'edit', $comment = null, $rsvp = null)
2084 {
2085 if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) {
2086 $event['cancelled'] = true;
2087 $is_cancelled = true;
2088 }
2089
2090 if ($rsvp === null)
2091 $rsvp = !$old || $event['sequence'] > $old['sequence'];
2092
2093 $itip = $this->load_itip();
2094 $emails = $this->get_user_emails();
2095 $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
2096
2097 // add comment to the iTip attachment
2098 $event['comment'] = $comment;
2099
2100 // set a valid recurrence-id if this is a recurrence instance
2101 libcalendaring::identify_recurrence_instance($event);
2102
2103 // compose multipart message using PEAR:Mail_Mime
2104 $method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
2105 $message = $itip->compose_itip_message($event, $method, $rsvp);
2106
2107 // list existing attendees from $old event
2108 $old_attendees = array();
2109 foreach ((array)$old['attendees'] as $attendee) {
2110 $old_attendees[] = $attendee['email'];
2111 }
2112
2113 // send to every attendee
2114 $sent = 0; $current = array();
2115 foreach ((array)$event['attendees'] as $attendee) {
2116 $current[] = strtolower($attendee['email']);
2117
2118 // skip myself for obvious reasons
2119 if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails))
2120 continue;
2121
2122 // skip if notification is disabled for this attendee
2123 if ($attendee['noreply'] && $itip_notify & 2)
2124 continue;
2125
2126 // skip if this attendee has delegated and set RSVP=FALSE
2127 if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false)
2128 continue;
2129
2130 // which template to use for mail text
2131 $is_new = !in_array($attendee['email'], $old_attendees);
2132 $is_rsvp = $is_new || $event['sequence'] > $old['sequence'];
2133 $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
2134 $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
2135
2136 $event['comment'] = $comment;
2137
2138 // finally send the message
2139 if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
2140 $sent++;
2141 else
2142 $sent = -100;
2143 }
2144
2145 // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
2146
2147 // send CANCEL message to removed attendees
2148 foreach ((array)$old['attendees'] as $attendee) {
2149 if ($attendee['role'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
2150 continue;
2151
2152 $vevent = $old;
2153 $vevent['cancelled'] = $is_cancelled;
2154 $vevent['attendees'] = array($attendee);
2155 $vevent['comment'] = $comment;
2156 if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
2157 $sent++;
2158 else
2159 $sent = -100;
2160 }
2161
2162 return $sent;
2163 }
2164
2165 /**
2166 * Echo simple free/busy status text for the given user and time range
2167 */
2168 public function freebusy_status()
2169 {
2170 $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
2171 $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
2172 $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
2173
2174 // convert dates into unix timestamps
2175 if (!empty($start) && !is_numeric($start)) {
2176 $dts = new DateTime($start, $this->timezone);
2177 $start = $dts->format('U');
2178 }
2179 if (!empty($end) && !is_numeric($end)) {
2180 $dte = new DateTime($end, $this->timezone);
2181 $end = $dte->format('U');
2182 }
2183
2184 if (!$start) $start = time();
2185 if (!$end) $end = $start + 3600;
2186
2187 $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
2188 $status = 'UNKNOWN';
2189
2190 // if the backend has free-busy information
2191 $fblist = $this->driver->get_freebusy_list($email, $start, $end);
2192
2193 if (is_array($fblist)) {
2194 $status = 'FREE';
2195
2196 foreach ($fblist as $slot) {
2197 list($from, $to, $type) = $slot;
2198 if ($from < $end && $to > $start) {
2199 $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
2200 break;
2201 }
2202 }
2203 }
2204
2205 // let this information be cached for 5min
2206 $this->rc->output->future_expire_header(300);
2207
2208 echo $status;
2209 exit;
2210 }
2211
2212 /**
2213 * Return a list of free/busy time slots within the given period
2214 * Echo data in JSON encoding
2215 */
2216 public function freebusy_times()
2217 {
2218 $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
2219 $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
2220 $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
2221 $interval = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC));
2222 $strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
2223
2224 // convert dates into unix timestamps
2225 if (!empty($start) && !is_numeric($start)) {
2226 $dts = rcube_utils::anytodatetime($start, $this->timezone);
2227 $start = $dts ? $dts->format('U') : null;
2228 }
2229 if (!empty($end) && !is_numeric($end)) {
2230 $dte = rcube_utils::anytodatetime($end, $this->timezone);
2231 $end = $dte ? $dte->format('U') : null;
2232 }
2233
2234 if (!$start) $start = time();
2235 if (!$end) $end = $start + 86400 * 30;
2236 if (!$interval) $interval = 60; // 1 hour
2237
2238 if (!$dte) {
2239 $dts = new DateTime('@'.$start);
2240 $dts->setTimezone($this->timezone);
2241 }
2242
2243 $fblist = $this->driver->get_freebusy_list($email, $start, $end);
2244 $slots = '';
2245
2246 // prepare freebusy list before use (for better performance)
2247 if (is_array($fblist)) {
2248 foreach ($fblist as $idx => $slot) {
2249 list($from, $to, ) = $slot;
2250
2251 // check for possible all-day times
2252 if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') {
2253 // shift into the user's timezone for sane matching
2254 $fblist[$idx][0] -= $this->gmt_offset;
2255 $fblist[$idx][1] -= $this->gmt_offset;
2256 }
2257 }
2258 }
2259
2260 // build a list from $start till $end with blocks representing the fb-status
2261 for ($s = 0, $t = $start; $t <= $end; $s++) {
2262 $t_end = $t + $interval * 60;
2263 $dt = new DateTime('@'.$t);
2264 $dt->setTimezone($this->timezone);
2265
2266 // determine attendee's status
2267 if (is_array($fblist)) {
2268 $status = self::FREEBUSY_FREE;
2269
2270 foreach ($fblist as $slot) {
2271 list($from, $to, $type) = $slot;
2272
2273 if ($from < $t_end && $to > $t) {
2274 $status = isset($type) ? $type : self::FREEBUSY_BUSY;
2275 if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
2276 break;
2277 }
2278 }
2279 }
2280 else {
2281 $status = self::FREEBUSY_UNKNOWN;
2282 }
2283
2284 // use most compact format, assume $status is one digit/character
2285 $slots .= $status;
2286 $t = $t_end;
2287 }
2288
2289 $dte = new DateTime('@'.$t_end);
2290 $dte->setTimezone($this->timezone);
2291
2292 // let this information be cached for 5min
2293 $this->rc->output->future_expire_header(300);
2294
2295 echo rcube_output::json_serialize(array(
2296 'email' => $email,
2297 'start' => $dts->format('c'),
2298 'end' => $dte->format('c'),
2299 'interval' => $interval,
2300 'slots' => $slots,
2301 ));
2302 exit;
2303 }
2304
2305 /**
2306 * Handler for printing calendars
2307 */
2308 public function print_view()
2309 {
2310 $title = $this->gettext('print');
2311
2312 $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
2313 if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
2314 $view = 'agendaDay';
2315
2316 $this->rc->output->set_env('view',$view);
2317
2318 if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC))
2319 $this->rc->output->set_env('date', $date);
2320
2321 if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC))
2322 $this->rc->output->set_env('listRange', intval($range));
2323
2324 if (isset($_REQUEST['sections']))
2325 $this->rc->output->set_env('listSections', rcube_utils::get_input_value('sections', rcube_utils::INPUT_GPC));
2326
2327 if ($search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC)) {
2328 $this->rc->output->set_env('search', $search);
2329 $title .= ' "' . $search . '"';
2330 }
2331
2332 // Add CSS stylesheets to the page header
2333 $skin_path = $this->local_skin_path();
2334 $this->include_stylesheet($skin_path . '/fullcalendar.css');
2335 $this->include_stylesheet($skin_path . '/print.css');
2336
2337 // Add JS files to the page header
2338 $this->include_script('print.js');
2339 $this->include_script('lib/js/fullcalendar.js');
2340
2341 $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
2342 $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
2343
2344 $this->rc->output->set_pagetitle($title);
2345 $this->rc->output->send("calendar.print");
2346 }
2347
2348 /**
2349 *
2350 */
2351 public function get_inline_ui()
2352 {
2353 foreach (array('save','cancel','savingdata') as $label)
2354 $texts['calendar.'.$label] = $this->gettext($label);
2355
2356 $texts['calendar.new_event'] = $this->gettext('createfrommail');
2357
2358 $this->ui->init_templates();
2359 $this->ui->calendar_list(); # set env['calendars']
2360 echo $this->api->output->parse('calendar.eventedit', false, false);
2361 echo html::tag('script', array('type' => 'text/javascript'),
2362 "rcmail.set_env('calendars', " . rcube_output::json_serialize($this->api->output->env['calendars']) . ");\n".
2363 "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
2364 "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
2365 "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
2366 "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n".
2367 "rcmail.add_label(" . rcube_output::json_serialize($texts) . ");\n"
2368 );
2369 exit;
2370 }
2371
2372 /**
2373 * Compare two event objects and return differing properties
2374 *
2375 * @param array Event A
2376 * @param array Event B
2377 * @return array List of differing event properties
2378 */
2379 public static function event_diff($a, $b)
2380 {
2381 $diff = array();
2382 $ignore = array('changed' => 1, 'attachments' => 1);
2383 foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
2384 if (!$ignore[$key] && $key[0] != '_' && $a[$key] != $b[$key])
2385 $diff[] = $key;
2386 }
2387
2388 // only compare number of attachments
2389 if (count($a['attachments']) != count($b['attachments']))
2390 $diff[] = 'attachments';
2391
2392 return $diff;
2393 }
2394
2395 /**
2396 * Update attendee properties on the given event object
2397 *
2398 * @param array The event object to be altered
2399 * @param array List of hash arrays each represeting an updated/added attendee
2400 */
2401 public static function merge_attendee_data(&$event, $attendees, $removed = null)
2402 {
2403 if (!empty($attendees) && !is_array($attendees[0])) {
2404 $attendees = array($attendees);
2405 }
2406
2407 foreach ($attendees as $attendee) {
2408 $found = false;
2409
2410 foreach ($event['attendees'] as $i => $candidate) {
2411 if ($candidate['email'] == $attendee['email']) {
2412 $event['attendees'][$i] = $attendee;
2413 $found = true;
2414 break;
2415 }
2416 }
2417
2418 if (!$found) {
2419 $event['attendees'][] = $attendee;
2420 }
2421 }
2422
2423 // filter out removed attendees
2424 if (!empty($removed)) {
2425 $event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) {
2426 return !in_array($attendee['email'], $removed);
2427 });
2428 }
2429 }
2430
2431
2432 /**** Resource management functions ****/
2433
2434 /**
2435 * Getter for the configured implementation of the resource directory interface
2436 */
2437 private function resources_directory()
2438 {
2439 if (is_object($this->resources_dir)) {
2440 return $this->resources_dir;
2441 }
2442
2443 if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
2444 $driver_class = 'resources_driver_' . $driver_name;
2445
2446 require_once($this->home . '/drivers/resources_driver.php');
2447 require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
2448
2449 $this->resources_dir = new $driver_class($this);
2450 }
2451
2452 return $this->resources_dir;
2453 }
2454
2455 /**
2456 * Handler for resoruce autocompletion requests
2457 */
2458 public function resources_autocomplete()
2459 {
2460 $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
2461 $sid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
2462 $maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
2463 $results = array();
2464
2465 if ($directory = $this->resources_directory()) {
2466 foreach ($directory->load_resources($search, $maxnum) as $rec) {
2467 $results[] = array(
2468 'name' => $rec['name'],
2469 'email' => $rec['email'],
2470 'type' => $rec['_type'],
2471 );
2472 }
2473 }
2474
2475 $this->rc->output->command('ksearch_query_results', $results, $search, $sid);
2476 $this->rc->output->send();
2477 }
2478
2479 /**
2480 * Handler for load-requests for resource data
2481 */
2482 function resources_list()
2483 {
2484 $data = array();
2485
2486 if ($directory = $this->resources_directory()) {
2487 foreach ($directory->load_resources() as $rec) {
2488 $data[] = $rec;
2489 }
2490 }
2491
2492 $this->rc->output->command('plugin.resource_data', $data);
2493 $this->rc->output->send();
2494 }
2495
2496 /**
2497 * Handler for requests loading resource owner information
2498 */
2499 function resources_owner()
2500 {
2501 if ($directory = $this->resources_directory()) {
2502 $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
2503 $data = $directory->get_resource_owner($id);
2504 }
2505
2506 $this->rc->output->command('plugin.resource_owner', $data);
2507 $this->rc->output->send();
2508 }
2509
2510 /**
2511 * Deliver event data for a resource's calendar
2512 */
2513 function resources_calendar()
2514 {
2515 $events = array();
2516
2517 if ($directory = $this->resources_directory()) {
2518 $events = $directory->get_resource_calendar(
2519 rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
2520 rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
2521 rcube_utils::get_input_value('end', rcube_utils::INPUT_GET));
2522 }
2523
2524 echo $this->encode($events);
2525 exit;
2526 }
2527
2528
2529 /**** Event invitation plugin hooks ****/
2530
2531 /**
2532 * Find an event in user calendars
2533 */
2534 protected function find_event($event, &$mode)
2535 {
2536 $this->load_driver();
2537
2538 // We search for writeable calendars in personal namespace by default
2539 $mode = calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL;
2540 $result = $this->driver->get_event($event, $mode);
2541 // ... now check shared folders if not found
2542 if (!$result) {
2543 $result = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_SHARED);
2544 if ($result) {
2545 $mode |= calendar_driver::FILTER_SHARED;
2546 }
2547 }
2548
2549 return $result;
2550 }
2551
2552 /**
2553 * Handler for calendar/itip-status requests
2554 */
2555 function event_itip_status()
2556 {
2557 $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
2558
2559 $this->load_driver();
2560
2561 // find local copy of the referenced event (in personal namespace)
2562 $existing = $this->find_event($data, $mode);
2563 $is_shared = $mode & calendar_driver::FILTER_SHARED;
2564 $itip = $this->load_itip();
2565 $response = $itip->get_itip_status($data, $existing);
2566
2567 // get a list of writeable calendars to save new events to
2568 if ((!$existing || $is_shared)
2569 && !$data['nosave']
2570 && ($response['action'] == 'rsvp' || $response['action'] == 'import')
2571 ) {
2572 $calendars = $this->driver->list_calendars($mode);
2573 $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true));
2574 $calendar_select->add('--', '');
2575 $numcals = 0;
2576 foreach ($calendars as $calendar) {
2577 if ($calendar['editable']) {
2578 $calendar_select->add($calendar['name'], $calendar['id']);
2579 $numcals++;
2580 }
2581 }
2582 if ($numcals < 1)
2583 $calendar_select = null;
2584 }
2585
2586 if ($calendar_select) {
2587 $default_calendar = $this->get_default_calendar($data['sensitivity'], $calendars);
2588 $response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . '&nbsp;' .
2589 $calendar_select->show($is_shared ? $existing['calendar'] : $default_calendar['id']));
2590 }
2591 else if ($data['nosave']) {
2592 $response['select'] = html::tag('input', array('type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => ''));
2593 }
2594
2595 // render small agenda view for the respective day
2596 if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') {
2597 $event_start = rcube_utils::anytodatetime($data['date']);
2598 $day_start = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone);
2599 $day_end = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone);
2600
2601 // get events on that day from the user's personal calendars
2602 $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
2603 $events = $this->driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars));
2604 usort($events, function($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; });
2605
2606 $before = $after = array();
2607 foreach ($events as $event) {
2608 // TODO: skip events with free_busy == 'free' ?
2609 if ($event['uid'] == $data['uid'] || $event['end'] < $day_start || $event['start'] > $day_end)
2610 continue;
2611 else if ($event['start'] < $event_start)
2612 $before[] = $this->mail_agenda_event_row($event);
2613 else
2614 $after[] = $this->mail_agenda_event_row($event);
2615 }
2616
2617 $response['append'] = array(
2618 'selector' => '.calendar-agenda-preview',
2619 'replacements' => array(
2620 '%before%' => !empty($before) ? join("\n", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')),
2621 '%after%' => !empty($after) ? join("\n", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')),
2622 ),
2623 );
2624 }
2625
2626 $this->rc->output->command('plugin.update_itip_object_status', $response);
2627 }
2628
2629 /**
2630 * Handler for calendar/itip-remove requests
2631 */
2632 function event_itip_remove()
2633 {
2634 $success = false;
2635 $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
2636 $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
2637 $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
2638 $listmode = calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL;
2639
2640 // search for event if only UID is given
2641 if ($event = $this->driver->get_event(array('uid' => $uid, '_instance' => $instance), $listmode)) {
2642 $event['_savemode'] = $savemode;
2643 $success = $this->driver->remove_event($event, true);
2644 }
2645
2646 if ($success) {
2647 $this->rc->output->show_message('calendar.successremoval', 'confirmation');
2648 }
2649 else {
2650 $this->rc->output->show_message('calendar.errorsaving', 'error');
2651 }
2652 }
2653
2654 /**
2655 * Handler for URLs that allow an invitee to respond on his invitation mail
2656 */
2657 public function itip_attend_response($p)
2658 {
2659 $this->setup();
2660
2661 if ($p['action'] == 'attend') {
2662 $this->ui->init();
2663
2664 $this->rc->output->set_env('task', 'calendar'); // override some env vars
2665 $this->rc->output->set_env('refresh_interval', 0);
2666 $this->rc->output->set_pagetitle($this->gettext('calendar'));
2667
2668 $itip = $this->load_itip();
2669 $token = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
2670
2671 // read event info stored under the given token
2672 if ($invitation = $itip->get_invitation($token)) {
2673 $this->token = $token;
2674 $this->event = $invitation['event'];
2675
2676 // show message about cancellation
2677 if ($invitation['cancelled']) {
2678 $this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled'));
2679 }
2680 // save submitted RSVP status
2681 else if (!empty($_POST['rsvp'])) {
2682 $status = null;
2683 foreach (array('accepted','tentative','declined') as $method) {
2684 if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) {
2685 $status = $method;
2686 break;
2687 }
2688 }
2689
2690 // send itip reply to organizer
2691 $invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
2692 if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
2693 $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave'.strtolower($status)));
2694 }
2695 else
2696 $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
2697
2698 // if user is logged in...
2699 // FIXME: we should really consider removing this functionality
2700 // it's confusing that it creates/updates an event only for logged-in user
2701 // what if the logged-in user is not the same as the attendee?
2702 if ($this->rc->user->ID) {
2703 $this->load_driver();
2704
2705 $invitation = $itip->get_invitation($token);
2706 $existing = $this->driver->get_event($this->event);
2707
2708 // save the event to his/her default calendar if not yet present
2709 if (!$existing && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) {
2710 $invitation['event']['calendar'] = $calendar['id'];
2711 if ($this->driver->new_event($invitation['event']))
2712 $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
2713 else
2714 $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
2715 }
2716 else if ($existing
2717 && ($this->event['sequence'] >= $existing['sequence'] || $this->event['changed'] >= $existing['changed'])
2718 && ($calendar = $this->driver->get_calendar($existing['calendar']))
2719 ) {
2720 $this->event = $invitation['event'];
2721 $this->event['id'] = $existing['id'];
2722
2723 unset($this->event['comment']);
2724
2725 // merge attendees status
2726 // e.g. preserve my participant status for regular updates
2727 $this->lib->merge_attendees($this->event, $existing, $status);
2728
2729 // update attachments list
2730 $event['deleted_attachments'] = true;
2731
2732 // show me as free when declined (#1670)
2733 if ($status == 'declined')
2734 $this->event['free_busy'] = 'free';
2735
2736 if ($this->driver->edit_event($this->event))
2737 $this->rc->output->command('display_message', $this->gettext(array('name' => 'updatedsuccessfully', 'vars' => array('calendar' => $calendar->get_name()))), 'confirmation');
2738 else
2739 $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
2740 }
2741 }
2742 }
2743
2744 $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
2745 $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
2746
2747 if (!$this->invitestatus) {
2748 $this->itip->set_rsvp_actions(array('accepted','tentative','declined'));
2749 $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
2750 }
2751
2752 $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']);
2753 }
2754 else
2755 $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
2756
2757 $this->rc->output->send('calendar.itipattend');
2758 }
2759 }
2760
2761 /**
2762 *
2763 */
2764 public function itip_event_inviteform($attrib)
2765 {
2766 $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
2767 return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
2768 }
2769
2770 /**
2771 *
2772 */
2773 private function mail_agenda_event_row($event, $class = '')
2774 {
2775 $time = $event['allday'] ? $this->gettext('all-day') :
2776 $this->rc->format_date($event['start'], $this->rc->config->get('time_format')) . ' - ' .
2777 $this->rc->format_date($event['end'], $this->rc->config->get('time_format'));
2778
2779 return html::div(rtrim('event-row ' . $class),
2780 html::span('event-date', $time) .
2781 html::span('event-title', rcube::Q($event['title']))
2782 );
2783 }
2784
2785 /**
2786 *
2787 */
2788 public function mail_messages_list($p)
2789 {
2790 if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) {
2791 foreach ($p['messages'] as $header) {
2792 $part = new StdClass;
2793 $part->mimetype = $header->ctype;
2794 if (libcalendaring::part_is_vcalendar($part)) {
2795 $header->list_flags['attachmentClass'] = 'ical';
2796 }
2797 else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) {
2798 // TODO: fetch bodystructure and search for ical parts. Maybe too expensive?
2799
2800 if (!empty($header->structure) && is_array($header->structure->parts)) {
2801 foreach ($header->structure->parts as $part) {
2802 if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) {
2803 $header->list_flags['attachmentClass'] = 'ical';
2804 break;
2805 }
2806 }
2807 }
2808 }
2809 }
2810 }
2811 }
2812
2813 /**
2814 * Add UI element to copy event invitations or updates to the calendar
2815 */
2816 public function mail_messagebody_html($p)
2817 {
2818 // load iCalendar functions (if necessary)
2819 if (!empty($this->lib->ical_parts)) {
2820 $this->get_ical();
2821 $this->load_itip();
2822 }
2823
2824 $html = '';
2825 $has_events = false;
2826 $ical_objects = $this->lib->get_mail_ical_objects();
2827
2828 // show a box for every event in the file
2829 foreach ($ical_objects as $idx => $event) {
2830 if ($event['_type'] != 'event') // skip non-event objects (#2928)
2831 continue;
2832
2833 $has_events = true;
2834
2835 // get prepared inline UI for this event object
2836 if ($ical_objects->method) {
2837 $append = '';
2838
2839 // prepare a small agenda preview to be filled with actual event data on async request
2840 if ($ical_objects->method == 'REQUEST') {
2841 $append = html::div('calendar-agenda-preview',
2842 html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' .
2843 html::span('date', $this->rc->format_date($event['start'], $this->rc->config->get('date_format')))
2844 ) . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%');
2845 }
2846
2847 $html .= html::div('calendar-invitebox',
2848 $this->itip->mail_itip_inline_ui(
2849 $event,
2850 $ical_objects->method,
2851 $ical_objects->mime_id . ':' . $idx,
2852 'calendar',
2853 rcube_utils::anytodatetime($ical_objects->message_date),
2854 $this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $event['start']->format('U')
2855 ) . $append
2856 );
2857 }
2858
2859 // limit listing
2860 if ($idx >= 3)
2861 break;
2862 }
2863
2864 // prepend event boxes to message body
2865 if ($html) {
2866 $this->ui->init();
2867 $p['content'] = $html . $p['content'];
2868 $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
2869 }
2870
2871 // add "Save to calendar" button into attachment menu
2872 if ($has_events) {
2873 $this->add_button(array(
2874 'id' => 'attachmentsavecal',
2875 'name' => 'attachmentsavecal',
2876 'type' => 'link',
2877 'wrapper' => 'li',
2878 'command' => 'attachment-save-calendar',
2879 'class' => 'icon calendarlink',
2880 'classact' => 'icon calendarlink active',
2881 'innerclass' => 'icon calendar',
2882 'label' => 'calendar.savetocalendar',
2883 ), 'attachmentmenu');
2884 }
2885
2886 return $p;
2887 }
2888
2889
2890 /**
2891 * Handler for POST request to import an event attached to a mail message
2892 */
2893 public function mail_import_itip()
2894 {
2895 $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
2896
2897 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
2898 $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
2899 $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
2900 $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
2901 $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
2902 $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
2903 $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
2904 $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
2905 $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
2906 $comment = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
2907
2908 $error_msg = $this->gettext('errorimportingevent');
2909 $success = false;
2910
2911 if ($status == 'delegated') {
2912 $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
2913 $delegate = reset($delegates);
2914
2915 if (empty($delegate) || empty($delegate['mailto'])) {
2916 $this->rc->output->command('display_message', $this->rc->gettext('libcalendaring.delegateinvalidaddress'), 'error');
2917 return;
2918 }
2919 }
2920
2921 // successfully parsed events?
2922 if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
2923 // forward iTip request to delegatee
2924 if ($delegate) {
2925 $rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST);
2926 $itip = $this->load_itip();
2927
2928 $event['comment'] = $comment;
2929
2930 if ($itip->delegate_to($event, $delegate, !empty($rsvpme))) {
2931 $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
2932 }
2933 else {
2934 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
2935 }
2936
2937 unset($event['comment']);
2938
2939 // the delegator is set to non-participant, thus save as non-blocking
2940 $event['free_busy'] = 'free';
2941 }
2942
2943 $mode = calendar_driver::FILTER_PERSONAL
2944 | calendar_driver::FILTER_SHARED
2945 | calendar_driver::FILTER_WRITEABLE;
2946
2947 // find writeable calendar to store event
2948 $cal_id = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST);
2949 $dontsave = $cal_id === '' && $event['_method'] == 'REQUEST';
2950 $calendars = $this->driver->list_calendars($mode);
2951 $calendar = $calendars[$cal_id];
2952
2953 // select default calendar except user explicitly selected 'none'
2954 if (!$calendar && !$dontsave)
2955 $calendar = $this->get_default_calendar($event['sensitivity'], $calendars);
2956
2957 $metadata = array(
2958 'uid' => $event['uid'],
2959 '_instance' => $event['_instance'],
2960 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
2961 'sequence' => intval($event['sequence']),
2962 'fallback' => strtoupper($status),
2963 'method' => $event['_method'],
2964 'task' => 'calendar',
2965 );
2966
2967 // update my attendee status according to submitted method
2968 if (!empty($status)) {
2969 $organizer = null;
2970 $emails = $this->get_user_emails();
2971 foreach ($event['attendees'] as $i => $attendee) {
2972 if ($attendee['role'] == 'ORGANIZER') {
2973 $organizer = $attendee;
2974 }
2975 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
2976 $event['attendees'][$i]['status'] = strtoupper($status);
2977 if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED')))
2978 $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
2979
2980 $metadata['attendee'] = $attendee['email'];
2981 $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
2982 $reply_sender = $attendee['email'];
2983 $event_attendee = $attendee;
2984 }
2985 }
2986
2987 // add attendee with this user's default identity if not listed
2988 if (!$reply_sender) {
2989 $sender_identity = $this->rc->user->list_emails(true);
2990 $event['attendees'][] = array(
2991 'name' => $sender_identity['name'],
2992 'email' => $sender_identity['email'],
2993 'role' => 'OPT-PARTICIPANT',
2994 'status' => strtoupper($status),
2995 );
2996 $metadata['attendee'] = $sender_identity['email'];
2997 }
2998 }
2999
3000 // save to calendar
3001 if ($calendar && $calendar['editable']) {
3002 // check for existing event with the same UID
3003 $existing = $this->find_event($event, $mode);
3004
3005 // we'll create a new copy if user decided to change the calendar
3006 if ($existing && $cal_id && $calendar && $calendar['id'] != $existing['calendar']) {
3007 $existing = null;
3008 }
3009
3010 if ($existing) {
3011 $calendar = $calendars[$existing['calendar']];
3012
3013 // forward savemode for correct updates of recurring events
3014 $existing['_savemode'] = $savemode ?: $event['_savemode'];
3015
3016 // only update attendee status
3017 if ($event['_method'] == 'REPLY') {
3018 // try to identify the attendee using the email sender address
3019 $existing_attendee = -1;
3020 $existing_attendee_emails = array();
3021 foreach ($existing['attendees'] as $i => $attendee) {
3022 $existing_attendee_emails[] = $attendee['email'];
3023 if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
3024 $existing_attendee = $i;
3025 }
3026 }
3027 $event_attendee = null;
3028 $update_attendees = array();
3029 foreach ($event['attendees'] as $attendee) {
3030 if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
3031 $event_attendee = $attendee;
3032 $update_attendees[] = $attendee;
3033 $metadata['fallback'] = $attendee['status'];
3034 $metadata['attendee'] = $attendee['email'];
3035 $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
3036 if ($attendee['status'] != 'DELEGATED') {
3037 break;
3038 }
3039 }
3040 // also copy delegate attendee
3041 else if (!empty($attendee['delegated-from'])
3042 && $this->itip->compare_email($attendee['delegated-from'], $event['_sender'], $event['_sender_utf'])
3043 ) {
3044 $update_attendees[] = $attendee;
3045 if (!in_array_nocase($attendee['email'], $existing_attendee_emails)) {
3046 $existing['attendees'][] = $attendee;
3047 }
3048 }
3049 }
3050
3051 // if delegatee has declined, set delegator's RSVP=True
3052 if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
3053 foreach ($existing['attendees'] as $i => $attendee) {
3054 if ($attendee['email'] == $event_attendee['delegated-from']) {
3055 $existing['attendees'][$i]['rsvp'] = true;
3056 break;
3057 }
3058 }
3059 }
3060
3061 // found matching attendee entry in both existing and new events
3062 if ($existing_attendee >= 0 && $event_attendee) {
3063 $existing['attendees'][$existing_attendee] = $event_attendee;
3064 $success = $this->driver->update_attendees($existing, $update_attendees);
3065 }
3066 // update the entire attendees block
3067 else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
3068 $existing['attendees'][] = $event_attendee;
3069 $success = $this->driver->update_attendees($existing, $update_attendees);
3070 }
3071 else {
3072 $error_msg = $this->gettext('newerversionexists');
3073 }
3074 }
3075 // delete the event when declined (#1670)
3076 else if ($status == 'declined' && $delete) {
3077 $deleted = $this->driver->remove_event($existing, true);
3078 $success = true;
3079 }
3080 // import the (newer) event
3081 else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
3082 $event['id'] = $existing['id'];
3083 $event['calendar'] = $existing['calendar'];
3084
3085 // merge attendees status
3086 // e.g. preserve my participant status for regular updates
3087 $this->lib->merge_attendees($event, $existing, $status);
3088
3089 // set status=CANCELLED on CANCEL messages
3090 if ($event['_method'] == 'CANCEL')
3091 $event['status'] = 'CANCELLED';
3092
3093 // update attachments list, allow attachments update only on REQUEST (#5342)
3094 if ($event['_method'] == 'REQUEST')
3095 $event['deleted_attachments'] = true;
3096 else
3097 unset($event['attachments']);
3098
3099 // show me as free when declined (#1670)
3100 if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT')
3101 $event['free_busy'] = 'free';
3102
3103 $success = $this->driver->edit_event($event);
3104 }
3105 else if (!empty($status)) {
3106 $existing['attendees'] = $event['attendees'];
3107 if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // show me as free when declined (#1670)
3108 $existing['free_busy'] = 'free';
3109 $success = $this->driver->edit_event($existing);
3110 }
3111 else
3112 $error_msg = $this->gettext('newerversionexists');
3113 }
3114 else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) {
3115 if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
3116 $event['free_busy'] = 'free';
3117 }
3118
3119 // if the RSVP reply only refers to a single instance:
3120 // store unmodified master event with current instance as exception
3121 if (!empty($instance) && !empty($savemode) && $savemode != 'all') {
3122 $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
3123 if ($master['recurrence'] && !$master['_instance']) {
3124 // compute recurring events until this instance's date
3125 if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
3126 $recurrence_date->setTime(23,59,59);
3127
3128 foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
3129 if ($recurring['_instance'] == $instance) {
3130 // copy attendees block with my partstat to exception
3131 $recurring['attendees'] = $event['attendees'];
3132 $master['recurrence']['EXCEPTIONS'][] = $recurring;
3133 $event = $recurring; // set reference for iTip reply
3134 break;
3135 }
3136 }
3137
3138 $master['calendar'] = $event['calendar'] = $calendar['id'];
3139 $success = $this->driver->new_event($master);
3140 }
3141 else {
3142 $master = null;
3143 }
3144 }
3145 else {
3146 $master = null;
3147 }
3148 }
3149
3150 // save to the selected/default calendar
3151 if (!$master) {
3152 $event['calendar'] = $calendar['id'];
3153 $success = $this->driver->new_event($event);
3154 }
3155 }
3156 else if ($status == 'declined')
3157 $error_msg = null;
3158 }
3159 else if ($status == 'declined' || $dontsave)
3160 $error_msg = null;
3161 else
3162 $error_msg = $this->gettext('nowritecalendarfound');
3163 }
3164
3165 if ($success) {
3166 $message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
3167 $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
3168 }
3169
3170 if ($success || $dontsave) {
3171 $metadata['calendar'] = $event['calendar'];
3172 $metadata['nosave'] = $dontsave;
3173 $metadata['rsvp'] = intval($metadata['rsvp']);
3174 $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
3175 $this->rc->output->command('plugin.itip_message_processed', $metadata);
3176 $error_msg = null;
3177 }
3178 else if ($error_msg) {
3179 $this->rc->output->command('display_message', $error_msg, 'error');
3180 }
3181
3182 // send iTip reply
3183 if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
3184 $event['comment'] = $comment;
3185 $itip = $this->load_itip();
3186 $itip->set_sender_email($reply_sender);
3187 if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
3188 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
3189 else
3190 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
3191 }
3192
3193 $this->rc->output->send();
3194 }
3195
3196 /**
3197 * Handler for calendar/itip-remove requests
3198 */
3199 function mail_itip_decline_reply()
3200 {
3201 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
3202 $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
3203 $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
3204
3205 if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') {
3206 $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
3207
3208 foreach ($event['attendees'] as $_attendee) {
3209 if ($_attendee['role'] != 'ORGANIZER') {
3210 $attendee = $_attendee;
3211 break;
3212 }
3213 }
3214
3215 $itip = $this->load_itip();
3216 if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel'))
3217 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation');
3218 else
3219 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
3220 }
3221 else {
3222 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
3223 }
3224 }
3225
3226 /**
3227 * Handler for calendar/itip-delegate requests
3228 */
3229 function mail_itip_delegate()
3230 {
3231 // forward request to mail_import_itip() with the right status
3232 $_POST['_status'] = $_REQUEST['_status'] = 'delegated';
3233 $this->mail_import_itip();
3234 }
3235
3236 /**
3237 * Import the full payload from a mail message attachment
3238 */
3239 public function mail_import_attachment()
3240 {
3241 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
3242 $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
3243 $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
3244 $charset = RCUBE_CHARSET;
3245
3246 // establish imap connection
3247 $imap = $this->rc->get_storage();
3248 $imap->set_folder($mbox);
3249
3250 if ($uid && $mime_id) {
3251 $part = $imap->get_message_part($uid, $mime_id);
3252 if ($part->ctype_parameters['charset'])
3253 $charset = $part->ctype_parameters['charset'];
3254 // $headers = $imap->get_message_headers($uid);
3255
3256 if ($part) {
3257 $events = $this->get_ical()->import($part, $charset);
3258 }
3259 }
3260
3261 $success = $existing = 0;
3262 if (!empty($events)) {
3263 // find writeable calendar to store event
3264 $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null;
3265 $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);
3266
3267 foreach ($events as $event) {
3268 // save to calendar
3269 $calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']);
3270 if ($calendar && $calendar['editable'] && $event['_type'] == 'event') {
3271 $event['calendar'] = $calendar['id'];
3272
3273 if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) {
3274 $success += (bool)$this->driver->new_event($event);
3275 }
3276 else {
3277 $existing++;
3278 }
3279 }
3280 }
3281 }
3282
3283 if ($success) {
3284 $this->rc->output->command('display_message', $this->gettext(array(
3285 'name' => 'importsuccess',
3286 'vars' => array('nr' => $success),
3287 )), 'confirmation');
3288 }
3289 else if ($existing) {
3290 $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
3291 }
3292 else {
3293 $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
3294 }
3295 }
3296
3297 /**
3298 * Read email message and return contents for a new event based on that message
3299 */
3300 public function mail_message2event()
3301 {
3302 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
3303 $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
3304 $event = array();
3305
3306 // establish imap connection
3307 $imap = $this->rc->get_storage();
3308 $imap->set_folder($mbox);
3309 $message = new rcube_message($uid);
3310
3311 if ($message->headers) {
3312 $event['title'] = trim($message->subject);
3313 $event['description'] = trim($message->first_text_part());
3314
3315 $this->load_driver();
3316
3317 // add a reference to the email message
3318 if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
3319 $event['links'] = array($msgref);
3320 }
3321 else {
3322 // hack around missing database_driver implementation of that method
3323 $event['links'] = array(
3324 array('mailurl' => "https://hppllc.org/roundcube/?_task=mail&_mbox=Bookings%2FBigHouse%2FPending&_uid=$uid",
3325 'subject' => 'link to original email'));
3326 }
3327 // copy mail attachments to event
3328 if ($message->attachments) {
3329 $eventid = 'cal-';
3330 if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
3331 $_SESSION[self::SESSION_KEY] = array();
3332 $_SESSION[self::SESSION_KEY]['id'] = $eventid;
3333 $_SESSION[self::SESSION_KEY]['attachments'] = array();
3334 }
3335
3336 foreach ((array)$message->attachments as $part) {
3337 $attachment = array(
3338 'data' => $imap->get_message_part($uid, $part->mime_id, $part),
3339 'size' => $part->size,
3340 'name' => $part->filename,
3341 'mimetype' => $part->mimetype,
3342 'group' => $eventid,
3343 );
3344
3345 $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
3346
3347 if ($attachment['status'] && !$attachment['abort']) {
3348 $id = $attachment['id'];
3349 $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
3350
3351 // store new attachment in session
3352 unset($attachment['status'], $attachment['abort'], $attachment['data']);
3353 $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
3354
3355 $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
3356 $event['attachments'][] = $attachment;
3357 }
3358 }
3359 }
3360
3361 $this->rc->output->command('plugin.mail2event_dialog', $event);
3362 }
3363 else {
3364 $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
3365 }
3366
3367 $this->rc->output->send();
3368 }
3369
3370 /**
3371 * Handler for the 'message_compose' plugin hook. This will check for
3372 * a compose parameter 'calendar_event' and create an attachment with the
3373 * referenced event in iCal format
3374 */
3375 public function mail_message_compose($args)
3376 {
3377 // set the submitted event ID as attachment
3378 if (!empty($args['param']['calendar_event'])) {
3379 $this->load_driver();
3380
3381 list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
3382 if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) {
3383 $filename = asciiwords($event['title']);
3384 if (empty($filename))
3385 $filename = 'event';
3386
3387 // save ics to a temp file and register as attachment
3388 $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
3389 file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body')));
3390
3391 $args['attachments'][] = array(
3392 'path' => $tmp_path,
3393 'name' => $filename . '.ics',
3394 'mimetype' => 'text/calendar',
3395 'size' => filesize($tmp_path),
3396 );
3397 $args['param']['subject'] = $event['title'];
3398 }
3399 }
3400
3401 return $args;
3402 }
3403
3404
3405 /**
3406 * Get a list of email addresses of the current user (from login and identities)
3407 */
3408 public function get_user_emails()
3409 {
3410 return $this->lib->get_user_emails();
3411 }
3412
3413
3414 /**
3415 * Build an absolute URL with the given parameters
3416 */
3417 public function get_url($param = array())
3418 {
3419 $param += array('task' => 'calendar');
3420 return $this->rc->url($param, true, true);
3421 }
3422
3423
3424 public function ical_feed_hash($source)
3425 {
3426 return base64_encode($this->rc->user->get_username() . ':' . $source);
3427 }
3428
3429 /**
3430 * Handler for user_delete plugin hook
3431 */
3432 public function user_delete($args)
3433 {
3434 // delete itipinvitations entries related to this user
3435 $db = $this->rc->get_dbh();
3436 $table_itipinvitations = $db->table_name('itipinvitations', true);
3437 $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID);
3438
3439 $this->setup();
3440 $this->load_driver();
3441 return $this->driver->user_delete($args);
3442 }
3443
3444 /**
3445 * Magic getter for public access to protected members
3446 */
3447 public function __get($name)
3448 {
3449 switch ($name) {
3450 case 'ical':
3451 return $this->get_ical();
3452
3453 case 'itip':
3454 return $this->load_itip();
3455
3456 case 'driver':
3457 $this->load_driver();
3458 return $this->driver;
3459 }
3460
3461 return null;
3462 }
3463
3464 }