From 593a3d3faaba487b1585f4917231523c309699e5 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 29 Jul 2011 17:51:04 +0200 Subject: [PATCH] Ask about notifying attendess when moving event; finish import from ical mail attachments --- plugins/calendar/calendar.php | 92 +++++++++- plugins/calendar/calendar_base.js | 14 ++ plugins/calendar/calendar_ui.js | 137 +++++++++------ plugins/calendar/drivers/calendar_driver.php | 1 + .../drivers/database/database_driver.php | 1 + .../calendar/drivers/kolab/kolab_calendar.php | 1 + .../calendar/drivers/kolab/kolab_driver.php | 6 +- plugins/calendar/lib/calendar_ical.php | 162 +++++++++++++++--- plugins/calendar/localization/en_US.inc | 11 +- plugins/calendar/skins/default/calendar.css | 22 ++- 10 files changed, 347 insertions(+), 100 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index ae7b2048..775fb56b 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -120,6 +120,7 @@ class calendar extends rcube_plugin $this->register_action('freebusy-times', array($this, 'freebusy_times')); $this->register_action('randomdata', array($this, 'generate_randomdata')); $this->register_action('print', array($this,'print_view')); + $this->register_action('mailimportevent', array($this, 'mail_import_event')); // remove undo information... if ($undo = $_SESSION['calendar_event_undo']) { @@ -137,11 +138,14 @@ class calendar extends rcube_plugin $this->add_hook('preferences_list', array($this, 'preferences_list')); $this->add_hook('preferences_save', array($this, 'preferences_save')); } - else if ($this->rc->task == 'mail' && ($this->rc->action == 'show' || $this->rc->action == 'preview')) { + else if ($this->rc->task == 'mail') { + // hooks to catch event invitations on incoming mails + if ($this->rc->action == 'show' || $this->rc->action == 'preview') { $this->add_hook('message_load', array($this, 'mail_message_load')); $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); + } } - + // add hook to display alarms $this->add_hook('keep_alive', array($this, 'keep_alive')); } @@ -583,7 +587,7 @@ class calendar extends rcube_plugin } // send out notifications - if ($success && $event['_notify'] && $event['attendees']) { + if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) { // make sure we have the complete record $event = $this->driver->get_event($event); @@ -657,10 +661,10 @@ class calendar extends rcube_plugin $events = $this->driver->load_events($start, $end, null, $calendar_name, 0); header("Content-Type: text/calendar"); - header("Content-Disposition: inline; filename=".$calendar_name); + header("Content-Disposition: inline; filename=".$calendar_name.'.ics'); $this->load_ical(); - echo $this->ical->export($events); + $this->ical->export($events, '', true); exit; } @@ -841,7 +845,7 @@ class calendar extends rcube_plugin break; } - if ($rrule['INTERVAL'] == 1) + if ($rrule['INTERVAL'] <= 1) $freq = $this->gettext(strtolower($rrule['FREQ'])); if ($rrule['COUNT']) @@ -1328,7 +1332,7 @@ class calendar extends rcube_plugin $message->headers($headers); $message->setTXTBody(rcube_message::format_flowed($body, 79)); - + // finally send the message if (rcmail_deliver_message($message, $from, $mailto, $smtp_error)) $sent++; @@ -1526,7 +1530,7 @@ class calendar extends rcube_plugin } /** - * Add UI elements to copy event invitations or updates to the calendar + * Add UI element to copy event invitations or updates to the calendar */ public function mail_messagebody_html($p) { @@ -1545,6 +1549,7 @@ class calendar extends rcube_plugin if (empty($events)) continue; + // TODO: show more iTip options like (accept, deny, etc.) foreach ($events as $idx => $event) { // add box below messsage body $html .= html::p('calendar-invitebox', @@ -1553,7 +1558,7 @@ class calendar extends rcube_plugin html::tag('input', array( 'type' => 'button', 'class' => 'button', - # 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($part.':'.$idx) . "', '" . JQ($event['title']) . "')", + 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($part.':'.$idx) . "', '" . JQ($event['title']) . "')", 'value' => $this->gettext('importtocalendar'), )) ); @@ -1568,6 +1573,75 @@ class calendar extends rcube_plugin return $p; } + + /** + * Handler for POST request to import an event attached to a mail message + */ + public function mail_import_event() + { + $uid = get_input_value('_uid', RCUBE_INPUT_POST); + $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); + $mime_id = get_input_value('_part', RCUBE_INPUT_POST); + + // establish imap connection + $this->rc->imap_connect(); + $this->rc->imap->set_mailbox($mbox); + + if ($uid && $mime_id) { + list($mime_id, $index) = explode(':', $mime_id); + $part = $this->rc->imap->get_message_part($uid, $mime_id); + } + + $this->load_ical(); + $events = $this->ical->import($part); + + $error_msg = $this->gettext('errorimportingevent'); + $success = false; + + // successfully parsed events? + if (!empty($events) && ($event = $events[$index])) { + // find writeable calendar to store event + $cal_id = $this->rc->config->get('calendar_default_calendar'); + $calendars = $this->driver->list_calendars(); + $calendar = $calendars[$cal_id] ? $calendars[$calname] : null; + if (!$calendar || $calendar['readonly']) { + foreach ($calendars as $cal) { + if (!$cal['readonly']) { + $calendar = $cal; + break; + } + } + } + + if ($calendar && !$calendar['readonly']) { + $event['id'] = $event['uid']; + $event['calendar'] = $calendar['id']; + + // check for existing event with the same UID + $existing = $this->driver->get_event($event); + + if ($existing) { + if ($event['changed'] >= $existing['changed']) + $success = $this->driver->edit_event($event); + else + $error_msg = $this->gettext('newerversionexists'); + } + else if (!$existing) { + $success = $this->driver->new_event($event); + } + } + else + $error_msg = $this->gettext('nowritecalendarfound'); + } + + if ($success) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); + else + $this->rc->output->command('display_message', $error_msg, 'error'); + + $this->rc->output->send(); + } + /** * Checks if specified message part is a vcalendar data diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js index 638e8604..aa482b7c 100644 --- a/plugins/calendar/calendar_base.js +++ b/plugins/calendar/calendar_base.js @@ -166,6 +166,15 @@ function rcube_calendar(settings) } +// static methods +rcube_calendar.add_event_from_mail = function(mime_id, title) +{ + var lock = rcmail.set_busy(true, 'loading'); + rcmail.http_post('calendar/mailimportevent', '_uid='+rcmail.env.uid+'&_mbox='+urlencode(rcmail.env.mailbox)+'&_part='+urlencode(mime_id), lock); + return false; +}; + + // extend jQuery (function($){ $.fn.serializeJSON = function(){ @@ -183,5 +192,10 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { var cal = new rcube_calendar(rcmail.env.calendar_settings); rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); }); } + rcmail.addEventListener('plugin.ping_url', function(p){ + var action = p.action; + p.action = p.event = null; + new Image().src = rcmail.url(action, p); + }); }); diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 2baeb49d..bc1ccd63 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -139,6 +139,12 @@ function rcube_calendar_ui(settings) else return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end']; }; + + // check if the event has 'real' attendees, excluding the current user + var has_attendees = function(event) + { + return (event.attendees && (event.attendees.length > 1 || event.attendees[0].email != settings.event_owner.email)); + }; // create a nice human-readable string for the date/time range var event_date_text = function(event) @@ -465,7 +471,7 @@ function rcube_calendar_ui(settings) for (var j=0; j < event.attendees.length; j++) add_attendee(event.attendees[j], true); - if (event.attendees.length > 1 || event.attendees[0].email != settings.event_owner.email) { + if (has_attendees(event)) { notify.checked = invite.checked = true; // enable notification by default } } @@ -1292,43 +1298,86 @@ function rcube_calendar_ui(settings) return true; }; - // display confirm dialog when modifying/deleting a recurring event where the user needs to select the savemode - var recurring_edit_confirm = function(event, action) { - var $dialog = $('
').addClass('edit-recurring-warning'); - $dialog.html('
' + - rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '
' + - ''); + // display confirm dialog when modifying/deleting an event + var update_event_confirm = function(action, event, data) + { + if (!data) data = event; + var html = ''; - $dialog.find('a.button').button().click(function(e){ - event.savemode = String(this.href).replace(/.+#/, ''); - update_event(action, event); - $dialog.dialog("destroy").hide(); - return false; - }); + // event has attendees, ask whether to notify them + if (has_attendees(event)) { + html += '
' + + '
'; + } - $dialog.dialog({ - modal: true, - width: 420, - dialogClass: 'warning', - title: rcmail.gettext((action == 'remove' ? 'removerecurringevent' : 'changerecurringevent'), 'calendar'), - buttons: [ - { - text: rcmail.gettext('cancel', 'calendar'), + // recurring event: user needs to select the savemode + if (event.recurrence) { + html += '
' + + rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '
' + + ''; + } + + // show dialog + if (html) { + var $dialog = $('
').html(html); + + $dialog.find('a.button').button().click(function(e){ + data.savemode = String(this.href).replace(/.+#/, ''); + if ($dialog.find('input.confirm-attendees-donotify').get(0)) + data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0; + update_event(action, data); + $dialog.dialog("destroy").hide(); + return false; + }); + + var buttons = [{ + text: rcmail.gettext('cancel', 'calendar'), + click: function() { + $(this).dialog("close"); + } + }]; + + if (!event.recurrence) { + buttons.push({ + text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'), click: function() { + data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0; + update_event(action, data); $(this).dialog("close"); } - } - ], - close: function(){ - $dialog.dialog("destroy").hide(); - fc.fullCalendar('refetchEvents'); + }); } - }).show(); + + $dialog.dialog({ + modal: true, + width: 420, + dialogClass: 'warning', + title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'), + buttons: buttons, + close: function(){ + $dialog.dialog("destroy").hide(); + if (!rcmail.busy) + fc.fullCalendar('refetchEvents'); + } + }).addClass('event-update-confirm').show(); + + return false; + } + // show regular confirm box when deleting + else if (action == 'remove') { + if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar'))) + return false; + } + + // do update + update_event(action, data); return true; }; @@ -1361,17 +1410,8 @@ function rcube_calendar_ui(settings) // delete the given event after showing a confirmation dialog this.delete_event = function(event) { - // show extended confirm dialog for recurring events, use jquery UI dialog - if (event.recurrence) - return recurring_edit_confirm({ id:event.id, calendar:event.calendar }, 'remove'); - - // send remove request to plugin - if (confirm(rcmail.gettext('deleteventconfirm', 'calendar'))) { - update_event('remove', { id:event.id, calendar:event.calendar }); - return true; - } - - return false; + // show confirm dialog for recurring events, use jquery UI dialog + return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar }); }; // opens a jquery UI dialog with event properties (or empty for creating a new calendar) @@ -1752,10 +1792,7 @@ function rcube_calendar_ui(settings) end: date2unixtime(event.end), allday: allDay?1:0 }; - if (event.recurrence) - recurring_edit_confirm(data, 'move'); - else - update_event('move', data); + update_event_confirm('move', event, data); }, // callback for event resizing eventResize: function(event, delta) { @@ -1766,10 +1803,7 @@ function rcube_calendar_ui(settings) start: date2unixtime(event.start), end: date2unixtime(event.end) }; - if (event.recurrence) - recurring_edit_confirm(data, 'resize'); - else - update_event('resize', data); + update_event_confirm('resize', event, data); }, viewDisplay: function(view) { me.eventcount = []; @@ -2046,7 +2080,6 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.addEventListener('plugin.reload_calendar', function(p){ $('#calendar').fullCalendar('refetchEvents', cal.calendars[p.source]); }); rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.saving_lock); }); - rcmail.addEventListener('plugin.ping_url', function(p){ p.event = null; new Image().src = rcmail.url(p.action, p); }); // let's go var cal = new rcube_calendar_ui(rcmail.env.calendar_settings); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 0940e861..67deae92 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -33,6 +33,7 @@ * 'start' => , // Event start date/time as unix timestamp * 'end' => , // Event end date/time as unix timestamp * 'allday' => true|false, // Boolean flag if this is an all-day event + * 'changed' => , // Last modification date of event * 'title' => 'Event title/summary', * 'location' => 'Location string', * 'description' => 'Event description', diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 69898a8a..53dcb8c4 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -686,6 +686,7 @@ class database_driver extends calendar_driver $event['start'] = strtotime($event['start']); $event['end'] = strtotime($event['end']); $event['allday'] = intval($event['all_day']); + $event['changed'] = strtotime($event['changed']); $event['free_busy'] = $free_busy_map[$event['free_busy']]; $event['calendar'] = $event['calendar_id']; $event['recurrence_id'] = intval($event['recurrence_id']); diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 59ebb609..ce05a50e 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -561,6 +561,7 @@ class kolab_calendar 'free_busy' => $rec['show-time-as'], 'priority' => isset($priority_map[$rec['priority']]) ? $priority_map[$rec['priority']] : 1, 'sensitivity' => $sensitivity_map[$rec['sensitivity']], + 'changed' => $rec['last-modification-date'], 'calendar' => $this->id, ); } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index a5002bab..872fecab 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -310,7 +310,7 @@ class kolab_driver extends calendar_driver $success = $storage->insert_event($event); if ($success) - $this->rc->output->command('plugin.ping_url', array('action' => 'push-freebusy', 'source' => $storage->id)); + $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); return $success; } @@ -405,7 +405,7 @@ class kolab_driver extends calendar_driver } if ($success) - $this->rc->output->command('plugin.ping_url', array('action' => 'push-freebusy', 'source' => $storage->id)); + $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); return $success; } @@ -558,7 +558,7 @@ class kolab_driver extends calendar_driver } if ($success) - $this->rc->output->command('plugin.ping_url', array('action' => 'push-freebusy', 'source' => $storage->id)); + $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id)); return $success; } diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index b5d6cb0e..f0410505 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -78,13 +78,15 @@ class calendar_ical { $event = array( 'uid' => $ve->getAttributeDefault('UID'), + 'changed' => $ve->getAttributeDefault('DTSTAMP', 0), 'title' => $ve->getAttributeDefault('SUMMARY'), - 'description' => $ve->getAttributeDefault('DESCRIPTION'), - 'location' => $ve->getAttributeDefault('LOCATION'), 'start' => $ve->getAttribute('DTSTART'), 'end' => $ve->getAttribute('DTEND'), + // set defaults + 'free_busy' => 'busy', + 'priority' => 1, ); - + // check for all-day dates if (is_array($event['start'])) { $event['start'] = gmmktime(0, 0, 0, $event['start']['month'], $event['start']['mday'], $event['start']['year']) + $this->cal->gmt_offset; @@ -93,11 +95,97 @@ class calendar_ical if (is_array($event['end'])) { $event['end'] = gmmktime(0, 0, 0, $event['end']['month'], $event['end']['mday'], $event['end']['year']) + $this->cal->gmt_offset - 60; } + + // map other attributes to internal fields + $_attendees = array(); + foreach ($ve->getAllAttributes() as $attr) { + switch ($attr['name']) { + case 'ORGANIZER': + $organizer = array( + 'name' => $attr['params']['CN'], + 'email' => preg_replace('/^mailto:/', '', $attr['value']), + 'role' => 'ORGANIZER', + 'status' => 'ACCEPTED', + ); + if (isset($_attendees[$organizer['email']])) { + $i = $_attendees[$organizer['email']]; + $event['attendees'][$i]['role'] = $organizer['role']; + } + break; + + case 'ATTENDEE': + $attendee = array( + 'name' => $attr['params']['CN'], + 'email' => preg_replace('/^mailto:/', '', $attr['value']), + 'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT', + 'status' => $attr['params']['PARTSTAT'], + 'rsvp' => $attr['params']['RSVP'] == 'TRUE', + ); + if ($organizer && $organizer['email'] == $attendee['email']) + $attendee['role'] = 'ORGANIZER'; + + $event['attendees'][] = $attendee; + $_attendees[$attendee['email']] = count($event['attendees']) - 1; + break; + + case 'TRANSP': + $event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy'; + break; + + case 'STATUS': + if ($attr['value'] == 'TENTATIVE') + $event['free_busy'] == 'tentative'; + break; + + case 'PRIORITY': + if (is_numeric($attr['value'])) { + $event['priority'] = $attr['value'] <= 4 ? 2 /* high */ : + ($attr['value'] == 5 ? 1 /* normal */ : 0 /* low */); + } + break; + + case 'RRULE': + // parse recurrence rule attributes + foreach (explode(';', $attr['value']) as $par) { + list($k, $v) = explode('=', $par); + $params[$k] = $v; + } + if ($params['UNTIL']) + $params['UNTIL'] = $ve->_parseDateTime($params['UNTIL']); + if (!$params['INTERVAL']) + $params['INTERVAL'] = 1; + + $event['recurrence'] = $params; + break; + + case 'EXDATE': + break; + + case 'DESCRIPTION': + case 'LOCATION': + $event[strtolower($attr['name'])] = $attr['value']; + break; + + case 'CLASS': + case 'X-CALENDARSERVER-ACCESS': + $sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2); + $event['sensitivity'] = $sensitivity_map[$attr['value']]; + break; - // TODO: complete this + case 'X-MICROSOFT-CDO-BUSYSTATUS': + if ($attr['value'] == 'OOF') + $event['free_busy'] == 'outofoffice'; + else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE'))) + $event['free_busy'] = strtolower($attr['value']); + break; + } + } - - // make sure event has an UID + // add organizer to attendees list if not already present + if ($organizer && !isset($_attendees[$organizer['email']])) + array_unshift($event['attendees'], $organizer); + + // make sure the event has an UID if (!$event['uid']) $event['uid'] = $this->cal->$this->generate_uid(); @@ -108,10 +196,12 @@ class calendar_ical /** * Export events to iCalendar format * - * @param array Events as array + * @param array Events as array + * @param string VCalendar method to advertise + * @param boolean Directly send data to stdout instead of returning * @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545) */ - public function export($events, $method = null) + public function export($events, $method = null, $write = false) { if (!empty($this->rc->user->ID)) { $ical = "BEGIN:VCALENDAR" . self::EOL; @@ -121,56 +211,72 @@ class calendar_ical if ($method) $ical .= "METHOD:" . strtoupper($method) . self::EOL; + + if ($write) { + echo $ical; + $ical = ''; + } foreach ($events as $event) { - $ical .= "BEGIN:VEVENT" . self::EOL; - $ical .= "UID:" . self::escpape($event['uid']) . self::EOL; + $vevent = "BEGIN:VEVENT" . self::EOL; + $vevent .= "UID:" . self::escpape($event['uid']) . self::EOL; + $vevent .= "DTSTAMP:" . gmdate('Ymd\THis\Z', $event['changed'] ? $event['changed'] : time()) . self::EOL; // correctly set all-day dates if ($event['allday']) { - $ical .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL; - $ical .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 60) . self::EOL; // ends the next day + $vevent .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL; + $vevent .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 60) . self::EOL; // ends the next day } else { - $ical .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL; - $ical .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL; + $vevent .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL; + $vevent .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL; } - $ical .= "SUMMARY:" . self::escpape($event['title']) . self::EOL; - $ical .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL; + $vevent .= "SUMMARY:" . self::escpape($event['title']) . self::EOL; + $vevent .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL; if (!empty($event['attendees'])){ - $ical .= $this->_get_attendees($event['attendees']); + $vevent .= $this->_get_attendees($event['attendees']); } if (!empty($event['location'])) { - $ical .= "LOCATION:" . self::escpape($event['location']) . self::EOL; + $vevent .= "LOCATION:" . self::escpape($event['location']) . self::EOL; } if ($event['recurrence']) { - $ical .= "RRULE:" . calendar::to_rrule($event['recurrence']) . self::EOL; + $vevent .= "RRULE:" . calendar::to_rrule($event['recurrence'], self::EOL) . self::EOL; } if(!empty($event['categories'])) { - $ical .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL; + $vevent .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL; } if ($event['sensitivity'] > 0) { - $ical .= "X-CALENDARSERVER-ACCESS:CONFIDENTIAL"; + $vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL; } if ($event['alarms']) { list($trigger, $action) = explode(':', $event['alarms']); $val = calendar::parse_alaram_value($trigger); - $ical .= "BEGIN:VALARM\n"; - if ($val[1]) $ical .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL; - else $ical .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL; - if ($action) $ical .= "ACTION:" . self::escpape(strtoupper($action)) . self::EOL; - $ical .= "END:VALARM\n"; + $vevent .= "BEGIN:VALARM\n"; + if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL; + else $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL; + if ($action) $vevent .= "ACTION:" . self::escpape(strtoupper($action)) . self::EOL; + $vevent .= "END:VALARM\n"; } - $ical .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL; + $vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL; // TODO: export attachments - $ical .= "END:VEVENT" . self::EOL; + $vevent .= "END:VEVENT" . self::EOL; + + if ($write) + echo rcube_vcard::rfc2425_fold($vevent); + else + $ical .= $vevent; } $ical .= "END:VCALENDAR" . self::EOL; + + if ($write) { + echo $ical; + return true; + } // fold lines to 75 chars return rcube_vcard::rfc2425_fold($ical); diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index d24252a7..e3dd99b1 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -60,6 +60,7 @@ $labels['generated'] = 'generated at'; $labels['selectdate'] = 'Select date'; $labels['printdescriptions'] = 'Print descriptions'; $labels['parentcalendar'] = 'Superior calendar'; +$labels['importtocalendar'] = 'Save to my calendar'; // alarm/reminder settings $labels['alarmemail'] = 'Send Email'; @@ -104,7 +105,7 @@ $labels['availtentative'] = 'Tentative'; $labels['availoutofoffice'] = 'Out of Office'; $labels['scheduletime'] = 'Find availability'; $labels['sendinvitations'] = 'Send invitations'; -$labels['sendnotifications'] = 'Notify attendees about modifications'; +$labels['sendnotifications'] = 'Notify participants about modifications'; $labels['onlyworkinghours'] = 'Find availability within my working hours'; $labels['reqallattendees'] = 'Required/all participants'; $labels['prevslot'] = 'Previous Slot'; @@ -133,6 +134,10 @@ $labels['searchnoresults'] = 'No events found in the selected calendars.'; $labels['successremoval'] = 'The event has been deleted successfully.'; $labels['successrestore'] = 'The event has been restored successfully.'; $labels['errornotifying'] = 'Failed to send notifications to event participants'; +$labels['errorimportingevent'] = 'Failed to import the event'; +$labels['newerversionexists'] = 'A newer version of this event already exists! Aborted.'; +$labels['nowritecalendarfound'] = 'No calendar found to save the event'; +$labels['importedsuccessfully'] = 'The event was successfully added to \'$calendar\''; // recurrence form $labels['repeat'] = 'Repeat'; @@ -162,8 +167,8 @@ $labels['fourth'] = 'fourth'; $labels['last'] = 'last'; $labels['dayofmonth'] = 'Day of month'; -$labels['changerecurringevent'] = 'Change recurring event'; -$labels['removerecurringevent'] = 'Remove recurring event'; +$labels['changeeventconfirm'] = 'Change event'; +$labels['removeeventconfirm'] = 'Remove event'; $labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?'; $labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to remove the current event only, this and all future occurences or all occurences of this event?'; $labels['currentevent'] = 'Current'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 95a8f322..3354810c 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -472,14 +472,20 @@ td.topalign { padding: 0.2em 0; } -.edit-recurring-warning { +.ui-dialog .event-update-confirm { + padding: 0 0.5em 0.5em 0.5em; +} + +.edit-recurring-warning, +.event-update-confirm .message { margin-top: 0.5em; padding: 0.8em; background-color: #F7FDCB; border: 1px solid #C2D071; } -.edit-recurring-warning .message { +.edit-recurring-warning .message, +.event-update-confirm .message { margin-bottom: 0.5em; } @@ -487,17 +493,23 @@ td.topalign { padding-left: 20px; } -.edit-recurring-warning span.ui-icon { +.event-update-confirm .savemode { + padding-left: 30px; +} + +.edit-recurring-warning span.ui-icon, +.event-update-confirm span.ui-icon { float: left; margin: 0 7px 20px 0; } -.edit-recurring-warning label { +.edit-recurring-warning label, +.event-update-confirm label { min-width: 3em; padding-right: 1em; } -.edit-recurring-warning a.button { +.event-update-confirm a.button { margin: 0 0.5em 0 0.2em; min-width: 5em; }