comparison plugins/libcalendaring/lib/libcalendaring_itip.php @ 4:888e774ee983

libcalendar plugin as distributed
author Charlie Root
date Sat, 13 Jan 2018 08:57:56 -0500
parents
children
comparison
equal deleted inserted replaced
3:f6fe4b6ae66a 4:888e774ee983
1 <?php
2
3 /**
4 * iTIP functions for the calendar-based Roudncube plugins
5 *
6 * Class providing functionality to manage iTIP invitations
7 *
8 * @author Thomas Bruederli <bruederli@kolabsys.com>
9 *
10 * Copyright (C) 2011-2014, 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 class libcalendaring_itip
26 {
27 protected $rc;
28 protected $lib;
29 protected $plugin;
30 protected $sender;
31 protected $domain;
32 protected $itip_send = false;
33 protected $rsvp_actions = array('accepted','tentative','declined','delegated');
34 protected $rsvp_status = array('accepted','tentative','declined','delegated');
35
36 function __construct($plugin, $domain = 'libcalendaring')
37 {
38 $this->plugin = $plugin;
39 $this->rc = rcube::get_instance();
40 $this->lib = libcalendaring::get_instance();
41 $this->domain = $domain;
42
43 $hook = $this->rc->plugins->exec_hook('calendar_load_itip',
44 array('identity' => $this->rc->user->list_emails(true)));
45 $this->sender = $hook['identity'];
46
47 $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook'));
48 $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook'));
49 }
50
51 public function set_sender_email($email)
52 {
53 if (!empty($email))
54 $this->sender['email'] = $email;
55 }
56
57 public function set_rsvp_actions($actions)
58 {
59 $this->rsvp_actions = (array)$actions;
60 $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
61 }
62
63 public function set_rsvp_status($status)
64 {
65 $this->rsvp_status = $status;
66 }
67
68 /**
69 * Wrapper for rcube_plugin::gettext()
70 * Checking for a label in different domains
71 *
72 * @see rcube::gettext()
73 */
74 public function gettext($p)
75 {
76 $label = is_array($p) ? $p['name'] : $p;
77 $domain = $this->domain;
78 if (!$this->rc->text_exists($label, $domain)) {
79 $domain = 'libcalendaring';
80 }
81 return $this->rc->gettext($p, $domain);
82 }
83
84 /**
85 * Send an iTip mail message
86 *
87 * @param array Event object to send
88 * @param string iTip method (REQUEST|REPLY|CANCEL)
89 * @param array Hash array with recipient data (name, email)
90 * @param string Mail subject
91 * @param string Mail body text label
92 * @param object Mail_mime object with message data
93 * @param boolean Request RSVP
94 * @return boolean True on success, false on failure
95 */
96 public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true)
97 {
98 if (!$this->sender['name'])
99 $this->sender['name'] = $this->sender['email'];
100
101 if (!$message) {
102 libcalendaring::identify_recurrence_instance($event);
103 $message = $this->compose_itip_message($event, $method, $rsvp);
104 }
105
106 $mailto = rcube_utils::idn_to_ascii($recipient['email']);
107
108 $headers = $message->headers();
109 $headers['To'] = format_email_recipient($mailto, $recipient['name']);
110 $headers['Subject'] = $this->gettext(array(
111 'name' => $subject,
112 'vars' => array(
113 'title' => $event['title'],
114 'name' => $this->sender['name']
115 )
116 ));
117
118 // compose a list of all event attendees
119 $attendees_list = array();
120 foreach ((array)$event['attendees'] as $attendee) {
121 $attendees_list[] = ($attendee['name'] && $attendee['email']) ?
122 $attendee['name'] . ' <' . $attendee['email'] . '>' :
123 ($attendee['name'] ? $attendee['name'] : $attendee['email']);
124 }
125
126 $recurrence_info = '';
127 if (!empty($event['recurrence_id'])) {
128 $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
129 }
130 else if (!empty($event['recurrence'])) {
131 $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
132 }
133
134 $mailbody = $this->gettext(array(
135 'name' => $bodytext,
136 'vars' => array(
137 'title' => $event['title'],
138 'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
139 'attendees' => join(",\n ", $attendees_list),
140 'sender' => $this->sender['name'],
141 'organizer' => $this->sender['name'],
142 )
143 ));
144
145 // if (!empty($event['comment'])) {
146 // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment'];
147 // }
148
149 // append links for direct invitation replies
150 if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) {
151 $mailbody .= "\n\n" . $this->gettext(array(
152 'name' => 'invitationattendlinks',
153 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))),
154 ));
155 }
156 else if ($method == 'CANCEL' && $event['cancelled']) {
157 $this->cancel_itip_invitation($event);
158 }
159
160 $message->headers($headers, true);
161 $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
162
163 if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
164 rcube::console('iTip ' . $method, $message->txtHeaders() . "\r\n" . $message->get());
165 }
166
167 // finally send the message
168 $this->itip_send = true;
169 $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
170 $this->itip_send = false;
171
172 return $sent;
173 }
174
175 /**
176 * Plugin hook triggered by rcube::deliver_message() before delivering a message.
177 * Here we can set the 'smtp_server' config option to '' in order to use
178 * PHP's mail() function for unauthenticated email sending.
179 */
180 public function before_send_hook($p)
181 {
182 if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') {
183 $this->rc->config->set('smtp_server', '');
184 }
185
186 return $p;
187 }
188
189 /**
190 * Plugin hook to alter SMTP authentication.
191 * This is used if iTip messages are to be sent from an unauthenticated session
192 */
193 public function smtp_connect_hook($p)
194 {
195 // replace smtp auth settings if we're not in an authenticated session
196 if ($this->itip_send && !$this->rc->user->ID) {
197 foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) {
198 $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]);
199 }
200 }
201
202 return $p;
203 }
204
205 /**
206 * Helper function to build a Mail_mime object to send an iTip message
207 *
208 * @param array Event object to send
209 * @param string iTip method (REQUEST|REPLY|CANCEL)
210 * @param boolean Request RSVP
211 * @return object Mail_mime object with message data
212 */
213 public function compose_itip_message($event, $method, $rsvp = true)
214 {
215 $from = rcube_utils::idn_to_ascii($this->sender['email']);
216 $from_utf = rcube_utils::idn_to_utf8($from);
217 $sender = format_email_recipient($from, $this->sender['name']);
218
219 // truncate list attendees down to the recipient of the iTip Reply.
220 // constraints for a METHOD:REPLY according to RFC 5546
221 if ($method == 'REPLY') {
222 $replying_attendee = null;
223 $reply_attendees = array();
224 foreach ($event['attendees'] as $attendee) {
225 if ($attendee['role'] == 'ORGANIZER') {
226 $reply_attendees[] = $attendee;
227 }
228 else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
229 $replying_attendee = $attendee;
230 if ($attendee['status'] != 'DELEGATED') {
231 unset($replying_attendee['rsvp']); // unset the RSVP attribute
232 }
233 }
234 // include attendees relevant for delegation (RFC 5546, Section 4.2.5)
235 else if ((!empty($attendee['delegated-to']) &&
236 (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) ||
237 (!empty($attendee['delegated-from']) &&
238 (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) {
239 $reply_attendees[] = $attendee;
240 }
241 }
242 if ($replying_attendee) {
243 array_unshift($reply_attendees, $replying_attendee);
244 $event['attendees'] = $reply_attendees;
245 }
246 if ($event['recurrence']) {
247 unset($event['recurrence']['EXCEPTIONS']);
248 }
249 }
250 // set RSVP for every attendee
251 else if ($method == 'REQUEST') {
252 foreach ($event['attendees'] as $i => $attendee) {
253 if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) {
254 $event['attendees'][$i]['rsvp']= (bool)$rsvp;
255 }
256 }
257 }
258 else if ($method == 'CANCEL') {
259 if ($event['recurrence']) {
260 unset($event['recurrence']['EXCEPTIONS']);
261 }
262 }
263
264 // Set SENT-BY property if the sender is not the organizer
265 if ($method == 'CANCEL' || $method == 'REQUEST') {
266 foreach ((array)$event['attendees'] as $idx => $attendee) {
267 if ($attendee['role'] == 'ORGANIZER'
268 && $attendee['email']
269 && strcasecmp($attendee['email'], $from) != 0
270 && strcasecmp($attendee['email'], $from_utf) != 0
271 ) {
272 $attendee['sent-by'] = 'mailto:' . $from_utf;
273 $event['organizer'] = $event['attendees'][$idx] = $attendee;
274 break;
275 }
276 }
277 }
278
279 // compose multipart message using PEAR:Mail_Mime
280 $message = new Mail_mime("\r\n");
281 $message->setParam('text_encoding', 'quoted-printable');
282 $message->setParam('head_encoding', 'quoted-printable');
283 $message->setParam('head_charset', RCUBE_CHARSET);
284 $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed");
285 $message->setContentType('multipart/alternative');
286
287 // compose common headers array
288 $headers = array(
289 'From' => $sender,
290 'Date' => $this->rc->user_date(),
291 'Message-ID' => $this->rc->gen_message_id(),
292 'X-Sender' => $from,
293 );
294 if ($agent = $this->rc->config->get('useragent')) {
295 $headers['User-Agent'] = $agent;
296 }
297
298 $message->headers($headers);
299
300 // attach ics file for this event
301 $ical = libcalendaring::get_ical();
302 $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
303 $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
304 $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method);
305
306 return $message;
307 }
308
309 /**
310 * Forward the given iTip event as delegation to another person
311 *
312 * @param array Event object to delegate
313 * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
314 * @param boolean The delegator's RSVP flag
315 * @param array List with indexes of new/updated attendees
316 * @return boolean True on success, False on failure
317 */
318 public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
319 {
320 if (is_string($delegate)) {
321 $delegates = rcube_mime::decode_address_list($delegate, 1, false);
322 if (count($delegates) > 0) {
323 $delegate = reset($delegates);
324 }
325 }
326
327 $emails = $this->lib->get_user_emails();
328 $me = $this->rc->user->list_emails(true);
329
330 // find/create the delegate attendee
331 $delegate_attendee = array(
332 'email' => $delegate['mailto'],
333 'name' => $delegate['name'],
334 'role' => 'REQ-PARTICIPANT',
335 );
336 $delegate_index = count($event['attendees']);
337
338 foreach ($event['attendees'] as $i => $attendee) {
339 // set myself the DELEGATED-TO parameter
340 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
341 $event['attendees'][$i]['delegated-to'] = $delegate['mailto'];
342 $event['attendees'][$i]['status'] = 'DELEGATED';
343 $event['attendees'][$i]['role'] = 'NON-PARTICIPANT';
344 $event['attendees'][$i]['rsvp'] = $rsvp;
345
346 $me['email'] = $attendee['email'];
347 $delegate_attendee['role'] = $attendee['role'];
348 }
349 // the disired delegatee is already listed as an attendee
350 else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') {
351 $delegate_attendee = $attendee;
352 $delegate_index = $i;
353 break;
354 }
355 // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me)
356 }
357
358 // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
359 $delegate_attendee['rsvp'] = true;
360 $delegate_attendee['status'] = 'NEEDS-ACTION';
361 $delegate_attendee['delegated-from'] = $me['email'];
362 $event['attendees'][$delegate_index] = $delegate_attendee;
363
364 $attendees[] = $delegate_index;
365
366 $this->set_sender_email($me['email']);
367 return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
368 }
369
370 /**
371 * Handler for calendar/itip-status requests
372 */
373 public function get_itip_status($event, $existing = null)
374 {
375 $action = $event['rsvp'] ? 'rsvp' : '';
376 $status = $event['fallback'];
377 $latest = $resheduled = false;
378 $html = '';
379
380 if (is_numeric($event['changed']))
381 $event['changed'] = new DateTime('@'.$event['changed']);
382
383 // check if the given itip object matches the last state
384 if ($existing) {
385 $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
386 (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
387 }
388
389 // determine action for REQUEST
390 if ($event['method'] == 'REQUEST') {
391 $html = html::div('rsvp-status', $this->gettext('acceptinvitation'));
392
393 if ($existing) {
394 $rsvp = $event['rsvp'];
395 $emails = $this->lib->get_user_emails();
396 foreach ($existing['attendees'] as $attendee) {
397 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
398 $status = strtoupper($attendee['status']);
399 break;
400 }
401 }
402
403 // Detect re-sheduling
404 if (!$latest) {
405 // FIXME: This is probably to simplistic, or maybe we should just check
406 // attendee's RSVP flag in the new event?
407 $resheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end'];
408 }
409 }
410 else {
411 $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true);
412 }
413
414 $status_lc = strtolower($status);
415
416 if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) {
417 $html = html::div('rsvp-status', $this->gettext('notanattendee'));
418 $action = 'import';
419 }
420 else if (in_array($status_lc, $this->rsvp_status)) {
421 $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc);
422
423 if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) {
424 $action = ''; // nothing to do here, outdated invitation
425 if ($status_lc == 'needs-action')
426 $status_text = $this->gettext('outdatedinvitation');
427 }
428 else if (!$existing && !$rsvp) {
429 $action = 'import';
430 }
431 else if ($resheduled) {
432 $action = 'rsvp';
433 }
434 else if ($status_lc != 'needs-action') {
435 $action = !$latest ? 'update' : '';
436 }
437
438 $html = html::div('rsvp-status ' . $status_lc, $status_text);
439 }
440 }
441 // determine action for REPLY
442 else if ($event['method'] == 'REPLY') {
443 // check whether the sender already is an attendee
444 if ($existing) {
445 $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : '';
446 $listed = false;
447 foreach ($existing['attendees'] as $attendee) {
448 if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
449 $status_lc = strtolower($status);
450 if (in_array($status_lc, $this->rsvp_status)) {
451 $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
452 'name' => 'attendee' . $status_lc,
453 'vars' => array(
454 'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')),
455 )
456 )));
457 }
458 $action = $attendee['status'] == $status || !$latest ? '' : 'update';
459 $listed = true;
460 break;
461 }
462 }
463
464 if (!$listed) {
465 $html = html::div('rsvp-status', $this->gettext('itipnewattendee'));
466 }
467 }
468 else {
469 $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
470 $action = '';
471 }
472 }
473 else if ($event['method'] == 'CANCEL') {
474 if (!$existing) {
475 $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
476 $action = '';
477 }
478 }
479
480 return array(
481 'uid' => $event['uid'],
482 'id' => asciiwords($event['uid'], true),
483 'existing' => $existing ? true : false,
484 'saved' => $existing ? true : false,
485 'latest' => $latest,
486 'status' => $status,
487 'action' => $action,
488 'resheduled' => $resheduled,
489 'html' => $html,
490 );
491 }
492
493 /**
494 * Build inline UI elements for iTip messages
495 */
496 public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null)
497 {
498 $buttons = array();
499 $dom_id = asciiwords($event['uid'], true);
500 $rsvp_status = 'unknown';
501
502 // pass some metadata about the event and trigger the asynchronous status check
503 $changed = is_object($event['changed']) ? $event['changed'] : $message_date;
504 $metadata = array(
505 'uid' => $event['uid'],
506 '_instance' => $event['_instance'],
507 'changed' => $changed ? $changed->format('U') : 0,
508 'sequence' => intval($event['sequence']),
509 'method' => $method,
510 'task' => $task,
511 );
512
513 // create buttons to be activated from async request checking existence of this event in local calendars
514 $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
515
516 // on iTip REPLY we have two options:
517 if ($method == 'REPLY') {
518 $title = $this->gettext('itipreply');
519
520 foreach ($event['attendees'] as $attendee) {
521 if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') {
522 if (empty($event['_sender']) || self::compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
523 $metadata['attendee'] = $attendee['email'];
524 $rsvp_status = strtoupper($attendee['status']);
525 if ($attendee['delegated-to']) {
526 $metadata['delegated-to'] = $attendee['delegated-to'];
527 }
528 break;
529 }
530 }
531 }
532
533 // 1. update the attendee status on our copy
534 $update_button = html::tag('input', array(
535 'type' => 'button',
536 'class' => 'button',
537 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
538 'value' => $this->gettext('updateattendeestatus'),
539 ));
540
541 // 2. accept or decline a new or delegate attendee
542 $accept_buttons = html::tag('input', array(
543 'type' => 'button',
544 'class' => "button accept",
545 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
546 'value' => $this->gettext('acceptattendee'),
547 ));
548 $accept_buttons .= html::tag('input', array(
549 'type' => 'button',
550 'class' => "button decline",
551 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . rcube::JQ($mime_id) . "', '$task')",
552 'value' => $this->gettext('declineattendee'),
553 ));
554
555 $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
556 $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons);
557 }
558 // when receiving iTip REQUEST messages:
559 else if ($method == 'REQUEST') {
560 $emails = $this->lib->get_user_emails();
561 $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
562 $metadata['rsvp'] = true;
563 $metadata['sensitivity'] = $event['sensitivity'];
564
565 if (is_object($event['start'])) {
566 $metadata['date'] = $event['start']->format('U');
567 }
568
569 // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons
570 if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') {
571 $this->rsvp_actions = array('accepted','declined');
572 $metadata['nosave'] = true;
573 }
574
575 // 1. display RSVP buttons (if the user was invited)
576 foreach ($this->rsvp_actions as $method) {
577 $rsvp_buttons .= html::tag('input', array(
578 'type' => 'button',
579 'class' => "button $method",
580 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task', '$method', '$dom_id')",
581 'value' => $this->gettext('itip' . $method),
582 ));
583 }
584
585 // add button to open calendar/preview
586 if (!empty($preview_url)) {
587 $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id;
588 $rsvp_buttons .= html::tag('input', array(
589 'type' => 'button',
590 'class' => "button preview",
591 'onclick' => "rcube_libcalendaring.open_itip_preview('" . rcube::JQ($preview_url) . "', '" . rcube::JQ($msgref) . "')",
592 'value' => $this->gettext('openpreview'),
593 ));
594 }
595
596 // 2. update the local copy with minor changes
597 $update_button = html::tag('input', array(
598 'type' => 'button',
599 'class' => 'button',
600 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
601 'value' => $this->gettext('updatemycopy'),
602 ));
603
604 // 3. Simply import the event without replying
605 $import_button = html::tag('input', array(
606 'type' => 'button',
607 'class' => 'button',
608 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
609 'value' => $this->gettext('importtocalendar'),
610 ));
611
612 // check my status
613 foreach ($event['attendees'] as $attendee) {
614 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
615 $metadata['attendee'] = $attendee['email'];
616 $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
617 $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION';
618 break;
619 }
620 }
621
622 // add itip reply message controls
623 $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave']));
624
625 $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
626 $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
627
628 // prepare autocompletion for delegation dialog
629 if (in_array('delegated', $this->rsvp_actions)) {
630 $this->rc->autocomplete_init();
631 }
632 }
633 // for CANCEL messages, we can:
634 else if ($method == 'CANCEL') {
635 $title = $this->gettext('itipcancellation');
636 $event_prop = array_filter(array(
637 'uid' => $event['uid'],
638 '_instance' => $event['_instance'],
639 '_savemode' => $event['_savemode'],
640 ));
641
642 // 1. remove the event from our calendar
643 $button_remove = html::tag('input', array(
644 'type' => 'button',
645 'class' => 'button',
646 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . rcube::JQ($event['title']) . "')",
647 'value' => $this->gettext('removefromcalendar'),
648 ));
649
650 // 2. update our copy with status=cancelled
651 $button_update = html::tag('input', array(
652 'type' => 'button',
653 'class' => 'button',
654 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
655 'value' => $this->gettext('updatemycopy'),
656 ));
657
658 $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update);
659
660 $rsvp_status = 'CANCELLED';
661 $metadata['rsvp'] = true;
662 }
663
664 // append generic import button
665 if ($import_button) {
666 $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
667 }
668
669 // pass some metadata about the event and trigger the asynchronous status check
670 $metadata['fallback'] = $rsvp_status;
671 $metadata['rsvp'] = intval($metadata['rsvp']);
672
673 $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . rcube_output::json_serialize($metadata) . ")", 'docready');
674
675 // get localized texts from the right domain
676 foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee',
677 'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation',
678 'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
679 $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
680 }
681
682 // show event details with buttons
683 return $this->itip_object_details_table($event, $title) .
684 html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons));
685 }
686
687 /**
688 * Render an RSVP UI widget with buttons to respond on iTip invitations
689 */
690 function itip_rsvp_buttons($attrib = array(), $actions = null)
691 {
692 $attrib += array('type' => 'button');
693
694 if (!$actions)
695 $actions = $this->rsvp_actions;
696
697 foreach ($actions as $method) {
698 $buttons .= html::tag('input', array(
699 'type' => $attrib['type'],
700 'name' => $attrib['iname'],
701 'class' => 'button',
702 'rel' => $method,
703 'value' => $this->gettext('itip' . $method),
704 ));
705 }
706
707 // add localized texts for the delegation dialog
708 if (in_array('delegated', $actions)) {
709 foreach (array('itipdelegated','itipcomment','delegateinvitation',
710 'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) {
711 $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
712 }
713 }
714
715 foreach (array('all','current','future') as $mode) {
716 $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
717 }
718
719 $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
720
721 return html::div($attrib,
722 html::div('label', $this->gettext('acceptinvitation')) .
723 html::div('rsvp-buttons',
724 $buttons .
725 html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
726 )
727 );
728 }
729
730 /**
731 * Render UI elements to control iTip reply message sending
732 */
733 public function itip_rsvp_options_ui($dom_id, $disable = false)
734 {
735 $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3);
736
737 // itip sending is entirely disabled
738 if ($itip_sending === 0) {
739 return '';
740 }
741 // add checkbox to suppress itip reply message
742 else if ($itip_sending >= 2) {
743 $rsvp_additions = html::label(array('class' => 'noreply-toggle'),
744 html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0))
745 . ' ' . $this->gettext('itipsuppressreply')
746 );
747 }
748
749 // add input field for reply comment
750 $toggle_attrib = array(
751 'href' => '#toggle',
752 'class' => 'reply-comment-toggle',
753 'onclick' => '$(this).hide().parent().find(\'textarea\').show().focus()'
754 );
755 $textarea_attrib = array(
756 'id' => 'reply-comment-' . $dom_id,
757 'name' => '_comment',
758 'cols' => 40,
759 'rows' => 6,
760 'style' => 'display:none',
761 'placeholder' => $this->gettext('itipcomment')
762 );
763
764 $rsvp_additions .= html::a($toggle_attrib, $this->gettext('itipeditresponse'))
765 . html::div('itip-reply-comment', html::tag('textarea', $textarea_attrib, ''));
766
767 return $rsvp_additions;
768 }
769
770 /**
771 * Render event/task details in a table
772 */
773 function itip_object_details_table($event, $title)
774 {
775 $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
776 $table->add('ititle', $title);
777 $table->add('title', rcube::Q($event['title']));
778 if ($event['start'] && $event['end']) {
779 $table->add('label', $this->gettext('date'));
780 $table->add('date', rcube::Q($this->lib->event_date_text($event)));
781 }
782 else if ($event['due'] && $event['_type'] == 'task') {
783 $table->add('label', $this->gettext('date'));
784 $table->add('date', rcube::Q($this->lib->event_date_text($event)));
785 }
786 if (!empty($event['recurrence_date'])) {
787 $table->add('label', '');
788 $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
789 }
790 else if (!empty($event['recurrence'])) {
791 $table->add('label', $this->gettext('recurring'));
792 $table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
793 }
794 if ($event['location']) {
795 $table->add('label', $this->gettext('location'));
796 $table->add('location', rcube::Q($event['location']));
797 }
798 if ($event['sensitivity'] && $event['sensitivity'] != 'public') {
799 $table->add('label', $this->gettext('sensitivity'));
800 $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!');
801 }
802 if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') {
803 $table->add('label', $this->gettext('status'));
804 $table->add('status', $this->gettext('status-' . strtolower($event['status'])));
805 }
806 if ($event['comment']) {
807 $table->add('label', $this->gettext('comment'));
808 $table->add('location', rcube::Q($event['comment']));
809 }
810
811 return $table->show();
812 }
813
814
815 /**
816 * Create iTIP invitation token for later replies via URL
817 *
818 * @param array Hash array with event properties
819 * @param string Attendee email address
820 * @return string Invitation token
821 */
822 public function store_invitation($event, $attendee)
823 {
824 // empty stub
825 return false;
826 }
827
828 /**
829 * Mark invitations for the given event as cancelled
830 *
831 * @param array Hash array with event properties
832 */
833 public function cancel_itip_invitation($event)
834 {
835 // empty stub
836 return false;
837 }
838
839 /**
840 * Utility function to get the value of a custom property
841 */
842 public static function get_custom_property($event, $name)
843 {
844 $ret = false;
845
846 if (is_array($event['x-custom'])) {
847 array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) {
848 if (strcasecmp($prop[0], $name) === 0) {
849 $ret = $prop[1];
850 }
851 });
852 }
853
854 return $ret;
855 }
856
857 /**
858 * Compare email address
859 */
860 public static function compare_email($value, $email, $email_utf = null)
861 {
862 $v1 = !empty($email) && strcasecmp($value, $email) === 0;
863 $v2 = !empty($email_utf) && strcasecmp($value, $email_utf) === 0;
864
865 return $v1 || $v2;
866 }
867 }