Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
92 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 4c5ecafd..1b0788c7 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -1,867 +1,932 @@
<?php
/**
* iTIP functions for the calendar-based Roudncube plugins
*
* Class providing functionality to manage iTIP invitations
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2011-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class libcalendaring_itip
{
protected $rc;
protected $lib;
protected $plugin;
protected $sender;
protected $domain;
protected $itip_send = false;
protected $rsvp_actions = array('accepted','tentative','declined','delegated');
protected $rsvp_status = array('accepted','tentative','declined','delegated');
function __construct($plugin, $domain = 'libcalendaring')
{
$this->plugin = $plugin;
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->domain = $domain;
$hook = $this->rc->plugins->exec_hook('calendar_load_itip',
array('identity' => $this->rc->user->list_emails(true)));
$this->sender = $hook['identity'];
$this->plugin->add_hook('message_before_send', array($this, 'before_send_hook'));
$this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook'));
}
public function set_sender_email($email)
{
if (!empty($email))
$this->sender['email'] = $email;
}
public function set_rsvp_actions($actions)
{
$this->rsvp_actions = (array)$actions;
$this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
}
public function set_rsvp_status($status)
{
$this->rsvp_status = $status;
}
/**
* Wrapper for rcube_plugin::gettext()
* Checking for a label in different domains
*
* @see rcube::gettext()
*/
public function gettext($p)
{
$label = is_array($p) ? $p['name'] : $p;
$domain = $this->domain;
if (!$this->rc->text_exists($label, $domain)) {
$domain = 'libcalendaring';
}
return $this->rc->gettext($p, $domain);
}
/**
* 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
* @param boolean Request RSVP
* @return boolean True on success, false on failure
*/
public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true)
{
if (!$this->sender['name'])
$this->sender['name'] = $this->sender['email'];
if (!$message) {
libcalendaring::identify_recurrence_instance($event);
$message = $this->compose_itip_message($event, $method, $rsvp);
}
$mailto = rcube_utils::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' => $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']);
}
$recurrence_info = '';
if (!empty($event['recurrence_id'])) {
$recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
}
else if (!empty($event['recurrence'])) {
$recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
}
$mailbody = $this->gettext(array(
'name' => $bodytext,
'vars' => array(
'title' => $event['title'],
'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
'attendees' => join(",\n ", $attendees_list),
'sender' => $this->sender['name'],
'organizer' => $this->sender['name'],
)
));
// if (!empty($event['comment'])) {
// $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment'];
// }
// append links for direct invitation replies
if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) {
$mailbody .= "\n\n" . $this->gettext(array(
'name' => 'invitationattendlinks',
'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))),
));
}
else if ($method == 'CANCEL' && $event['cancelled']) {
$this->cancel_itip_invitation($event);
}
$message->headers($headers, true);
$message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
rcube::console('iTip ' . $method, $message->txtHeaders() . "\r\n" . $message->get());
}
// finally send the message
$this->itip_send = true;
$sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
$this->itip_send = false;
return $sent;
}
/**
* Plugin hook triggered by rcube::deliver_message() before delivering a message.
* Here we can set the 'smtp_server' config option to '' in order to use
* PHP's mail() function for unauthenticated email sending.
*/
public function before_send_hook($p)
{
if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') {
$this->rc->config->set('smtp_server', '');
}
return $p;
}
/**
* Plugin hook to alter SMTP authentication.
* This is used if iTip messages are to be sent from an unauthenticated session
*/
public function smtp_connect_hook($p)
{
// replace smtp auth settings if we're not in an authenticated session
if ($this->itip_send && !$this->rc->user->ID) {
foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) {
$p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]);
}
}
return $p;
}
/**
* 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)
* @param boolean Request RSVP
* @return object Mail_mime object with message data
*/
public function compose_itip_message($event, $method, $rsvp = true)
{
$from = rcube_utils::idn_to_ascii($this->sender['email']);
$from_utf = rcube_utils::idn_to_utf8($from);
$sender = format_email_recipient($from, $this->sender['name']);
// truncate list attendees down to the recipient of the iTip Reply.
// constraints for a METHOD:REPLY according to RFC 5546
if ($method == 'REPLY') {
$replying_attendee = null;
$reply_attendees = array();
foreach ($event['attendees'] as $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$reply_attendees[] = $attendee;
}
else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
$replying_attendee = $attendee;
if ($attendee['status'] != 'DELEGATED') {
unset($replying_attendee['rsvp']); // unset the RSVP attribute
}
}
// include attendees relevant for delegation (RFC 5546, Section 4.2.5)
else if ((!empty($attendee['delegated-to']) &&
(strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) ||
(!empty($attendee['delegated-from']) &&
(strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) {
$reply_attendees[] = $attendee;
}
}
if ($replying_attendee) {
array_unshift($reply_attendees, $replying_attendee);
$event['attendees'] = $reply_attendees;
}
if ($event['recurrence']) {
unset($event['recurrence']['EXCEPTIONS']);
}
}
// set RSVP for every attendee
else if ($method == 'REQUEST') {
foreach ($event['attendees'] as $i => $attendee) {
if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) {
$event['attendees'][$i]['rsvp']= (bool)$rsvp;
}
}
}
else if ($method == 'CANCEL') {
if ($event['recurrence']) {
unset($event['recurrence']['EXCEPTIONS']);
}
}
// Set SENT-BY property if the sender is not the organizer
if ($method == 'CANCEL' || $method == 'REQUEST') {
foreach ((array)$event['attendees'] as $idx => $attendee) {
if ($attendee['role'] == 'ORGANIZER'
&& $attendee['email']
&& strcasecmp($attendee['email'], $from) != 0
&& strcasecmp($attendee['email'], $from_utf) != 0
) {
$attendee['sent-by'] = 'mailto:' . $from_utf;
$event['organizer'] = $event['attendees'][$idx] = $attendee;
break;
}
}
}
// 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', RCUBE_CHARSET);
$message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed");
$message->setContentType('multipart/alternative');
// compose common headers array
$headers = array(
'From' => $sender,
'Date' => $this->rc->user_date(),
'Message-ID' => $this->rc->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 = libcalendaring::get_ical();
$ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
$filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
$message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method);
return $message;
}
/**
* Forward the given iTip event as delegation to another person
*
* @param array Event object to delegate
* @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
* @param boolean The delegator's RSVP flag
* @param array List with indexes of new/updated attendees
* @return boolean True on success, False on failure
*/
public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
{
if (is_string($delegate)) {
$delegates = rcube_mime::decode_address_list($delegate, 1, false);
if (count($delegates) > 0) {
$delegate = reset($delegates);
}
}
$emails = $this->lib->get_user_emails();
$me = $this->rc->user->list_emails(true);
// find/create the delegate attendee
$delegate_attendee = array(
'email' => $delegate['mailto'],
'name' => $delegate['name'],
'role' => 'REQ-PARTICIPANT',
);
$delegate_index = count($event['attendees']);
foreach ($event['attendees'] as $i => $attendee) {
// set myself the DELEGATED-TO parameter
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$event['attendees'][$i]['delegated-to'] = $delegate['mailto'];
$event['attendees'][$i]['status'] = 'DELEGATED';
$event['attendees'][$i]['role'] = 'NON-PARTICIPANT';
$event['attendees'][$i]['rsvp'] = $rsvp;
$me['email'] = $attendee['email'];
$delegate_attendee['role'] = $attendee['role'];
}
// the disired delegatee is already listed as an attendee
else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') {
$delegate_attendee = $attendee;
$delegate_index = $i;
break;
}
// TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me)
}
// set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
$delegate_attendee['rsvp'] = true;
$delegate_attendee['status'] = 'NEEDS-ACTION';
$delegate_attendee['delegated-from'] = $me['email'];
$event['attendees'][$delegate_index] = $delegate_attendee;
$attendees[] = $delegate_index;
$this->set_sender_email($me['email']);
return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
}
/**
* Handler for calendar/itip-status requests
*/
public function get_itip_status($event, $existing = null)
{
$action = $event['rsvp'] ? 'rsvp' : '';
$status = $event['fallback'];
- $latest = $resheduled = false;
+ $latest = $rescheduled = false;
$html = '';
if (is_numeric($event['changed']))
$event['changed'] = new DateTime('@'.$event['changed']);
// check if the given itip object matches the last state
if ($existing) {
$latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
(!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
}
// determine action for REQUEST
if ($event['method'] == 'REQUEST') {
$html = html::div('rsvp-status', $this->gettext('acceptinvitation'));
if ($existing) {
$rsvp = $event['rsvp'];
$emails = $this->lib->get_user_emails();
foreach ($existing['attendees'] as $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$status = strtoupper($attendee['status']);
break;
}
}
// Detect re-sheduling
if (!$latest) {
// FIXME: This is probably to simplistic, or maybe we should just check
// attendee's RSVP flag in the new event?
- $resheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end'];
+ $rescheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end'];
}
}
else {
$rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true);
}
$status_lc = strtolower($status);
if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) {
$html = html::div('rsvp-status', $this->gettext('notanattendee'));
$action = 'import';
}
else if (in_array($status_lc, $this->rsvp_status)) {
$status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc);
if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) {
$action = ''; // nothing to do here, outdated invitation
if ($status_lc == 'needs-action')
$status_text = $this->gettext('outdatedinvitation');
}
else if (!$existing && !$rsvp) {
$action = 'import';
}
- else if ($resheduled) {
+ else if ($rescheduled) {
$action = 'rsvp';
}
else if ($status_lc != 'needs-action') {
+ // check if there are any changes
+ if ($latest) {
+ $diff = $this->get_itip_diff($event, $existing);
+ $latest = empty($diff);
+ }
+
$action = !$latest ? 'update' : '';
}
$html = html::div('rsvp-status ' . $status_lc, $status_text);
}
}
// determine action for REPLY
else if ($event['method'] == 'REPLY') {
// check whether the sender already is an attendee
if ($existing) {
$action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : '';
$listed = false;
foreach ($existing['attendees'] as $attendee) {
if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
$status_lc = strtolower($status);
if (in_array($status_lc, $this->rsvp_status)) {
$html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
'name' => 'attendee' . $status_lc,
'vars' => array(
'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')),
)
)));
}
$action = $attendee['status'] == $status || !$latest ? '' : 'update';
$listed = true;
break;
}
}
if (!$listed) {
$html = html::div('rsvp-status', $this->gettext('itipnewattendee'));
}
}
else {
$html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
$action = '';
}
}
else if ($event['method'] == 'CANCEL') {
if (!$existing) {
$html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
$action = '';
}
}
return array(
'uid' => $event['uid'],
'id' => asciiwords($event['uid'], true),
'existing' => $existing ? true : false,
'saved' => $existing ? true : false,
'latest' => $latest,
'status' => $status,
'action' => $action,
- 'resheduled' => $resheduled,
+ 'rescheduled' => $rescheduled,
'html' => $html,
);
}
+ protected function get_itip_diff($event, $existing)
+ {
+ if (empty($event) || empty($existing) || empty($event['message_uid'])) {
+ return;
+ }
+
+ $itip = $this->lib->mail_get_itip_object($event['mbox'], $event['message_uid'], $event['mime_id'],
+ $event['task'] == 'calendar' ? 'event' : 'task');
+
+ if ($itip) {
+ // List of properties that could change without SEQUENCE bump
+ $attrs = array('description', 'title', 'location', 'url');
+ $diff = array();
+
+ foreach ($attrs as $attr) {
+ if (isset($itip[$attr]) && $itip[$attr] != $existing[$attr]) {
+ $diff[$attr] = array(
+ 'new' => $itip[$attr],
+ 'old' => $existing[$attr]
+ );
+ }
+ }
+
+ $status = array();
+ $itip_attendees = array();
+ $existing_attendees = array();
+ $emails = $this->lib->get_user_emails();
+
+ // Compare list of attendees (ignoring current user status)
+ foreach ((array) $existing['attendees'] as $idx => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $status[strtolower($attendee['email'])] = $attendee['status'];
+ }
+ if ($attendee['role'] == 'ORGANIZER') {
+ $attendee['status'] = 'ACCEPTED'; // sometimes is not set for exceptions
+ $existing['attendees'][$idx] = $attendee;
+ }
+ $existing_attendees[] = $attendee['email'].$attendee['name'];
+ }
+ foreach ((array) $itip['attendees'] as $idx => $attendee) {
+ if ($attendee['email'] && ($_status = $status[strtolower($attendee['email'])])) {
+ $attendee['status'] = $_status;
+ $itip['attendees'][$idx] = $attendee;
+ }
+ $itip_attendees[] = $attendee['email'].$attendee['name'];
+ }
+
+ if ($itip_attendees != $existing_attendees) {
+ $diff['attendees'] = array(
+ 'new' => $itip['attendees'],
+ 'old' => $existing['attendees']
+ );
+ }
+
+ return $diff;
+ }
+ }
+
/**
* Build inline UI elements for iTip messages
*/
public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null)
{
$buttons = array();
$dom_id = asciiwords($event['uid'], true);
$rsvp_status = 'unknown';
// pass some metadata about the event and trigger the asynchronous status check
$changed = is_object($event['changed']) ? $event['changed'] : $message_date;
$metadata = array(
'uid' => $event['uid'],
'_instance' => $event['_instance'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'method' => $method,
'task' => $task,
+ 'mime_id' => $mime_id,
);
// create buttons to be activated from async request checking existence of this event in local calendars
$buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
// on iTip REPLY we have two options:
if ($method == 'REPLY') {
$title = $this->gettext('itipreply');
foreach ($event['attendees'] as $attendee) {
if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') {
if (empty($event['_sender']) || self::compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
$metadata['attendee'] = $attendee['email'];
$rsvp_status = strtoupper($attendee['status']);
if ($attendee['delegated-to']) {
$metadata['delegated-to'] = $attendee['delegated-to'];
}
break;
}
}
}
// 1. update the attendee status on our copy
$update_button = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
'value' => $this->gettext('updateattendeestatus'),
));
// 2. accept or decline a new or delegate attendee
$accept_buttons = html::tag('input', array(
'type' => 'button',
'class' => "button accept",
'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
'value' => $this->gettext('acceptattendee'),
));
$accept_buttons .= html::tag('input', array(
'type' => 'button',
'class' => "button decline",
'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . rcube::JQ($mime_id) . "', '$task')",
'value' => $this->gettext('declineattendee'),
));
$buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
$buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons);
}
// when receiving iTip REQUEST messages:
else if ($method == 'REQUEST') {
$emails = $this->lib->get_user_emails();
$title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
$metadata['rsvp'] = true;
$metadata['sensitivity'] = $event['sensitivity'];
if (is_object($event['start'])) {
$metadata['date'] = $event['start']->format('U');
}
// check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons
if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') {
$this->rsvp_actions = array('accepted','declined');
$metadata['nosave'] = true;
}
// 1. display RSVP buttons (if the user was invited)
foreach ($this->rsvp_actions as $method) {
$rsvp_buttons .= html::tag('input', array(
'type' => 'button',
'class' => "button $method",
'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task', '$method', '$dom_id')",
'value' => $this->gettext('itip' . $method),
));
}
// add button to open calendar/preview
if (!empty($preview_url)) {
$msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id;
$rsvp_buttons .= html::tag('input', array(
'type' => 'button',
'class' => "button preview",
'onclick' => "rcube_libcalendaring.open_itip_preview('" . rcube::JQ($preview_url) . "', '" . rcube::JQ($msgref) . "')",
'value' => $this->gettext('openpreview'),
));
}
// 2. update the local copy with minor changes
$update_button = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
'value' => $this->gettext('updatemycopy'),
));
// 3. Simply import the event without replying
$import_button = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
'value' => $this->gettext('importtocalendar'),
));
// check my status
foreach ($event['attendees'] as $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
$rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION';
break;
}
}
// add itip reply message controls
$rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave']));
$buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
$buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
// prepare autocompletion for delegation dialog
if (in_array('delegated', $this->rsvp_actions)) {
$this->rc->autocomplete_init();
}
}
// for CANCEL messages, we can:
else if ($method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
$event_prop = array_filter(array(
'uid' => $event['uid'],
'_instance' => $event['_instance'],
'_savemode' => $event['_savemode'],
));
// 1. remove the event from our calendar
$button_remove = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . rcube::JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
// 2. update our copy with status=cancelled
$button_update = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
'value' => $this->gettext('updatemycopy'),
));
$buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update);
$rsvp_status = 'CANCELLED';
$metadata['rsvp'] = true;
}
// append generic import button
if ($import_button) {
$buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
}
// pass some metadata about the event and trigger the asynchronous status check
$metadata['fallback'] = $rsvp_status;
$metadata['rsvp'] = intval($metadata['rsvp']);
$this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . rcube_output::json_serialize($metadata) . ")", 'docready');
// get localized texts from the right domain
foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee',
'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation',
'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
$this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
}
// show event details with buttons
return $this->itip_object_details_table($event, $title) .
html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons));
}
/**
* Render an RSVP UI widget with buttons to respond on iTip invitations
*/
function itip_rsvp_buttons($attrib = array(), $actions = null)
{
$attrib += array('type' => 'button');
if (!$actions)
$actions = $this->rsvp_actions;
foreach ($actions as $method) {
$buttons .= html::tag('input', array(
'type' => $attrib['type'],
'name' => $attrib['iname'],
'class' => 'button',
'rel' => $method,
'value' => $this->gettext('itip' . $method),
));
}
// add localized texts for the delegation dialog
if (in_array('delegated', $actions)) {
foreach (array('itipdelegated','itipcomment','delegateinvitation',
'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) {
$this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
}
}
foreach (array('all','current','future') as $mode) {
$this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
}
$savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
return html::div($attrib,
html::div('label', $this->gettext('acceptinvitation')) .
html::div('rsvp-buttons',
$buttons .
html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
)
);
}
/**
* Render UI elements to control iTip reply message sending
*/
public function itip_rsvp_options_ui($dom_id, $disable = false)
{
$itip_sending = $this->rc->config->get('calendar_itip_send_option', 3);
// itip sending is entirely disabled
if ($itip_sending === 0) {
return '';
}
// add checkbox to suppress itip reply message
else if ($itip_sending >= 2) {
$rsvp_additions = html::label(array('class' => 'noreply-toggle'),
html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0))
. ' ' . $this->gettext('itipsuppressreply')
);
}
// add input field for reply comment
$toggle_attrib = array(
'href' => '#toggle',
'class' => 'reply-comment-toggle',
'onclick' => '$(this).hide().parent().find(\'textarea\').show().focus()'
);
$textarea_attrib = array(
'id' => 'reply-comment-' . $dom_id,
'name' => '_comment',
'cols' => 40,
'rows' => 6,
'style' => 'display:none',
'placeholder' => $this->gettext('itipcomment')
);
$rsvp_additions .= html::a($toggle_attrib, $this->gettext('itipeditresponse'))
. html::div('itip-reply-comment', html::tag('textarea', $textarea_attrib, ''));
return $rsvp_additions;
}
/**
* Render event/task details in a table
*/
function itip_object_details_table($event, $title)
{
$table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
$table->add('ititle', $title);
$table->add('title', rcube::Q($event['title']));
if ($event['start'] && $event['end']) {
$table->add('label', $this->gettext('date'));
$table->add('date', rcube::Q($this->lib->event_date_text($event)));
}
else if ($event['due'] && $event['_type'] == 'task') {
$table->add('label', $this->gettext('date'));
$table->add('date', rcube::Q($this->lib->event_date_text($event)));
}
if (!empty($event['recurrence_date'])) {
$table->add('label', '');
$table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
}
else if (!empty($event['recurrence'])) {
$table->add('label', $this->gettext('recurring'));
$table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
}
if ($event['location'] && trim($event['location'])) {
$table->add('label', $this->gettext('location'));
$table->add('location', rcube::Q($event['location']));
}
if ($event['sensitivity'] && !preg_match('/^(x-|public$)/i', $event['sensitivity'])) {
$table->add('label', $this->gettext('sensitivity'));
$table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!');
}
if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') {
$table->add('label', $this->gettext('status'));
$table->add('status', $this->gettext('status-' . strtolower($event['status'])));
}
if ($event['comment'] && trim($event['comment'])) {
$table->add('label', $this->gettext('comment'));
$table->add('location', rcube::Q($event['comment']));
}
return $table->show();
}
/**
* 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)
{
// empty stub
return false;
}
/**
* Mark invitations for the given event as cancelled
*
* @param array Hash array with event properties
*/
public function cancel_itip_invitation($event)
{
// empty stub
return false;
}
/**
* Utility function to get the value of a custom property
*/
public static function get_custom_property($event, $name)
{
$ret = false;
if (is_array($event['x-custom'])) {
array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) {
if (strcasecmp($prop[0], $name) === 0) {
$ret = $prop[1];
}
});
}
return $ret;
}
/**
* Compare email address
*/
public static function compare_email($value, $email, $email_utf = null)
{
$v1 = !empty($email) && strcasecmp($value, $email) === 0;
$v2 = !empty($email_utf) && strcasecmp($value, $email_utf) === 0;
return $v1 || $v2;
}
}
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index caae913b..a5f3ef14 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -1,1464 +1,1466 @@
/**
* Basic Javascript utilities for calendar-related plugins
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this page.
*
* Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice
* for the JavaScript code in this page.
*/
function rcube_libcalendaring(settings)
{
// member vars
this.settings = settings || {};
this.alarm_ids = [];
this.alarm_dialog = null;
this.snooze_popup = null;
this.dismiss_link = null;
this.group2expand = {};
// abort if env isn't set
if (!settings || !settings.date_format)
return;
// private vars
var me = this;
var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
var client_timezone = new Date().getTimezoneOffset();
// general datepicker settings
var datepicker_settings = {
// translate from fullcalendar format to datepicker format
dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings.first_day,
dayNamesMin: settings.days_short,
monthNames: settings.months,
monthNamesShort: settings.months,
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
/**
* Quote html entities
*/
var Q = this.quote_html = function(str)
{
return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
/**
* Create a nice human-readable string for the date/time range
*/
this.event_date_text = function(event, voice)
{
if (!event.start)
return '';
if (!event.end)
event.end = event.start;
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000,
until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — ';
if (event.allDay) {
fromto = this.format_datetime(event.start, 1, voice)
+ (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : '');
}
else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
fromto = this.format_datetime(event.start, 0, voice)
+ (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : '');
}
else {
fromto = this.format_datetime(event.start, 0, voice)
+ (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : '');
}
return fromto;
};
/**
* Checks if the event/task has 'real' attendees, excluding the current user
*/
this.has_attendees = function(event)
{
return !!(event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email));
};
/**
* Check if the current user is an attendee of this event/task
*/
this.is_attendee = function(event, role, email)
{
var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
for (i=0; event.attendees && i < event.attendees.length; i++) {
if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) {
return event.attendees[i];
}
}
return false;
};
/**
* Checks if the current user is the organizer of the event/task
*/
this.is_organizer = function(event, email)
{
return this.is_attendee(event, 'ORGANIZER', email) || !event.id;
};
/**
* Check permissions on the given folder object
*/
this.has_permission = function(folder, perm)
{
// multiple chars means "either of"
if (String(perm).length > 1) {
for (var i=0; i < perm.length; i++) {
if (this.has_permission(folder, perm[i])) {
return true;
}
}
}
if (folder.rights && String(folder.rights).indexOf(perm) >= 0) {
return true;
}
return (perm == 'i' && folder.editable) || (perm == 'v' && folder.editable);
};
/**
* From time and date strings to a real date object
*/
this.parse_datetime = function(time, date)
{
// we use the utility function from datepicker to parse dates
var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
if (!isNaN(time_arr[0])) {
date.setHours(time_arr[0]);
if (time.match(/p[.m]*/i) && date.getHours() < 12)
date.setHours(parseInt(time_arr[0]) + 12);
else if (time.match(/a[.m]*/i) && date.getHours() == 12)
date.setHours(0);
}
if (!isNaN(time_arr[1]))
date.setMinutes(time_arr[1]);
return date;
}
/**
* Convert an ISO 8601 formatted date string from the server into a Date object.
* Timezone information will be ignored, the server already provides dates in user's timezone.
*/
this.parseISO8601 = function(s)
{
// already a Date object?
if (s && s.getMonth) {
return s;
}
// force d to be on check's YMD, for daylight savings purposes
var fixDate = function(d, check) {
if (+d) { // prevent infinite looping on invalid dates
while (d.getDate() != check.getDate()) {
d.setTime(+d + (d < check ? 1 : -1) * 3600000);
}
}
}
// derived from http://delete.me.uk/2005/03/iso8601.html
var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
if (!m) {
return null;
}
var date = new Date(m[1], 0, 2),
check = new Date(m[1], 0, 2, 9, 0);
if (m[3]) {
date.setMonth(m[3] - 1);
check.setMonth(m[3] - 1);
}
if (m[5]) {
date.setDate(m[5]);
check.setDate(m[5]);
}
fixDate(date, check);
if (m[7]) {
date.setHours(m[7]);
}
if (m[8]) {
date.setMinutes(m[8]);
}
if (m[10]) {
date.setSeconds(m[10]);
}
if (m[12]) {
date.setMilliseconds(Number("0." + m[12]) * 1000);
}
fixDate(date, check);
return date;
}
/**
* Turn the given date into an ISO 8601 date string understandable by PHPs strtotime()
*/
this.date2ISO8601 = function(date)
{
var zeropad = function(num) { return (num < 10 ? '0' : '') + num; };
return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate())
+ 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds());
};
/**
* Format the given date object according to user's prefs
*/
this.format_datetime = function(date, mode, voice)
{
var res = '';
if (!mode || mode == 1) {
res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings);
}
if (!mode) {
res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' ';
}
if (!mode || mode == 2) {
res += this.format_time(date, voice);
}
return res;
}
/**
* Clone from fullcalendar.js
*/
this.format_time = function(date, voice)
{
var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; }
var formatters = {
s : function(d) { return d.getSeconds() },
ss : function(d) { return zeroPad(d.getSeconds()) },
m : function(d) { return d.getMinutes() },
mm : function(d) { return zeroPad(d.getMinutes()) },
h : function(d) { return d.getHours() % 12 || 12 },
hh : function(d) { return zeroPad(d.getHours() % 12 || 12) },
H : function(d) { return d.getHours() },
HH : function(d) { return zeroPad(d.getHours()) },
t : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
T : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }
};
var i, i2, c, formatter, res = '',
format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format'];
for (i=0; i < format.length; i++) {
c = format.charAt(i);
for (i2=Math.min(i+2, format.length); i2 > i; i2--) {
if (formatter = formatters[format.substring(i, i2)]) {
res += formatter(date);
i = i2 - 1;
break;
}
}
if (i2 == i) {
res += c;
}
}
return res;
}
/**
* Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
*/
this.date2unixtime = function(date)
{
var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset
return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
}
/**
* Turn a unix timestamp value into a Date object
*/
this.fromunixtime = function(ts)
{
ts -= gmt_offset * 3600;
var date = new Date(ts * 1000),
dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
if (dst_offset) // adjust DST offset
date.setTime((ts + 3600) * 1000);
return date;
}
/**
* Simple plaintext to HTML converter, makig URLs clickable
*/
this.text2html = function(str, maxlen, maxlines)
{
var html = Q(String(str));
// limit visible text length
if (maxlen) {
var morelink = '<span>... <a href="#more" onclick="$(this).parent().hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','libcalendaring')+'</a></span><span style="display:none">',
lines = html.split(/\r?\n/),
words, out = '', len = 0;
for (var i=0; i < lines.length; i++) {
len += lines[i].length;
if (maxlines && i == maxlines - 1) {
out += lines[i] + '\n' + morelink;
maxlen = html.length * 2;
}
else if (len > maxlen) {
len = out.length;
words = lines[i].split(' ');
for (var j=0; j < words.length; j++) {
len += words[j].length + 1;
out += words[j] + ' ';
if (len > maxlen) {
out += morelink;
maxlen = html.length * 2;
maxlines = 0;
}
}
out += '\n';
}
else
out += lines[i] + '\n';
}
if (maxlen > str.length)
out += '</span>';
html = out;
}
// simple link parser (similar to rcube_string_replacer class in PHP)
var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig');
var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
var link_replace = function(matches, p1, p2) {
var title = '', text = p2;
if (p2 && p2.length > 55) {
text = p2.substr(0, 45) + '...' + p2.substr(-8);
title = p1 + p2;
}
return '<a href="'+p1+p2+'" class="extlink" target="_blank" title="'+title+'">'+p1+text+'</a>'
};
return html
.replace(link_pattern, link_replace)
.replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
.replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
.replace(/\n/g, "<br/>");
};
this.init_alarms_edit = function(prefix, index)
{
var edit_type = $(prefix+' select.edit-alarm-type'),
dom_id = edit_type.attr('id');
// register events on alarm fields
edit_type.change(function(){
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
});
$(prefix+' select.edit-alarm-offset').change(function(){
var val = $(this).val(), parent = $(this).parent();
parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide']();
parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0');
parent.find('.edit-alarm-related')[val == '@' ? 'hide' : 'show']();
});
$(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings);
$(prefix).on('click', 'a.delete-alarm', function(e){
if ($(this).closest('.edit-alarm-item').siblings().length > 0) {
$(this).closest('.edit-alarm-item').remove();
}
return false;
});
// set a unique id attribute and set label reference accordingly
if ((index || 0) > 0 && dom_id) {
dom_id += ':' + (new Date().getTime());
edit_type.attr('id', dom_id);
$(prefix+' label:first').attr('for', dom_id);
}
$(prefix).on('click', 'a.add-alarm', function(e){
var i = $(this).closest('.edit-alarm-item').siblings().length + 1;
var item = $(this).closest('.edit-alarm-item').clone(false)
.removeClass('first')
.appendTo(prefix);
me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
$('select.edit-alarm-type, select.edit-alarm-offset', item).change();
return false;
});
}
this.set_alarms_edit = function(prefix, valarms)
{
$(prefix + ' .edit-alarm-item:gt(0)').remove();
var i, alarm, domnode, val, offset;
for (i=0; i < valarms.length; i++) {
alarm = valarms[i];
if (!alarm.action)
alarm.action = 'DISPLAY';
if (i == 0) {
domnode = $(prefix + ' .edit-alarm-item').eq(0);
}
else {
domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix);
this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
}
$('select.edit-alarm-type', domnode).val(alarm.action);
$('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start');
if (String(alarm.trigger).match(/@(\d+)/)) {
var ondate = this.fromunixtime(parseInt(RegExp.$1));
$('select.edit-alarm-offset', domnode).val('@');
$('input.edit-alarm-value', domnode).val('');
$('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1));
$('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2));
}
else if (String(alarm.trigger).match(/^[-+]*0[MHDS]$/)) {
$('input.edit-alarm-value', domnode).val('0');
$('select.edit-alarm-offset', domnode).val('0');
}
else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) {
val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3;
$('input.edit-alarm-value', domnode).val(val);
$('select.edit-alarm-offset', domnode).val(offset);
}
}
// set correct visibility by triggering onchange handlers
$(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change();
};
this.serialize_alarms = function(prefix)
{
var valarms = [];
$(prefix + ' .edit-alarm-item').each(function(i, elem) {
var val, offset, alarm = {
action: $('select.edit-alarm-type', elem).val(),
related: $('select.edit-alarm-related', elem).val()
};
if (alarm.action) {
offset = $('select.edit-alarm-offset', elem).val();
if (offset == '@') {
alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val()));
}
else if (offset === '0') {
alarm.trigger = '0S';
}
else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) {
alarm.trigger = offset[0] + val + offset[1];
}
valarms.push(alarm);
}
});
return valarms;
};
// format time string
var time_autocomplete_format = function(hour, minutes, start) {
var time, diff, unit, duration = '', d = new Date();
d.setHours(hour);
d.setMinutes(minutes);
time = me.format_time(d);
if (start) {
diff = Math.floor((d.getTime() - start.getTime()) / 60000);
if (diff > 0) {
unit = 'm';
if (diff >= 60) {
unit = 'h';
diff = Math.round(diff / 3) / 20;
}
duration = ' (' + diff + unit + ')';
}
}
return [time, duration];
};
var time_autocomplete_list = function(p, callback) {
// Time completions
var st, h, step = 15, result = [], now = new Date(),
id = String(this.element.attr('id')),
m = id.match(/^(.*)-(starttime|endtime)$/),
start = (m && m[2] == 'endtime'
&& (st = $('#' + m[1] + '-starttime').val())
&& $('#' + m[1] + '-startdate').val() == $('#' + m[1] + '-enddate').val())
? me.parse_datetime(st, '') : null,
full = p.term - 1 > 0 || p.term.length > 1,
hours = start ? start.getHours() : (full ? me.parse_datetime(p.term, '') : now).getHours(),
minutes = hours * 60 + (full ? 0 : now.getMinutes()),
min = Math.ceil(minutes / step) * step % 60,
hour = Math.floor(Math.ceil(minutes / step) * step / 60);
// list hours from 0:00 till now
for (h = start ? start.getHours() : 0; h < hours; h++)
result.push(time_autocomplete_format(h, 0, start));
// list 15min steps for the next two hours
for (; h < hour + 2 && h < 24; h++) {
while (min < 60) {
result.push(time_autocomplete_format(h, min, start));
min += step;
}
min = 0;
}
// list the remaining hours till 23:00
while (h < 24)
result.push(time_autocomplete_format((h++), 0, start));
return callback(result);
};
var time_autocomplete_open = function(event, ui) {
// scroll to current time
var $this = $(this),
widget = $this.autocomplete('widget')
menu = $this.data('ui-autocomplete').menu,
amregex = /^(.+)(a[.m]*)/i,
pmregex = /^(.+)(a[.m]*)/i,
val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
widget.css('width', '10em');
if (val === '')
menu._scrollIntoView(widget.children('li:first'));
else
widget.children().each(function() {
var li = $(this),
html = li.children().first().html()
.replace(/\s+\(.+\)$/, '')
.replace(amregex, '0:$1')
.replace(pmregex, '1:$1');
if (html.indexOf(val) == 0)
menu._scrollIntoView(li);
});
};
/**
* Initializes time autocompletion
*/
this.init_time_autocomplete = function(elem, props)
{
var default_props = {
delay: 100,
minLength: 1,
appendTo: props.container,
source: time_autocomplete_list,
open: time_autocomplete_open,
// change: time_autocomplete_change,
select: function(event, ui) {
$(this).val(ui.item[0]).change();
return false;
}
};
$(elem).attr('autocomplete', "off")
.autocomplete($.extend(default_props, props))
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
});
$(elem).data('ui-autocomplete')._renderItem = function(ul, item) {
return $('<li>')
.data('ui-autocomplete-item', item)
.append('<a>' + item[0] + item[1] + '</a>')
.appendTo(ul);
};
};
/***** Alarms handling *****/
/**
* Display a notification for the given pending alarms
*/
this.display_alarms = function(alarms)
{
// clear old alert first
if (this.alarm_dialog)
this.alarm_dialog.dialog('destroy').remove();
var i, actions, adismiss, asnooze, alarm, html,
audio_alarms = [], records = [], event_ids = [], buttons = {};
for (i=0; i < alarms.length; i++) {
alarm = alarms[i];
alarm.start = this.parseISO8601(alarm.start);
alarm.end = this.parseISO8601(alarm.end);
if (alarm.action == 'AUDIO') {
audio_alarms.push(alarm);
continue;
}
event_ids.push(alarm.id);
html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>';
html += '<div class="event-section">' + Q(alarm.location || '') + '</div>';
html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>';
adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','libcalendaring')).click(function(e){
me.dismiss_link = $(this);
me.dismiss_alarm(me.dismiss_link.data('id'), 0, e);
});
asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){
me.snooze_dropdown($(this), e);
e.stopPropagation();
return false;
});
actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
records.push($('<div>').addClass('alarm-item').html(html).append(actions));
}
if (audio_alarms.length)
this.audio_alarms(audio_alarms);
if (!records.length)
return;
this.alarm_dialog = $('<div>').attr('id', 'alarm-display').append(records);
buttons[rcmail.gettext('close')] = function() {
$(this).dialog('close');
};
buttons[rcmail.gettext('dismissall','libcalendaring')] = function(e) {
// submit dismissed event_ids to server
me.dismiss_alarm(me.alarm_ids.join(','), 0, e);
$(this).dialog('close');
};
this.alarm_dialog.appendTo(document.body).dialog({
modal: false,
resizable: true,
closeOnEscape: false,
dialogClass: 'alarms',
title: rcmail.gettext('alarmtitle','libcalendaring'),
buttons: buttons,
open: function() {
setTimeout(function() {
me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function() {
$('#alarm-snooze-dropdown').hide();
$(this).dialog('destroy').remove();
me.alarm_dialog = null;
me.alarm_ids = null;
},
drag: function(event, ui) {
$('#alarm-snooze-dropdown').hide();
}
});
this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog');
this.alarm_ids = event_ids;
};
/**
* Display a notification and play a sound for a set of alarms
*/
this.audio_alarms = function(alarms)
{
var elem, txt = [],
src = rcmail.assets_path('plugins/libcalendaring/alarm'),
plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {};
// first generate and display notification text
$.each(alarms, function() { txt.push(this.title); });
rcmail.display_message(rcmail.gettext('alarmtitle','libcalendaring') + ': ' + Q(txt.join(', ')), 'notice', 10000);
// Internet Explorer does not support wav files,
// support in other browsers depends on enabled plugins,
// so we use wav as a fallback
src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav';
// HTML5
try {
elem = $('<audio>').attr('src', src);
elem.get(0).play();
}
// old method
catch (e) {
elem = $('<embed id="libcalsound" src="' + src + '" hidden=true autostart=true loop=false />');
elem.appendTo($('body'));
window.setTimeout("$('#libcalsound').remove()", 10000);
}
};
/**
* Show a drop-down menu with a selection of snooze times
*/
this.snooze_dropdown = function(link, event)
{
if (!this.snooze_popup) {
this.snooze_popup = $('#alarm-snooze-dropdown');
// create popup if not found
if (!this.snooze_popup.length) {
this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body);
this.snooze_popup.html(rcmail.env.snooze_select)
}
$('#alarm-snooze-dropdown a').click(function(e){
var time = String(this.href).replace(/.+#/, '');
me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time, e);
return false;
});
}
// hide visible popup
if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
rcmail.command('menu-close', 'alarm-snooze-dropdown', link.get(0), event);
this.dismiss_link = null;
}
else { // open popup below the clicked link
rcmail.command('menu-open', 'alarm-snooze-dropdown', link.get(0), event);
this.snooze_popup.data('id', link.data('id'));
this.dismiss_link = link;
}
};
/**
* Dismiss or snooze alarms for the given event
*/
this.dismiss_alarm = function(id, snooze, event)
{
rcmail.command('menu-close', 'alarm-snooze-dropdown', null, event);
rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } });
// remove dismissed alarm from list
if (this.dismiss_link) {
this.dismiss_link.closest('div.alarm-item').hide();
var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; });
if (new_ids.length)
this.alarm_ids = new_ids;
else
this.alarm_dialog.dialog('close');
}
this.dismiss_link = null;
};
/***** Recurrence form handling *****/
/**
* Install event handlers on recurrence form elements
*/
this.init_recurrence_edit = function(prefix)
{
// toggle recurrence frequency forms
$('#edit-recurrence-frequency').change(function(e){
var freq = $(this).val().toLowerCase();
$('.recurrence-form').hide();
if (freq) {
$('#recurrence-form-'+freq).show();
if (freq != 'rdate')
$('#recurrence-form-until').show();
}
});
$('#recurrence-form-rdate input.button.add').click(function(e){
var dt, dv = $('#edit-recurrence-rdate-input').val();
if (dv && (dt = me.parse_datetime('12:00', dv))) {
me.add_rdate(dt);
me.sort_rdates();
$('#edit-recurrence-rdate-input').val('')
}
else {
$('#edit-recurrence-rdate-input').select();
}
});
$('#edit-recurrence-rdates').on('click', 'a.delete', function(e){
$(this).closest('li').remove();
return false;
});
$('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
$('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); });
$('#edit-recurrence-rdate-input').datepicker(datepicker_settings);
};
/**
* Set recurrence form according to the given event/task record
*/
this.set_recurrence_edit = function(rec)
{
var recurrence = $('#edit-recurrence-frequency').val(rec.recurrence ? rec.recurrence.FREQ || (rec.recurrence.RDATE ? 'RDATE' : '') : '').change(),
interval = $('.recurrence-form select.edit-recurrence-interval').val(rec.recurrence ? rec.recurrence.INTERVAL || 1 : 1),
rrtimes = $('#edit-recurrence-repeat-times').val(rec.recurrence ? rec.recurrence.COUNT || 1 : 1),
rrenddate = $('#edit-recurrence-enddate').val(rec.recurrence && rec.recurrence.UNTIL ? this.format_datetime(this.parseISO8601(rec.recurrence.UNTIL), 1) : '');
$('.recurrence-form input.edit-recurrence-until:checked').prop('checked', false);
$('#edit-recurrence-rdates').html('');
var weekdays = ['SU','MO','TU','WE','TH','FR','SA'],
rrepeat_id = '#edit-recurrence-repeat-forever';
if (rec.recurrence && rec.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
else if (rec.recurrence && rec.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
$(rrepeat_id).prop('checked', true);
if (rec.recurrence && rec.recurrence.BYDAY && rec.recurrence.FREQ == 'WEEKLY') {
var wdays = rec.recurrence.BYDAY.split(',');
$('input.edit-recurrence-weekly-byday').val(wdays);
}
if (rec.recurrence && rec.recurrence.BYMONTHDAY) {
$('input.edit-recurrence-monthly-bymonthday').val(String(rec.recurrence.BYMONTHDAY).split(','));
$('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
}
if (rec.recurrence && rec.recurrence.BYDAY && (rec.recurrence.FREQ == 'MONTHLY' || rec.recurrence.FREQ == 'YEARLY')) {
var byday, section = rec.recurrence.FREQ.toLowerCase();
if ((byday = String(rec.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
$('#edit-recurrence-'+section+'-prefix').val(byday[1]);
$('#edit-recurrence-'+section+'-byday').val(byday[2]);
}
$('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
}
else if (rec.start) {
$('#edit-recurrence-monthly-byday').val(weekdays[rec.start.getDay()]);
}
if (rec.recurrence && rec.recurrence.BYMONTH) {
$('input.edit-recurrence-yearly-bymonth').val(String(rec.recurrence.BYMONTH).split(','));
}
else if (rec.start) {
$('input.edit-recurrence-yearly-bymonth').val([String(rec.start.getMonth()+1)]);
}
if (rec.recurrence && rec.recurrence.RDATE) {
$.each(rec.recurrence.RDATE, function(i,rdate){
me.add_rdate(me.parseISO8601(rdate));
});
}
};
/**
* Gather recurrence settings from form
*/
this.serialize_recurrence = function(timestr)
{
var recurrence = '',
freq = $('#edit-recurrence-frequency').val();
if (freq != '') {
recurrence = {
FREQ: freq,
INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
};
var until = $('input.edit-recurrence-until:checked').val();
if (until == 'count')
recurrence.COUNT = $('#edit-recurrence-repeat-times').val();
else if (until == 'until')
recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val()));
if (freq == 'WEEKLY') {
var byday = [];
$('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
if (byday.length)
recurrence.BYDAY = byday.join(',');
}
else if (freq == 'MONTHLY') {
var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
if (mode == 'BYMONTHDAY') {
$('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
if (bymonday.length)
recurrence.BYMONTHDAY = bymonday.join(',');
}
else
recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
}
else if (freq == 'YEARLY') {
var byday, bymonth = [];
$('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
if (bymonth.length)
recurrence.BYMONTH = bymonth.join(',');
if ((byday = $('#edit-recurrence-yearly-byday').val()))
recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
}
else if (freq == 'RDATE') {
recurrence = { RDATE:[] };
// take selected but not yet added date into account
if ($('#edit-recurrence-rdate-input').val() != '') {
$('#recurrence-form-rdate input.button.add').click();
}
$('#edit-recurrence-rdates li').each(function(i, li){
recurrence.RDATE.push($(li).attr('data-value'));
});
}
}
return recurrence;
};
// add the given date to the RDATE list
this.add_rdate = function(date)
{
var li = $('<li>')
.attr('data-value', this.date2ISO8601(date))
.html('<span>' + Q(this.format_datetime(date, 1)) + '</span>')
.appendTo('#edit-recurrence-rdates');
$('<a>').attr('href', '#del')
.addClass('iconbutton delete')
.html(rcmail.get_label('delete', 'libcalendaring'))
.attr('title', rcmail.get_label('delete', 'libcalendaring'))
.appendTo(li);
};
// re-sort the list items by their 'data-value' attribute
this.sort_rdates = function()
{
var mylist = $('#edit-recurrence-rdates'),
listitems = mylist.children('li').get();
listitems.sort(function(a, b) {
var compA = $(a).attr('data-value');
var compB = $(b).attr('data-value');
return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
})
$.each(listitems, function(idx, item) { mylist.append(item); });
};
/***** Attendee form handling *****/
// expand the given contact group into individual event/task attendees
this.expand_attendee_group = function(e, add, remove)
{
var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'),
role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected');
this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove }
// copy group role from the according form element
if (role_select.length) {
this.group2expand[id].data.role = role_select.val();
}
// register callback handler
if (!this._expand_attendee_listener) {
this._expand_attendee_listener = this.expand_attendee_callback;
rcmail.addEventListener('plugin.expand_attendee_callback', function(result) {
me._expand_attendee_listener(result);
});
}
rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading'));
};
// callback from server to expand an attendee group
this.expand_attendee_callback = function(result)
{
var attendee, id = result.id,
data = this.group2expand[id],
row = $(data.link).closest('tr');
// replace group entry with all members returned by the server
if (data && data.adder && result.members && result.members.length) {
for (var i=0; i < result.members.length; i++) {
attendee = result.members[i];
attendee.role = data.data.role;
attendee.cutype = 'INDIVIDUAL';
attendee.status = 'NEEDS-ACTION';
data.adder(attendee, null, row);
}
if (data.remover) {
data.remover(data.link, id)
}
else {
row.remove();
}
delete this.group2expand[id];
}
else {
rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error');
}
};
// Render message reference links to the given container
this.render_message_links = function(links, container, edit, plugin)
{
var ul = $('<ul>').addClass('attachmentslist');
$.each(links, function(i, link) {
if (!link.mailurl)
return true; // continue
var li = $('<li>').addClass('link')
.addClass('message eml')
.append($('<a>')
.attr('href', link.mailurl)
.addClass('messagelink')
.text(link.subject || link.uri)
)
.appendTo(ul);
// add icon to remove the link
if (edit) {
$('<a>')
.attr('href', '#delete')
.attr('title', rcmail.gettext('removelink', plugin))
.attr('data-uri', link.uri)
.addClass('delete')
.text(rcmail.gettext('delete'))
.appendTo(li);
}
});
container.empty().append(ul);
}
// resize and reposition (center) the dialog window
this.dialog_resize = function(id, height, width)
{
var win = $(window), w = win.width(), h = win.height();
$(id).dialog('option', {
height: Math.min(h-20, height+130),
width: Math.min(w-20, width+50)
});
};
}
////// static methods
// render HTML code for displaying an attendee record
rcube_libcalendaring.attendee_html = function(data)
{
var name, tooltip = '', context = 'libcalendaring',
dispname = data.name || data.email,
status = data.role == 'ORGANIZER' ? 'ORGANIZER' : data.status;
if (status)
status = status.toLowerCase();
if (data.email) {
tooltip = data.email;
name = $('<a>').attr({href: 'mailto:' + data.email, 'class': 'mailtolink', 'data-cutype': data.cutype})
if (status)
tooltip += ' (' + rcmail.gettext('status' + status, context) + ')';
}
else {
name = $('<span>');
}
if (data['delegated-to'])
tooltip = rcmail.gettext('libcalendaring.delegatedto') + ' ' + data['delegated-to'];
else if (data['delegated-from'])
tooltip = rcmail.gettext('libcalendaring.delegatedfrom') + ' ' + data['delegated-from'];
return $('<span>').append(
$('<span>').attr({'class': 'attendee ' + status, title: tooltip}).append(name.text(dispname))
).html();
};
/**
*
*/
rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id)
{
// ask user to delete the declined event from the local calendar (#1670)
var del = false;
if (rcmail.env.rsvp_saved && status == 'declined') {
del = confirm(rcmail.gettext('itip.declinedeleteconfirm'));
}
// open dialog for iTip delegation
if (status == 'delegated') {
rcube_libcalendaring.itip_delegate_dialog(function(data) {
rcmail.http_post(task + '/itip-delegate', {
_uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox,
_part: mime_id,
_to: data.to,
_rsvp: data.rsvp ? 1 : 0,
_comment: data.comment,
_folder: data.target
}, rcmail.set_busy(true, 'itip.savingdata'));
}, $('#rsvp-'+dom_id+' .folder-select'));
return false;
}
var noreply = 0, comment = '';
if (dom_id) {
noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0;
if (!noreply)
comment = $('#reply-comment-'+dom_id).val();
}
rcmail.http_post(task + '/mailimportitip', {
_uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox,
_part: mime_id,
_folder: $('#itip-saveto').val(),
_status: status,
_del: del?1:0,
_noreply: noreply,
_comment: comment
}, rcmail.set_busy(true, 'itip.savingdata'));
return false;
};
/**
* Helper function to render the iTip delegation dialog
* and trigger a callback function when submitted.
*/
rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
{
// show dialog for entering the delegatee address and comment
var html = '<form class="itip-dialog-form" action="javascript:void()">' +
'<div class="form-section">' +
'<label for="itip-delegate-to">' + rcmail.gettext('itip.delegateto') + '</label><br/>' +
'<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' +
'</div>' +
'<div class="form-section">' +
'<label for="itip-delegate-rsvp">' +
'<input type="checkbox" id="itip-delegate-rsvp" class="checkbox" size="40" value="" />' +
rcmail.gettext('itip.delegatersvpme') +
'</label>' +
'</div>' +
'<div class="form-section">' +
'<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' +
rcmail.gettext('itip.itipcomment') + '"></textarea>' +
'</div>' +
'<div class="form-section">' +
(selector && selector.length ? selector.html() : '') +
'</div>' +
'</form>';
var dialog, buttons = [];
buttons.push({
text: rcmail.gettext('itipdelegated', 'itip'),
click: function() {
var doc = window.parent.document,
delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, '');
if (delegatee != '' && rcube_check_email(delegatee, true)) {
callback({
to: delegatee,
rsvp: $('#itip-delegate-rsvp', doc).prop('checked'),
comment: $('#itip-delegate-comment', doc).val(),
target: $('#itip-saveto', doc).val()
});
setTimeout(function() { dialog.dialog("close"); }, 500);
}
else {
alert(rcmail.gettext('itip.delegateinvalidaddress'));
$('#itip-delegate-to', doc).focus();
}
}
});
buttons.push({
text: rcmail.gettext('cancel', 'itip'),
click: function() {
dialog.dialog('close');
}
});
dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, {
width: 460,
open: function(event, ui) {
$(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
$(this).find('#itip-saveto').val('');
// initialize autocompletion
var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props);
rcm.env.recipients_delimiter = '';
},
close: function(event, ui) {
rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
rcm.ksearch_blur();
$(this).remove();
}
});
return dialog;
};
/**
* Show a menu for selecting the RSVP reply mode
*/
rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
{
var mnu = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode');
$.each(['all','current'/*,'future'*/], function(i, mode) {
$('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>')
.addClass('ui-menu-item')
.attr('rel', mode)
.appendTo(mnu);
});
var action = btn.attr('rel');
// open the mennu
mnu.menu({
select: function(event, ui) {
callback(action, ui.item.attr('rel'));
}
})
.appendTo(document.body)
.position({ my: 'left top', at: 'left bottom+2', of: btn })
.data('action', action);
setTimeout(function() {
$(document).one('click', function() {
mnu.menu('destroy');
mnu.remove();
});
}, 100);
};
/**
*
*/
rcube_libcalendaring.remove_from_itip = function(event, task, title)
{
if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
rcmail.http_post(task + '/itip-remove',
event,
rcmail.set_busy(true, 'itip.savingdata')
);
}
};
/**
*
*/
rcube_libcalendaring.decline_attendee_reply = function(mime_id, task)
{
// show dialog for entering a comment and send to server
var html = '<div class="itip-dialog-confirm-text">' + rcmail.gettext('itip.declineattendeeconfirm') + '</div>' +
'<textarea id="itip-decline-comment" class="itip-comment" cols="40" rows="8"></textarea>';
var dialog, buttons = [];
buttons.push({
text: rcmail.gettext('declineattendee', 'itip'),
click: function() {
rcmail.http_post(task + '/itip-decline-reply', {
_uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox,
_part: mime_id,
_comment: $('#itip-decline-comment', window.parent.document).val()
}, rcmail.set_busy(true, 'itip.savingdata'));
dialog.dialog("close");
}
});
buttons.push({
text: rcmail.gettext('cancel', 'itip'),
click: function() {
dialog.dialog('close');
}
});
dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, {
width: 460,
open: function() {
$(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
$('#itip-decline-comment').focus();
}
});
return false;
};
/**
*
*/
rcube_libcalendaring.fetch_itip_object_status = function(p)
{
+ p.mbox = rcmail.env.mailbox;
+ p.message_uid = rcmail.env.uid;
rcmail.http_post(p.task + '/itip-status', { data: p });
};
/**
*
*/
rcube_libcalendaring.update_itip_object_status = function(p)
{
rcmail.env.rsvp_saved = p.saved;
rcmail.env.itip_existing = p.existing;
// hide all elements first
$('#itip-buttons-'+p.id+' > div').hide();
$('#rsvp-'+p.id+' .folder-select').remove();
if (p.html) {
// append/replace rsvp status display
$('#loading-'+p.id).next('.rsvp-status').remove();
$('#loading-'+p.id).hide().after(p.html);
}
// enable/disable rsvp buttons
if (p.action == 'rsvp') {
$('#rsvp-'+p.id+' input.button').prop('disabled', false)
.filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest);
}
// show rsvp/import buttons (with calendar selector)
$('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select);
// highlight date if date change detected
- if (p.resheduled)
+ if (p.rescheduled)
$('.calendar-eventdetails td.date').addClass('modified');
// show itip box appendix after replacing the given placeholders
if (p.append && p.append.selector) {
var elem = $(p.append.selector);
if (p.append.replacements) {
$.each(p.append.replacements, function(k, html) {
elem.html(elem.html().replace(k, html));
});
}
else if (p.append.html) {
elem.html(p.append.html)
}
elem.show();
}
};
/**
* Callback from server after an iTip message has been processed
*/
rcube_libcalendaring.itip_message_processed = function(metadata)
{
if (metadata.after_action) {
setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200);
}
else {
rcube_libcalendaring.fetch_itip_object_status(metadata);
}
};
/**
* After-action on iTip request message. Action types:
* 0 - no action
* 1 - move to Trash
* 2 - delete the message
* 3 - flag as deleted
* folder_name - move the message to the specified folder
*/
rcube_libcalendaring.itip_after_action = function(action)
{
if (!action) {
return;
}
var rc = rcmail.is_framed() ? parent.rcmail : rcmail;
if (action === 2) {
rc.permanently_remove_messages();
}
else if (action === 3) {
rc.mark_message('delete');
}
else {
rc.move_messages(action === 1 ? rc.env.trash_mailbox : action);
}
};
/**
* Open the calendar preview for the current iTip event
*/
rcube_libcalendaring.open_itip_preview = function(url, msgref)
{
if (!rcmail.env.itip_existing)
url += '&itip=' + escape(msgref);
var win = rcmail.open_window(url);
};
// extend jQuery
(function($){
$.fn.serializeJSON = function(){
var json = {};
jQuery.map($(this).serializeArray(), function(n, i) {
json[n['name']] = n['value'];
});
return json;
};
})(jQuery);
/* libcalendaring plugin initialization */
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.env.libcal_settings) {
var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings);
rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); });
}
rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status)
.addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status)
.addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed);
if (rcmail.env.action == 'get-attachment' && rcmail.gui_objects['attachmentframe']) {
rcmail.register_command('print-attachment', function() {
var frame = rcmail.get_frame_window(rcmail.gui_objects['attachmentframe'].id);
if (frame) frame.print();
}, true);
}
if (rcmail.env.action == 'get-attachment' && rcmail.env.attachment_download_url) {
rcmail.register_command('download-attachment', function() {
rcmail.location_href(rcmail.env.attachment_download_url, window);
}, true);
}
});

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 6, 3:22 AM (3 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832182
Default Alt Text
(92 KB)

Event Timeline