Template for caldav_driver.php
[caldav_driver.git] / calendar.php
1 <?php
2
3 /**
4 * Calendar plugin for Roundcube webmail
5 *
6 * @version @package_version@
7 * @author Lazlo Westerhof <hello@lazlo.me>
8 * @author Thomas Bruederli <bruederli@kolabsys.com>
9 *
10 * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
11 * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License as
15 * published by the Free Software Foundation, either version 3 of the
16 * License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU Affero General Public License for more details.
22 *
23 * You should have received a copy of the GNU Affero General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 class calendar extends rcube_plugin
28 {
29 const FREEBUSY_UNKNOWN = 0;
30 const FREEBUSY_FREE = 1;
31 const FREEBUSY_BUSY = 2;
32 const FREEBUSY_TENTATIVE = 3;
33 const FREEBUSY_OOF = 4;
34
35 const SESSION_KEY = 'calendar_temp';
36
37 public $task = '?(?!logout).*';
38 public $rc;
39 public $lib;
40 public $driver;
41 public $home; // declare public to be used in other classes
42 public $urlbase;
43 public $timezone;
44 public $timezone_offset;
45 public $gmt_offset;
46
47 public $ical;
48 public $ui;
49
50 public $defaults = array(
51 'calendar_default_view' => "agendaWeek",
52 'calendar_timeslots' => 2,
53 'calendar_work_start' => 6,
54 'calendar_work_end' => 18,
55 'calendar_agenda_range' => 60,
56 'calendar_agenda_sections' => 'smart',
57 'calendar_event_coloring' => 0,
58 'calendar_time_indicator' => true,
59 'calendar_allow_invite_shared' => false,
60 );
61
62 private $ics_parts = array();
63
64
65 /**
66 * Plugin initialization.
67 */
68 function init()
69 {
70 $this->require_plugin('libcalendaring');
71
72 $this->rc = rcube::get_instance();
73 $this->lib = libcalendaring::get_instance();
74
75 $this->register_task('calendar', 'calendar');
76
77 // load calendar configuration
78 $this->load_config();
79
80 // load localizations
81 $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
82
83 $this->timezone = $this->lib->timezone;
84 $this->gmt_offset = $this->lib->gmt_offset;
85 $this->dst_active = $this->lib->dst_active;
86 $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
87
88 require($this->home . '/lib/calendar_ui.php');
89 $this->ui = new calendar_ui($this);
90
91 // catch iTIP confirmation requests that don're require a valid session
92 if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
93 $this->add_hook('startup', array($this, 'itip_attend_response'));
94 }
95 else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
96 $this->add_hook('startup', array($this, 'ical_feed_export'));
97 }
98 else {
99 // default startup routine
100 $this->add_hook('startup', array($this, 'startup'));
101 }
102 }
103
104 /**
105 * Startup hook
106 */
107 public function startup($args)
108 {
109 // the calendar module can be enabled/disabled by the kolab_auth plugin
110 if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true))
111 return;
112
113 // load Calendar user interface
114 if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
115 $this->ui->init();
116
117 // settings are required in (almost) every GUI step
118 if ($args['action'] != 'attend')
119 $this->rc->output->set_env('calendar_settings', $this->load_settings());
120 }
121
122 if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
123 if ($args['action'] != 'upload') {
124 $this->load_driver();
125 }
126
127 // register calendar actions
128 $this->register_action('index', array($this, 'calendar_view'));
129 $this->register_action('event', array($this, 'event_action'));
130 $this->register_action('calendar', array($this, 'calendar_action'));
131 $this->register_action('load_events', array($this, 'load_events'));
132 $this->register_action('export_events', array($this, 'export_events'));
133 $this->register_action('import_events', array($this, 'import_events'));
134 $this->register_action('upload', array($this, 'attachment_upload'));
135 $this->register_action('get-attachment', array($this, 'attachment_get'));
136 $this->register_action('freebusy-status', array($this, 'freebusy_status'));
137 $this->register_action('freebusy-times', array($this, 'freebusy_times'));
138 $this->register_action('randomdata', array($this, 'generate_randomdata'));
139 $this->register_action('print', array($this,'print_view'));
140 $this->register_action('mailimportevent', array($this, 'mail_import_event'));
141 $this->register_action('mailtoevent', array($this, 'mail_message2event'));
142 $this->register_action('inlineui', array($this, 'get_inline_ui'));
143 $this->register_action('check-recent', array($this, 'check_recent'));
144 $this->add_hook('refresh', array($this, 'refresh'));
145
146 // remove undo information...
147 if ($undo = $_SESSION['calendar_event_undo']) {
148 // ...after timeout
149 $undo_time = $this->rc->config->get('undo_timeout', 0);
150 if ($undo['ts'] < time() - $undo_time) {
151 $this->rc->session->remove('calendar_event_undo');
152 // @TODO: do EXPUNGE on kolab objects?
153 }
154 }
155 }
156 else if ($args['task'] == 'settings') {
157 // add hooks for Calendar settings
158 $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
159 $this->add_hook('preferences_list', array($this, 'preferences_list'));
160 $this->add_hook('preferences_save', array($this, 'preferences_save'));
161 }
162 else if ($args['task'] == 'mail') {
163 // hooks to catch event invitations on incoming mails
164 if ($args['action'] == 'show' || $args['action'] == 'preview') {
165 $this->add_hook('message_load', array($this, 'mail_message_load'));
166 $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
167 }
168
169 // add 'Create event' item to message menu
170 if ($this->api->output->type == 'html') {
171 $this->api->add_content(html::tag('li', null,
172 $this->api->output->button(array(
173 'command' => 'calendar-create-from-mail',
174 'label' => 'calendar.createfrommail',
175 'type' => 'link',
176 'classact' => 'icon calendarlink active',
177 'class' => 'icon calendarlink',
178 'innerclass' => 'icon calendar',
179 ))),
180 'messagemenu');
181
182 $this->api->output->add_label('calendar.createfrommail');
183 }
184 }
185
186 // add hooks to display alarms
187 $this->add_hook('pending_alarms', array($this, 'pending_alarms'));
188 $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
189 }
190
191 /**
192 * Helper method to load the backend driver according to local config
193 */
194 private function load_driver()
195 {
196 if (is_object($this->driver))
197 return;
198
199 $driver_name = $this->rc->config->get('calendar_driver', 'database');
200 $driver_class = $driver_name . '_driver';
201
202 require_once($this->home . '/drivers/calendar_driver.php');
203 require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
204
205 switch ($driver_name) {
206 case "kolab":
207 $this->require_plugin('libkolab');
208 default:
209 $this->driver = new $driver_class($this);
210 break;
211 }
212
213 if ($this->driver->undelete)
214 $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
215 }
216
217 /**
218 * Load iTIP functions
219 */
220 private function load_itip()
221 {
222 if (!$this->itip) {
223 require_once($this->home . '/lib/calendar_itip.php');
224
225 $plugin = $this->rc->plugins->exec_hook('calendar_load_itip',
226 array('identity' => null));
227
228 $this->itip = new calendar_itip($this, $plugin['identity']);
229 }
230
231 return $this->itip;
232 }
233
234 /**
235 * Load iCalendar functions
236 */
237 public function get_ical()
238 {
239 if (!$this->ical) {
240 $this->ical = libcalendaring::get_ical();
241 }
242
243 return $this->ical;
244 }
245
246 /**
247 * Get properties of the calendar this user has specified as default
248 */
249 public function get_default_calendar($writeable = false)
250 {
251 $default_id = $this->rc->config->get('calendar_default_calendar');
252 $calendars = $this->driver->list_calendars(false, true);
253 $calendar = $calendars[$default_id] ?: null;
254 if (!$calendar || ($writeable && $calendar['readonly'])) {
255 foreach ($calendars as $cal) {
256 if ($cal['default']) {
257 $calendar = $cal;
258 break;
259 }
260 if (!$writeable || !$cal['readonly']) {
261 $first = $cal;
262 }
263 }
264 }
265
266 return $calendar ?: $first;
267 }
268
269
270 /**
271 * Render the main calendar view from skin template
272 */
273 function calendar_view()
274 {
275 $this->rc->output->set_pagetitle($this->gettext('calendar'));
276
277 // Add CSS stylesheets to the page header
278 $this->ui->addCSS();
279
280 // Add JS files to the page header
281 $this->ui->addJS();
282
283 $this->ui->init_templates();
284 $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning');
285
286 // initialize attendees autocompletion
287 rcube_autocomplete_init();
288
289 $this->rc->output->set_env('timezone', $this->timezone->getName());
290 $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
291 $this->rc->output->set_env('mscolors', $this->driver->get_color_values());
292 $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
293
294 $view = get_input_value('view', RCUBE_INPUT_GPC);
295 if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
296 $this->rc->output->set_env('view', $view);
297
298 if ($date = get_input_value('date', RCUBE_INPUT_GPC))
299 $this->rc->output->set_env('date', $date);
300
301 $this->rc->output->send("calendar.calendar");
302 }
303
304 /**
305 * Handler for preferences_sections_list hook.
306 * Adds Calendar settings sections into preferences sections list.
307 *
308 * @param array Original parameters
309 * @return array Modified parameters
310 */
311 function preferences_sections_list($p)
312 {
313 $p['list']['calendar'] = array(
314 'id' => 'calendar', 'section' => $this->gettext('calendar'),
315 );
316
317 return $p;
318 }
319
320 /**
321 * Handler for preferences_list hook.
322 * Adds options blocks into Calendar settings sections in Preferences.
323 *
324 * @param array Original parameters
325 * @return array Modified parameters
326 */
327 function preferences_list($p)
328 {
329 if ($p['section'] != 'calendar') {
330 return $p;
331 }
332
333 $no_override = array_flip((array)$this->rc->config->get('dont_override'));
334
335 $p['blocks']['view']['name'] = $this->gettext('mainoptions');
336
337 if (!isset($no_override['calendar_default_view'])) {
338 if (!$p['current']) {
339 $p['blocks']['view']['content'] = true;
340 return $p;
341 }
342
343 $field_id = 'rcmfd_default_view';
344 $select = new html_select(array('name' => '_default_view', 'id' => $field_id));
345 $select->add($this->gettext('day'), "agendaDay");
346 $select->add($this->gettext('week'), "agendaWeek");
347 $select->add($this->gettext('month'), "month");
348 $select->add($this->gettext('agenda'), "table");
349 $p['blocks']['view']['options']['default_view'] = array(
350 'title' => html::label($field_id, Q($this->gettext('default_view'))),
351 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
352 );
353 }
354
355 if (!isset($no_override['calendar_timeslots'])) {
356 if (!$p['current']) {
357 $p['blocks']['view']['content'] = true;
358 return $p;
359 }
360
361 $field_id = 'rcmfd_timeslot';
362 $choices = array('1', '2', '3', '4', '6');
363 $select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
364 $select->add($choices);
365 $p['blocks']['view']['options']['timeslots'] = array(
366 'title' => html::label($field_id, Q($this->gettext('timeslots'))),
367 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))),
368 );
369 }
370
371 if (!isset($no_override['calendar_first_day'])) {
372 if (!$p['current']) {
373 $p['blocks']['view']['content'] = true;
374 return $p;
375 }
376
377 $field_id = 'rcmfd_firstday';
378 $select = new html_select(array('name' => '_first_day', 'id' => $field_id));
379 $select->add(rcube_label('sunday'), '0');
380 $select->add(rcube_label('monday'), '1');
381 $select->add(rcube_label('tuesday'), '2');
382 $select->add(rcube_label('wednesday'), '3');
383 $select->add(rcube_label('thursday'), '4');
384 $select->add(rcube_label('friday'), '5');
385 $select->add(rcube_label('saturday'), '6');
386 $p['blocks']['view']['options']['first_day'] = array(
387 'title' => html::label($field_id, Q($this->gettext('first_day'))),
388 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
389 );
390 }
391
392 if (!isset($no_override['calendar_first_hour'])) {
393 if (!$p['current']) {
394 $p['blocks']['view']['content'] = true;
395 return $p;
396 }
397
398 $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
399 $select_hours = new html_select();
400 for ($h = 0; $h < 24; $h++)
401 $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
402
403 $field_id = 'rcmfd_firsthour';
404 $p['blocks']['view']['options']['first_hour'] = array(
405 'title' => html::label($field_id, Q($this->gettext('first_hour'))),
406 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
407 );
408 }
409
410 if (!isset($no_override['calendar_work_start'])) {
411 if (!$p['current']) {
412 $p['blocks']['view']['content'] = true;
413 return $p;
414 }
415
416 $field_id = 'rcmfd_workstart';
417 $p['blocks']['view']['options']['workinghours'] = array(
418 'title' => html::label($field_id, Q($this->gettext('workinghours'))),
419 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
420 ' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
421 );
422 }
423
424 if (!isset($no_override['calendar_event_coloring'])) {
425 if (!$p['current']) {
426 $p['blocks']['view']['content'] = true;
427 return $p;
428 }
429
430 $field_id = 'rcmfd_coloring';
431 $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
432 $select_colors->add($this->gettext('coloringmode0'), 0);
433 $select_colors->add($this->gettext('coloringmode1'), 1);
434 $select_colors->add($this->gettext('coloringmode2'), 2);
435 $select_colors->add($this->gettext('coloringmode3'), 3);
436
437 $p['blocks']['view']['options']['eventcolors'] = array(
438 'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
439 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
440 );
441 }
442
443 // loading driver is expensive, don't do it if not needed
444 $this->load_driver();
445
446 if (!isset($no_override['calendar_default_alarm_type'])) {
447 if (!$p['current']) {
448 $p['blocks']['view']['content'] = true;
449 return $p;
450 }
451
452 $field_id = 'rcmfd_alarm';
453 $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
454 $select_type->add($this->gettext('none'), '');
455 foreach ($this->driver->alarm_types as $type)
456 $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
457
458 $p['blocks']['view']['options']['alarmtype'] = array(
459 'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
460 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
461 );
462 }
463
464 if (!isset($no_override['calendar_default_alarm_offset'])) {
465 if (!$p['current']) {
466 $p['blocks']['view']['content'] = true;
467 return $p;
468 }
469
470 $field_id = 'rcmfd_alarm';
471 $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
472 $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
473 foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
474 $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
475
476 $preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
477 $p['blocks']['view']['options']['alarmoffset'] = array(
478 'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
479 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
480 );
481 }
482
483 if (!isset($no_override['calendar_default_calendar'])) {
484 if (!$p['current']) {
485 $p['blocks']['view']['content'] = true;
486 return $p;
487 }
488 // default calendar selection
489 $field_id = 'rcmfd_default_calendar';
490 $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
491 foreach ((array)$this->driver->list_calendars(false, true) as $id => $prop) {
492 $select_cal->add($prop['name'], strval($id));
493 if ($prop['default'])
494 $default_calendar = $id;
495 }
496 $p['blocks']['view']['options']['defaultcalendar'] = array(
497 'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
498 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),
499 );
500 }
501
502 // category definitions
503 if (!$this->driver->nocategories && !isset($no_override['calendar_categories'])) {
504 $p['blocks']['categories']['name'] = $this->gettext('categories');
505
506 if (!$p['current']) {
507 $p['blocks']['categories']['content'] = true;
508 return $p;
509 }
510
511 $categories = (array) $this->driver->list_categories();
512 $categories_list = '';
513 foreach ($categories as $name => $color) {
514 $key = md5($name);
515 $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
516 $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
517 $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable));
518 $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
519 $hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
520 $categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
521 }
522
523 $p['blocks']['categories']['options']['category_' . $name] = array(
524 'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
525 );
526
527 $field_id = 'rcmfd_new_category';
528 $new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
529 $add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
530 $p['blocks']['categories']['options']['categories'] = array(
531 'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
532 );
533
534 $this->rc->output->add_script('function rcube_calendar_add_category(){
535 var name = $("#rcmfd_new_category").val();
536 if (name.length) {
537 var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
538 var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
539 var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
540 $("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
541 color.miniColors({ colorValues:(rcmail.env.mscolors || []) });
542 $("#rcmfd_new_category").val("");
543 }
544 }');
545
546 $this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){
547 if (event.which == 13) {
548 rcube_calendar_add_category();
549 event.preventDefault();
550 }
551 });
552 ', 'docready');
553
554 // include color picker
555 $this->include_script('lib/js/jquery.miniColors.min.js');
556 $this->include_stylesheet($this->local_skin_path() . '/jquery.miniColors.css');
557 $this->rc->output->set_env('mscolors', $this->driver->get_color_values());
558 $this->rc->output->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready');
559 }
560
561 return $p;
562 }
563
564 /**
565 * Handler for preferences_save hook.
566 * Executed on Calendar settings form submit.
567 *
568 * @param array Original parameters
569 * @return array Modified parameters
570 */
571 function preferences_save($p)
572 {
573 if ($p['section'] == 'calendar') {
574 $this->load_driver();
575
576 // compose default alarm preset value
577 $alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST);
578 $default_alarm = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1];
579
580 $p['prefs'] = array(
581 'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
582 'calendar_timeslots' => intval(get_input_value('_timeslots', RCUBE_INPUT_POST)),
583 'calendar_first_day' => intval(get_input_value('_first_day', RCUBE_INPUT_POST)),
584 'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST)),
585 'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST)),
586 'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST)),
587 'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST)),
588 'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST),
589 'calendar_default_alarm_offset' => $default_alarm,
590 'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST),
591 'calendar_date_format' => null, // clear previously saved values
592 'calendar_time_format' => null,
593 );
594
595 // categories
596 if (!$this->driver->nocategories) {
597 $old_categories = $new_categories = array();
598 foreach ($this->driver->list_categories() as $name => $color) {
599 $old_categories[md5($name)] = $name;
600 }
601
602 $categories = (array) get_input_value('_categories', RCUBE_INPUT_POST);
603 $colors = (array) get_input_value('_colors', RCUBE_INPUT_POST);
604
605 foreach ($categories as $key => $name) {
606 $color = preg_replace('/^#/', '', strval($colors[$key]));
607
608 // rename categories in existing events -> driver's job
609 if ($oldname = $old_categories[$key]) {
610 $this->driver->replace_category($oldname, $name, $color);
611 unset($old_categories[$key]);
612 }
613 else
614 $this->driver->add_category($name, $color);
615
616 $new_categories[$name] = $color;
617 }
618
619 // these old categories have been removed, alter events accordingly -> driver's job
620 foreach ((array)$old_categories[$key] as $key => $name) {
621 $this->driver->remove_category($name);
622 }
623
624 $p['prefs']['calendar_categories'] = $new_categories;
625 }
626 }
627
628 return $p;
629 }
630
631 /**
632 * Dispatcher for calendar actions initiated by the client
633 */
634 function calendar_action()
635 {
636 $action = get_input_value('action', RCUBE_INPUT_GPC);
637 $cal = get_input_value('c', RCUBE_INPUT_GPC);
638 $success = $reload = false;
639
640 if (isset($cal['showalarms']))
641 $cal['showalarms'] = intval($cal['showalarms']);
642
643 switch ($action) {
644 case "form-new":
645 case "form-edit":
646 echo $this->ui->calendar_editform($action, $cal);
647 exit;
648 case "new":
649 $success = $this->driver->create_calendar($cal);
650 $reload = true;
651 break;
652 case "edit":
653 $success = $this->driver->edit_calendar($cal);
654 $reload = true;
655 break;
656 case "remove":
657 if ($success = $this->driver->remove_calendar($cal))
658 $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
659 break;
660 case "subscribe":
661 if (!$this->driver->subscribe_calendar($cal))
662 $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
663 return;
664 }
665
666 if ($success)
667 $this->rc->output->show_message('successfullysaved', 'confirmation');
668 else {
669 $error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :'');
670 $this->rc->output->show_message($error_msg, 'error');
671 }
672
673 $this->rc->output->command('plugin.unlock_saving');
674
675 // TODO: keep view and date selection
676 if ($success && $reload)
677 $this->rc->output->redirect('');
678 }
679
680
681 /**
682 * Dispatcher for event actions initiated by the client
683 */
684 function event_action()
685 {
686 $action = get_input_value('action', RCUBE_INPUT_GPC);
687 $event = get_input_value('e', RCUBE_INPUT_POST, true);
688 $success = $reload = $got_msg = false;
689
690 // don't notify if modifying a recurring instance (really?)
691 if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify'])
692 unset($event['_notify']);
693
694 // read old event data in order to find changes
695 if (($event['_notify'] || $event['decline']) && $action != 'new')
696 $old = $this->driver->get_event($event);
697
698 switch ($action) {
699 case "new":
700 // create UID for new event
701 $event['uid'] = $this->generate_uid();
702 $this->prepare_event($event, $action);
703 if ($success = $this->driver->new_event($event)) {
704 $event['id'] = $event['uid'];
705 $this->cleanup_event($event);
706 }
707 $reload = $success && $event['recurrence'] ? 2 : 1;
708 break;
709
710 case "edit":
711 $this->prepare_event($event, $action);
712 if ($success = $this->driver->edit_event($event))
713 $this->cleanup_event($event);
714 $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
715 break;
716
717 case "resize":
718 $this->prepare_event($event, $action);
719 $success = $this->driver->resize_event($event);
720 $reload = $event['_savemode'] ? 2 : 1;
721 break;
722
723 case "move":
724 $this->prepare_event($event, $action);
725 $success = $this->driver->move_event($event);
726 $reload = $success && $event['_savemode'] ? 2 : 1;
727 break;
728
729 case "remove":
730 // remove previous deletes
731 $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
732 $this->rc->session->remove('calendar_event_undo');
733
734 // search for event if only UID is given
735 if (!isset($event['calendar']) && $event['uid']) {
736 if (!($event = $this->driver->get_event($event, true))) {
737 break;
738 }
739 $undo_time = 0;
740 }
741
742 $success = $this->driver->remove_event($event, $undo_time < 1);
743 $reload = (!$success || $event['_savemode']) ? 2 : 1;
744
745 if ($undo_time > 0 && $success) {
746 $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
747 // display message with Undo link.
748 $msg = html::span(null, $this->gettext('successremoval'))
749 . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
750 JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
751 $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
752 $got_msg = true;
753 }
754 else if ($success) {
755 $this->rc->output->show_message('calendar.successremoval', 'confirmation');
756 $got_msg = true;
757 }
758
759 // send iTIP reply that participant has declined the event
760 if ($success && $event['decline']) {
761 $emails = $this->get_user_emails();
762 foreach ($old['attendees'] as $i => $attendee) {
763 if ($attendee['role'] == 'ORGANIZER')
764 $organizer = $attendee;
765 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
766 $old['attendees'][$i]['status'] = 'DECLINED';
767 $reply_sender = $attendee['email'];
768 }
769 }
770
771 $itip = $this->load_itip();
772 $itip->set_sender_email($reply_sender);
773 if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
774 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
775 else
776 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
777 }
778 break;
779
780 case "undo":
781 // Restore deleted event
782 $event = $_SESSION['calendar_event_undo']['data'];
783
784 if ($event)
785 $success = $this->driver->restore_event($event);
786
787 if ($success) {
788 $this->rc->session->remove('calendar_event_undo');
789 $this->rc->output->show_message('calendar.successrestore', 'confirmation');
790 $got_msg = true;
791 $reload = 2;
792 }
793
794 break;
795
796 case "rsvp-status":
797 $action = 'rsvp';
798 $status = $event['fallback'];
799 $latest = false;
800 $html = html::div('rsvp-status', $status != 'CANCELLED' ? $this->gettext('acceptinvitation') : '');
801 if (is_numeric($event['changed']))
802 $event['changed'] = new DateTime('@'.$event['changed']);
803 $this->load_driver();
804 if ($existing = $this->driver->get_event($event, true, false, true)) {
805 $latest = ($event['sequence'] && $existing['sequence'] == $event['sequence']) || (!$event['sequence'] && $existing['changed'] && $existing['changed'] >= $event['changed']);
806 $emails = $this->get_user_emails();
807 foreach ($existing['attendees'] as $i => $attendee) {
808 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
809 $status = $attendee['status'];
810 break;
811 }
812 }
813 }
814 else {
815 // get a list of writeable calendars
816 $calendars = $this->driver->list_calendars(false, true);
817 $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto', 'is_escaped' => true));
818 $numcals = 0;
819 foreach ($calendars as $calendar) {
820 if (!$calendar['readonly']) {
821 $calendar_select->add($calendar['name'], $calendar['id']);
822 $numcals++;
823 }
824 }
825 if ($numcals <= 1)
826 $calendar_select = null;
827 }
828
829 if ($status == 'unknown') {
830 $html = html::div('rsvp-status', $this->gettext('notanattendee'));
831 $action = 'import';
832 }
833 else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) {
834 $html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
835 if ($existing['sequence'] > $event['sequence'] || (!$event['sequence'] && $existing['changed'] && $existing['changed'] > $event['changed'])) {
836 $action = ''; // nothing to do here, outdated invitation
837 }
838 }
839
840 $default_calendar = $calendar_select ? $this->get_default_calendar(true) : null;
841 $this->rc->output->command('plugin.update_event_rsvp_status', array(
842 'uid' => $event['uid'],
843 'id' => asciiwords($event['uid'], true),
844 'saved' => $existing ? true : false,
845 'latest' => $latest,
846 'status' => $status,
847 'action' => $action,
848 'html' => $html,
849 'select' => $calendar_select ? html::span('calendar-select', $this->gettext('saveincalendar') . '&nbsp;' . $calendar_select->show($this->rc->config->get('calendar_default_calendar', $default_calendar['id']))) : '',
850 ));
851 return;
852
853 case "rsvp":
854 $ev = $this->driver->get_event($event);
855 $ev['attendees'] = $event['attendees'];
856 $event = $ev;
857
858 if ($success = $this->driver->edit_event($event)) {
859 $status = get_input_value('status', RCUBE_INPUT_GPC);
860 $organizer = null;
861 foreach ($event['attendees'] as $i => $attendee) {
862 if ($attendee['role'] == 'ORGANIZER') {
863 $organizer = $attendee;
864 break;
865 }
866 }
867 $itip = $this->load_itip();
868 if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
869 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
870 else
871 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
872 }
873 break;
874
875 case "dismiss":
876 $event['ids'] = explode(',', $event['id']);
877 $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event);
878 $success = $plugin['success'];
879 foreach ($event['ids'] as $id) {
880 if (strpos($id, 'cal:') === 0)
881 $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
882 }
883 break;
884 }
885
886 // show confirmation/error message
887 if (!$got_msg) {
888 if ($success)
889 $this->rc->output->show_message('successfullysaved', 'confirmation');
890 else
891 $this->rc->output->show_message('calendar.errorsaving', 'error');
892 }
893
894 // send out notifications
895 if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
896 // make sure we have the complete record
897 $event = $action == 'remove' ? $old : $this->driver->get_event($event);
898
899 // only notify if data really changed (TODO: do diff check on client already)
900 if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
901 $sent = $this->notify_attendees($event, $old, $action);
902 if ($sent > 0)
903 $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
904 else if ($sent < 0)
905 $this->rc->output->show_message('calendar.errornotifying', 'error');
906 }
907 }
908
909 // unlock client
910 $this->rc->output->command('plugin.unlock_saving');
911
912 // update event object on the client or trigger a complete refretch if too complicated
913 if ($reload) {
914 $args = array('source' => $event['calendar']);
915 if ($reload > 1)
916 $args['refetch'] = true;
917 else if ($success && $action != 'remove')
918 $args['update'] = $this->_client_event($this->driver->get_event($event));
919 $this->rc->output->command('plugin.refresh_calendar', $args);
920 }
921 }
922
923 /**
924 * Handler for load-requests from fullcalendar
925 * This will return pure JSON formatted output
926 */
927 function load_events()
928 {
929 $events = $this->driver->load_events(
930 get_input_value('start', RCUBE_INPUT_GET),
931 get_input_value('end', RCUBE_INPUT_GET),
932 ($query = get_input_value('q', RCUBE_INPUT_GET)),
933 get_input_value('source', RCUBE_INPUT_GET)
934 );
935 echo $this->encode($events, !empty($query));
936 exit;
937 }
938
939 /**
940 * Handler for keep-alive requests
941 * This will check for updated data in active calendars and sync them to the client
942 */
943 public function refresh($attr)
944 {
945 // refresh the entire calendar every 10th time to also sync deleted events
946 if (rand(0,10) == 10) {
947 $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true));
948 return;
949 }
950
951 foreach ($this->driver->list_calendars(true) as $cal) {
952 $events = $this->driver->load_events(
953 get_input_value('start', RCUBE_INPUT_GET),
954 get_input_value('end', RCUBE_INPUT_GET),
955 get_input_value('q', RCUBE_INPUT_GET),
956 $cal['id'],
957 1,
958 $attr['last']
959 );
960
961 foreach ($events as $event) {
962 $this->rc->output->command('plugin.refresh_calendar',
963 array('source' => $cal['id'], 'update' => $this->_client_event($event)));
964 }
965 }
966 }
967
968 /**
969 * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
970 * This will check for pending notifications and pass them to the client
971 */
972 public function pending_alarms($p)
973 {
974 $this->load_driver();
975 if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
976 foreach ($alarms as $alarm) {
977 $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
978 $p['alarms'][] = $alarm;
979 }
980 }
981
982 return $p;
983 }
984
985 /**
986 * Handler for alarm dismiss hook triggered by libcalendaring
987 */
988 public function dismiss_alarms($p)
989 {
990 $this->load_driver();
991 foreach ((array)$p['ids'] as $id) {
992 if (strpos($id, 'cal:') === 0)
993 $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);
994 }
995
996 return $p;
997 }
998
999 /**
1000 * Handler for check-recent requests which are accidentally sent to calendar taks
1001 */
1002 function check_recent()
1003 {
1004 // NOP
1005 $this->rc->output->send();
1006 }
1007
1008 /**
1009 *
1010 */
1011 function import_events()
1012 {
1013 // Upload progress update
1014 if (!empty($_GET['_progress'])) {
1015 rcube_upload_progress();
1016 }
1017
1018 @set_time_limit(0);
1019
1020 // process uploaded file if there is no error
1021 $err = $_FILES['_data']['error'];
1022
1023 if (!$err && $_FILES['_data']['tmp_name']) {
1024 $calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
1025 $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
1026 $user_email = $this->rc->user->get_username();
1027
1028 $ical = $this->get_ical();
1029 $errors = !$ical->fopen($_FILES['_data']['tmp_name']);
1030 $count = $i = 0;
1031 foreach ($ical as $event) {
1032 // keep the browser connection alive on long import jobs
1033 if (++$i > 100 && $i % 100 == 0) {
1034 echo "<!-- -->";
1035 ob_flush();
1036 }
1037
1038 // TODO: correctly handle recurring events which start before $rangestart
1039 if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
1040 continue;
1041
1042 $event['_owner'] = $user_email;
1043 $event['calendar'] = $calendar;
1044 if ($this->driver->new_event($event)) {
1045 $count++;
1046 }
1047 else
1048 $errors++;
1049 }
1050
1051 if ($count) {
1052 $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
1053 $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
1054 }
1055 else if (!$errors) {
1056 $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
1057 $this->rc->output->command('plugin.import_success', array('source' => $calendar));
1058 }
1059 else {
1060 $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
1061 }
1062 }
1063 else {
1064 if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
1065 $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
1066 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
1067 }
1068 else {
1069 $msg = rcube_label('fileuploaderror');
1070 }
1071
1072 $this->rc->output->command('plugin.import_error', array('message' => $msg));
1073 $this->rc->output->command('plugin.unlock_saving', false);
1074 }
1075
1076 $this->rc->output->send('iframe');
1077 }
1078
1079 /**
1080 * Construct the ics file for exporting events to iCalendar format;
1081 */
1082 function export_events($terminate = true)
1083 {
1084 $start = get_input_value('start', RCUBE_INPUT_GET);
1085 $end = get_input_value('end', RCUBE_INPUT_GET);
1086 if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
1087 if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10);
1088 $calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
1089 $calendars = $this->driver->list_calendars(true);
1090
1091 if ($calendars[$calid]) {
1092 $calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
1093 $calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
1094 if (empty($calname)) $calname = $calid;
1095 $events = $this->driver->load_events($start, $end, null, $calid, 0);
1096 }
1097 else
1098 $events = array();
1099
1100 header("Content-Type: text/calendar");
1101 header("Content-Disposition: inline; filename=".$calname.'.ics');
1102
1103 $this->get_ical()->export($events, '', true, array($this->driver, 'get_attachment_body'));
1104
1105 if ($terminate)
1106 exit;
1107 }
1108
1109
1110 /**
1111 * Handler for iCal feed requests
1112 */
1113 function ical_feed_export()
1114 {
1115 // process HTTP auth info
1116 if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
1117 $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
1118 $auth = $this->rc->plugins->exec_hook('authenticate', array(
1119 'host' => $this->rc->autoselect_host(),
1120 'user' => trim($_SERVER['PHP_AUTH_USER']),
1121 'pass' => $_SERVER['PHP_AUTH_PW'],
1122 'cookiecheck' => true,
1123 'valid' => true,
1124 ));
1125 if ($auth['valid'] && !$auth['abort'])
1126 $this->rc->login($auth['user'], $auth['pass'], $auth['host']);
1127 }
1128
1129 // require HTTP auth
1130 if (empty($_SESSION['user_id'])) {
1131 header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
1132 header('HTTP/1.0 401 Unauthorized');
1133 exit;
1134 }
1135
1136 // decode calendar feed hash
1137 $format = 'ics';
1138 $calhash = get_input_value('_cal', RCUBE_INPUT_GET);
1139 if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
1140 $format = strtolower($m[1]);
1141 $calhash = preg_replace($suff_regex, '', $calhash);
1142 }
1143
1144 if (!strpos($calhash, ':'))
1145 $calhash = base64_decode($calhash);
1146
1147 list($user, $_GET['source']) = explode(':', $calhash, 2);
1148
1149 // sanity check user
1150 if ($this->rc->user->get_username() == $user) {
1151 $this->load_driver();
1152 $this->export_events(false);
1153 }
1154 else {
1155 header('HTTP/1.0 404 Not Found');
1156 }
1157
1158 // don't save session data
1159 session_destroy();
1160 exit;
1161 }
1162
1163
1164 /**
1165 *
1166 */
1167 function load_settings()
1168 {
1169 $this->lib->load_settings();
1170 $this->defaults += $this->lib->defaults;
1171
1172 $settings = array();
1173
1174 // configuration
1175 $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
1176 $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
1177 $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
1178
1179 $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
1180 $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
1181 $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
1182 $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
1183 $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
1184 $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
1185 $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
1186 $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
1187 $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
1188 $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
1189
1190 // get user identity to create default attendee
1191 if ($this->ui->screen == 'calendar') {
1192 foreach ($this->rc->user->list_identities() as $rec) {
1193 if (!$identity)
1194 $identity = $rec;
1195 $identity['emails'][] = $rec['email'];
1196 $settings['identities'][$rec['identity_id']] = $rec['email'];
1197 }
1198 $identity['emails'][] = $this->rc->user->get_username();
1199 $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])));
1200 }
1201
1202 return $settings;
1203 }
1204
1205 /**
1206 * Encode events as JSON
1207 *
1208 * @param array Events as array
1209 * @param boolean Add CSS class names according to calendar and categories
1210 * @return string JSON encoded events
1211 */
1212 function encode($events, $addcss = false)
1213 {
1214 $json = array();
1215 foreach ($events as $event) {
1216 $json[] = $this->_client_event($event, $addcss);
1217 }
1218 return json_encode($json);
1219 }
1220
1221 /**
1222 * Convert an event object to be used on the client
1223 */
1224 private function _client_event($event, $addcss = false)
1225 {
1226 // compose a human readable strings for alarms_text and recurrence_text
1227 if ($event['alarms'])
1228 $event['alarms_text'] = libcalendaring::alarms_text($event['alarms']);
1229 if ($event['recurrence']) {
1230 $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
1231 if ($event['recurrence']['UNTIL'])
1232 $event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c');
1233 unset($event['recurrence']['EXCEPTIONS']);
1234 }
1235
1236 foreach ((array)$event['attachments'] as $k => $attachment) {
1237 $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
1238 }
1239
1240 // check for organizer in attendees list
1241 $organizer = null;
1242 foreach ((array)$event['attendees'] as $i => $attendee) {
1243 if ($attendee['role'] == 'ORGANIZER') {
1244 $organizer = $attendee;
1245 break;
1246 }
1247 }
1248
1249 if ($organizer === null && !empty($event['organizer'])) {
1250 $organizer = $event['organizer'];
1251 $organizer['role'] = 'ORGANIZER';
1252 if (!is_array($event['attendees']))
1253 $event['attendees'] = array();
1254 array_unshift($event['attendees'], $organizer);
1255 }
1256
1257 // mapping url => vurl because of the fullcalendar client script
1258 $event['vurl'] = $event['url'];
1259 unset($event['url']);
1260
1261 return array(
1262 '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
1263 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'),
1264 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
1265 // 'changed' might be empty for event recurrences (Bug #2185)
1266 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
1267 'title' => strval($event['title']),
1268 'description' => strval($event['description']),
1269 'location' => strval($event['location']),
1270 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true),
1271 'allDay' => ($event['allday'] == 1),
1272 ) + $event;
1273 }
1274
1275
1276 /**
1277 * Render localized text describing the recurrence rule of an event
1278 */
1279 private function _recurrence_text($rrule)
1280 {
1281 // TODO: finish this
1282 $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
1283 $details = '';
1284 switch ($rrule['FREQ']) {
1285 case 'DAILY':
1286 $freq .= $this->gettext('days');
1287 break;
1288 case 'WEEKLY':
1289 $freq .= $this->gettext('weeks');
1290 break;
1291 case 'MONTHLY':
1292 $freq .= $this->gettext('months');
1293 break;
1294 case 'YEARLY':
1295 $freq .= $this->gettext('years');
1296 break;
1297 }
1298
1299 if ($rrule['INTERVAL'] <= 1)
1300 $freq = $this->gettext(strtolower($rrule['FREQ']));
1301
1302 if ($rrule['COUNT'])
1303 $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
1304 else if ($rrule['UNTIL'])
1305 $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
1306 else
1307 $until = $this->gettext('forever');
1308
1309 return rtrim($freq . $details . ', ' . $until);
1310 }
1311
1312 /**
1313 * Generate a unique identifier for an event
1314 */
1315 public function generate_uid()
1316 {
1317 return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
1318 }
1319
1320
1321 /**
1322 * TEMPORARY: generate random event data for testing
1323 * Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500
1324 */
1325 public function generate_randomdata()
1326 {
1327 $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
1328 $cats = array_keys($this->driver->list_categories());
1329 $cals = $this->driver->list_calendars(true);
1330 $count = 0;
1331
1332 while ($count++ < $num) {
1333 $start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300;
1334 $duration = round(rand(30, 360) / 30) * 30 * 60;
1335 $allday = rand(0,20) > 18;
1336 $alarm = rand(-30,12) * 5;
1337 $fb = rand(0,2);
1338
1339 if (date('G', $start) > 23)
1340 $start -= 3600;
1341
1342 if ($allday) {
1343 $start = strtotime(date('Y-m-d 00:00:00', $start));
1344 $duration = 86399;
1345 }
1346
1347 $title = '';
1348 $len = rand(2, 12);
1349 $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
1350 // $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
1351 for ($i = 0; $i < $len; $i++)
1352 $title .= $words[rand(0,count($words)-1)] . " ";
1353
1354 $this->driver->new_event(array(
1355 'uid' => $this->generate_uid(),
1356 'start' => new DateTime('@'.$start),
1357 'end' => new DateTime('@'.($start + $duration)),
1358 'allday' => $allday,
1359 'title' => rtrim($title),
1360 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
1361 'categories' => $cats[array_rand($cats)],
1362 'calendar' => array_rand($cals),
1363 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
1364 'priority' => rand(0,9),
1365 ));
1366 }
1367
1368 $this->rc->output->redirect('');
1369 }
1370
1371 /**
1372 * Handler for attachments upload
1373 */
1374 public function attachment_upload()
1375 {
1376 $this->lib->attachment_upload(self::SESSION_KEY, 'cal:');
1377 }
1378
1379 /**
1380 * Handler for attachments download/displaying
1381 */
1382 public function attachment_get()
1383 {
1384 // show loading page
1385 if (!empty($_GET['_preload'])) {
1386 return $this->lib->attachment_loading_page();
1387 }
1388
1389 $event_id = get_input_value('_event', RCUBE_INPUT_GPC);
1390 $calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
1391 $id = get_input_value('_id', RCUBE_INPUT_GPC);
1392
1393 $event = array('id' => $event_id, 'calendar' => $calendar);
1394 $attachment = $this->driver->get_attachment($id, $event);
1395
1396 // show part page
1397 if (!empty($_GET['_frame'])) {
1398 $this->lib->attachment = $attachment;
1399 $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
1400 $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
1401 $this->rc->output->send('calendar.attachment');
1402 }
1403 // deliver attachment content
1404 else if ($attachment) {
1405 $attachment['body'] = $this->driver->get_attachment_body($id, $event);
1406 $this->lib->attachment_get($attachment);
1407 }
1408
1409 // if we arrive here, the requested part was not found
1410 header('HTTP/1.1 404 Not Found');
1411 exit;
1412 }
1413
1414
1415 /**
1416 * Prepares new/edited event properties before save
1417 */
1418 private function prepare_event(&$event, $action)
1419 {
1420 // convert dates into DateTime objects in user's current timezone
1421 $event['start'] = new DateTime($event['start'], $this->timezone);
1422 $event['end'] = new DateTime($event['end'], $this->timezone);
1423
1424 // start/end is all we need for 'move' action (#1480)
1425 if ($action == 'move') {
1426 return;
1427 }
1428
1429 if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL']))
1430 $event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone);
1431
1432 $attachments = array();
1433 $eventid = 'cal:'.$event['id'];
1434 if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
1435 if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
1436 foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
1437 if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
1438 $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
1439 }
1440 }
1441 }
1442 }
1443
1444 $event['attachments'] = $attachments;
1445
1446 // check for organizer in attendees
1447 if ($action == 'new' || $action == 'edit') {
1448 if (!$event['attendees'])
1449 $event['attendees'] = array();
1450
1451 $emails = $this->get_user_emails();
1452 $organizer = $owner = false;
1453 foreach ((array)$event['attendees'] as $i => $attendee) {
1454 if ($attendee['role'] == 'ORGANIZER')
1455 $organizer = $i;
1456 if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails))
1457 $owner = $i;
1458 else if (!isset($attendee['rsvp']))
1459 $event['attendees'][$i]['rsvp'] = true;
1460 }
1461
1462 // set new organizer identity
1463 if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) {
1464 $event['attendees'][$organizer]['name'] = $identity['name'];
1465 $event['attendees'][$organizer]['email'] = $identity['email'];
1466 }
1467
1468 // set owner as organizer if yet missing
1469 if ($organizer === false && $owner !== false) {
1470 $event['attendees'][$owner]['role'] = 'ORGANIZER';
1471 unset($event['attendees'][$owner]['rsvp']);
1472 }
1473 else if ($organizer === false && $action == 'new' && ($identity = $this->rc->user->get_identity($event['_identity'])) && $identity['email']) {
1474 array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
1475 }
1476 }
1477
1478 // mapping url => vurl because of the fullcalendar client script
1479 $event['url'] = $event['vurl'];
1480 unset($event['vurl']);
1481 }
1482
1483 /**
1484 * Releases some resources after successful event save
1485 */
1486 private function cleanup_event(&$event)
1487 {
1488 // remove temp. attachment files
1489 if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
1490 $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
1491 $this->rc->session->remove(self::SESSION_KEY);
1492 }
1493 }
1494
1495 /**
1496 * Send out an invitation/notification to all event attendees
1497 */
1498 private function notify_attendees($event, $old, $action = 'edit')
1499 {
1500 if ($action == 'remove') {
1501 $event['cancelled'] = true;
1502 $is_cancelled = true;
1503 }
1504
1505 $itip = $this->load_itip();
1506 $emails = $this->get_user_emails();
1507
1508 // compose multipart message using PEAR:Mail_Mime
1509 $method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
1510 $message = $itip->compose_itip_message($event, $method);
1511
1512 // list existing attendees from $old event
1513 $old_attendees = array();
1514 foreach ((array)$old['attendees'] as $attendee) {
1515 $old_attendees[] = $attendee['email'];
1516 }
1517
1518 // send to every attendee
1519 $sent = 0;
1520 foreach ((array)$event['attendees'] as $attendee) {
1521 // skip myself for obvious reasons
1522 if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails))
1523 continue;
1524
1525 // which template to use for mail text
1526 $is_new = !in_array($attendee['email'], $old_attendees);
1527 $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
1528 $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
1529
1530 // finally send the message
1531 if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
1532 $sent++;
1533 else
1534 $sent = -100;
1535 }
1536
1537 return $sent;
1538 }
1539
1540 /**
1541 * Echo simple free/busy status text for the given user and time range
1542 */
1543 public function freebusy_status()
1544 {
1545 $email = get_input_value('email', RCUBE_INPUT_GPC);
1546 $start = get_input_value('start', RCUBE_INPUT_GPC);
1547 $end = get_input_value('end', RCUBE_INPUT_GPC);
1548
1549 // convert dates into unix timestamps
1550 if (!empty($start) && !is_numeric($start)) {
1551 $dts = new DateTime($start, $this->timezone);
1552 $start = $dts->format('U');
1553 }
1554 if (!empty($end) && !is_numeric($end)) {
1555 $dte = new DateTime($end, $this->timezone);
1556 $end = $dte->format('U');
1557 }
1558
1559 if (!$start) $start = time();
1560 if (!$end) $end = $start + 3600;
1561
1562 $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
1563 $status = 'UNKNOWN';
1564
1565 // if the backend has free-busy information
1566 $fblist = $this->driver->get_freebusy_list($email, $start, $end);
1567 if (is_array($fblist)) {
1568 $status = 'FREE';
1569
1570 foreach ($fblist as $slot) {
1571 list($from, $to, $type) = $slot;
1572 if ($from < $end && $to > $start) {
1573 $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
1574 break;
1575 }
1576 }
1577 }
1578
1579 // let this information be cached for 5min
1580 send_future_expire_header(300);
1581
1582 echo $status;
1583 exit;
1584 }
1585
1586 /**
1587 * Return a list of free/busy time slots within the given period
1588 * Echo data in JSON encoding
1589 */
1590 public function freebusy_times()
1591 {
1592 $email = get_input_value('email', RCUBE_INPUT_GPC);
1593 $start = get_input_value('start', RCUBE_INPUT_GPC);
1594 $end = get_input_value('end', RCUBE_INPUT_GPC);
1595 $interval = intval(get_input_value('interval', RCUBE_INPUT_GPC));
1596 $strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
1597
1598 // convert dates into unix timestamps
1599 if (!empty($start) && !is_numeric($start)) {
1600 $dts = new DateTime($start, $this->timezone);
1601 $start = $dts->format('U');
1602 }
1603 if (!empty($end) && !is_numeric($end)) {
1604 $dte = new DateTime($end, $this->timezone);
1605 $end = $dte->format('U');
1606 }
1607
1608 if (!$start) $start = time();
1609 if (!$end) $end = $start + 86400 * 30;
1610 if (!$interval) $interval = 60; // 1 hour
1611
1612 if (!$dte) {
1613 $dts = new DateTime('@'.$start);
1614 $dts->setTimezone($this->timezone);
1615 }
1616
1617 $fblist = $this->driver->get_freebusy_list($email, $start, $end);
1618 $slots = array();
1619
1620 // build a list from $start till $end with blocks representing the fb-status
1621 for ($s = 0, $t = $start; $t <= $end; $s++) {
1622 $status = self::FREEBUSY_UNKNOWN;
1623 $t_end = $t + $interval * 60;
1624 $dt = new DateTime('@'.$t);
1625 $dt->setTimezone($this->timezone);
1626
1627 // determine attendee's status
1628 if (is_array($fblist)) {
1629 $status = self::FREEBUSY_FREE;
1630 foreach ($fblist as $slot) {
1631 list($from, $to, $type) = $slot;
1632 if ($from < $t_end && $to > $t) {
1633 $status = isset($type) ? $type : self::FREEBUSY_BUSY;
1634 if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
1635 break;
1636 }
1637 }
1638 }
1639
1640 $slots[$s] = $status;
1641 $times[$s] = intval($dt->format($strformat));
1642 $t = $t_end;
1643 }
1644
1645 $dte = new DateTime('@'.$t_end);
1646 $dte->setTimezone($this->timezone);
1647
1648 // let this information be cached for 5min
1649 send_future_expire_header(300);
1650
1651 echo json_encode(array(
1652 'email' => $email,
1653 'start' => $dts->format('c'),
1654 'end' => $dte->format('c'),
1655 'interval' => $interval,
1656 'slots' => $slots,
1657 'times' => $times,
1658 ));
1659 exit;
1660 }
1661
1662 /**
1663 * Handler for printing calendars
1664 */
1665 public function print_view()
1666 {
1667 $title = $this->gettext('print');
1668
1669 $view = get_input_value('view', RCUBE_INPUT_GPC);
1670 if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
1671 $view = 'agendaDay';
1672
1673 $this->rc->output->set_env('view',$view);
1674
1675 if ($date = get_input_value('date', RCUBE_INPUT_GPC))
1676 $this->rc->output->set_env('date', $date);
1677
1678 if ($range = get_input_value('range', RCUBE_INPUT_GPC))
1679 $this->rc->output->set_env('listRange', intval($range));
1680
1681 if (isset($_REQUEST['sections']))
1682 $this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC));
1683
1684 if ($search = get_input_value('search', RCUBE_INPUT_GPC)) {
1685 $this->rc->output->set_env('search', $search);
1686 $title .= ' "' . $search . '"';
1687 }
1688
1689 // Add CSS stylesheets to the page header
1690 $skin_path = $this->local_skin_path();
1691 $this->include_stylesheet($skin_path . '/fullcalendar.css');
1692 $this->include_stylesheet($skin_path . '/print.css');
1693
1694 // Add JS files to the page header
1695 $this->include_script('print.js');
1696 $this->include_script('lib/js/fullcalendar.js');
1697
1698 $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
1699 $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
1700
1701 $this->rc->output->set_pagetitle($title);
1702 $this->rc->output->send("calendar.print");
1703 }
1704
1705 /**
1706 *
1707 */
1708 public function get_inline_ui()
1709 {
1710 foreach (array('save','cancel','savingdata') as $label)
1711 $texts['calendar.'.$label] = $this->gettext($label);
1712
1713 $texts['calendar.new_event'] = $this->gettext('createfrommail');
1714
1715 $this->ui->init_templates();
1716 $this->ui->calendar_list(); # set env['calendars']
1717 echo $this->api->output->parse('calendar.eventedit', false, false);
1718 echo html::tag('script', array('type' => 'text/javascript'),
1719 "rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n".
1720 "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
1721 "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
1722 "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
1723 "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n".
1724 "rcmail.add_label(" . json_encode($texts) . ");\n"
1725 );
1726 exit;
1727 }
1728
1729 /**
1730 * Compare two event objects and return differing properties
1731 *
1732 * @param array Event A
1733 * @param array Event B
1734 * @return array List of differing event properties
1735 */
1736 public static function event_diff($a, $b)
1737 {
1738 $diff = array();
1739 $ignore = array('changed' => 1, 'attachments' => 1);
1740 foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
1741 if (!$ignore[$key] && $a[$key] != $b[$key])
1742 $diff[] = $key;
1743 }
1744
1745 // only compare number of attachments
1746 if (count($a['attachments']) != count($b['attachments']))
1747 $diff[] = 'attachments';
1748
1749 return $diff;
1750 }
1751
1752
1753 /**** Event invitation plugin hooks ****/
1754
1755 /**
1756 * Handler for URLs that allow an invitee to respond on his invitation mail
1757 */
1758 public function itip_attend_response($p)
1759 {
1760 if ($p['action'] == 'attend') {
1761 $this->rc->output->set_env('task', 'calendar'); // override some env vars
1762 $this->rc->output->set_env('refresh_interval', 0);
1763 $this->rc->output->set_pagetitle($this->gettext('calendar'));
1764
1765 $itip = $this->load_itip();
1766 $token = get_input_value('_t', RCUBE_INPUT_GPC);
1767
1768 // read event info stored under the given token
1769 if ($invitation = $itip->get_invitation($token)) {
1770 $this->token = $token;
1771 $this->event = $invitation['event'];
1772
1773 // show message about cancellation
1774 if ($invitation['cancelled']) {
1775 $this->invitestatus = html::div('rsvp-status declined', $this->gettext('eventcancelled'));
1776 }
1777 // save submitted RSVP status
1778 else if (!empty($_POST['rsvp'])) {
1779 $status = null;
1780 foreach (array('accepted','tentative','declined') as $method) {
1781 if ($_POST['rsvp'] == $this->gettext('itip' . $method)) {
1782 $status = $method;
1783 break;
1784 }
1785 }
1786
1787 // send itip reply to organizer
1788 if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
1789 $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
1790 }
1791 else
1792 $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
1793
1794 // if user is logged in...
1795 if ($this->rc->user->ID) {
1796 $this->load_driver();
1797 $invitation = $itip->get_invitation($token);
1798
1799 // save the event to his/her default calendar if not yet present
1800 if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar(true))) {
1801 $invitation['event']['calendar'] = $calendar['id'];
1802 if ($this->driver->new_event($invitation['event']))
1803 $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
1804 }
1805 }
1806 }
1807
1808 $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
1809 $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
1810
1811 if (!$this->invitestatus)
1812 $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
1813
1814 $this->rc->output->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event['title']);
1815 }
1816 else
1817 $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
1818
1819 $this->rc->output->send('calendar.itipattend');
1820 }
1821 }
1822
1823 /**
1824 *
1825 */
1826 public function itip_event_inviteform($attrib)
1827 {
1828 $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
1829 return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
1830 }
1831
1832 /**
1833 * Check mail message structure of there are .ics files attached
1834 */
1835 public function mail_message_load($p)
1836 {
1837 $this->message = $p['object'];
1838 $itip_part = null;
1839
1840 // check all message parts for .ics files
1841 foreach ((array)$this->message->mime_parts as $part) {
1842 if ($this->is_vcalendar($part)) {
1843 if ($part->ctype_parameters['method'])
1844 $itip_part = $part->mime_id;
1845 else
1846 $this->ics_parts[] = $part->mime_id;
1847 }
1848 }
1849
1850 // priorize part with method parameter
1851 if ($itip_part)
1852 $this->ics_parts = array($itip_part);
1853 }
1854
1855 /**
1856 * Add UI element to copy event invitations or updates to the calendar
1857 */
1858 public function mail_messagebody_html($p)
1859 {
1860 // load iCalendar functions (if necessary)
1861 if (!empty($this->ics_parts)) {
1862 $this->get_ical();
1863 }
1864
1865 $html = '';
1866 foreach ($this->ics_parts as $mime_id) {
1867 $part = $this->message->mime_parts[$mime_id];
1868 $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
1869 $events = $this->ical->import($this->message->get_part_content($mime_id), $charset);
1870 $title = $this->gettext('title');
1871 $date = rcube_utils::anytodatetime($this->message->headers->date);
1872
1873 // successfully parsed events?
1874 if (empty($events))
1875 continue;
1876
1877 // show a box for every event in the file
1878 foreach ($events as $idx => $event) {
1879 // define buttons according to method
1880 if ($this->ical->method == 'REPLY') {
1881 $title = $this->gettext('itipreply');
1882 $buttons = html::tag('input', array(
1883 'type' => 'button',
1884 'class' => 'button',
1885 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1886 'value' => $this->gettext('updateattendeestatus'),
1887 ));
1888 }
1889 else if ($this->ical->method == 'REQUEST') {
1890 $emails = $this->get_user_emails();
1891 $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
1892
1893 // add (hidden) buttons and activate them from asyncronous request
1894 foreach (array('accepted','tentative','declined') as $method) {
1895 $rsvp_buttons .= html::tag('input', array(
1896 'type' => 'button',
1897 'class' => "button $method",
1898 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')",
1899 'value' => $this->gettext('itip' . $method),
1900 ));
1901 }
1902 $import_button = html::tag('input', array(
1903 'type' => 'button',
1904 'class' => 'button',
1905 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1906 'value' => $this->gettext('importtocalendar'),
1907 ));
1908
1909 // check my status
1910 $status = 'unknown';
1911 foreach ($event['attendees'] as $attendee) {
1912 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
1913 $status = strtoupper($attendee['status']);
1914 break;
1915 }
1916 }
1917
1918 $dom_id = asciiwords($event['uid'], true);
1919 $buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
1920 $buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
1921 $buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
1922 $changed = is_object($event['changed']) ? $event['changed'] : $date;
1923
1924 $script = json_serialize(array(
1925 'uid' => $event['uid'],
1926 'changed' => $changed ? $changed->format('U') : 0,
1927 'sequence' => intval($event['sequence']),
1928 'fallback' => $status,
1929 ));
1930
1931 $this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
1932 }
1933 else if ($this->ical->method == 'CANCEL') {
1934 $title = $this->gettext('itipcancellation');
1935
1936 // create buttons to be activated from async request checking existence of this event in local calendars
1937 $button_import = html::tag('input', array(
1938 'type' => 'button',
1939 'class' => 'button',
1940 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1941 'value' => $this->gettext('importtocalendar'),
1942 ));
1943 $button_remove = html::tag('input', array(
1944 'type' => 'button',
1945 'class' => 'button',
1946 'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
1947 'value' => $this->gettext('removefromcalendar'),
1948 ));
1949
1950 $dom_id = asciiwords($event['uid'], true);
1951 $buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
1952 $buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
1953 $buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
1954 $changed = is_object($event['changed']) ? $event['changed'] : $date;
1955
1956 $script = json_serialize(array(
1957 'uid' => $event['uid'],
1958 'changed' => $changed ? $changed->format('U') : 0,
1959 'sequence' => intval($event['sequence']),
1960 'fallback' => 'CANCELLED',
1961 ));
1962
1963 $this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
1964 }
1965 else {
1966 $buttons = html::tag('input', array(
1967 'type' => 'button',
1968 'class' => 'button',
1969 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
1970 'value' => $this->gettext('importtocalendar'),
1971 ));
1972 }
1973
1974 // show event details with buttons
1975 $html .= html::div('calendar-invitebox', $this->ui->event_details_table($event, $title) . $buttons_pre . html::div('rsvp-buttons', $buttons));
1976
1977 // limit listing
1978 if ($idx >= 3)
1979 break;
1980 }
1981 }
1982
1983 // prepend event boxes to message body
1984 if ($html) {
1985 $this->ui->init();
1986 $p['content'] = $html . $p['content'];
1987 $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
1988 }
1989
1990 return $p;
1991 }
1992
1993
1994 /**
1995 * Handler for POST request to import an event attached to a mail message
1996 */
1997 public function mail_import_event()
1998 {
1999 $uid = get_input_value('_uid', RCUBE_INPUT_POST);
2000 $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
2001 $mime_id = get_input_value('_part', RCUBE_INPUT_POST);
2002 $status = get_input_value('_status', RCUBE_INPUT_POST);
2003 $delete = intval(get_input_value('_del', RCUBE_INPUT_POST));
2004 $charset = RCMAIL_CHARSET;
2005
2006 // establish imap connection
2007 $imap = $this->rc->get_storage();
2008 $imap->set_mailbox($mbox);
2009
2010 if ($uid && $mime_id) {
2011 list($mime_id, $index) = explode(':', $mime_id);
2012 $part = $imap->get_message_part($uid, $mime_id);
2013 if ($part->ctype_parameters['charset'])
2014 $charset = $part->ctype_parameters['charset'];
2015 $headers = $imap->get_message_headers($uid);
2016 }
2017
2018 $events = $this->get_ical()->import($part, $charset);
2019
2020 $error_msg = $this->gettext('errorimportingevent');
2021 $success = false;
2022
2023 // successfully parsed events?
2024 if (!empty($events) && ($event = $events[$index])) {
2025 // find writeable calendar to store event
2026 $cal_id = !empty($_REQUEST['_calendar']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : null;
2027 $calendars = $this->driver->list_calendars(false, true);
2028 $calendar = $calendars[$cal_id] ?: $this->get_default_calendar(true);
2029
2030 // update my attendee status according to submitted method
2031 if (!empty($status)) {
2032 $organizer = null;
2033 $emails = $this->get_user_emails();
2034 foreach ($event['attendees'] as $i => $attendee) {
2035 if ($attendee['role'] == 'ORGANIZER') {
2036 $organizer = $attendee;
2037 }
2038 else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
2039 $event['attendees'][$i]['status'] = strtoupper($status);
2040 $reply_sender = $attendee['email'];
2041 }
2042 }
2043 }
2044
2045 // save to calendar
2046 if ($calendar && !$calendar['readonly']) {
2047 $event['calendar'] = $calendar['id'];
2048
2049 // check for existing event with the same UID
2050 $existing = $this->driver->get_event($event['uid'], true, false, true);
2051
2052 if ($existing) {
2053 // only update attendee status
2054 if ($this->ical->method == 'REPLY') {
2055 // try to identify the attendee using the email sender address
2056 $sender = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
2057 $sender_utf = rcube_idn_to_utf8($sender);
2058
2059 $existing_attendee = -1;
2060 foreach ($existing['attendees'] as $i => $attendee) {
2061 if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
2062 $existing_attendee = $i;
2063 break;
2064 }
2065 }
2066 $event_attendee = null;
2067 foreach ($event['attendees'] as $attendee) {
2068 if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
2069 $event_attendee = $attendee;
2070 break;
2071 }
2072 }
2073
2074 // found matching attendee entry in both existing and new events
2075 if ($existing_attendee >= 0 && $event_attendee) {
2076 $existing['attendees'][$existing_attendee] = $event_attendee;
2077 $success = $this->driver->edit_event($existing);
2078 }
2079 // update the entire attendees block
2080 else if ($event['changed'] >= $existing['changed'] && $event['attendees']) {
2081 $existing['attendees'] = $event['attendees'];
2082 $success = $this->driver->edit_event($existing);
2083 }
2084 else {
2085 $error_msg = $this->gettext('newerversionexists');
2086 }
2087 }
2088 // delete the event when declined (#1670)
2089 else if ($status == 'declined' && $delete) {
2090 $deleted = $this->driver->remove_event($existing, true);
2091 $success = true;
2092 }
2093 // import the (newer) event
2094 else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
2095 $event['id'] = $existing['id'];
2096 $event['calendar'] = $existing['calendar'];
2097 if ($status == 'declined') // show me as free when declined (#1670)
2098 $event['free_busy'] = 'free';
2099 $success = $this->driver->edit_event($event);
2100 }
2101 else if (!empty($status)) {
2102 $existing['attendees'] = $event['attendees'];
2103 if ($status == 'declined') // show me as free when declined (#1670)
2104 $existing['free_busy'] = 'free';
2105 $success = $this->driver->edit_event($existing);
2106 }
2107 else
2108 $error_msg = $this->gettext('newerversionexists');
2109 }
2110 else if (!$existing && $status != 'declined') {
2111 $success = $this->driver->new_event($event);
2112 }
2113 else if ($status == 'declined')
2114 $error_msg = null;
2115 }
2116 else if ($status == 'declined')
2117 $error_msg = null;
2118 else
2119 $error_msg = $this->gettext('nowritecalendarfound');
2120 }
2121
2122 if ($success) {
2123 $message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : 'importedsuccessfully');
2124 $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
2125 $this->rc->output->command('plugin.fetch_event_rsvp_status', array('uid' => $event['uid'], 'changed' => $event['changed']->format('U'), 'sequence' => intval($event['sequence']), 'fallback' => strtoupper($status)));
2126 $error_msg = null;
2127 }
2128 else if ($error_msg)
2129 $this->rc->output->command('display_message', $error_msg, 'error');
2130
2131
2132 // send iTip reply
2133 if ($this->ical->method == 'REQUEST' && $organizer && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
2134 $itip = $this->load_itip();
2135 $itip->set_sender_email($reply_sender);
2136 if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
2137 $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
2138 else
2139 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
2140 }
2141
2142 $this->rc->output->send();
2143 }
2144
2145
2146 /**
2147 * Read email message and return contents for a new event based on that message
2148 */
2149 public function mail_message2event()
2150 {
2151 $uid = get_input_value('_uid', RCUBE_INPUT_POST);
2152 $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
2153 $event = array();
2154
2155 // establish imap connection
2156 $imap = $this->rc->get_storage();
2157 $imap->set_mailbox($mbox);
2158 $message = new rcube_message($uid);
2159
2160 if ($message->headers) {
2161 $event['title'] = trim($message->subject);
2162 $event['description'] = trim($message->first_text_part());
2163
2164 // copy mail attachments to event
2165 if ($message->attachments) {
2166 $eventid = 'cal:';
2167 if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
2168 $_SESSION[self::SESSION_KEY] = array();
2169 $_SESSION[self::SESSION_KEY]['id'] = $eventid;
2170 $_SESSION[self::SESSION_KEY]['attachments'] = array();
2171 }
2172
2173 foreach ((array)$message->attachments as $part) {
2174 $attachment = array(
2175 'data' => $imap->get_message_part($uid, $part->mime_id, $part),
2176 'size' => $part->size,
2177 'name' => $part->filename,
2178 'mimetype' => $part->mimetype,
2179 'group' => $eventid,
2180 );
2181
2182 $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
2183
2184 if ($attachment['status'] && !$attachment['abort']) {
2185 $id = $attachment['id'];
2186 $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
2187
2188 // store new attachment in session
2189 unset($attachment['status'], $attachment['abort'], $attachment['data']);
2190 $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
2191
2192 $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
2193 $event['attachments'][] = $attachment;
2194 }
2195 }
2196 }
2197
2198 $this->rc->output->command('plugin.mail2event_dialog', $event);
2199 }
2200 else {
2201 $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
2202 }
2203
2204 $this->rc->output->send();
2205 }
2206
2207
2208 /**
2209 * Checks if specified message part is a vcalendar data
2210 *
2211 * @param rcube_message_part Part object
2212 * @return boolean True if part is of type vcard
2213 */
2214 private function is_vcalendar($part)
2215 {
2216 return (
2217 in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
2218 // Apple sends files as application/x-any (!?)
2219 ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
2220 );
2221 }
2222
2223
2224 /**
2225 * Get a list of email addresses of the current user (from login and identities)
2226 */
2227 private function get_user_emails()
2228 {
2229 $emails = array();
2230 $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
2231 $emails = array_map('strtolower', $plugin['emails']);
2232
2233 if ($plugin['abort']) {
2234 return $emails;
2235 }
2236
2237 $emails[] = $this->rc->user->get_username();
2238 foreach ($this->rc->user->list_identities() as $identity)
2239 $emails[] = strtolower($identity['email']);
2240
2241 return array_unique($emails);
2242 }
2243
2244
2245 /**
2246 * Build an absolute URL with the given parameters
2247 */
2248 public function get_url($param = array())
2249 {
2250 $param += array('task' => 'calendar');
2251
2252 $schema = 'http';
2253 $default_port = 80;
2254 if (rcube_https_check()) {
2255 $schema = 'https';
2256 $default_port = 443;
2257 }
2258 $url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
2259 if ($_SERVER['SERVER_PORT'] != $default_port)
2260 $url .= ':' . $_SERVER['SERVER_PORT'];
2261 if (dirname($_SERVER['SCRIPT_NAME']) != '/')
2262 $url .= dirname($_SERVER['SCRIPT_NAME']);
2263 $url .= preg_replace('!^\./!', '/', $this->rc->url($param));
2264
2265 return $url;
2266 }
2267
2268
2269 public function ical_feed_hash($source)
2270 {
2271 return base64_encode($this->rc->user->get_username() . ':' . $source);
2272 }
2273
2274 }
2275
This page took 0.43506 seconds and 6 git commands to generate.