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