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 } |
