Updated calendar to version 3.1.13 from git.kolab.org

This commit is contained in:
Thomas Bruederli 2014-09-15 21:24:14 +02:00
parent 4636da34f9
commit 2eef94389e
26 changed files with 953 additions and 199 deletions

View file

@ -88,15 +88,6 @@ class calendar extends rcube_plugin
require($this->home . '/lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
// load Calendar user interface which includes jquery-ui
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->ui->init();
// settings are required in (almost) every GUI step
if ($this->rc->action != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
// catch iTIP confirmation requests that don're require a valid session
if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
$this->add_hook('startup', array($this, 'itip_attend_response'));
@ -104,8 +95,32 @@ class calendar extends rcube_plugin
else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
$this->add_hook('startup', array($this, 'ical_feed_export'));
}
else if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') {
if ($this->rc->action != 'upload') {
else {
// default startup routine
$this->add_hook('startup', array($this, 'startup'));
}
}
/**
* Startup hook
*/
public function startup($args)
{
// the calendar module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true))
return;
// load Calendar user interface
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->ui->init();
// settings are required in (almost) every GUI step
if ($args['action'] != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
if ($args['action'] != 'upload') {
$this->load_driver();
}
@ -126,6 +141,7 @@ class calendar extends rcube_plugin
$this->register_action('mailtoevent', array($this, 'mail_message2event'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('check-recent', array($this, 'check_recent'));
$this->add_hook('refresh', array($this, 'refresh'));
// remove undo information...
if ($undo = $_SESSION['calendar_event_undo']) {
@ -137,19 +153,19 @@ class calendar extends rcube_plugin
}
}
}
else if ($this->rc->task == 'settings') {
else if ($args['task'] == 'settings') {
// add hooks for Calendar settings
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
}
else if ($this->rc->task == 'mail') {
else if ($args['task'] == 'mail') {
// hooks to catch event invitations on incoming mails
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
@ -162,9 +178,11 @@ class calendar extends rcube_plugin
'innerclass' => 'icon calendar',
))),
'messagemenu');
$this->api->output->add_label('calendar.createfrommail');
}
}
// add hooks to display alarms
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
@ -918,6 +936,35 @@ class calendar extends rcube_plugin
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active calendars and sync them to the client
*/
public function refresh($attr)
{
// refresh the entire calendar every 10th time to also sync deleted events
if (rand(0,10) == 10) {
$this->rc->output->command('plugin.refresh_calendar', array('refetch' => true));
return;
}
foreach ($this->driver->list_calendars(true) as $cal) {
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GPC),
get_input_value('end', RCUBE_INPUT_GPC),
get_input_value('q', RCUBE_INPUT_GPC),
$cal['id'],
1,
$attr['last']
);
foreach ($events as $event) {
$this->rc->output->command('plugin.refresh_calendar',
array('source' => $cal['id'], 'update' => $this->_client_event($event)));
}
}
}
/**
* Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
* This will check for pending notifications and pass them to the client
@ -968,22 +1015,31 @@ class calendar extends rcube_plugin
rcube_upload_progress();
}
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
@set_time_limit(0);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']);
$count = $errors = 0;
$rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
foreach ($events as $event) {
$user_email = $this->rc->user->get_username();
$ical = $this->get_ical();
$errors = !$ical->fopen($_FILES['_data']['tmp_name']);
$count = $i = 0;
foreach ($ical as $event) {
// keep the browser connection alive on long import jobs
if (++$i > 100 && $i % 100 == 0) {
echo "<!-- -->";
ob_flush();
}
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['_owner'] = $user_email;
$event['calendar'] = $calendar;
if ($this->driver->new_event($event)) {
$count++;
@ -1000,8 +1056,9 @@ class calendar extends rcube_plugin
$this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
$this->rc->output->command('plugin.import_success', array('source' => $calendar));
}
else
$this->rc->output->command('display_message', $this->gettext('importerror'), 'error');
else {
$this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
}
}
else {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
@ -1012,7 +1069,7 @@ class calendar extends rcube_plugin
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('plugin.import_error', array('message' => $msg));
$this->rc->output->command('plugin.unlock_saving', false);
}
@ -1026,11 +1083,20 @@ class calendar extends rcube_plugin
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10);
if (!isset($start))
$start = 'today -1 year';
if (!is_numeric($start))
$start = strtotime($start . ' 00:00:00');
if (!$end)
$end = 'today +10 years';
if (!is_numeric($end))
$end = strtotime($end . ' 23:59:59');
$attachments = get_input_value('attachments', RCUBE_INPUT_GET);
$calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars(true);
$calendars = $this->driver->list_calendars();
if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
@ -1043,7 +1109,7 @@ class calendar extends rcube_plugin
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
$this->get_ical()->export($events, '', true, array($this->driver, 'get_attachment_body'));
$this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
if ($terminate)
exit;
@ -1172,8 +1238,16 @@ class calendar extends rcube_plugin
if ($event['recurrence']) {
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
if ($event['recurrence']['UNTIL'])
$event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'])->format('c');
$event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'], $event['allday'])->format('c');
unset($event['recurrence']['EXCEPTIONS']);
// format RDATE values
if (is_array($event['recurrence']['RDATE'])) {
$libcal = $this->lib;
$event['recurrence']['RDATE'] = array_map(function($rdate) use ($libcal) {
return $libcal->adjust_timezone($rdate, true)->format('c');
}, $event['recurrence']['RDATE']);
}
}
foreach ((array)$event['attachments'] as $k => $attachment) {
@ -1203,9 +1277,11 @@ class calendar extends rcube_plugin
return array(
'_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
'start' => $this->lib->adjust_timezone($event['start'])->format('c'),
'end' => $this->lib->adjust_timezone($event['end'])->format('c'),
'changed' => $this->lib->adjust_timezone($event['changed'])->format('c'),
'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'),
'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
// 'changed' might be empty for event recurrences (Bug #2185)
'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null,
'title' => strval($event['title']),
'description' => strval($event['description']),
'location' => strval($event['location']),
@ -1220,6 +1296,34 @@ class calendar extends rcube_plugin
*/
private function _recurrence_text($rrule)
{
// derive missing FREQ and INTERVAL from RDATE list
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
$first = $rrule['RDATE'][0];
$second = $rrule['RDATE'][1];
$third = $rrule['RDATE'][2];
if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
$diff = $first->diff($second);
foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
if ($diff->$k != 0) {
$rrule['FREQ'] = $freq;
$rrule['INTERVAL'] = $diff->$k;
// verify interval with next item
if (is_a($third, 'DateTime')) {
$diff2 = $second->diff($third);
if ($diff2->$k != $diff->$k) {
unset($rrule['INTERVAL']);
}
}
break;
}
}
}
if (!$rrule['INTERVAL'])
$rrule['FREQ'] = 'RDATE';
$rrule['UNTIL'] = end($rrule['RDATE']);
}
// TODO: finish this
$freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
$details = '';
@ -1368,9 +1472,24 @@ class calendar extends rcube_plugin
return;
}
if ($event['recurrence']['UNTIL'])
if (is_array($event['recurrence']) && !empty($event['recurrence']['UNTIL']))
$event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone);
if (is_array($event['recurrence']) && is_array($event['recurrence']['RDATE'])) {
$tz = $this->timezone;
$start = $event['start'];
$event['recurrence']['RDATE'] = array_map(function($rdate) use ($tz, $start) {
try {
$dt = new DateTime($rdate, $tz);
$dt->setTime($start->format('G'), $start->format('i'));
return $dt;
}
catch (Exception $e) {
return null;
}
}, $event['recurrence']['RDATE']);
}
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
@ -1418,8 +1537,10 @@ class calendar extends rcube_plugin
}
// mapping url => vurl because of the fullcalendar client script
$event['url'] = $event['vurl'];
unset($event['vurl']);
if (array_key_exists('vurl', $event)) {
$event['url'] = $event['vurl'];
unset($event['vurl']);
}
}
/**
@ -1700,6 +1821,8 @@ class calendar extends rcube_plugin
public function itip_attend_response($p)
{
if ($p['action'] == 'attend') {
$this->ui->init();
$this->rc->output->set_env('task', 'calendar'); // override some env vars
$this->rc->output->set_env('refresh_interval', 0);
$this->rc->output->set_pagetitle($this->gettext('calendar'));
@ -1806,17 +1929,21 @@ class calendar extends rcube_plugin
$html = '';
foreach ($this->ics_parts as $mime_id) {
$part = $this->message->mime_parts[$mime_id];
$part = $this->message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
$events = $this->ical->import($this->message->get_part_content($mime_id), $charset);
$title = $this->gettext('title');
$events = $this->ical->import($this->message->get_part_content($mime_id), $charset);
$title = $this->gettext('title');
$date = rcube_utils::anytodatetime($this->message->headers->date);
// successfully parsed events?
if (empty($events))
continue;
// show a box for every event in the file
foreach ($events as $idx => $event) {
if ($event['_type'] != 'event') // skip non-event objects (#2928)
continue;
// define buttons according to method
if ($this->ical->method == 'REPLY') {
$title = $this->gettext('itipreply');
@ -1851,17 +1978,25 @@ class calendar extends rcube_plugin
$status = 'unknown';
foreach ($event['attendees'] as $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$status = strtoupper($attendee['status']);
$status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION';
break;
}
}
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$this->rc->output->add_script('rcube_calendar.fetch_event_rsvp_status(' . json_serialize(array('uid' => $event['uid'], 'changed' => $event['changed']->format('U'), 'sequence' => intval($event['sequence']), 'fallback' => $status)) . ')', 'docready');
$changed = is_object($event['changed']) ? $event['changed'] : $date;
$script = json_serialize(array(
'uid' => $event['uid'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => $status,
));
$this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
}
else if ($this->ical->method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
@ -1879,13 +2014,21 @@ class calendar extends rcube_plugin
'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$this->rc->output->add_script('rcube_calendar.fetch_event_rsvp_status(' . json_serialize(array('uid' => $event['uid'], 'changed' => $event['changed']->format('U'), 'sequence' => intval($event['sequence']), 'fallback' => 'CANCELLED')) . ')', 'docready');
$changed = is_object($event['changed']) ? $event['changed'] : $date;
$script = json_serialize(array(
'uid' => $event['uid'],
'changed' => $changed ? $changed->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => 'CANCELLED',
));
$this->rc->output->add_script("rcube_calendar.fetch_event_rsvp_status($script)", 'docready');
}
else {
$buttons = html::tag('input', array(
@ -2047,7 +2190,12 @@ class calendar extends rcube_plugin
if ($success) {
$message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : 'importedsuccessfully');
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
$this->rc->output->command('plugin.fetch_event_rsvp_status', array('uid' => $event['uid'], 'changed' => $event['changed']->format('U'), 'sequence' => intval($event['sequence']), 'fallback' => strtoupper($status)));
$this->rc->output->command('plugin.fetch_event_rsvp_status', array(
'uid' => $event['uid'],
'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
'sequence' => intval($event['sequence']),
'fallback' => strtoupper($status),
));
$error_msg = null;
}
else if ($error_msg)
@ -2180,7 +2328,7 @@ class calendar extends rcube_plugin
$schema = 'https';
$default_port = 443;
}
$url = $schema . '://' . $_SERVER['HTTP_HOST'];
$url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
if ($_SERVER['SERVER_PORT'] != $default_port)
$url .= ':' . $_SERVER['SERVER_PORT'];
if (dirname($_SERVER['SCRIPT_NAME']) != '/')

View file

@ -6,7 +6,7 @@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* Copyright (C) 2013, 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
@ -36,10 +36,9 @@ function rcube_calendar(settings)
var me = this;
// create new event from current mail message
this.create_from_mail = function()
this.create_from_mail = function(uid)
{
var uid;
if ((uid = rcmail.get_single_uid())) {
if (uid || (uid = rcmail.get_single_uid())) {
// load calendar UI (scripts and edit dialog template)
if (!this.ui_loaded) {
$.when(
@ -53,7 +52,7 @@ function rcube_calendar(settings)
me.ui_loaded = true;
me.ui = new rcube_calendar_ui(me.settings);
me.create_from_mail(); // start over
me.create_from_mail(uid); // start over
});
return;
}
@ -156,9 +155,6 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
// register create-from-mail command to message_commands array
if (rcmail.env.task == 'mail') {
// place link above 'view source'
$('#messagemenu a.calendarlink').parent().insertBefore($('#messagemenu a.sourcelink').parent());
rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() });
rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) });
rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); });
@ -169,6 +165,15 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
}
else
rcmail.enable_command('calendar-create-from-mail', true);
// add contextmenu item
if (window.rcm_contextmenu_register_command) {
rcm_contextmenu_register_command(
'calendar-create-from-mail',
function(cmd,el){ cal.create_from_mail() },
'calendar.createfrommail',
'moveto');
}
}
}

View file

@ -332,7 +332,7 @@ function rcube_calendar_ui(settings)
// list event attendees
if (calendar.attendees && event.attendees) {
var data, dispname, organizer = false, rsvp = false, html = '';
var data, dispname, organizer = false, rsvp = false, line, morelink, html = '',overflow = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
@ -343,12 +343,16 @@ function rcube_calendar_ui(settings)
else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE') && settings.identity.emails.indexOf(';'+data.email) >= 0)
rsvp = data.status.toLowerCase();
}
html += '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
if (morelink)
overflow += line;
else
html += line;
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
html += ' <em>' + rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1) + '</em>';
break;
morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1));
}
}
@ -357,6 +361,20 @@ function rcube_calendar_ui(settings)
.children('.event-text')
.html(html)
.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
// display all attendees in a popup when clicking the "more" link
if (morelink) {
$('#event-attendees .event-text').append(morelink);
morelink.click(function(e){
rcmail.show_popup_dialog(
'<div id="all-event-attendees" class="event-attendees">' + html + overflow + '</div>',
rcmail.gettext('tabattendees','calendar'),
null,
{ width:450, modal:false });
$('#all-event-attendees a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
return false;
})
}
}
$('#event-rsvp')[(rsvp&&!organizer?'show':'hide')]();
@ -496,11 +514,12 @@ function rcube_calendar_ui(settings)
var recurrence, interval, rrtimes, rrenddate;
var load_recurrence_tab = function()
{
recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ || (event.recurrence.RDATE ? 'RDATE' : '') : '').change();
interval = $('#eventedit select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(parseISO8601(event.recurrence.UNTIL), settings['date_format']) : '');
$('#eventedit input.edit-recurrence-until:checked').prop('checked', false);
$('#edit-recurrence-rdates').html('');
var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
var rrepeat_id = '#edit-recurrence-repeat-forever';
@ -533,6 +552,11 @@ function rcube_calendar_ui(settings)
else if (event.start) {
$('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]);
}
if (event.recurrence && event.recurrence.RDATE) {
$.each(event.recurrence.RDATE, function(i,rdate){
add_rdate(parseISO8601(rdate));
});
}
};
// show warning if editing a recurring event
@ -697,6 +721,16 @@ function rcube_calendar_ui(settings)
if ((byday = $('#edit-recurrence-yearly-byday').val()))
data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
}
else if (freq == 'RDATE') {
data.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){
data.recurrence.RDATE.push($(li).attr('data-value'));
});
}
}
data.calendar = calendars.val();
@ -1547,6 +1581,34 @@ function rcube_calendar_ui(settings)
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
}
};
// add the given date to the RDATE list
var add_rdate = function(date)
{
var li = $('<li>')
.attr('data-value', date2servertime(date))
.html('<span>' + Q($.fullCalendar.formatDate(date, settings['date_format'])) + '</span>')
.appendTo('#edit-recurrence-rdates');
$('<a>').attr('href', '#del')
.addClass('iconbutton delete')
.html(rcmail.get_label('delete', 'calendar'))
.attr('title', rcmail.get_label('delete', 'calendar'))
.appendTo(li);
};
// re-sort the list items by their 'data-value' attribute
var 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); });
}
// post the given event data to server
@ -1911,7 +1973,7 @@ function rcube_calendar_ui(settings)
this.calendar_remove = function(calendar)
{
if (confirm(rcmail.gettext('deletecalendarconfirm', 'calendar'))) {
if (confirm(rcmail.gettext(calendar.children ? 'deletecalendarconfirmrecursive' : 'deletecalendarconfirm', 'calendar'))) {
rcmail.http_post('calendar', { action:'remove', c:{ id:calendar.id } });
return true;
}
@ -1920,7 +1982,24 @@ function rcube_calendar_ui(settings)
this.calendar_destroy_source = function(id)
{
var delete_ids = [];
if (this.calendars[id]) {
// find sub-calendars
if (this.calendars[id].children) {
for (var child_id in this.calendars) {
if (String(child_id).indexOf(id) == 0)
delete_ids.push(child_id);
}
}
else {
delete_ids.push(id);
}
}
// delete all calendars in the list
for (var i=0; i < delete_ids.length; i++) {
id = delete_ids[i];
fc.fullCalendar('removeEventSource', this.calendars[id]);
$(rcmail.get_folder_li(id, 'rcmlical')).remove();
$('#edit-calendar option[value="'+id+'"]').remove();
@ -1938,17 +2017,30 @@ function rcube_calendar_ui(settings)
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
$('#event-import-calendar').val(calendar.id);
if (calendar)
$('#event-import-calendar').val(calendar.id);
var buttons = {};
buttons[rcmail.gettext('import', 'calendar')] = function() {
if (form && form.elements._data.value) {
rcmail.async_upload_form(form, 'import_events', function(e) {
rcmail.set_busy(false, null, me.saving_lock);
$('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
// display error message if no sophisticated response from server arrived (e.g. iframe load error)
if (me.import_succeeded === null)
rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error');
});
// display upload indicator
// display upload indicator (with extended timeout)
var timeout = rcmail.env.request_timeout;
rcmail.env.request_timeout = 600;
me.import_succeeded = null;
me.saving_lock = rcmail.set_busy(true, 'uploading');
$('.ui-dialog-buttonpane button', $dialog.parent()).button('disable');
// restore settings
rcmail.env.request_timeout = timeout;
}
};
@ -1963,6 +2055,7 @@ function rcube_calendar_ui(settings)
closeOnEscape: false,
title: rcmail.gettext('importevents', 'calendar'),
close: function() {
$('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
$dialog.dialog("destroy").hide();
},
buttons: buttons,
@ -1974,6 +2067,7 @@ function rcube_calendar_ui(settings)
// callback from server if import succeeded
this.import_success = function(p)
{
this.import_succeeded = true;
$("#eventsimport:ui-dialog").dialog('close');
rcmail.set_busy(false, null, me.saving_lock);
rcmail.gui_objects.importform.reset();
@ -1982,6 +2076,70 @@ function rcube_calendar_ui(settings)
this.refresh(p);
};
// callback from server to report errors on import
this.import_error = function(p)
{
this.import_succeeded = false;
rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error');
}
// open a dialog to select calendars for export
this.export_events = function(calendar)
{
// close show dialog first
var $dialog = $("#eventsexport"),
form = rcmail.gui_objects.exportform;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
if (calendar)
$('#event-export-calendar').val(calendar.id);
$('#event-export-range').change(function(e){
var custom = $('option:selected', this).val() == 'custom',
input = $('#event-export-startdate')
input.parent()[(custom?'show':'hide')]();
if (custom)
input.select();
})
var buttons = {};
buttons[rcmail.gettext('export', 'calendar')] = function() {
if (form) {
var start = 0, range = $('#event-export-range option:selected', this).val(),
source = $('#event-export-calendar option:selected').val(),
attachmt = $('#event-export-attachments').get(0).checked;
if (range == 'custom')
start = date2unixtime(parse_datetime('00:00', $('#event-export-startdate').val()));
else if (range > 0)
start = 'today -' + range + '^months';
rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 });
}
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('exporttitle', 'calendar'),
close: function() {
$('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
@ -1991,6 +2149,14 @@ function rcube_calendar_ui(settings)
$dialog.dialog('close');
if (calendar.feedurl) {
if (calendar.caldavurl) {
$('#caldavurl').val(calendar.caldavurl);
$('#calendarcaldavurl').show();
}
else {
$('#calendarcaldavurl').hide();
}
$dialog.dialog({
resizable: true,
closeOnEscape: true,
@ -2038,11 +2204,29 @@ function rcube_calendar_ui(settings)
if (me.fisheye_date)
me.fisheye_view(me.fisheye_date);
}
// refetch all calendars
else if (p.refetch) {
fc.fullCalendar('refetchEvents');
}
// remove temp events
fc.fullCalendar('removeEvents', function(e){ return e.temp; });
};
// modify query parameters for refresh requests
this.before_refresh = function(query)
{
var view = fc.fullCalendar('getView');
query.start = date2unixtime(view.visStart);
query.end = date2unixtime(view.visEnd);
if (this.search_query)
query.q = this.search_query;
return query;
};
/*** event searching ***/
@ -2235,7 +2419,7 @@ function rcube_calendar_ui(settings)
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlical');
rcmail.enable_command('calendar-edit', true);
rcmail.enable_command('calendar-remove', 'events-import', 'calendar-showurl', true);
rcmail.enable_command('calendar-remove', 'calendar-showurl', true);
me.selected_calendar = id;
})
.dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
@ -2533,8 +2717,8 @@ function rcube_calendar_ui(settings)
minical = $('#datepicker').datepicker($.extend(datepicker_settings, {
inline: true,
showWeek: true,
changeMonth: false, // maybe enable?
changeYear: false, // maybe enable?
changeMonth: true,
changeYear: true,
onSelect: function(dateText, inst) {
ignore_click = true;
var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText);
@ -2623,12 +2807,32 @@ function rcube_calendar_ui(settings)
$('#edit-recurrence-frequency').change(function(e){
var freq = $(this).val().toLowerCase();
$('.recurrence-form').hide();
if (freq)
$('#recurrence-form-'+freq+', #recurrence-form-until').show();
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 = parse_datetime('12:00', dv))) {
add_rdate(dt);
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, #event-export-startdate').datepicker(datepicker_settings);
// init attendees autocompletion
var ac_props;
// parallel autocompletion
@ -2723,11 +2927,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true);
rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
// search and export events
rcmail.register_command('export', function(){ rcmail.goto_url('export_events', { source:cal.selected_calendar }); }, true);
rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
@ -2736,6 +2940,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
// let's go
var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));

View file

@ -1,31 +1,30 @@
{
"name": "kolab/calendar",
"type": "roundcube-plugin",
"description": "Calendar module for Roundcube",
"keywords": ["apps","calendar"],
"description": "Calendar plugin",
"homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
"license": "GPL-3.0+",
"license": "AGPLv3",
"authors": [
{
"name": "Thomas Bruederli",
"email": "bruederli@kolabsys.com",
"role": "Lead"
},
{
"name": "Alensader Machniak",
"email": "machniak@kolabsys.com",
"role": "Developer"
}
],
"repositories": [
{
"type": "composer",
"url": "http://33.33.108.2"
"url": "http://plugins.roundcube.net"
}
],
"require": {
"php": ">=5.3.0",
"roundcube/plugin-installer": "dev-master"
},
"extra": {
"roundcube": {
"min-version": "0.9.2",
"sql-dir": "drivers/database/SQL"
}
"roundcube/plugin-installer": ">=0.1.3",
"kolab/libcalendaring": ">=1.0.0"
}
}
}

