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