Mercurial > hg > rc1
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 } |