View file

@ -110,7 +110,7 @@ $rcmail_config['calendar_allow_invite_shared'] = false;
// enable asynchronous free-busy triggering after data changed
$rcmail_config['calendar_freebusy_trigger'] = false;
// SMTP username used to send (anonymous) itip messages
// SMTP server host used to send (anonymous) itip messages
$rcmail_config['calendar_itip_smtp_server'] = null;
// SMTP username used to send (anonymous) itip messages
@ -119,5 +119,12 @@ $rcmail_config['calendar_itip_smtp_user'] = 'smtpauth';
// SMTP password used to send (anonymous) itip messages
$rcmail_config['calendar_itip_smtp_pass'] = '123456';
// Base URL to build fully qualified URIs to access calendars via CALDAV
// The following replacement variables are supported:
// %h - Current HTTP host
// %u - Current webmail user name
// %n - Calendar name
// %i - Calendar UUID
// $rcmail_config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i';
?>
?>

View file

@ -54,7 +54,18 @@
* 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
* 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest)
* 'sensitivity' => 'public|private|confidential', // Event sensitivity
* 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
* 'alarms' => '-15M:DISPLAY', // DEPRECATED Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
* 'valarms' => array( // List of reminders (new format), each represented as a hash array:
* array(
* 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object
* 'action' => 'DISPLAY|EMAIL|AUDIO',
* 'duration' => 'PT15M', // ISO 8601 period string
* 'repeat' => 0, // number of repetitions
* 'description' => '', // text to display for DISPLAY actions
* 'summary' => '', // message text for EMAIL actions
* 'attendees' => array(), // list of email addresses to receive alarm messages
* ),
* ),
* 'attachments' => array( // List of attachments
* 'name' => 'File name',
* 'mimetype' => 'Content type',
@ -236,9 +247,11 @@ abstract class calendar_driver
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Include virtual/recurring events (optional)
* @param integer Only list events modified since this time (unix timestamp)
* @return array A list of event objects (see header of this file for struct of an event)
*/
abstract function load_events($start, $end, $query = null, $calendars = null);
abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
/**
* Get a list of pending alarms to be displayed to the user

View file

@ -90,6 +90,7 @@ class database_driver extends calendar_driver
$arr['showalarms'] = intval($arr['showalarms']);
$arr['active'] = !in_array($arr['id'], $hidden);
$arr['name'] = html::quote($arr['name']);
$arr['listname'] = html::quote($arr['name']);
$this->calendars[$arr['calendar_id']] = $arr;
$calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
}
@ -723,7 +724,7 @@ class database_driver extends calendar_driver
*
* @see calendar_driver::load_events()
*/
public function load_events($start, $end, $query = null, $calendars = null)
public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
{
if (empty($calendars))
$calendars = array_keys($this->calendars);
@ -741,6 +742,12 @@ class database_driver extends calendar_driver
$sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
}
if (!$virtual)
$sql_arr .= ' AND e.recurrence_id = 0';
if ($modifiedsince)
$sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince));
$events = array();
if (!empty($calendar_ids)) {
$result = $this->rc->db->query(sprintf(
@ -792,6 +799,8 @@ class database_driver extends calendar_driver
$rr[2] = intval($rr[2]);
else if ($rr[1] == 'UNTIL')
$rr[2] = date_create($rr[2]);
else if ($rr[1] == 'RDATE')
$rr[2] = array_map('date_create', explode(',', $rr[2]));
else if ($rr[1] == 'EXDATE')
$rr[2] = array_map('date_create', explode(',', $rr[2]));
$event['recurrence'][$rr[1]] = $rr[2];

View file

@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS `kolab_alarms` (
`user_id` int(10) UNSIGNED NOT NULL,
`notifyat` DATETIME DEFAULT NULL,
`dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY(`event_id`),
PRIMARY KEY(`event_id`,`user_id`),
CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */;

View file

@ -0,0 +1,2 @@
ALTER TABLE `kolab_alarms` DROP PRIMARY KEY;
ALTER TABLE `kolab_alarms` ADD PRIMARY KEY (`alarm_id`, `user_id`);

View file

@ -33,6 +33,7 @@ class kolab_calendar
public $alarms = false;
public $categories = array();
public $storage;
public $name;
private $cal;
private $events = array();
@ -48,7 +49,7 @@ class kolab_calendar
$this->cal = $calendar;
if (strlen($imap_folder))
$this->imap_folder = $imap_folder;
$this->imap_folder = $this->name = $imap_folder;
// ID is derrived from folder name
$this->id = kolab_storage::folder_id($this->imap_folder);
@ -155,6 +156,24 @@ class kolab_calendar
return 'cc0000';
}
/**
* Compose an URL for CalDAV access to this calendar (if configured)
*/
public function get_caldav_url()
{
$url = null;
if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
return strtr($template, array(
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($this->cal->rc->get_user_name()),
'%i' => urlencode($this->storage->get_uid()),
'%n' => urlencode($this->imap_folder),
));
}
return false;
}
/**
* Return the corresponding kolab_storage_folder instance
*/
@ -214,7 +233,7 @@ class kolab_calendar
}
$events = array();
foreach ((array)$this->storage->select($query) as $record) {
foreach ($this->storage->select($query) as $record) {
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
@ -248,9 +267,18 @@ class kolab_calendar
$add = true;
// skip the first instance of a recurring event if listed in exdate
if ($virtual && !empty($event['recurrence']['EXDATE'])) {
if ($virtual && (!empty($event['recurrence']['EXDATE']) || !empty($event['recurrence']['EXCEPTIONS']))) {
$event_date = $event['start']->format('Ymd');
foreach ($event['recurrence']['EXDATE'] as $exdate) {
$exdates = (array)$event['recurrence']['EXDATE'];
// add dates from exceptions to list
if (is_array($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
$exdates[] = clone $exception['start'];
}
}
foreach ($exdates as $exdate) {
if ($exdate->format('Ymd') == $event_date) {
$add = false;
break;
@ -298,7 +326,7 @@ class kolab_calendar
}
else {
$event['id'] = $event['uid'];
$this->events[$event['uid']] = $this->_to_rcube_event($object);
$this->events = array($event['uid'] => $this->_to_rcube_event($object));
}
return $saved;
@ -318,7 +346,6 @@ class kolab_calendar
if (!$old || PEAR::isError($old))
return false;
$old['recurrence'] = ''; # clear old field, could have been removed in new, too
$object = $this->_from_rcube_event($event, $old);
$saved = $this->storage->save($object, 'event', $event['id']);
@ -560,12 +587,16 @@ class kolab_calendar
if (is_array($record['categories']))
$record['categories'] = $record['categories'][0];
// The web client only supports DISPLAY type of alarms
if (!empty($record['alarms']))
$record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
// remove empty recurrence array
if (empty($record['recurrence']))
unset($record['recurrence']);
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments']);
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
return $record;
}
@ -616,6 +647,11 @@ class kolab_calendar
$event['_owner'] = $identity['email'];
# remove EXDATE values if RDATE is given
if (!empty($event['recurrence']['RDATE'])) {
$event['recurrence']['EXDATE'] = array();
}
// remove some internal properties which should not be saved
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']);

View file

@ -33,7 +33,7 @@ class kolab_driver extends calendar_driver
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $alarm_types = array('DISPLAY');
public $alarm_types = array('DISPLAY','AUDIO');
public $categoriesimmutable = true;
private $rc;
@ -105,30 +105,48 @@ class kolab_driver extends calendar_driver
}
}
$calendars = $this->filter_calendars(false, $active, $personal);
$names = array();
$folders = $this->filter_calendars(false, $active, $personal);
$calendars = $names = array();
foreach ($calendars as $id => $cal) {
$name = kolab_storage::folder_displayname($cal->get_name(), $names);
// include virtual folders for a full folder tree
if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
$folders = kolab_storage::folder_hierarchy($folders);
$calendars[$id] = array(
'id' => $cal->id,
'name' => $name,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
'default' => $cal->storage->default,
'active' => $cal->storage->is_active(),
'owner' => $cal->get_owner(),
);
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
$listname = kolab_storage::folder_displayname($fullname, $names);
// special handling for virtual folders
if ($cal->virtual) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'virtual' => true,
);
}
else {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
'default' => $cal->storage->default,
'active' => $cal->storage->is_active(),
'owner' => $cal->get_owner(),
'children' => true, // TODO: determine if that folder indeed has child folders
'caldavurl' => $cal->get_caldav_url(),
);
}
}
return $calendars;
}
/**
* Get list of calendars according to specified filters
*
@ -448,6 +466,15 @@ class kolab_driver extends calendar_driver
if ($master['recurrence']['COUNT'])
$master['recurrence']['COUNT']--;
}
// remove the matching RDATE entry
else if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
unset($master['recurrence']['RDATE'][$j]);
break;
}
}
}
else { // add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
}
@ -464,8 +491,18 @@ class kolab_driver extends calendar_driver
unset($master['recurrence']['COUNT']);
// if all future instances are deleted, remove recurrence rule entirely (bug #1677)
if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd'))
if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
$master['recurrence'] = array();
}
// remove matching RDATE entries
else if ($master['recurrence']['RDATE']) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
$master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
break;
}
}
}
$success = $storage->update_event($master);
break;
@ -622,8 +659,24 @@ class kolab_driver extends calendar_driver
}
}
$add_exception = true;
// adjust matching RDATE entry if dates changed
if ($savemode == 'current' && $master['recurrence']['RDATE'] && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
if ($rdate->format('Ymd') == $old_date) {
$master['recurrence']['RDATE'][$j] = $event['start'];
sort($master['recurrence']['RDATE']);
$add_exception = false;
break;
}
}
}
// save as new exception to master event
$master['recurrence']['EXCEPTIONS'][] = $event;
if ($add_exception) {
$master['recurrence']['EXCEPTIONS'][] = $event;
}
$success = $storage->update_event($master);
break;
@ -662,6 +715,9 @@ class kolab_driver extends calendar_driver
$event['end'] = $master['end'];
}
// unset _dateonly flags in (cached) date objects
unset($event['start']->_dateonly, $event['end']->_dateonly);
$success = $storage->update_event($event);
break;
}
@ -679,26 +735,31 @@ class kolab_driver extends calendar_driver
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Strip virtual events (optional)
* @param boolean Include virtual events (optional)
* @param integer Only list events modified since this time (unix timestamp)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1)
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$query = array();
if ($modifiedsince)
$query[] = array('changed', '>=', $modifiedsince);
$events = $categories = array();
foreach (array_keys($this->calendars) as $cid) {
if ($calendars && !in_array($cid, $calendars))
continue;
$events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual));
$events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual, $query));
$categories += $this->calendars[$cid]->categories;
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
if ($newcats = array_diff(array_map('strtolower', array_keys($categories)), array_map('strtolower', array_keys($old_categories)))) {
if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
@ -874,8 +935,6 @@ class kolab_driver extends calendar_driver
*/
public function get_freebusy_list($email, $start, $end)
{
require_once('HTTP/Request2.php');
if (empty($email)/* || $end < time()*/)
return false;
@ -888,14 +947,11 @@ class kolab_driver extends calendar_driver
// ask kolab server first
try {
$rcmail = rcube::get_instance();
$request = new HTTP_Request2(kolab_storage::get_freebusy_url($email));
$request->setConfig(array(
$request_config = array(
'store_body' => true,
'follow_redirects' => true,
'ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true),
));
);
$request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config);
$response = $request->send();
// authentication required
@ -935,28 +991,25 @@ class kolab_driver extends calendar_driver
// parse free-busy information using Horde classes
if ($fbdata) {
$fbcal = $this->cal->get_ical()->get_parser();
$fbcal->parsevCalendar($fbdata);
if ($fb = $fbcal->findComponent('vfreebusy')) {
$ical = $this->cal->get_ical();
$ical->import($fbdata);
if ($fb = $ical->freebusy) {
$result = array();
$params = $fb->getExtraParams();
foreach ($fb->getBusyPeriods() as $from => $to) {
if ($to == null) // no information, assume free
break;
$type = $params[$from]['FBTYPE'];
$result[] = array($from, $to, isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
foreach ($fb['periods'] as $tuple) {
list($from, $to, $type) = $tuple;
$result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
}
// we take 'dummy' free-busy lists as "unknown"
if (empty($result) && ($comment = $fb->getAttribute('COMMENT')) && stripos($comment, 'dummy'))
if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy'))
return false;
// set period from $start till the begin of the free-busy information as 'unknown'
if (($fbstart = $fb->getStart()) && $start < $fbstart) {
if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
}
// pad period till $end with status 'unknown'
if (($fbend = $fb->getEnd()) && $fbend < $end) {
if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
$result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
}
@ -1039,7 +1092,7 @@ class kolab_driver extends calendar_driver
// Disable folder name input
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
$formfields['name']['value'] = Q(str_replace($delim, ' &raquo; ', kolab_storage::object_name($folder)))
$formfields['name']['value'] = kolab_storage::object_name($folder)
. $input_name->show($folder);
}

View file

@ -126,6 +126,13 @@ class Horde_Date_Recurrence
*/
public $recurMonths = array();
/**
* RDATE recurrence values
*
* @var array
*/
public $rdates = array();
/**
* All the exceptions from recurrence for this event.
*
@ -427,7 +434,7 @@ class Horde_Date_Recurrence
return clone $this->start;
}
if ($this->recurInterval == 0) {
if ($this->recurInterval == 0 && empty($this->rdates)) {
return false;
}
@ -779,6 +786,19 @@ class Horde_Date_Recurrence
return $next;
}
// fall-back to RDATE properties
if (!empty($this->rdates)) {
$next = clone $this->start;
foreach ($this->rdates as $rdate) {
$next->year = $rdate->year;
$next->month = $rdate->month;
$next->mday = $rdate->mday;
if ($next->compareDateTime($after) > 0) {
return $next;
}
}
}
// We didn't find anything, the recurType was bad, or something else
// went wrong - return false.
return false;
@ -834,6 +854,18 @@ class Horde_Date_Recurrence
return false;
}
/**
* Adds an absolute recurrence date.
*
* @param integer $year The year of the instance.
* @param integer $month The month of the instance.
* @param integer $mday The day of the month of the instance.
*/
public function addRDate($year, $month, $mday)
{
$this->rdates[] = new Horde_Date($year, $month, $mday);
}
/**
* Adds an exception to a recurring event.
*

View file

@ -57,8 +57,18 @@ class calendar_recurrence
$this->engine->fromRRule20(libcalendaring::to_rrule($event['recurrence']));
if (is_array($event['recurrence']['EXDATE'])) {
foreach ($event['recurrence']['EXDATE'] as $exdate)
$this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
foreach ($event['recurrence']['EXDATE'] as $exdate) {
if (is_a($exdate, 'DateTime')) {
$this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
}
}
}
if (is_array($event['recurrence']['RDATE'])) {
foreach ($event['recurrence']['RDATE'] as $rdate) {
if (is_a($rdate, 'DateTime')) {
$this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
}
}
}
}

View file

@ -90,6 +90,7 @@ class calendar_ui
$this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
$this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
$this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
$this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
}
@ -194,20 +195,25 @@ class calendar_ui
$prop['attachments'] = $this->cal->driver->attachments;
$prop['undelete'] = $this->cal->driver->undelete;
$prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
$jsenv[$id] = $prop;
if (!$prop['virtual'])
$jsenv[$id] = $prop;
$html_id = html_identifier($id);
$class = 'cal-' . asciiwords($id, true);
$title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
if ($prop['readonly'])
if ($prop['virtual'])
$class .= ' virtual';
else if ($prop['readonly'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class),
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
html::span('handle', '&nbsp;') .
html::span('calname', $prop['name']));
($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
html::span('handle', '&nbsp;')) .
html::span(array('class' => 'calname', 'title' => $title), $prop['listname']));
}
$this->rc->output->set_env('calendars', $jsenv);
@ -386,6 +392,7 @@ class calendar_ui
$select->add($this->cal->gettext('weekly'), 'WEEKLY');
$select->add($this->cal->gettext('monthly'), 'MONTHLY');
$select->add($this->cal->gettext('yearly'), 'YEARLY');
$select->add($this->cal->gettext('rdate'), 'RDATE');
$html = html::label('edit-frequency', $this->cal->gettext('frequency')) . $select->show('');
break;
@ -474,6 +481,13 @@ class calendar_ui
$this->cal->gettext('untildate') . ' ' . $input->show(''));
$html = $table->show();
break;
case 'rdate':
$ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
$input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10"));
$button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->cal->gettext('addrdate')));
$html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
break;
}
return $html;
@ -537,11 +551,12 @@ class calendar_ui
$select->add(array(
$this->cal->gettext('onemonthback'),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
$this->cal->gettext('all'),
),
array('1','2','6','12',0));
array('1','2','3','6','12',0));
$html .= html::div('form-section',
html::div(null, $input->show()) .
@ -567,6 +582,53 @@ class calendar_ui
);
}
/**
* Form to select options for exporting events
*/
function events_export_form($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'rcmExportForm';
$html .= html::div('form-section',
html::label('event-export-calendar', $this->cal->gettext('calendar')) .
$this->calendar_select(array('name' => 'calendar', 'id' => 'event-export-calendar'))
);
$select = new html_select(array('name' => 'range', 'id' => 'event-export-range'));
$select->add(array(
$this->cal->gettext('all'),
$this->cal->gettext('onemonthback'),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
$this->cal->gettext('customdate'),
),
array(0,'1','2','3','6','12','custom'));
$startdate = new html_inputfield(array('name' => 'start', 'size' => 11, 'id' => 'event-export-startdate'));
$html .= html::div('form-section',
html::label('event-export-range', $this->cal->gettext('exportrange')) .
$select->show(0) .
html::span(array('style'=>'display:none'), $startdate->show())
);
$checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'event-export-attachments', 'value' => 1));
$html .= html::div('form-section',
html::label('event-export-range', $this->cal->gettext('exportattachments')) .
$checkbox->show(1)
);
$this->rc->output->add_gui_object('exportform', $attrib['id']);
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'export_events')),
'method' => "post", 'id' => $attrib['id']),
$html
);
}
/**
* Generate the form for event attachments upload
*/

View file

@ -5913,5 +5913,5 @@ function TableView(element, calendar) {
}
}
})(jQuery);

View file

@ -46,6 +46,9 @@ $labels['description'] = 'Beschrieb';
$labels['all-day'] = 'ganztägig';
$labels['export'] = 'Exportieren';
$labels['exporttitle'] = 'Kalender als iCalendar exportieren';
$labels['exportrange'] = 'Termine ab';
$labels['exportattachments'] = 'Mit Anhängen';
$labels['customdate'] = 'Eigenes Datum';
$labels['location'] = 'Ort';
$labels['date'] = 'Datum';
$labels['start'] = 'Beginn';
@ -76,6 +79,7 @@ $labels['onemonthback'] = '1 Monat zurück';
$labels['nmonthsback'] = '$nr Monate zurück';
$labels['showurl'] = 'URL anzeigen';
$labels['showurldescription'] = 'Über die folgende Adresse können Sie mit einem beliebigen Kalenderprogramm Ihren Kalender abrufen (nur lesend), sofern dieses das iCal-Format unterstützt.';
$labels['caldavurldescription'] = 'Benutzen Sie folgende Addresse in einer <a href="http://de.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a>-Anwendung (wie z.B. Evolution oder Mozilla Thunderbird) um diesen spezifischen Kalender mit dem Computer oder Mobiltelefon zu synchronisieren.';
// agenda view
$labels['listrange'] = 'Angezeigter Bereich:';
@ -167,6 +171,7 @@ $labels['tabsharing'] = 'Freigabe';
// messages
$labels['deleteventconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
$labels['deletecalendarconfirm'] = 'Möchten Sie diesen Kalender mit allen Terminen wirklich löschen?';
$labels['deletecalendarconfirmrecursive'] = 'Möchten Sie diesen Kalender mit allen Terminen und Unter-Kalendern wirklich löschen?';
$labels['savingdata'] = 'Speichere Daten...';
$labels['errorsaving'] = 'Fehler beim Speichern.';
$labels['operationfailed'] = 'Die Aktion ist fehlgeschlagen.';
@ -197,6 +202,7 @@ $labels['daily'] = 'täglich';
$labels['weekly'] = 'wöchentlich';
$labels['monthly'] = 'monatlich';
$labels['yearly'] = 'jährlich';
$labels['rdate'] = 'per Datum';
$labels['every'] = 'Alle';
$labels['days'] = 'Tag(e)';
$labels['weeks'] = 'Woche(n)';
@ -216,7 +222,7 @@ $labels['third'] = 'dritter';
$labels['fourth'] = 'vierter';
$labels['last'] = 'letzter';
$labels['dayofmonth'] = 'Tag des Montats';
$labels['addrdate'] = 'Datum hinzufügen';
$labels['changeeventconfirm'] = 'Termin ändern';
$labels['removeeventconfirm'] = 'Termin löschen';
$labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';

View file

@ -46,6 +46,9 @@ $labels['description'] = 'Beschreibung';
$labels['all-day'] = 'ganztägig';
$labels['export'] = 'Exportieren';
$labels['exporttitle'] = 'Kalender als iCalendar exportieren';
$labels['exportrange'] = 'Termine ab';
$labels['exportattachments'] = 'Mit Anhängen';
$labels['customdate'] = 'Eigenes Datum';
$labels['location'] = 'Ort';
$labels['date'] = 'Datum';
$labels['start'] = 'Beginn';
@ -76,6 +79,7 @@ $labels['onemonthback'] = '1 Monat zurück';
$labels['nmonthsback'] = '$nr Monate zurück';
$labels['showurl'] = 'URL anzeigen';
$labels['showurldescription'] = 'Über die folgende Adresse können Sie mit einem beliebigen Kalenderprogramm Ihren Kalender abrufen (nur lesend), sofern dieses das iCal-Format unterstützt.';
$labels['caldavurldescription'] = 'Benutzen Sie folgende Addresse in einer <a href="http://de.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a>-Anwendung (wie z.B. Evolution oder Mozilla Thunderbird) um diesen spezifischen Kalender mit dem Computer oder Mobiltelefon zu synchronisieren.';
// agenda view
$labels['listrange'] = 'Angezeigter Bereich:';
@ -167,6 +171,7 @@ $labels['tabsharing'] = 'Freigabe';
// messages
$labels['deleteventconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
$labels['deletecalendarconfirm'] = 'Möchten Sie diesen Kalender mit allen Terminen wirklich löschen?';
$labels['deletecalendarconfirmrecursive'] = 'Möchten Sie diesen Kalender mit allen Terminen und Unter-Kalendern wirklich löschen?';
$labels['savingdata'] = 'Speichere Daten...';
$labels['errorsaving'] = 'Fehler beim Speichern.';
$labels['operationfailed'] = 'Die Aktion ist fehlgeschlagen.';
@ -197,6 +202,7 @@ $labels['daily'] = 'täglich';
$labels['weekly'] = 'wöchentlich';
$labels['monthly'] = 'monatlich';
$labels['yearly'] = 'jährlich';
$labels['rdate'] = 'per Datum';
$labels['every'] = 'Alle';
$labels['days'] = 'Tag(e)';
$labels['weeks'] = 'Woche(n)';
@ -216,7 +222,7 @@ $labels['third'] = 'dritter';
$labels['fourth'] = 'vierter';
$labels['last'] = 'letzter';
$labels['dayofmonth'] = 'Tag des Montats';
$labels['addrdate'] = 'Datum hinzufügen';
$labels['changeeventconfirm'] = 'Termin ändern';
$labels['removeeventconfirm'] = 'Termin löschen';
$labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';

View file

@ -46,6 +46,9 @@ $labels['description'] = 'Description';
$labels['all-day'] = 'all-day';
$labels['export'] = 'Export';
$labels['exporttitle'] = 'Export to iCalendar';
$labels['exportrange'] = 'Events from';
$labels['exportattachments'] = 'With attachments';
$labels['customdate'] = 'Custom date';
$labels['location'] = 'Location';
$labels['url'] = 'URL';
$labels['date'] = 'Date';
@ -77,6 +80,7 @@ $labels['onemonthback'] = '1 month back';
$labels['nmonthsback'] = '$nr months back';
$labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
// agenda view
$labels['listrange'] = 'Range to display:';
@ -172,6 +176,7 @@ $labels['tabsharing'] = 'Sharing';
// messages
$labels['deleteventconfirm'] = 'Do you really want to delete this event?';
$labels['deletecalendarconfirm'] = 'Do you really want to delete this calendar with all its events?';
$labels['deletecalendarconfirmrecursive'] = 'Do you really want to delete this calendar with all its events and sub-calendars?';
$labels['savingdata'] = 'Saving data...';
$labels['errorsaving'] = 'Failed to save changes.';
$labels['operationfailed'] = 'The requested operation failed.';
@ -204,6 +209,7 @@ $labels['daily'] = 'daily';
$labels['weekly'] = 'weekly';
$labels['monthly'] = 'monthly';
$labels['yearly'] = 'annually';
$labels['rdate'] = 'on dates';
$labels['every'] = 'Every';
$labels['days'] = 'day(s)';
$labels['weeks'] = 'week(s)';
@ -223,6 +229,7 @@ $labels['third'] = 'third';
$labels['fourth'] = 'fourth';
$labels['last'] = 'last';
$labels['dayofmonth'] = 'Day of month';
$labels['addrdate'] = 'Add repeat date';
$labels['changeeventconfirm'] = 'Change event';
$labels['removeeventconfirm'] = 'Remove event';

View file

@ -164,7 +164,12 @@ pre {
background-position: 0 -92px;
}
#calfeedurl {
#calendarslist li.virtual span.calname {
color: #666;
}
#calfeedurl,
#caldavurl {
width: 98%;
background: #fbfbfb;
padding: 4px;
@ -248,6 +253,14 @@ pre {
background-position: -64px -32px;
}
#calendartoolbar a.import {
background-position: -168px 0;
}
#calendartoolbar a.importSel {
background-position: -168px -32px;
}
#calendartoolbar a.export {
background-position: -128px 0;
}
@ -369,37 +382,44 @@ a.miniColors-trigger {
margin: 0.5em 0;
}
#event-attendees span.attendee {
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.gif) right 0 no-repeat;
}
#event-attendees span.attendee a.mailtolink {
.event-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
}
#event-attendees span.attendee a.mailtolink:hover {
.event-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
#event-attendees span.accepted {
.event-attendees span.accepted {
background-position: right -20px;
}
#event-attendees span.declined {
.event-attendees span.declined {
background-position: right -40px;
}
#event-attendees span.tentative {
.event-attendees span.tentative {
background-position: right -60px;
}
#event-attendees span.organizer {
.event-attendees span.organizer {
background-position: right -80px;
}
#all-event-attendees span.attendee {
display: block;
margin-bottom: 4px;
padding-bottom: 3px;
border-bottom: 1px solid #ddd;
}
/* jQuery UI overrides */
#eventshow h1 {
@ -440,6 +460,12 @@ a.miniColors-trigger {
margin-bottom: 0.3em;
}
#eventshow #event-url .event-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#eventedit {
position: relative;
padding: 0.5em 0.1em;
@ -517,6 +543,28 @@ td.topalign {
margin-left: 7.5em;
}
#edit-recurrence-rdates {
display: block;
list-style: none;
margin: 0 0 0.8em 0;
padding: 0;
max-height: 300px;
overflow: auto;
}
#edit-recurrence-rdates li {
display: block;
position: relative;
width: 14em;
padding: 1px;
}
#edit-recurrence-rdates li a.delete {
position: absolute;
top: 1px;
right: 0;
}
#eventedit .recurrence-form {
display: none;
}
@ -1139,8 +1187,8 @@ div.fc-event-location {
color: #333;
}
.fc-view-table col.fc-event-location {
width: 20%;
.fc-view-table table.fc-list-smart {
table-layout: auto;
}
.fc-listappend {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -35,7 +35,6 @@
<ul>
<li><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
<li><roundcube:button command="events-import" label="calendar.importevents" classAct="active" /></li>
<li><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<li class="separator_above"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
@ -63,7 +62,7 @@
<h5 class="label"><roundcube:label name="calendar.alarms" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-attendees">
<div class="event-section event-attendees" id="event-attendees">
<h5 class="label"><roundcube:label name="calendar.tabattendees" /></h5>
<div class="event-text"></div>
</div>
@ -147,14 +146,23 @@
<roundcube:object name="plugin.events_import_form" id="events-import-form" uploadFieldSize="30" />
</div>
<div id="eventsexport" class="uidialog">
<roundcube:object name="plugin.events_export_form" id="events-export-form" />
</div>
<div id="calendarurlbox" class="uidialog">
<p><roundcube:label name="calendar.showurldescription" /></p>
<textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
<div id="calendarcaldavurl" style="display:none">
<p><roundcube:label name="calendar.caldavurldescription" html="yes" /></p>
<textarea id="caldavurl" rows="2" readonly="readonly"></textarea>
</div>
</div>
<div id="calendartoolbar">
<roundcube:button command="addevent" type="link" class="buttonPas addevent" classAct="button addevent" classSel="button addeventSel" title="calendar.new_event" content=" " />
<roundcube:button command="print" type="link" class="buttonPas print" classAct="button print" classSel="button printSel" title="calendar.print" content=" " />
<roundcube:button command="events-import" type="link" class="buttonPas import" classAct="button import" classSel="button importSel" title="calendar.importevents" content=" " />
<roundcube:button command="export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="calendar.export" content=" " />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>

View file

@ -84,6 +84,9 @@
<div class="recurrence-form" id="recurrence-form-until">
<roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
</div>
<div class="recurrence-form" id="recurrence-form-rdate">
<roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
</div>
</div>
<!-- attendees list -->
<div id="event-tab-3">

View file

@ -18,6 +18,22 @@ body.calendarmain #mainscreen {
left: 0;
}
/* overrides for tablets and mobile phones */
@media screen and (max-device-width: 1024px){
body.calendarmain {
overflow: visible;
}
body.calendarmain #mainscreen {
min-width: 1000px !important;
min-height: 520px !important;
}
body.calendarmain #header {
min-width: 1020px !important;
}
}
body.attachmentwin #mainscreen {
top: 60px;
}
@ -117,7 +133,6 @@ div.sidebarclosed {
left: 266px;
right: 0;
bottom: 0;
padding-bottom: 28px;
}
.calendarmain #message.statusbar {
@ -125,10 +140,10 @@ div.sidebarclosed {
border-bottom-color: #ababab;
}
#calendar .timezonedisplay {
#timezonedisplay {
position: absolute;
bottom: 9px;
right: 8px;
bottom: 5px;
right: 12px;
font-size: 0.85em;
color: #666;
}
@ -157,6 +172,10 @@ pre {
position: relative;
}
#calendarslist li.virtual {
height: 12px;
}
#calendarslist li label {
display: block;
}
@ -225,7 +244,13 @@ pre {
background-position: right -92px;
}
#calfeedurl {
#calendarslist li.virtual span.calname {
color: #aaa;
top: 2px;
}
#calfeedurl,
#caldavurl {
width: 98%;
background: #fbfbfb;
padding: 4px;
@ -266,6 +291,7 @@ pre {
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
}
#calendartoolbar a.button {
@ -277,9 +303,17 @@ pre {
}
#calendartoolbar a.button.export {
min-width: 50px;
max-width: 55px;
background-position: center -40px;
}
#calendartoolbar a.button.import {
min-width: 50px;
max-width: 55px;
background-position: center -440px;
}
#calendartoolbar a.button.print {
background-position: center -80px;
}
@ -367,7 +401,7 @@ a.miniColors-trigger {
display: block;
color: #333;
font-weight: bold;
padding: 8px 4px 3px 30px;
padding: 4px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
@ -419,37 +453,45 @@ a.miniColors-trigger {
outline: none;
}
#event-attendees span.attendee {
.event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.gif) right 0 no-repeat;
}
#event-attendees span.attendee a.mailtolink {
.event-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
outline: none;
}
#event-attendees span.attendee a.mailtolink:hover {
.event-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
#event-attendees span.accepted {
.event-attendees span.accepted {
background-position: right -20px;
}
#event-attendees span.declined {
.event-attendees span.declined {
background-position: right -40px;
}
#event-attendees span.tentative {
.event-attendees span.tentative {
background-position: right -60px;
}
#event-attendees span.organizer {
.event-attendees span.organizer {
background-position: right -80px;
}
#all-event-attendees span.attendee {
display: block;
margin-bottom: 0.4em;
padding-bottom: 0.3em;
border-bottom: 1px solid #ddd;
}
.calendarmain .fc-view-table td.fc-list-header,
#attendees-freebusy-table h3.boxtitle,
#schedule-freebusy-times thead th,
@ -550,6 +592,12 @@ div.form-section,
padding-right: 0.5em;
}
.calendarmain .eventdialog #event-url .event-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#eventedit .formtable td.label {
min-width: 6em;
}
@ -575,6 +623,31 @@ td.topalign {
display: none;
}
#edit-recurrence-rdates {
display: block;
list-style: none;
margin: 0 0 0.8em 0;
padding: 0;
max-height: 300px;
overflow: auto;
}
#edit-recurrence-rdates li {
display: block;
position: relative;
width: 12em;
padding: 4px 0 4px 0;
}
#edit-recurrence-rdates li a.delete {
position: absolute;
top: 2px;
right: 0;
width: 20px;
height: 18px;
background-position: -7px -337px;
}
#eventedit .formtable td {
padding: 0.2em 0;
}
@ -963,7 +1036,7 @@ a.dropdown-link:after {
#agendaoptions {
position: absolute;
bottom: 28px;
bottom: 0;
left: 0;
right: 0;
height: auto;
@ -972,6 +1045,7 @@ a.dropdown-link:after {
border: 1px solid #c3c3c3;
border-top-color: #ddd;
border-bottom-color: #bbb;
border-radius: 0 0 4px 4px;
background: #ebebeb;
background: -moz-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ebebeb), color-stop(100%,#c6c6c6));
@ -1011,7 +1085,7 @@ a.dropdown-link:after {
.rcube-fc-content {
overflow: hidden;
border: 0;
border-radius: 4px 4px 0 0;
border-radius: 4px;
box-shadow: 0 0 2px #999;
-o-box-shadow: 0 0 2px #999;
-webkit-box-shadow: 0 0 2px #999;
@ -1023,7 +1097,7 @@ a.dropdown-link:after {
top: 40px;
left: 0;
right: 0;
bottom: 28px;
bottom: 0;
background: #fff;
}
@ -1281,14 +1355,13 @@ div.fc-event-location {
.calendarmain .fc-view-table tr.fc-event td {
border-color: #ddd;
padding: 4px 7px;
}
.calendarmain .fc-view-table col.fc-event-location {
width: 20%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.calendarmain .fc-view-table tr.fc-event td.fc-event-handle {
padding: 5px 10px 2px 7px;
padding: 5px 0 2px 7px;
width: 12px;
}
@ -1315,6 +1388,13 @@ div.fc-event-location {
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
.calendarmain .fc-view-table col.fc-event-location {
width: 25%;
}
.fc-view-table table.fc-list-smart {
/* table-layout: auto; */
}
.fc-listappend {
text-align: center;
@ -1347,7 +1427,7 @@ fieldset #calendarcategories div {
/* Invitation UI in mail */
#messagemenu li a.calendarlink span.calendar {
background-position: 0px -1948px;
background-position: 0px -2197px;
}
div.calendar-invitebox {
@ -1410,6 +1490,7 @@ div.calendar-invitebox .rsvp-status.tentative {
.calendaritipattend .centerbox {
width: 40em;
min-height: 7em;
margin: 80px auto 0 auto;
padding: 10px 10px 10px 90px;
background: url(images/invitation.png) 10px 10px no-repeat #fff;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -5,7 +5,7 @@
<roundcube:include file="/includes/links.html" />
<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="/this/iehacks.css" /><![endif]-->
</head>
<body class="calendarmain noscroll">
<body class="calendarmain">
<roundcube:include file="/includes/header.html" />
@ -14,6 +14,7 @@
<div id="calendartoolbar" class="toolbar">
<roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" />
<roundcube:button command="events-import" type="link" class="button import disabled" classAct="button import" classSel="button import pressed" label="import" title="calendar.importevents" />
<roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="calendar.export" title="calendar.exporttitle" />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
@ -40,16 +41,17 @@
<div id="calendar">
<roundcube:object name="plugin.angenda_options" class="boxfooter" id="agendaoptions" />
<roundcube:object name="message" id="message" class="statusbar" />
<div class="timezonedisplay"><roundcube:var name="env:timezone" /></div>
</div>
</div>
<div id="timezonedisplay"><roundcube:var name="env:timezone" /></div>
<roundcube:object name="message" id="messagestack" />
<div id="calendaroptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
<li><roundcube:button command="events-import" label="calendar.importevents" classAct="active" /></li>
<li><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<li class="separator_above"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
@ -77,7 +79,7 @@
<h5 class="label"><roundcube:label name="calendar.alarms" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-attendees">
<div class="event-section event-attendees" id="event-attendees">
<h5 class="label"><roundcube:label name="calendar.tabattendees" /></h5>
<div class="event-text"></div>
</div>
@ -161,9 +163,17 @@
<roundcube:object name="plugin.events_import_form" id="events-import-form" uploadFieldSize="30" />
</div>
<div id="eventsexport" class="uidialog">
<roundcube:object name="plugin.events_export_form" id="events-export-form" />
</div>
<div id="calendarurlbox" class="uidialog">
<p><roundcube:label name="calendar.showurldescription" /></p>
<textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
<div id="calendarcaldavurl" style="display:none">
<p><roundcube:label name="calendar.caldavurldescription" html="yes" /></p>
<textarea id="caldavurl" rows="2" readonly="readonly"></textarea>
</div>
</div>
<roundcube:object name="plugin.calendar_css" />

View file

@ -81,6 +81,9 @@
<div class="recurrence-form" id="recurrence-form-until">
<roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
</div>
<div class="recurrence-form" id="recurrence-form-rdate">
<roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
</div>
</div>
<!-- attendees list -->
<div id="event-tab-3">