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