From 37cb27f2f362c649fe02abcfa24177b82c93da8c Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 2 Sep 2011 18:49:30 +0200 Subject: [PATCH] Added fallback to accept/decline invitations by URL if recipient doesn't understand iTIP + small refactoring if itip code --- plugins/calendar/calendar.php | 255 +++++++------- .../calendar/drivers/database/sql/mysql.sql | 13 + .../calendar/drivers/database/sql/sqlite.sql | 13 + plugins/calendar/drivers/kolab/SQL/mysql.sql | 16 +- plugins/calendar/lib/calendar_ical.php | 2 - plugins/calendar/lib/calendar_itip.php | 322 ++++++++++++++++++ plugins/calendar/lib/calendar_ui.php | 224 ++++++------ plugins/calendar/localization/de_CH.inc | 2 + plugins/calendar/localization/de_DE.inc | 2 + plugins/calendar/localization/en_US.inc | 3 + plugins/calendar/skins/default/calendar.css | 31 ++ .../skins/default/images/invitation.png | Bin 0 -> 1909 bytes .../skins/default/templates/itipattend.html | 21 ++ 13 files changed, 680 insertions(+), 224 deletions(-) create mode 100644 plugins/calendar/lib/calendar_itip.php create mode 100644 plugins/calendar/skins/default/images/invitation.png create mode 100644 plugins/calendar/skins/default/templates/itipattend.html diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 5d3a2a11..a645f0a6 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -33,7 +33,7 @@ class calendar extends rcube_plugin const FREEBUSY_TENTATIVE = 3; const FREEBUSY_OOF = 4; - public $task = '?(?!login|logout).*'; + public $task = '?(?!logout).*'; public $rc; public $driver; public $home; // declare public to be used in other classes @@ -98,11 +98,16 @@ class calendar extends rcube_plugin $this->ui->init(); - // settings are required in every GUI step - $this->rc->output->set_env('calendar_settings', $this->load_settings()); + // settings are required in (almost) every GUI step + if ($this->rc->action != 'attend') + $this->rc->output->set_env('calendar_settings', $this->load_settings()); } - if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') { + // catch iTIP confirmation requests that don're require a valid session + if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) { + $this->add_hook('startup', array($this, 'itip_attend_response')); + } + else if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') { if ($this->rc->action != 'upload') { $this->load_driver(); } @@ -172,13 +177,26 @@ class calendar extends rcube_plugin } } + /** + * Load iTIP functions + */ + private function load_itip() + { + if (!$this->itip) { + require_once($this->home . '/lib/calendar_itip.php'); + $this->itip = new calendar_itip($this); + } + + return $this->itip; + } + /** * Load iCalendar functions */ - private function load_ical() + public function get_ical() { if (!$this->ical) { - require($this->home . '/lib/calendar_ical.php'); + require_once($this->home . '/lib/calendar_ical.php'); $this->ical = new calendar_ical($this); } @@ -618,8 +636,9 @@ class calendar extends rcube_plugin } } - if ($organizer && $this->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) - $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name']))), 'confirmation'); + $itip = $this->load_itip(); + if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } @@ -688,8 +707,9 @@ class calendar extends rcube_plugin break; } } - if ($organizer && $this->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) - $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name']))), 'confirmation'); + $itip = $this->load_itip(); + if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } @@ -784,8 +804,7 @@ class calendar extends rcube_plugin header("Content-Type: text/calendar"); header("Content-Disposition: inline; filename=".$calendar_name.'.ics'); - $this->load_ical(); - $this->ical->export($events, '', true); + $this->get_ical()->export($events, '', true); exit; } @@ -1405,11 +1424,12 @@ class calendar extends rcube_plugin $is_cancelled = true; } + $itip = $this->load_itip(); $emails = $this->get_user_emails(); // compose multipart message using PEAR:Mail_Mime $method = $action == 'remove' ? 'CANCEL' : 'REQUEST'; - $message = $this->compose_itip_message($event, $method); + $message = $itip->compose_itip_message($event, $method); // list existing attendees from $old event $old_attendees = array(); @@ -1430,7 +1450,7 @@ class calendar extends rcube_plugin $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty')); // finally send the message - if ($this->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message)) + if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message)) $sent++; else $sent = -100; @@ -1614,7 +1634,71 @@ class calendar extends rcube_plugin /**** Event invitation plugin hooks ****/ + + /** + * Handler for URLs that allow an invitee to respond on his invitation mail + */ + public function itip_attend_response($p) + { + if ($p['action'] == 'attend') { + $this->rc->output->set_env('task', 'calendar'); // override some env vars + $this->rc->output->set_env('keep_alive', 0); + $this->rc->output->set_pagetitle($this->gettext('calendar')); + $itip = $this->load_itip(); + $token = get_input_value('_t', RCUBE_INPUT_GPC); + + // read event info stored under the given token + if ($invitation = $itip->get_invitation($token)) { + $this->token = $token; + $this->event = $invitation['event']; + + // show message about cancellation + if ($invitation['cancelled']) { + $this->invitestatus = html::div('rsvp-status declined', $this->gettext('eventcancelled')); + } + // save submitted RSVP status + else if (!empty($_POST['rsvp'])) { + $status = null; + foreach (array('accepted','tentative','declined') as $method) { + if ($_POST['rsvp'] == $this->gettext('itip' . $method)) { + $status = $method; + break; + } + } + + // send itip reply to organizer + if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) { + $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status))); + } + else + $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1); + } + + $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform')); + $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox')); + + if (!$this->invitestatus) + $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons')); + + $this->rc->output->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event['title']); + } + else + $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1); + + $this->rc->output->send('calendar.itipattend'); + } + } + + /** + * + */ + public function itip_event_inviteform($p) + { + $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token)); + return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true)) . $hidden->show(); + } + /** * Check mail message structure of there are .ics files attached */ @@ -1637,12 +1721,11 @@ class calendar extends rcube_plugin { // load iCalendar functions (if necessary) if (!empty($this->ics_parts)) { - $this->load_ical(); + $this->get_ical(); } $html = ''; foreach ($this->ics_parts as $mime_id) { - $this->load_ical(); $part = $this->message->mime_parts[$mime_id]; $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET; $events = $this->ical->import($this->message->get_part_content($mime_id), $charset); @@ -1732,19 +1815,8 @@ class calendar extends rcube_plugin )); } - // show event details in a table - $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); - $table->add('ititle', $title); - $table->add('title', Q($event['title'])); - $table->add('label', $this->gettext('date')); - $table->add('location', Q($this->event_date_text($event))); - if ($event['location']) { - $table->add('label', $this->gettext('location')); - $table->add('location', Q($event['location'])); - } - - // add box below messsage body - $html .= html::div('calendar-invitebox', $table->show() . $buttons_pre . html::div('rsvp-buttons', $buttons)); + // show event details with buttons + $html .= html::div('calendar-invitebox', $this->ui->event_details_table($event, $title) . $buttons_pre . html::div('rsvp-buttons', $buttons)); // limit listing if ($idx >= 3) @@ -1786,8 +1858,7 @@ class calendar extends rcube_plugin $headers = $this->rc->imap->get_headers($uid); } - $this->load_ical(); - $events = $this->ical->import($part, $charset); + $events = $this->get_ical()->import($part, $charset); $error_msg = $this->gettext('errorimportingevent'); $success = false; @@ -1900,8 +1971,9 @@ class calendar extends rcube_plugin // send iTip reply if ($this->ical->method == 'REQUEST' && $organizer && !in_array($organizer['email'], $emails) && !$error_msg) { - if ($this->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) - $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name']))), 'confirmation'); + $itip = $this->load_itip(); + if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); else $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); } @@ -1910,99 +1982,6 @@ class calendar extends rcube_plugin } - /** - * Send an iTip mail message - * - * @param array Event object to send - * @param string iTip method (REQUEST|REPLY|CANCEL) - * @param array Hash array with recipient data (name, email) - * @param string Mail subject - * @param string Mail body text label - * @param object Mail_mime object with message data - * @return boolean True on success, false on failure - */ - public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null) - { - if (!$message) - $message = $this->compose_itip_message($event, $method); - - $myself = $this->rc->user->get_identity(); - $mailto = rcube_idn_to_ascii($recipient['email']); - - $headers = $message->headers(); - $headers['To'] = format_email_recipient($mailto, $recipient['name']); - $headers['Subject'] = $this->gettext(array( - 'name' => $subject, - 'vars' => array('title' => $event['title'], 'name' => ($myself['name'] ? $myself['name'] : $myself['email'])) - )); - - // compose a list of all event attendees - $attendees_list = array(); - foreach ((array)$event['attendees'] as $attendee) { - $attendees_list[] = ($attendee['name'] && $attendee['email']) ? - $attendee['name'] . ' <' . $attendee['email'] . '>' : - ($attendee['name'] ? $attendee['name'] : $attendee['email']); - } - - $mailbody = $this->gettext(array( - 'name' => $bodytext, - 'vars' => array( - 'title' => $event['title'], - 'date' => $this->event_date_text($event), - 'attendees' => join(', ', $attendees_list), - 'sender' => $myself['name'] ? $myself['name'] : $myself['email'], - 'organizer' => $myself['name'], - ) - )); - - $message->headers($headers); - $message->setTXTBody(rcube_message::format_flowed($mailbody, 79)); - - // finally send the message - return rcmail_deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error); - } - - /** - * Helper function to build a Mail_mime object to send an iTip message - * - * @param array Event object to send - * @param string iTip method (REQUEST|REPLY|CANCEL) - * @return object Mail_mime object with message data - */ - private function compose_itip_message($event, $method) - { - $myself = $this->rc->user->get_identity(); - $from = rcube_idn_to_ascii($myself['email']); - $sender = format_email_recipient($from, $myself['name']); - - // compose multipart message using PEAR:Mail_Mime - $message = new Mail_mime("\r\n"); - $message->setParam('text_encoding', 'quoted-printable'); - $message->setParam('head_encoding', 'quoted-printable'); - $message->setParam('head_charset', RCMAIL_CHARSET); - $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed"); - - // compose common headers array - $headers = array( - 'From' => $sender, - 'Date' => rcmail_user_date(), - 'Message-ID' => rcmail_gen_message_id(), - 'X-Sender' => $from, - ); - if ($agent = $this->rc->config->get('useragent')) - $headers['User-Agent'] = $agent; - - $message->headers($headers); - - // attach ics file for this event - $this->load_ical(); - $vcal = $this->ical->export(array($event), $method); - $message->addAttachment($vcal, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET . "; method=" . $metod); - - return $message; - } - - /** * Checks if specified message part is a vcalendar data * @@ -2031,5 +2010,29 @@ class calendar extends rcube_plugin return array_unique($emails); } + + /** + * Build an absolute URL with the given parameters + */ + public function get_url($param = array()) + { + $param += array('task' => 'calendar'); + + $schema = 'http'; + $default_port = 80; + if (rcube_https_check()) { + $schema = 'https'; + $default_port = 143; + } + $url = $schema . '://' . $_SERVER['HTTP_HOST']; + if ($_SERVER['SERVER_PORT'] != $default_port) + $url .= ':' . $_SERVER['SERVER_PORT']; + if (dirname($_SERVER['SCRIPT_NAME']) != '/') + $url .= dirname($_SERVER['SCRIPT_NAME']); + $url .= preg_replace('!^\./!', '/', $this->rc->url($param)); + + return $url; + } + } diff --git a/plugins/calendar/drivers/database/sql/mysql.sql b/plugins/calendar/drivers/database/sql/mysql.sql index da5b8747..e0db2359 100644 --- a/plugins/calendar/drivers/database/sql/mysql.sql +++ b/plugins/calendar/drivers/database/sql/mysql.sql @@ -62,3 +62,16 @@ CREATE TABLE `attachments` ( CONSTRAINT `fk_attachments_event_id` FOREIGN KEY (`event_id`) REFERENCES `events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + +CREATE TABLE `itipinvitations` ( + `token` VARCHAR(64) NOT NULL, + `event_uid` VARCHAR(255) NOT NULL, + `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0', + `event` TEXT NOT NULL, + `expires` DATETIME DEFAULT NULL, + `cancelled` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY(`token`), + INDEX `uid_idx` (`event_uid`,`user_id`), + CONSTRAINT `fk_itipinvitations_user_id` FOREIGN KEY (`user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; diff --git a/plugins/calendar/drivers/database/sql/sqlite.sql b/plugins/calendar/drivers/database/sql/sqlite.sql index 42fad77f..7c114e65 100644 --- a/plugins/calendar/drivers/database/sql/sqlite.sql +++ b/plugins/calendar/drivers/database/sql/sqlite.sql @@ -59,3 +59,16 @@ CREATE TABLE attachments ( REFERENCES events(event_id) ); +CREATE TABLE itipinvitations ( + token varchar(64) NOT NULL PRIMARY KEY, + event_uid varchar(255) NOT NULL, + user_id integer NOT NULL default '0', + event text NOT NULL, + expires datetime NOT NULL default '1000-01-01 00:00:00', + cancelled tinyint(1) NOT NULL default '0', + CONSTRAINT fk_itipinvitations_user_id FOREIGN KEY (user_id) + REFERENCES users(user_id) +); + +CREATE INDEX ix_itipinvitations_uid ON itipinvitations(event_uid,user_id); + diff --git a/plugins/calendar/drivers/kolab/SQL/mysql.sql b/plugins/calendar/drivers/kolab/SQL/mysql.sql index 6cbd75bd..bc350159 100644 --- a/plugins/calendar/drivers/kolab/SQL/mysql.sql +++ b/plugins/calendar/drivers/kolab/SQL/mysql.sql @@ -1,7 +1,7 @@ /** * Roundcube Calendar Kolab backend * - * @version 0.3 beta + * @version 0.6 beta * @author Thomas Bruederli * @licence GNU GPL **/ @@ -12,3 +12,17 @@ CREATE TABLE `kolab_alarms` ( `dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY(`event_id`) ) /*!40000 ENGINE=INNODB */; + +CREATE TABLE `itipinvitations` ( + `token` VARCHAR(64) NOT NULL, + `event_uid` VARCHAR(255) NOT NULL, + `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0', + `event` TEXT NOT NULL, + `expires` DATETIME DEFAULT NULL, + `cancelled` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY(`token`), + INDEX `uid_idx` (`event_uid`,`user_id`), + CONSTRAINT `fk_itipinvitations_user_id` FOREIGN KEY (`user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index 372958d7..497c39d8 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -245,7 +245,6 @@ class calendar_ical */ public function export($events, $method = null, $write = false) { - if (!empty($this->rc->user->ID)) { $ical = "BEGIN:VCALENDAR" . self::EOL; $ical .= "VERSION:2.0" . self::EOL; $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL; @@ -332,7 +331,6 @@ class calendar_ical // fold lines to 75 chars return rcube_vcard::rfc2425_fold($ical); - } } private function escpape($str) diff --git a/plugins/calendar/lib/calendar_itip.php b/plugins/calendar/lib/calendar_itip.php new file mode 100644 index 00000000..4ac4cbf1 --- /dev/null +++ b/plugins/calendar/lib/calendar_itip.php @@ -0,0 +1,322 @@ + + * @package calendar + * + * Copyright (C) 2011, Kolab Systems AG + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +class calendar_itip +{ + private $rc; + private $cal; + private $event; + + function __construct($cal) + { + $this->cal = $cal; + $this->rc = $cal->rc; + $this->sender = $this->rc->user->get_identity(); + } + + /** + * Send an iTip mail message + * + * @param array Event object to send + * @param string iTip method (REQUEST|REPLY|CANCEL) + * @param array Hash array with recipient data (name, email) + * @param string Mail subject + * @param string Mail body text label + * @param object Mail_mime object with message data + * @return boolean True on success, false on failure + */ + public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null) + { + if (!$this->sender['name']) + $this->sender['name'] = $this->sender['email']; + + if (!$message) + $message = $this->compose_itip_message($event, $method); + + $mailto = rcube_idn_to_ascii($recipient['email']); + + $headers = $message->headers(); + $headers['To'] = format_email_recipient($mailto, $recipient['name']); + $headers['Subject'] = $this->cal->gettext(array( + 'name' => $subject, + 'vars' => array('title' => $event['title'], 'name' => $this->sender['name']) + )); + + // compose a list of all event attendees + $attendees_list = array(); + foreach ((array)$event['attendees'] as $attendee) { + $attendees_list[] = ($attendee['name'] && $attendee['email']) ? + $attendee['name'] . ' <' . $attendee['email'] . '>' : + ($attendee['name'] ? $attendee['name'] : $attendee['email']); + } + + $mailbody = $this->cal->gettext(array( + 'name' => $bodytext, + 'vars' => array( + 'title' => $event['title'], + 'date' => $this->cal->event_date_text($event), + 'attendees' => join(', ', $attendees_list), + 'sender' => $this->sender['name'], + 'organizer' => $this->sender['name'], + ) + )); + + // append links for direct invitation replies + if ($method == 'REQUEST' && ($token = $this->store_invitation($event, $recipient['email']))) { + $mailbody .= "\n\n" . $this->cal->gettext(array( + 'name' => 'invitationattendlinks', + 'vars' => array('url' => $this->cal->get_url(array('action' => 'attend', 't' => $token))), + )); + } + else if ($method == 'CANCEL') { + $this->cancel_itip_invitation($event); + } + + $message->headers($headers); + $message->setTXTBody(rcube_message::format_flowed($mailbody, 79)); + + // finally send the message + return rcmail_deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error); + } + + /** + * Helper function to build a Mail_mime object to send an iTip message + * + * @param array Event object to send + * @param string iTip method (REQUEST|REPLY|CANCEL) + * @return object Mail_mime object with message data + */ + public function compose_itip_message($event, $method) + { + $from = rcube_idn_to_ascii($this->sender['email']); + $sender = format_email_recipient($from, $this->sender['name']); + + // compose multipart message using PEAR:Mail_Mime + $message = new Mail_mime("\r\n"); + $message->setParam('text_encoding', 'quoted-printable'); + $message->setParam('head_encoding', 'quoted-printable'); + $message->setParam('head_charset', RCMAIL_CHARSET); + $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed"); + + // compose common headers array + $headers = array( + 'From' => $sender, + 'Date' => rcmail_user_date(), + 'Message-ID' => rcmail_gen_message_id(), + 'X-Sender' => $from, + ); + if ($agent = $this->rc->config->get('useragent')) + $headers['User-Agent'] = $agent; + + $message->headers($headers); + + // attach ics file for this event + $ical = $this->cal->get_ical(); + $ics = $ical->export(array($event), $method); + $message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET . "; method=" . $method); + + return $message; + } + + + /** + * Find invitation record by token + * + * @param string Invitation token + * @return mixed Invitation record as hash array or False if not found + */ + public function get_invitation($token) + { + if ($parts = $this->decode_token($token)) { + $result = $this->rc->db->query("SELECT * FROM itipinvitations WHERE token=?", $parts['base']); + if ($result && ($rec = $this->rc->db->fetch_assoc($result))) { + $rec['event'] = unserialize($rec['event']); + $rec['attendee'] = $parts['attendee']; + return $rec; + } + } + + return false; + } + + /** + * Update the attendee status of the given invitation record + * + * @param array Invitation record as fetched with calendar_itip::get_invitation() + * @param string Attendee email address + * @param string New attendee status + */ + public function update_invitation($invitation, $email, $newstatus) + { + if (is_string($invitation)) + $invitation = $this->get_invitation($invitation); + + if ($invitation['token'] && $invitation['event']) { + // update attendee record in event data + foreach ($invitation['event']['attendees'] as $i => $attendee) { + if ($attendee['role'] == 'ORGANIZER') { + $organizer = $attendee; + } + else if ($attendee['email'] == $email) { + // nothing to be done here + if ($attendee['status'] == $newstatus) + return true; + + $invitation['event']['attendees'][$i]['status'] = $newstatus; + $this->sender = $attendee; + } + } + $invitation['event']['changed'] = time(); + + // send iTIP REPLY message to organizer + if ($organizer) { + $status = strtolower($newstatus); + if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) + $this->rc->output->command('display_message', $this->cal->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->cal->gettext('itipresponseerror'), 'error'); + } + + // update record in DB + $query = $this->rc->db->query( + "UPDATE itipinvitations + SET event=? + WHERE token=?", + self::serialize_event($invitation['event']), + $invitation['token'] + ); + + if ($this->rc->db->affected_rows($query)) + return true; + } + + return false; + } + + + /** + * Create iTIP invitation token for later replies via URL + * + * @param array Hash array with event properties + * @param string Attendee email address + * @return string Invitation token + */ + public function store_invitation($event, $attendee) + { + static $stored = array(); + + if (!$event['uid'] || !$attendee) + return false; + + // generate token for this invitation + $token = $this->generate_token($event, $attendee); + $base = substr($token, 0, 40); + + // already stored this + if ($stored[$base]) + return $token; + + $query = $this->rc->db->query( + "REPLACE INTO itipinvitations + (token, event_uid, user_id, event, expires) + VALUES(?, ?, ?, ?, ?)", + $base, + $event['uid'], + $this->rc->user->ID, + self::serialize_event($event), + date('Y-m-d H:i:s', $event['end'] + 86400 * 2) + ); + + if ($this->rc->db->affected_rows($query)) { + $stored[$base] = 1; + return $token; + } + + return false; + } + + /** + * Mark invitations for the given event as cancelled + * + * @param array Hash array with event properties + */ + public function cancel_itip_invitation($event) + { + // flag invitation record as cancelled + $this->rc->db->query( + "UPDATE itipinvitations + SET cancelled=1 + WHERE event_uid=? AND user_id=?", + $event['uid'], + $this->rc->user->ID + ); + } + + /** + * Generate an invitation request token for the given event and attendee + * + * @param array Event hash array + * @param string Attendee email address + */ + public function generate_token($event, $attendee) + { + $base = sha1($event['uid'] . ';' . $this->rc->user->ID); + $mail = base64_encode($attendee); + $hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6); + + return "$base.$mail.$hash"; + } + + /** + * Decode the given iTIP request token and return its parts + * + * @param string Request token to decode + * @return mixed Hash array with parts or False if invalid + */ + public function decode_token($token) + { + list($base, $mail, $hash) = explode('.', $token); + + // validate and return parts + if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) { + return array('base' => $base, 'attendee' => base64_decode($mail)); + } + + return false; + } + + /** + * Helper method to serialize the given event for storing in invitations table + */ + private static function serialize_event($event) + { + $ev = $event; + $ev['description'] = abbreviate_string($ev['description'], 100); + unset($ev['attachments']); + return serialize($ev); + } + +} diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index d110ef70..8adbccd9 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -27,14 +27,14 @@ class calendar_ui { private $rc; - private $calendar; + private $cal; private $ready = false; public $screen; - function __construct($calendar) + function __construct($cal) { - $this->calendar = $calendar; - $this->rc = $calendar->rc; + $this->cal = $cal; + $this->rc = $cal->rc; $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other'; } @@ -47,7 +47,7 @@ class calendar_ui return; // add taskbar button - $this->calendar->add_button(array( + $this->cal->add_button(array( 'name' => 'calendar', 'class' => 'button-calendar', 'label' => 'calendar.calendar', @@ -55,11 +55,11 @@ class calendar_ui ), 'taskbar'); // load basic client script (which - unfortunately - requires fullcalendar) - $this->calendar->include_script('lib/js/fullcalendar.js'); - $this->calendar->include_script('calendar_base.js'); + $this->cal->include_script('lib/js/fullcalendar.js'); + $this->cal->include_script('calendar_base.js'); $skin = $this->rc->config->get('skin'); - $this->calendar->include_stylesheet('skins/' . $skin . '/calendar.css'); + $this->cal->include_stylesheet('skins/' . $skin . '/calendar.css'); $this->ready = true; } @@ -70,8 +70,8 @@ class calendar_ui public function addCSS() { $skin = $this->rc->config->get('skin'); - $this->calendar->include_stylesheet('skins/' . $skin . '/fullcalendar.css'); - $this->calendar->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css'); + $this->cal->include_stylesheet('skins/' . $skin . '/fullcalendar.css'); + $this->cal->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css'); } /** @@ -79,8 +79,8 @@ class calendar_ui */ public function addJS() { - $this->calendar->include_script('calendar_ui.js'); - $this->calendar->include_script('lib/js/jquery.miniColors.min.js'); + $this->cal->include_script('calendar_ui.js'); + $this->cal->include_script('lib/js/jquery.miniColors.min.js'); } /** @@ -88,8 +88,8 @@ class calendar_ui */ function calendar_css($attrib = array()) { - $mode = $this->rc->config->get('calendar_event_coloring', $this->calendar->defaults['calendar_event_coloring']); - $categories = $this->calendar->driver->list_categories(); + $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']); + $categories = $this->cal->driver->list_categories(); $css = "\n"; foreach ((array)$categories as $class => $color) { @@ -113,7 +113,7 @@ class calendar_ui } } - $calendars = $this->calendar->driver->list_calendars(); + $calendars = $this->cal->driver->list_calendars(); foreach ((array)$calendars as $id => $prop) { if (!$prop['color']) continue; @@ -146,7 +146,7 @@ class calendar_ui */ function calendar_list($attrib = array()) { - $calendars = $this->calendar->driver->list_calendars(); + $calendars = $this->cal->driver->list_calendars(); $li = ''; foreach ((array)$calendars as $id => $prop) { @@ -154,10 +154,10 @@ class calendar_ui continue; unset($prop['user_id']); - $prop['alarms'] = $this->calendar->driver->alarms; - $prop['attendees'] = $this->calendar->driver->attendees; - $prop['freebusy'] = $this->calendar->driver->freebusy; - $prop['attachments'] = $this->calendar->driver->attachments; + $prop['alarms'] = $this->cal->driver->alarms; + $prop['attendees'] = $this->cal->driver->attendees; + $prop['freebusy'] = $this->cal->driver->freebusy; + $prop['attachments'] = $this->cal->driver->attachments; $jsenv[$id] = $prop; $html_id = html_identifier($id); @@ -185,7 +185,7 @@ class calendar_ui { $attrib['name'] = 'calendar'; $select = new html_select($attrib); - foreach ((array)$this->calendar->driver->list_calendars() as $id => $prop) { + foreach ((array)$this->cal->driver->list_calendars() as $id => $prop) { if (!$prop['readonly']) $select->add($prop['name'], $id); } @@ -201,7 +201,7 @@ class calendar_ui $attrib['name'] = 'categories'; $select = new html_select($attrib); $select->add('---', ''); - foreach ((array)$this->calendar->driver->list_categories() as $cat => $color) { + foreach ((array)$this->cal->driver->list_categories() as $cat => $color) { $select->add($cat, $cat); } @@ -215,10 +215,10 @@ class calendar_ui { $attrib['name'] = 'freebusy'; $select = new html_select($attrib); - $select->add($this->calendar->gettext('free'), 'free'); - $select->add($this->calendar->gettext('busy'), 'busy'); - $select->add($this->calendar->gettext('outofoffice'), 'outofoffice'); - $select->add($this->calendar->gettext('tentative'), 'tentative'); + $select->add($this->cal->gettext('free'), 'free'); + $select->add($this->cal->gettext('busy'), 'busy'); + $select->add($this->cal->gettext('outofoffice'), 'outofoffice'); + $select->add($this->cal->gettext('tentative'), 'tentative'); return $select->show(null); } @@ -229,9 +229,9 @@ class calendar_ui { $attrib['name'] = 'priority'; $select = new html_select($attrib); - $select->add($this->calendar->gettext('normal'), '1'); - $select->add($this->calendar->gettext('low'), '0'); - $select->add($this->calendar->gettext('high'), '2'); + $select->add($this->cal->gettext('normal'), '1'); + $select->add($this->cal->gettext('low'), '0'); + $select->add($this->cal->gettext('high'), '2'); return $select->show(null); } @@ -242,9 +242,9 @@ class calendar_ui { $attrib['name'] = 'sensitivity'; $select = new html_select($attrib); - $select->add($this->calendar->gettext('public'), '0'); - $select->add($this->calendar->gettext('private'), '1'); - $select->add($this->calendar->gettext('confidential'), '2'); + $select->add($this->cal->gettext('public'), '0'); + $select->add($this->cal->gettext('private'), '1'); + $select->add($this->cal->gettext('confidential'), '2'); return $select->show(null); } @@ -255,9 +255,9 @@ class calendar_ui { unset($attrib['name']); $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type')); - $select_type->add($this->calendar->gettext('none'), ''); - foreach ($this->calendar->driver->alarm_types as $type) - $select_type->add($this->calendar->gettext(strtolower("alarm{$type}option")), $type); + $select_type->add($this->cal->gettext('none'), ''); + foreach ($this->cal->driver->alarm_types as $type) + $select_type->add($this->cal->gettext(strtolower("alarm{$type}option")), $type); $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3)); $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10)); @@ -265,7 +265,7 @@ class calendar_ui $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger) - $select_offset->add($this->calendar->gettext('trigger' . $trigger), $trigger); + $select_offset->add($this->cal->gettext('trigger' . $trigger), $trigger); // pre-set with default values from user settings $preset = calendar::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); @@ -281,7 +281,7 @@ class calendar_ui ); // TODO: support adding more alarms - #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->calendar->gettext('addalarm')), + #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->cal->gettext('addalarm')), # $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)'); return $html; @@ -304,7 +304,7 @@ class calendar_ui $items = array(); foreach ($steps as $n => $label) { $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'), - $this->calendar->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60)))))); + $this->cal->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60)))))); } return html::tag('ul', $attrib, join("\n", $items), html::$common_attrib); @@ -316,7 +316,7 @@ class calendar_ui function edit_attendees_notify($attrib = array()) { $checkbox = new html_checkbox(array('name' => 'notify', 'id' => 'edit-attendees-donotify', 'value' => 1)); - return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->calendar->gettext('sendnotifications'))); + return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications'))); } /** @@ -327,12 +327,12 @@ class calendar_ui $attrib['id'] = 'edit-recurring-warning'; $radio = new html_radiobutton(array('name' => 'savemode', 'class' => 'edit-recurring-savemode')); - $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->calendar->gettext('currentevent')) . ' ' . - html::label(null, $radio->show('', array('value' => 'future')) . $this->calendar->gettext('futurevents')) . ' ' . - html::label(null, $radio->show('all', array('value' => 'all')) . $this->calendar->gettext('allevents')) . ' ' . - html::label(null, $radio->show('', array('value' => 'new')) . $this->calendar->gettext('saveasnew')); + $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' . + html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' . + html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' . + html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew')); - return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->calendar->gettext('changerecurringeventwarning')) . html::div('savemode', $form)); + return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form)); } /** @@ -344,39 +344,39 @@ class calendar_ui // frequency selector case 'frequency': $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency')); - $select->add($this->calendar->gettext('never'), ''); - $select->add($this->calendar->gettext('daily'), 'DAILY'); - $select->add($this->calendar->gettext('weekly'), 'WEEKLY'); - $select->add($this->calendar->gettext('monthly'), 'MONTHLY'); - $select->add($this->calendar->gettext('yearly'), 'YEARLY'); - $html = html::label('edit-frequency', $this->calendar->gettext('frequency')) . $select->show(''); + $select->add($this->cal->gettext('never'), ''); + $select->add($this->cal->gettext('daily'), 'DAILY'); + $select->add($this->cal->gettext('weekly'), 'WEEKLY'); + $select->add($this->cal->gettext('monthly'), 'MONTHLY'); + $select->add($this->cal->gettext('yearly'), 'YEARLY'); + $html = html::label('edit-frequency', $this->cal->gettext('frequency')) . $select->show(''); break; // daily recurrence case 'daily': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily')); - $html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('days'))); + $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('days'))); break; // weekly recurrence form case 'weekly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly')); - $html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('weeks'))); + $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('weeks'))); // weekday selection $daymap = array('sun','mon','tue','wed','thu','fri','sat'); $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday')); $first = $this->rc->config->get('calendar_first_day', 1); for ($weekdays = '', $j = $first; $j <= $first+6; $j++) { $d = $j % 7; - $weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->calendar->gettext($daymap[$d])) . ' '; + $weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->cal->gettext($daymap[$d])) . ' '; } - $html .= html::div($attrib, html::label(null, $this->calendar->gettext('bydays')) . $weekdays); + $html .= html::div($attrib, html::label(null, $this->cal->gettext('bydays')) . $weekdays); break; // monthly recurrence form case 'monthly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly')); - $html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('months'))); + $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('months'))); /* multiple month selection is not supported by Kolab $checkbox = new html_radiobutton(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday')); @@ -388,9 +388,9 @@ class calendar_ui // rule selectors $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode')); $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); - $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->calendar->gettext('onsamedate'))); // $this->calendar->gettext('each') + $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->cal->gettext('onsamedate'))); // $this->cal->gettext('each') $table->add(null, $monthdays); - $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->calendar->gettext('onevery'))); + $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->cal->gettext('onevery'))); $table->add(null, $this->rrule_selectors($attrib['part'])); $html .= html::div($attrib, $table->show()); @@ -400,19 +400,19 @@ class calendar_ui // annually recurrence form case 'yearly': $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly')); - $html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('years'))); + $html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('years'))); // month selector $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'); - $boxtype = is_a($this->calendar->driver, 'kolab_driver') ? 'radio' : 'checkbox'; + $boxtype = is_a($this->cal->driver, 'kolab_driver') ? 'radio' : 'checkbox'; $checkbox = new html_inputfield(array('type' => $boxtype, 'name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth')); for ($months = '', $m = 1; $m <= 12; $m++) { - $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->calendar->gettext($monthmap[$m])); + $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->cal->gettext($monthmap[$m])); $months .= $m % 4 ? ' ' : html::br(); } $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months); // day rule selection - $html .= html::div($attrib, html::label(null, $this->calendar->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---')); + $html .= html::div($attrib, html::label(null, $this->cal->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---')); break; // end of recurrence form @@ -423,20 +423,20 @@ class calendar_ui $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); - $table->add('label', ucfirst($this->calendar->gettext('recurrencend'))); + $table->add('label', ucfirst($this->cal->gettext('recurrencend'))); $table->add(null, html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' . - $this->calendar->gettext('forever'))); + $this->cal->gettext('forever'))); $table->add('label', ''); $table->add(null, html::label(null, $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count')) . ' ' . - $this->calendar->gettext(array( + $this->cal->gettext(array( 'name' => 'forntimes', 'vars' => array('nr' => $select->show(1))) ))); $table->add('label', ''); $table->add(null, $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until')) . ' ' . - $this->calendar->gettext('until') . ' ' . $input->show('')); + $this->cal->gettext('until') . ' ' . $input->show('')); $html = $table->show(); break; } @@ -463,16 +463,16 @@ class calendar_ui $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix")); if ($noselect) $select_prefix->add($noselect, ''); $select_prefix->add(array( - $this->calendar->gettext('first'), - $this->calendar->gettext('second'), - $this->calendar->gettext('third'), - $this->calendar->gettext('fourth') + $this->cal->gettext('first'), + $this->cal->gettext('second'), + $this->cal->gettext('third'), + $this->cal->gettext('fourth') ), array(1, 2, 3, 4)); // Kolab doesn't support 'last' but others do. - if (!is_a($this->calendar->driver, 'kolab_driver')) - $select_prefix->add($this->calendar->gettext('last'), -1); + if (!is_a($this->cal->driver, 'kolab_driver')) + $select_prefix->add($this->cal->gettext('last'), -1); $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday")); if ($noselect) $select_wday->add($noselect, ''); @@ -481,10 +481,10 @@ class calendar_ui $first = $this->rc->config->get('calendar_first_day', 1); for ($j = $first; $j <= $first+6; $j++) { $d = $j % 7; - $select_wday->add($this->calendar->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2))); + $select_wday->add($this->cal->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2))); } if ($part == 'monthly') - $select_wday->add($this->calendar->gettext('dayofmonth'), ''); + $select_wday->add($this->cal->gettext('dayofmonth'), ''); return $select_prefix->show() . ' ' . $select_wday->show(); } @@ -541,15 +541,15 @@ class calendar_ui { $table = new html_table(array('cols' => 3)); - if (!empty($this->calendar->attachment['name'])) { + if (!empty($this->cal->attachment['name'])) { $table->add('title', Q(rcube_label('filename'))); - $table->add(null, Q($this->calendar->attachment['name'])); + $table->add(null, Q($this->cal->attachment['name'])); $table->add(null, '[' . html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))) . ']'); } - if (!empty($this->calendar->attachment['size'])) { + if (!empty($this->cal->attachment['size'])) { $table->add('title', Q(rcube_label('filesize'))); - $table->add(null, Q(show_bytes($this->calendar->attachment['size']))); + $table->add(null, Q(show_bytes($this->cal->attachment['size']))); } return $table->show($attrib); @@ -567,21 +567,21 @@ class calendar_ui $formfields = array( 'name' => array( - 'label' => $this->calendar->gettext('name'), + 'label' => $this->cal->gettext('name'), 'value' => $input_name->show($name), 'id' => 'calendar-name', ), 'color' => array( - 'label' => $this->calendar->gettext('color'), + 'label' => $this->cal->gettext('color'), 'value' => $input_color->show($calendar['color']), 'id' => 'calendar-color', ), ); - if ($this->calendar->driver->alarms) { + if ($this->cal->driver->alarms) { $checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1)); $formfields['showalarms'] = array( - 'label' => $this->calendar->gettext('showalarms'), + 'label' => $this->cal->gettext('showalarms'), 'value' => $checkbox->show($calendar['showalarms']?1:0), 'id' => 'calendar-showalarms', ); @@ -589,7 +589,7 @@ class calendar_ui // allow driver to extend or replace the form content return html::tag('form', array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'), - $this->calendar->driver->calendar_form($action, $calendar, $formfields) + $this->cal->driver->calendar_form($action, $calendar, $formfields) ); } @@ -599,10 +599,10 @@ class calendar_ui function attendees_list($attrib = array()) { $table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); - $table->add_header('role', $this->calendar->gettext('role')); - $table->add_header('name', $this->calendar->gettext('attendee')); - $table->add_header('availability', $this->calendar->gettext('availability')); - $table->add_header('confirmstate', $this->calendar->gettext('confirmstate')); + $table->add_header('role', $this->cal->gettext('role')); + $table->add_header('name', $this->cal->gettext('attendee')); + $table->add_header('availability', $this->cal->gettext('availability')); + $table->add_header('confirmstate', $this->cal->gettext('confirmstate')); $table->add_header('options', ''); return $table->show($attrib); @@ -618,9 +618,9 @@ class calendar_ui return html::div($attrib, html::div(null, $input->show() . " " . - html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->calendar->gettext('addattendee'))) . " " . - html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->calendar->gettext('scheduletime').'...'))) . - html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->calendar->gettext('sendinvitations'))) + html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " . + html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) . + html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->cal->gettext('sendinvitations'))) ); } @@ -631,7 +631,7 @@ class calendar_ui { $table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0)); $table->add('attendees', - html::tag('h3', 'boxtitle', $this->calendar->gettext('tabattendees')) . + html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) . html::div('timesheader', ' ') . html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '') ); @@ -644,21 +644,55 @@ class calendar_ui return $table->show($attrib); } - - + + /** + * Render event details in a table + */ + function event_details_table($event, $title) + { + $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); + $table->add('ititle', $title); + $table->add('title', Q($event['title'])); + $table->add('label', $this->cal->gettext('date')); + $table->add('location', Q($this->cal->event_date_text($event))); + if ($event['location']) { + $table->add('label', $this->cal->gettext('location')); + $table->add('location', Q($event['location'])); + } + + return $table->show(); + } + + /** + * + */ + function event_invitebox($attrib = array()) + { + if ($this->cal->event) { + return html::div($attrib, + $this->event_details_table($this->cal->event, $this->cal->gettext('itipinvitation')) . + $this->cal->invitestatus + ); + } + + return ''; + } + function event_rsvp_buttons($attrib = array()) { + $attrib += array('type' => 'button'); foreach (array('accepted','tentative','declined') as $method) { $buttons .= html::tag('input', array( - 'type' => 'button', + 'type' => $attrib['type'], + 'name' => $attrib['iname'], 'class' => 'button', 'rel' => $method, - 'value' => $this->calendar->gettext('itip' . $method), + 'value' => $this->cal->gettext('itip' . $method), )); } return html::div($attrib, - html::div('label', $this->calendar->gettext('acceptinvitation')) . + html::div('label', $this->cal->gettext('acceptinvitation')) . html::div('rsvp-buttons', $buttons)); } diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc index fe125aa1..44afacc6 100644 --- a/plugins/calendar/localization/de_CH.inc +++ b/plugins/calendar/localization/de_CH.inc @@ -120,6 +120,7 @@ $labels['nextslot'] = 'Nächster Vorschlag'; $labels['noslotfound'] = 'Es konnten keine freien Zeiten gefunden werden'; $labels['invitationsubject'] = 'Sie wurden zu "$title" eingeladen'; $labels['invitationmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit allen Details des Termins. Diese können Sie in Ihre Kalenderanwendung importieren."; +$labels['invitationattendlinks'] = "Falls Ihr E-Mail-Programm keine iTip-Anfragen unterstützt, können Sie den folgenden Link verwenden, um den Termin zu bestätigen oder abzulehnen:\n\$url"; $labels['eventupdatesubject'] = '"$title" wurde aktualisiert'; $labels['eventupdatesubjectempty'] = 'An event that concerns you has been updated'; $labels['eventupdatemailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit den aktualisiereten Termindaten. Diese können Sie in Ihre Kalenderanwendung importieren"; @@ -148,6 +149,7 @@ $labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin anneh $labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen'; $labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen'; $labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt'; +$labels['eventcancelled'] = 'Der Termin wurde vom Organisator abgesagt'; // event dialog tabs $labels['tabsummary'] = 'Übersicht'; diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc index 743505b1..42ea4c1b 100644 --- a/plugins/calendar/localization/de_DE.inc +++ b/plugins/calendar/localization/de_DE.inc @@ -119,6 +119,7 @@ $labels['nextslot'] = 'Nächster Vorschlag'; $labels['noslotfound'] = 'Es konnten keine freien Zeiten gefunden werden'; $labels['invitationsubject'] = 'Sie wurden zu "$title" eingeladen'; $labels['invitationmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit allen Details des Termins. Diese können Sie in Ihre Kalenderanwendung importieren."; +$labels['invitationattendlinks'] = "Falls Ihr E-Mail-Programm keine iTip-Anfragen unterstützt, können Sie den folgenden Link verwenden, um den Termin zu bestätigen oder abzulehnen:\n\$url"; $labels['eventupdatesubject'] = '"$title" wurde aktualisiert'; $labels['eventupdatesubjectempty'] = 'An event that concerns you has been updated'; $labels['eventupdatemailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit den aktualisiereten Termindaten. Diese können Sie in Ihre Kalenderanwendung importieren"; @@ -147,6 +148,7 @@ $labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin anneh $labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen'; $labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen'; $labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt'; +$labels['eventcancelled'] = 'Der Termin wurde vom Organisator abgesagt'; // event dialog tabs $labels['tabsummary'] = 'Übersicht'; diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 5a293038..986068db 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -120,6 +120,7 @@ $labels['nextslot'] = 'Next Slot'; $labels['noslotfound'] = 'Unable to find a free time slot'; $labels['invitationsubject'] = 'You\'ve been invited to "$title"'; $labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application."; +$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url"; $labels['eventupdatesubject'] = '"$title" has been updated'; $labels['eventupdatesubjectempty'] = 'An event that concerns you has been updated'; $labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated event details which you can import to your calendar application."; @@ -148,6 +149,7 @@ $labels['acceptinvitation'] = 'Do you accept this invitation?'; $labels['youhaveaccepted'] = 'You have accepted this invitation'; $labels['youhavetentative'] = 'You have tentatively accepted this invitation'; $labels['youhavedeclined'] = 'You have declined this invitation'; +$labels['eventcancelled'] = 'The event has been cancelled'; // event dialog tabs $labels['tabsummary'] = 'Summary'; @@ -174,6 +176,7 @@ $labels['nowritecalendarfound'] = 'No calendar found to save the event'; $labels['importedsuccessfully'] = 'The event was successfully added to \'$calendar\''; $labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status'; $labels['itipresponseerror'] = 'Failed to send the response to this event invitation'; +$labels['itipinvalidrequest'] = 'This invitation is no longer valid'; $labels['sentresponseto'] = 'Successfully sent invitation response to $mailto'; $labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your personal calendar'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 29aca0c8..ddeda06d 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -1169,3 +1169,34 @@ div.calendar-invitebox .rsvp-status.tentative { background-position: 2px -60px; } +/* iTIP attend reply page */ + +.calendaritipattend .centerbox { + width: 40em; + margin: 80px auto; + padding: 10px 10px 10px 90px; + border: 1px solid #ccc; + box-shadow: 1px 1px 24px #ccc; + -moz-box-shadow: 1px 1px 18px #ccc; + -webkit-box-shadow: #ccc 1px 1px 18px; + background: url(images/invitation.png) 10px 10px no-repeat #fbfbfb; +} + +.calendaritipattend .calendar-invitebox { + background: none; + padding-left: 0; + border: 0; + margin: 0 0 2em 0; +} + +.calendaritipattend .calendar-invitebox .rsvp-status { + margin-top: 2.5em; + font-size: 110%; + font-weight: bold; +} + +.calendaritipattend .calendar-invitebox td.title, +.calendaritipattend .calendar-invitebox td.ititle { + font-size: 120%; +} + diff --git a/plugins/calendar/skins/default/images/invitation.png b/plugins/calendar/skins/default/images/invitation.png new file mode 100644 index 0000000000000000000000000000000000000000..f3df83a722f349ddab903916ea6e88d4ba50a139 GIT binary patch literal 1909 zcmV-*2a5QKP)K&KWAr#v*Q_dW_EUU*Z#@LIsfdO%Qye+oPTB=@EVijfAHoh0n1xh za%KLgT$jmpCze($gEp-*B=cDXpO2-8<3H8>o`xj`OOs9DLo#1L@ZFMhRD10QLUL$b z($O)G)<5Uvo#%Py^y$a2T4A@E4!5_j7tJU0pRAFjo7z5ZLdqdwW?uDJjWC z>t2~3zjEa|TIZIP*#f##p08JDzLMf%rUCA4l|d&U-YYwK3VC#I&dn&(r;kN4{n3|X2F+s@L`H4IOAIjcY0u%RNL%@ac8{_Y|z`LhS1LwGLkSnWU zN?IDLS0^N>{6v?_Ht?3_rBs^Y#LV zM~tX_w1PLK_G%~1!6qVzo+U5wKy5tyJnw|B4aAY{hdE9$~KITB&O zB{2@;HqSW<%0v`+B1s~(w-D+gT=JGe9p8yWnB*ma+DqT`}OQurUfDm`1;TxF&J*sV)y|F9JlGcy-~@(jd}D8 zd;pXt$w@|q(k4uR)2PCfhER>$Py^m8DA0ZIY;l;o8<9=NYZAOj^49kDv~6(WbQ!$% z{yC`Tzce(cJhfxdn4fRnthTS-xB<2G^}ek-uElFo^0qiUVnduKd}atKE-o(Mm~?)n z50kvf`7Aj+ZxE8#qYEu9e&EL;_&{S)X)AqbZB_DKQ<8ttR)UK24b9C^kC8wjS4IZB zgV~)xW75SdHc!EuwRv^@R`&M1!!z^LhtZxJFI<4Xy1L+>o*o!}`c(aG8pi7lrKN_( z^KjrtOiJE#s3Uskd6SawzIV@0(h3#KnFAj#Uk>r{@rK4DoZ5R$N?!MX7HY{SV~lrA zo(#PY9>CDhkb1rO(j`_tShfrnFIezuNnU%N=bxA<@VK%0xV&8D|7>lApH7^Bfq?Z|73lP6&|Zi*(zubb_5c(%s* zU_%|X^P<8+IDYDsdhG_?*w$+?Cpcv z-r6a7O8=tGk8sD_vFy12$rE-UQtofXWAbebom21&jF9f|LY2(;9Zz#S_`JNl7slPT zbEj_qukdZKLI)}#?@C7j17Xg1&h9 zGMqrqe&_C8^_mm2I;&T$5{`Lb|9-=2ZEL6_K}!B2ZeXt90cH>#+5P+X;URu4rIlm~ z?g6%J+5~R2U2llbzI(_ibp!Na4$Lcvem&+BgYR}<_ zH7)rsH*SPTCPR`3&6A-Aj}DvuE%CC5V$j1krrOiUKs(b~i4sbiFQV)GHKonh0qv8k zm7?VX-6N2EVDQmOdf=1fHv2=)HNB+mZx$V7ODLccn#@4UY-;fx$_if00000NkvXXu0mjfxoWh@ literal 0 HcmV?d00001 diff --git a/plugins/calendar/skins/default/templates/itipattend.html b/plugins/calendar/skins/default/templates/itipattend.html new file mode 100644 index 00000000..84968b58 --- /dev/null +++ b/plugins/calendar/skins/default/templates/itipattend.html @@ -0,0 +1,21 @@ + + + +<roundcube:object name="pagetitle" /> + + + + +