4 commits - plugins/calendar plugins/libcalendaring plugins/libkolab
Thomas Brüderli
bruederli at kolabsys.com
Tue Jul 29 15:35:13 CEST 2014
plugins/calendar/calendar.php | 145 +++
plugins/calendar/calendar_ui.js | 420 +++++++++-
plugins/calendar/drivers/calendar_driver.php | 74 +
plugins/calendar/drivers/kolab/kolab_calendar.php | 48 -
plugins/calendar/drivers/kolab/kolab_driver.php | 268 ++++++
plugins/calendar/drivers/kolab/kolab_user_calendar.php | 4
plugins/calendar/lib/calendar_ui.php | 17
plugins/calendar/localization/en_US.inc | 23
plugins/calendar/skins/classic/calendar.css | 4
plugins/calendar/skins/classic/templates/calendar.html | 7
plugins/calendar/skins/larry/calendar.css | 144 +++
plugins/calendar/skins/larry/images/calendars.png |binary
plugins/calendar/skins/larry/templates/calendar.html | 106 ++
plugins/libcalendaring/libcalendaring.php | 14
plugins/libcalendaring/localization/en_US.inc | 1
plugins/libkolab/config.inc.php.dist | 9
plugins/libkolab/lib/kolab_bonnie_api.php | 82 ++
plugins/libkolab/lib/kolab_bonnie_api_client.php | 239 +++++
plugins/libkolab/libkolab.php | 11
plugins/libkolab/vendor/finediff.php | 688 +++++++++++++++++
plugins/libkolab/vendor/finediff_modifications.diff | 121 ++
21 files changed, 2313 insertions(+), 112 deletions(-)
New commits:
commit a68982b0287abbf27b478c16038c5a58ec2e9a0e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Jul 29 15:28:35 2014 +0200
Add UI elements to display the history of a calendar event with data from the Bonnie API (#3093, #3094) + new option to download and send single events
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 343b3e5..6c23741 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -194,6 +194,7 @@ class calendar extends rcube_plugin
}
$this->add_hook('messages_list', array($this, 'mail_messages_list'));
+ $this->add_hook('message_compose', array($this, 'mail_message_compose'));
}
else if ($args['task'] == 'addressbook') {
if ($this->rc->config->get('calendar_contact_birthdays')) {
@@ -973,6 +974,95 @@ class calendar extends rcube_plugin
$success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
}
break;
+
+ case "changelog":
+ $data = $this->driver->get_event_changelog($event);
+ if (is_array($data) && !empty($data)) {
+ $lib = $this->lib;
+ array_walk($data, function($change) use ($lib) {
+ if ($change['date']) {
+ $dt = $lib->adjust_timezone($change['date']);
+ if ($dt instanceof DateTime)
+ $change['date'] = $dt->format('c');
+ }
+ });
+ $this->rc->output->command('plugin.render_event_changelog', $data);
+ }
+ else {
+ $this->rc->output->command('plugin.render_event_changelog', false);
+ $this->rc->output->command('display_message', $this->gettext('eventchangelognotavailable'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "diff":
+ $data = $this->driver->get_event_diff($event, $event['rev']);
+ if (is_array($data)) {
+ // convert some properties, similar to self::_client_event()
+ $lib = $this->lib;
+ array_walk($data['changes'], function(&$change, $i) use ($event, $lib) {
+ // convert date cols
+ foreach (array('start','end','created','changed') as $col) {
+ if ($change['property'] == $col) {
+ $change['old'] = $this->lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
+ $change['new'] = $this->lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
+ }
+ }
+ // create textual representation for alarms and recurrence
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']))
+ $change['old_'] = libcalendaring::alarm_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'recurrence') {
+ if (is_array($change['old']))
+ $change['old_'] = $lib->recurrence_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'attachments') {
+ if (is_array($change['old']))
+ $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
+ if (is_array($change['new']))
+ $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
+ }
+ // compute a nice diff of description texts
+ if ($change['property'] == 'description') {
+ $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
+ }
+ });
+ $this->rc->output->command('plugin.event_show_diff', $data);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('eventdiffnotavailable'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "show":
+ if ($event = $this->driver->get_event_revison($event, $event['rev'])) {
+ $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('eventnotfound'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "restore":
+ if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
+
+ }
+ else {
+ $this->rc->output->command('display_message', 'Not implemented yet', 'error');
+ $got_msg = true;
+ }
+ $reload = false;
+ break;
}
// show confirmation/error message
@@ -1271,22 +1361,33 @@ class calendar extends rcube_plugin
if (!is_numeric($end))
$end = strtotime($end . ' 23:59:59');
+ $event_id = get_input_value('id', RCUBE_INPUT_GET);
$attachments = get_input_value('attachments', RCUBE_INPUT_GET);
- $calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
+ $calid = $filename = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
+ $events = array();
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
- if (empty($calname)) $calname = $calid;
- $events = $this->driver->load_events($start, $end, null, $calid, 0);
+ $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
+ $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
+ if (!empty($event_id)) {
+ if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id))) {
+ $events = array($event);
+ $filename = asciiwords($event['title']);
+ if (empty($filename))
+ $filename = 'event';
+ }
+ }
+ else {
+ $events = $this->driver->load_events($start, $end, null, $calid, 0);
+ if (empty($filename))
+ $filename = $calid;
+ }
}
- else
- $events = array();
header("Content-Type: text/calendar");
- header("Content-Disposition: inline; filename=".$calname.'.ics');
+ header("Content-Disposition: inline; filename=".$filename.'.ics');
$this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
@@ -2672,6 +2773,34 @@ class calendar extends rcube_plugin
$this->rc->output->send();
}
+ /**
+ * Handler for the 'message_compose' plugin hook. This will check for
+ * a compose parameter 'calendar_event' and create an attachment with the
+ * referenced event in iCal format
+ */
+ public function mail_message_compose($args)
+ {
+ // set the submitted event ID as attachment
+ if (!empty($args['param']['calendar_event'])) {
+ $this->load_driver();
+
+ list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
+ if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) {
+ $filename = asciiwords($event['title']);
+ if (empty($filename))
+ $filename = 'event';
+
+ // save ics to a temp file and register as attachment
+ $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
+ file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body')));
+
+ $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar');
+ $args['param']['subject'] = $event['title'];
+ }
+ }
+
+ return $args;
+ }
/**
* Checks if specified message part is a vcalendar data
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 8b59ef3..8d4433d 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -252,6 +252,11 @@ function rcube_calendar_ui(settings)
return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14));
}
+ var format_datetime = function(date, mode, voice)
+ {
+ return me.format_datetime(date, mode, voice);
+ }
+
var render_link = function(url)
{
var islink = false, href = url;
@@ -304,11 +309,13 @@ function rcube_calendar_ui(settings)
var load_attachment = function(event, att)
{
- var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
+ var query = { _id: att.id, _event: event.recurrence_id || event.id, _cal:event.calendar, _frame: 1 };
+ if (event.rev)
+ query._rev = event.rev;
// open attachment in frame if it's of a supported mimetype
if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
- if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) {
+ if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
return;
}
}
@@ -378,15 +385,23 @@ function rcube_calendar_ui(settings)
};
// event details dialog (show only)
- var event_show_dialog = function(event, ev)
+ var event_show_dialog = function(event, ev, temp)
{
- var $dialog = $("#eventshow").attr('class', 'uidialog');
+ var $dialog = $("#eventshow");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
- me.selected_event = event;
+
+ if (!temp)
+ me.selected_event = event;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
+ // convert start/end dates if not done yet by fullcalendar
+ if (typeof event.start == 'string')
+ event.start = parseISO8601(event.start);
+ if (typeof event.end == 'string')
+ event.end = parseISO8601(event.end);
+
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
@@ -430,6 +445,13 @@ function rcube_calendar_ui(settings)
$('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
$dialog.addClass('sensitivity-'+event.sensitivity);
}
+ if (event.created || event.changed) {
+ var created = parseISO8601(event.created),
+ changed = parseISO8601(event.changed)
+ $('#event-created-changed .event-created').html(Q(created ? format_datetime(created) : rcmail.gettext('unknown','calendar')))
+ $('#event-created-changed .event-changed').html(Q(changed ? format_datetime(changed) : rcmail.gettext('unknown','calendar')))
+ $('#event-created-changed').show()
+ }
// create attachments list
if ($.isArray(event.attachments)) {
@@ -451,14 +473,10 @@ function rcube_calendar_ui(settings)
return (j - k);
});
- var data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '',overflow = '';
+ var data, mystatus = null, rsvp, line, morelink, html = '', overflow = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
- dispname = Q(data.name || data.email);
- tooltip = '';
if (data.email) {
- tooltip = data.email;
- dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
if (data.role == 'ORGANIZER')
organizer = true;
else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
@@ -467,18 +485,14 @@ function rcube_calendar_ui(settings)
rsvp = mystatus;
}
}
-
- if (data['delegated-to'])
- tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
- else if (data['delegated-from'])
- tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
-
- line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+
+ line = event_attendee_html(data);
+
if (morelink)
overflow += line;
else
html += line;
-
+
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1));
@@ -522,7 +536,7 @@ function rcube_calendar_ui(settings)
}
var buttons = {};
- if (calendar.editable && event.editable !== false) {
+ if (!temp && calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
@@ -551,6 +565,13 @@ function rcube_calendar_ui(settings)
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ rcmail.command('menu-close','eventoptionsmenu')
+ },
+ dragStart: function() {
+ rcmail.command('menu-close','eventoptionsmenu')
+ },
+ resizeStart: function() {
+ rcmail.command('menu-close','eventoptionsmenu')
},
buttons: buttons,
minWidth: 320,
@@ -566,15 +587,38 @@ function rcube_calendar_ui(settings)
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
-/*
+
// add link for "more options" drop-down
- $('<a>')
- .attr('href', '#')
- .html('More Options')
- .addClass('dropdown-link')
- .click(function(){ return false; })
- .insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
-*/
+ if (!temp) {
+ $('<a>')
+ .attr('href', '#')
+ .html(rcmail.gettext('eventoptions','calendar'))
+ .addClass('dropdown-link')
+ .click(function(e) {
+ return rcmail.command('menu-open','eventoptionsmenu', this, e)
+ })
+ .appendTo($dialog.parent().find('.ui-dialog-buttonset'));
+ }
+
+ rcmail.enable_command('event-history', calendar.history)
+ };
+
+ // render HTML code for displaying an attendee record
+ var event_attendee_html = function(data)
+ {
+ var dispname = Q(data.name || data.email), tooltip = '';
+
+ if (data.email) {
+ tooltip = data.email;
+ dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+ }
+
+ if (data['delegated-to'])
+ tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
+ else if (data['delegated-from'])
+ tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
+
+ return '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
};
// event handler for clicks on an attendee link
@@ -586,7 +630,7 @@ function rcube_calendar_ui(settings)
event_resources_dialog(mailto);
}
else {
- rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
+ rcmail.command('compose', mailto, e ? e.target : null, e);
}
return false;
};
@@ -876,6 +920,291 @@ function rcube_calendar_ui(settings)
window.setTimeout(load_attachments_tab, exec_deferred);
};
+ // show event changelog in a dialog
+ var event_history_dialog = function(event)
+ {
+ if (!event.id)
+ return false
+
+ // render dialog
+ $dialog = $('#eventhistory');
+
+ // close show dialog first
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ var buttons = {};
+ buttons[rcmail.gettext('close', 'calendar')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // hide and reset changelog table
+ $('#event-changelog-table').children('tbody')
+ .html('<tr><td colspan="6"><span class="loading">'+ rcmail.gettext('loading') +'</span></td></tr>');
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('eventchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ buttons: buttons,
+ minWidth: 450,
+ width: 650,
+ height: 350,
+ minHeight: 200,
+ })
+ .data('event', event)
+ .show().children('.compare-button').hide();
+
+ // set dialog size according to content
+ // me.dialog_resize($dialog.get(0), $dialog.height(), 650);
+
+ // fetch changelog data
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'changelog', e:{ id:event.id, calendar:event.calendar } }, me.loading_lock);
+
+ // initialize event handlers for history dialog UI elements
+ if (!$dialog.data('initialized')) {
+ // compare button
+ $dialog.find('.compare-button input').click(function(e) {
+ var rev1 = $('#event-changelog-table input.diff-rev1:checked').val(),
+ rev2 = $('#event-changelog-table input.diff-rev2:checked').val(),
+ event = $('#eventhistory').data('event');
+
+ if (rev1 && rev2 && rev1 != rev2) {
+ // swap revisions if the user got it wrong
+ if (rev1 > rev2) {
+ var tmp = rev2;
+ rev2 = rev1;
+ rev1 = tmp;
+ }
+
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'diff', e:{ id:event.id, calendar:event.calendar, rev: rev1+':'+rev2 } }, me.loading_lock);
+ }
+ else {
+ alert('Invalid selection!')
+ }
+ });
+
+ // delegate handlers for list actions
+ $('#event-changelog-table tbody').on('click', 'td.actions a', function(e) {
+ var link = $(this),
+ action = link.hasClass('restore') ? 'restore' : 'show',
+ event = $('#eventhistory').data('event'),
+ rev = link.attr('data-rev');
+
+ // ignore clicks on first row (current revision)
+ if (link.closest('tr').hasClass('first')) {
+ return false;
+ }
+
+ // let the user confirm the restore action
+ if (action == 'restore' && !confirm(rcmail.gettext('eventrestoreconfirm','calendar').replace('$rev', rev))) {
+ return false;
+ }
+
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:action, e:{ id:event.id, calendar:event.calendar, rev: rev } }, me.loading_lock);
+ return false;
+ });
+
+ $dialog.data('initialized', true);
+ }
+ };
+
+ // callback from server with changelog data
+ var render_event_changelog = function(data)
+ {
+ var $dialog = $('#eventhistory');
+
+ if (data === false || !data.length) {
+ $dialog.dialog('close');
+ return
+ }
+
+ var i, change, accessible, op_append, first = data.length -1, last = 0,
+ op_labels = { APPEND: 'actionappend', MOVE: 'actionmove', DELETE: 'actiondelete' },
+ actions = '<a href="#show" class="iconbutton preview" title="'+ rcmail.gettext('showrevision','calendar') +'" data-rev="{rev}" /> ' +
+ '<a href="#restore" class="iconbutton restore" title="'+ rcmail.gettext('restore','calendar') + '" data-rev="{rev}" />',
+ tbody = $('#event-changelog-table tbody').html('');
+
+ for (i=first; i >= 0; i--) {
+ change = data[i];
+ accessible = change.date && change.user;
+
+ if (change.op == 'MOVE' && change.folder) {
+ op_append = ' ⢠' + change.folder;
+ }
+ else {
+ op_append = '';
+ }
+
+ $('<tr class="' + (i == last ? 'last' : (i == first ? 'first' : '')) + (accessible ? '' : 'undisclosed') + '">')
+ .append('<td class="diff">' + (accessible && change.op != 'DELETE' ?
+ '<input type="radio" name="rev1" class="diff-rev1" value="' + change.rev + '" title="" '+ (i == last ? 'checked="checked"' : '') +' /> '+
+ '<input type="radio" name="rev2" class="diff-rev2" value="' + change.rev + '" title="" '+ (i == first ? 'checked="checked"' : '') +' /></td>'
+ : ''))
+ .append('<td class="revision">' + Q(change.rev) + '</td>')
+ .append('<td class="date">' + Q(change.date ? format_datetime(parseISO8601(change.date)) : '') + '</td>')
+ .append('<td class="user">' + Q(change.user || 'undisclosed') + '</td>')
+ .append('<td class="operation" title="' + op_append + '">' + Q(rcmail.gettext(op_labels[change.op] || '', 'calendar') + (op_append ? ' ...' : '')) + '</td>')
+ .append('<td class="actions">' + (accessible && change.op != 'DELETE' ? actions.replace(/\{rev\}/g, change.rev) : '') + '</td>')
+ .appendTo(tbody);
+ }
+
+ $('#eventhistory .compare-button').fadeIn(200);
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 600);
+ };
+
+ // callback from server with event diff data
+ var event_show_diff = function(data)
+ {
+ var event = me.selected_event,
+ $dialog = $("#eventdiff");
+
+ $dialog.find('div.event-section, div.event-line, h1.event-title-new').hide().data('set', false).find('.index').html('');
+ $dialog.find('div.event-section.clone, div.event-line.clone').remove();
+
+ // always show event title and date
+ $('.event-title', $dialog).html(Q(event.title)).removeClass('event-text-old').show();
+ $('.event-date', $dialog).html(Q(me.event_date_text(event))).show();
+
+ // show each property change
+ $.each(data.changes, function(i,change) {
+ var prop = change.property, r2, html = false,
+ row = $('div.event-' + prop, $dialog).first();
+
+ // special case: title
+ if (prop == 'title') {
+ $('.event-title', $dialog).addClass('event-text-old').html(Q(change.old || '--'));
+ $('.event-title-new', $dialog).html(Q(change.new || '--')).show();
+ }
+
+ // no display container for this property
+ if (!row.length) {
+ return true;
+ }
+
+ // clone row if already exists
+ if (row.data('set')) {
+ r2 = row.clone().addClass('clone').insertAfter(row);
+ row = r2;
+ }
+
+ // format dates
+ if (['start','end','changed'].indexOf(prop) >= 0) {
+ if (change.old) change.old_ = me.format_datetime(parseISO8601(change.old));
+ if (change.new) change.new_ = me.format_datetime(parseISO8601(change.new));
+ }
+ // render description text
+ else if (prop == 'description') {
+ // TODO: show real text diff
+ if (!change.diff_ && change.old) change.old_ = text2html(change.old);
+ if (!change.diff_ && change.new) change.new_ = text2html(change.new);
+ html = true;
+ }
+ // format attendees struct
+ else if (prop == 'attendees') {
+ if (change.old) change.old_ = event_attendee_html(change.old);
+ if (change.new) change.new_ = event_attendee_html($.extend({}, change.old || {}, change.new));
+ html = true;
+ }
+ // localize priority values
+ else if (prop == 'priority') {
+ var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+ if (change.old) change.old_ = change.old + ' ' + (priolabels[change.old] || '');
+ if (change.new) change.new_ = change.new + ' ' + (priolabels[change.new] || '');
+ }
+ // localize status
+ else if (prop == 'status') {
+ var status_lc = String(event.status).toLowerCase();
+ if (change.old) change.old_ = rcmail.gettext(String(change.old).toLowerCase(), 'calendar');
+ if (change.new) change.new_ = rcmail.gettext(String(change.new).toLowerCase(), 'calendar');
+ }
+
+ // format attachments struct
+ if (prop == 'attachments') {
+ if (change.old) event_show_attachments([change.old], row.children('.event-text-old'), event, false);
+ else row.children('.event-text-old').html('--');
+ if (change.new) event_show_attachments([$.extend({}, change.old || {}, change.new)], row.children('.event-text-new'), event, false);
+ else row.children('.event-text-new').html('--');
+ // remove click handler as we're currentyl not able to display the according attachment contents
+ $('.attachmentslist li a', row).unbind('click').removeAttr('href');
+ }
+ else if (change.diff_) {
+ row.children('.event-text-diff').html(change.diff_);
+ row.children('.event-text-old, .event-text-new').hide();
+ }
+ else {
+ if (!html) {
+ // escape HTML characters
+ change.old_ = Q(change.old_ || change.old || '--')
+ change.new_ = Q(change.new_ || change.new || '--')
+ }
+ row.children('.event-text-old').html(change.old_ || change.old || '--');
+ row.children('.event-text-new').html(change.new_ || change.new || '--');
+ }
+
+ // display index number
+ if (typeof change.index != 'undefined') {
+ row.find('.index').html('(' + change.index + ')');
+ }
+
+ row.show().data('set', true);
+
+ // hide event-date line
+ if (prop == 'start' || prop == 'end')
+ $('.event-date', $dialog).hide();
+ });
+
+ var buttons = {};
+ buttons[rcmail.gettext('close', 'calendar')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('eventdiff','calendar').replace('$rev', data.rev) + ' - ' + event.title,
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ buttons: buttons,
+ minWidth: 320,
+ width: 450
+ }).show();
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 400);
+ };
+
+ // exports
+ this.event_show_diff = event_show_diff;
+ this.event_show_dialog = event_show_dialog;
+ this.event_history_dialog = event_history_dialog;
+ this.render_event_changelog = render_event_changelog;
+
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
@@ -2055,7 +2384,7 @@ function rcube_calendar_ui(settings)
var dialog_check = function(e)
{
var showd = $("#eventshow");
- if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) {
+ if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length && !$(e.target).closest('.popupmenu').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
@@ -2589,6 +2918,23 @@ function rcube_calendar_ui(settings)
};
+ // download the selected event as iCal
+ this.event_download = function(event)
+ {
+ if (event && event.id) {
+ rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 });
+ }
+ };
+
+ // open the message compose step with a calendar_event parameter referencing the selected event.
+ // the server-side plugin hook will pick that up and attach the event to the message.
+ this.event_sendbymail = function(event, e)
+ {
+ if (event && event.id) {
+ rcmail.command('compose', { _calendar_event:event._id }, e ? e.target : null, e);
+ }
+ };
+
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
@@ -3452,9 +3798,11 @@ function rcube_calendar_ui(settings)
$('#eventshow .changersvp').click(function(e) {
var d = $('#eventshow'),
- h = $('#event-rsvp').show().height();
- h -= $(this).closest('.event-line').toggle().height();
- me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
+ h = -$(this).closest('.event-line').toggle().height();
+ $('#event-rsvp').slideDown(300, function() {
+ h += $(this).height();
+ me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
+ });
return false;
})
@@ -3508,7 +3856,10 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
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]); }, true);
rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
-
+ rcmail.register_command('event-download', function(){ cal.event_download(cal.selected_event); }, true);
+ rcmail.register_command('event-sendbymail', function(p, obj, e){ cal.event_sendbymail(cal.selected_event, e); }, true);
+ rcmail.register_command('event-history', function(p, obj, e){ cal.event_history_dialog(cal.selected_event); }, false);
+
// search and export events
rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
@@ -3528,6 +3879,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); });
rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); });
rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); });
+ rcmail.addEventListener('plugin.render_event_changelog', function(data){ cal.render_event_changelog(data); });
+ rcmail.addEventListener('plugin.event_show_diff', function(data){ cal.event_show_diff(data); });
+ rcmail.addEventListener('plugin.event_show_revision', function(data){ cal.event_show_dialog(data, null, true); });
rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
// let's go
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 4c4f516..e213a93 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -100,7 +100,8 @@ abstract class calendar_driver
public $attendees = false;
public $freebusy = false;
public $attachments = false;
- public $undelete = false; // event undelete action
+ public $undelete = false;
+ public $history = false;
public $categoriesimmutable = false;
public $alarm_types = array('DISPLAY');
public $alarm_absolute = true;
@@ -406,6 +407,77 @@ abstract class calendar_driver
}
/**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of changes, each as a hash array:
+ * rev: Revision number
+ * type: Type of the change (create, update, move, delete)
+ * date: Change date
+ * user: The user who executed the change
+ * ip: Client IP
+ * destination: Destination calendar for 'move' type
+ */
+ public function get_event_changelog($event)
+ {
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revisions: "from:to"
+ *
+ * @return array List of property changes, each as a hash array:
+ * property: Revision number
+ * old: Old property value
+ * new: Updated property value
+ */
+ public function get_event_diff($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see self::get_event()
+ */
+ public function get_event_revison($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ return false;
+ }
+
+
+ /**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 706c3cd..ba71846 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -30,6 +30,7 @@ class kolab_calendar extends kolab_storage_folder_api
public $readonly = true;
public $attachments = true;
public $alarms = false;
+ public $history = false;
public $subscriptions = true;
public $categories = array();
public $storage;
@@ -589,53 +590,8 @@ class kolab_calendar extends kolab_storage_folder_api
{
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
-/*
- // convert from DateTime to unix timestamp
- if (is_a($record['start'], 'DateTime'))
- $record['start'] = $record['start']->format('U');
- if (is_a($record['end'], 'DateTime'))
- $record['end'] = $record['end']->format('U');
-*/
- // all-day events go from 12:00 - 13:00
- if ($record['end'] <= $record['start'] && $record['allday']) {
- $record['end'] = clone $record['start'];
- $record['end']->add(new DateInterval('PT1H'));
- }
-
- if (!empty($record['_attachments'])) {
- foreach ($record['_attachments'] as $key => $attachment) {
- if ($attachment !== false) {
- if (!$attachment['name'])
- $attachment['name'] = $key;
-
- unset($attachment['path'], $attachment['content']);
- $attachments[] = $attachment;
- }
- }
-
- $record['attachments'] = $attachments;
- }
-
- // Roundcube only supports one category assignment
- if (is_array($record['categories']))
- $record['categories'] = $record['categories'][0];
-
- // the cancelled flag transltes into status=CANCELLED
- if ($record['cancelled'])
- $record['status'] = 'CANCELLED';
-
- // 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'], $record['x-custom']);
- return $record;
+ return kolab_driver::to_rcube_event($record);
}
/**
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ba634cb..f921b53 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -47,6 +47,7 @@ class kolab_driver extends calendar_driver
private $calendars;
private $has_writeable = false;
private $freebusy_trigger = false;
+ private $bonnie_api = false;
/**
* Default constructor
@@ -69,6 +70,10 @@ class kolab_driver extends calendar_driver
$this->alarm_absolute = false;
}
+ // get configuration for the Bonnie API
+ if ($bonnie_config = $this->cal->rc->config->get('kolab_bonnie_api', false))
+ $this->bonnie_api = new kolab_bonnie_api($bonnie_config);
+
// calendar uses fully encoded identifiers
kolab_storage::$encode_ids = true;
}
@@ -164,6 +169,7 @@ class kolab_driver extends calendar_driver
'active' => $cal->is_active(),
'title' => $cal->get_owner(),
'owner' => $cal->get_owner(),
+ 'history' => false,
'virtual' => false,
'readonly' => true,
'group' => 'other',
@@ -192,6 +198,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
'group' => $cal->get_namespace(),
'default' => $cal->default,
'active' => $cal->is_active(),
@@ -222,6 +229,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
@@ -252,6 +260,7 @@ class kolab_driver extends calendar_driver
'readonly' => true,
'default' => false,
'children' => false,
+ 'history' => false,
);
}
}
@@ -1285,6 +1294,265 @@ class kolab_driver extends calendar_driver
exit;
}
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ public static function to_rcube_event($record)
+ {
+ $record['id'] = $record['uid'];
+
+ // all-day events go from 12:00 - 13:00
+ if ($record['end'] <= $record['start'] && $record['allday']) {
+ $record['end'] = clone $record['start'];
+ $record['end']->add(new DateInterval('PT1H'));
+ }
+
+ if (!empty($record['_attachments'])) {
+ foreach ($record['_attachments'] as $key => $attachment) {
+ if ($attachment !== false) {
+ if (!$attachment['name'])
+ $attachment['name'] = $key;
+
+ unset($attachment['path'], $attachment['content']);
+ $attachments[] = $attachment;
+ }
+ }
+
+ $record['attachments'] = $attachments;
+ }
+
+ // Roundcube only supports one category assignment
+ if (is_array($record['categories']))
+ $record['categories'] = $record['categories'][0];
+
+ // the cancelled flag transltes into status=CANCELLED
+ if ($record['cancelled'])
+ $record['status'] = 'CANCELLED';
+
+ // 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'], $record['x-custom']);
+
+ return $record;
+ }
+
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties
+ *
+ * @return array List of changes, each as a hash array
+ * @see calendar_driver::get_event_changelog()
+ */
+ public function get_event_changelog($event)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $folder) = $this->_resolve_event_identity($event);
+
+ $result = $this->bonnie_api->changelog('event', $uid, $folder);
+ if (is_array($result) && $result['uid'] == $uid) {
+ return $result['changes'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties
+ * @param mixed $rev Revisions: "from:to"
+ *
+ * @return array List of property changes, each as a hash array
+ * @see calendar_driver::get_event_diff()
+ */
+ public function get_event_diff($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $folder) = $this->_resolve_event_identity($event);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->diff('event', $uid, $rev, $folder);
+ if (is_array($result) && $result['uid'] == $uid) {
+ $result['rev'] = $rev;
+
+ $keymap = array(
+ 'dtstart' => 'start',
+ 'dtend' => 'end',
+ 'dstamp' => 'changed',
+ 'summary' => 'title',
+ 'alarm' => 'alarms',
+ 'attendee' => 'attendees',
+ 'attach' => 'attachments',
+ 'rrule' => 'recurrence',
+ 'transparency' => 'free_busy',
+ 'classification' => 'sensitivity',
+ 'lastmodified-date' => 'changed',
+ );
+ $prop_keymaps = array(
+ 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
+ 'attendees' => array('partstat' => 'status'),
+ );
+ $special_changes = array();
+
+ // map kolab event properties to keys the client expects
+ array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
+ if (array_key_exists($change['property'], $keymap)) {
+ $change['property'] = $keymap[$change['property']];
+ }
+ // translate free_busy values
+ if ($change['property'] == 'free_busy') {
+ $change['old'] = $old['old'] ? 'free' : 'busy';
+ $change['new'] = $old['new'] ? 'free' : 'busy';
+ }
+ // map alarms trigger value
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']) && is_array($change['old']['trigger']))
+ $change['old']['trigger'] = $change['old']['trigger']['value'];
+ if (is_array($change['new']) && is_array($change['new']['trigger']))
+ $change['new']['trigger'] = $change['new']['trigger']['value'];
+ }
+ // make all property keys uppercase
+ if ($change['property'] == 'recurrence') {
+ $special_changes['recurrence'] = $i;
+ foreach (array('old','new') as $m) {
+ if (is_array($change[$m])) {
+ $props = array();
+ foreach ($change[$m] as $k => $v)
+ $props[strtoupper($k)] = $v;
+ $change[$m] = $props;
+ }
+ }
+ }
+ // map property keys names
+ if (is_array($prop_keymaps[$change['property']])) {
+ foreach ($prop_keymaps[$change['property']] as $k => $dest) {
+ if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
+ $change['old'][$dest] = $change['old'][$k];
+ unset($change['old'][$k]);
+ }
+ if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
+ $change['new'][$dest] = $change['new'][$k];
+ unset($change['new'][$k]);
+ }
+ }
+ }
+
+ if ($change['property'] == 'exdate') {
+ $special_changes['exdate'] = $i;
+ }
+ else if ($change['property'] == 'rdate') {
+ $special_changes['rdate'] = $i;
+ }
+ });
+
+ // merge some recurrence changes
+ foreach (array('exdate','rdate') as $prop) {
+ if (array_key_exists($prop, $special_changes)) {
+ $exdate = $result['changes'][$special_changes[$prop]];
+ if (array_key_exists('recurrence', $special_changes)) {
+ $recurrence = &$result['changes'][$special_changes['recurrence']];
+ }
+ else {
+ $i = count($result['changes']);
+ $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
+ $recurrence = &$result['changes'][$i]['recurrence'];
+ }
+ $key = strtoupper($prop);
+ $recurrence['old'][$key] = $exdate['old'];
+ $recurrence['new'][$key] = $exdate['new'];
+ unset($result['changes'][$special_changes[$prop]]);
+ }
+ }
+
+ return $result;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param array Hash array with event properties
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see calendar_driver::get_event_revison()
+ */
+ public function get_event_revison($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $calid = $event['calendar'];
+ list($uid, $folder) = $this->_resolve_event_identity($event);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->get('event', $uid, $rev, $folder);
+ if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+ $format = kolab_format::factory('event');
+ $format->load($result['xml']);
+ $event = $format->to_array();
+
+ if ($format->is_valid()) {
+ if ($result['folder'] && ($cal = $this->get_calendar(kolab_storage::id_encode($result['folder'])))) {
+ $event['calendar'] = $cal->id;
+ }
+ else {
+ $event['calendar'] = $calid;
+ }
+
+ $event['rev'] = $result['rev'];
+ return self::to_rcube_event($event);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to resolved the given event identifier into uid and folder
+ *
+ * @return array (uid,folder) tuple
+ */
+ private function _resolve_event_identity($event)
+ {
+ $folder = null;
+ if (is_array($event)) {
+ $uid = $event['id'] ?: $event['uid'];
+ if ($cal = $this->get_calendar($event['calendar']) && !($cal instanceof kolab_invitation_calendar)) {
+ $folder = $cal->name;
+ }
+ }
+ else {
+ $uid = $event;
+ }
+
+ // FIXME: hard-code UID for static Bonnie API demo
+ $demo_uids = $this->rc->config->get('kolab_static_bonnie_uids', array('0015c5fe-9baf-0561-11e3-d584fa2894b7'));
+ if (!in_array($uid, $demo_uids))
+ $uid = reset($demo_uids);
+
+ return array($uid, $folder);
+ }
+
/**
* Callback function to produce driver-specific calendar create/edit form
*
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index bcfc772..58c2296 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -391,9 +391,7 @@ class kolab_user_calendar extends kolab_calendar
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
- // TODO: implement this
-
- return $record;
+ return kolab_driver::to_rcube_event($record);
}
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 6e165ca..df15957 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -97,6 +97,7 @@ class calendar_ui
$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.event_changelog_table', array($this, 'event_changelog_table'));
$this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
}
@@ -841,6 +842,22 @@ class calendar_ui
}
/**
+ * Table oultine for event changelog display
+ */
+ function event_changelog_table($attrib = array())
+ {
+ $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
+ $table->add_header('diff', '');
+ $table->add_header('revision', $this->cal->gettext('revision'));
+ $table->add_header('date', $this->cal->gettext('date'));
+ $table->add_header('user', $this->cal->gettext('user'));
+ $table->add_header('operation', $this->cal->gettext('operation'));
+ $table->add_header('actions', ' ');
+
+ return $table->show($attrib);
+ }
+
+ /**
*
*/
function event_invitebox($attrib = array())
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index f7af672..7d1e7d2 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -81,7 +81,12 @@ $labels['private'] = 'private';
$labels['confidential'] = 'confidential';
$labels['alarms'] = 'Reminder';
$labels['comment'] = 'Comment';
+$labels['created'] = 'Created';
+$labels['changed'] = 'Last Modified';
+$labels['unknown'] = 'Unknown';
+$labels['eventoptions'] = 'Options';
$labels['generated'] = 'generated at';
+$labels['eventhistory'] = 'History';
$labels['printdescriptions'] = 'Print descriptions';
$labels['parentcalendar'] = 'Insert inside';
$labels['searchearlierdates'] = '« Search for earlier events';
@@ -253,6 +258,24 @@ $labels['birthdayscalendarsources'] = 'From these address books';
$labels['birthdayeventtitle'] = '$name\'s Birthday';
$labels['birthdayage'] = 'Age $age';
+// history dialog
+$labels['eventchangelog'] = 'Change History';
+$labels['eventdiff'] = 'Changes from revisions $rev';
+$labels['revision'] = 'Revision';
+$labels['user'] = 'User';
+$labels['operation'] = 'Action';
+$labels['actionappend'] = 'Saved';
+$labels['actionmove'] = 'Moved';
+$labels['actiondelete'] = 'Deleted';
+$labels['compare'] = 'Compare';
+$labels['showrevision'] = 'Show this version';
+$labels['restore'] = 'Restore this version';
+$labels['eventnotfound'] = 'Failed to load event data';
+$labels['eventchangelognotavailable'] = 'Change history is not available for this event';
+$labels['eventdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['eventrestoreconfirm'] = 'Do you really want to restore revision $rev of this event? This will replace the current event with the old version.';
+
+
// (hidden) titles and labels for accessibility annotations
$labels['arialabelminical'] = 'Calendar date selection';
$labels['arialabelcalendarview'] = 'Calendar view';
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 2855635..065cb68 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -526,6 +526,10 @@ a.miniColors-trigger {
/* jQuery UI overrides */
+.calendarmain .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+ float: left;
+}
+
#eventshow h1 {
font-size: 20px;
margin: 0.1em 0 0.4em 0;
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index 07839f0..2df1ce7 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -115,6 +115,13 @@
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
</div>
+<div id="eventoptionsmenu" class="popupmenu">
+ <ul>
+ <li><roundcube:button command="event-download" label="download" classAct="active" /></li>
+ <li><roundcube:button command="event-sendbymail" label="send" classAct="active" /></li>
+ </ul>
+</div>
+
<roundcube:include file="/templates/eventedit.html" />
<div id="eventresourcesdialog" class="uidialog">
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 391f4c1..8499177 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -663,53 +663,160 @@ a.miniColors-trigger {
/* jQuery UI overrides */
-#eventshow h1 {
+.calendarmain .eventdialog h1 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
-#eventshow label,
-#eventshow h5.label {
+.calendarmain .eventdialog label,
+.calendarmain .eventdialog h5.label {
font-weight: normal;
font-size: 1em;
color: #999;
margin: 0 0 0.2em 0;
}
-#eventshow {
+.calendarmain .eventdialog label span.index,
+.calendarmain .eventdialog h5.label .index {
+ vertical-align: inherit;
+ margin-left: 0.6em;
+}
+
+.calendarmain .eventdialog {
margin: 0 -0.2em;
}
-#eventshow.status-cancelled {
+.calendarmain .eventdialog.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
-#eventshow.sensitivity-private {
+.calendarmain .eventdialog.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
-#eventshow.sensitivity-confidential {
+.calendarmain .eventdialog.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
-.sensitivity-private #event-title {
+.calendarmain .sensitivity-private #event-title {
margin-right: 50px;
}
-.sensitivity-confidential #event-title {
+.calendarmain .sensitivity-confidential #event-title {
margin-right: 60px;
}
-#eventshow div.event-line {
+.calendarmain .eventdialog div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
-#eventshow div.event-line a.iconbutton {
+.calendarmain .eventdialog div.event-line a.iconbutton {
margin-left: 0.5em;
line-height: 17px;
}
+.calendarmain .eventdialog div.event-line span.event-text + label {
+ margin-left: 2em;
+}
+
+.calendarmain .eventdialog #event-created-changed {
+ margin-top: 0.6em;
+}
+
+.eventdialog .event-text-old,
+.eventdialog .event-text-new,
+.eventdialog .event-text-diff {
+ padding: 2px;
+}
+
+.eventdialog .event-text-diff del,
+.eventdialog .event-text-diff ins {
+ text-decoration: none;
+ color: inherit;
+}
+
+.eventdialog .event-text-old,
+.eventdialog .event-text-diff del {
+ background-color: #fdd;
+ /* text-decoration: line-through; */
+}
+
+.eventdialog .event-text-new,
+.eventdialog .event-text-diff ins {
+ background-color: #dfd;
+}
+
+#eventdiff .attachmentslist li a,
+#eventdiff .attachmentslist li a:hover {
+ cursor: default;
+ text-decoration: none;
+}
+
+#eventhistory .loading {
+ color: #666;
+ margin: 1em 0;
+ padding: 1px 0 2px 24px;
+ background: url(images/loading_blue.gif) top left no-repeat;
+}
+
+#eventhistory .compare-button {
+ margin: 4px 0;
+}
+
+#event-changelog-table tbody td {
+ padding: 4px 7px;
+ vertical-align: middle;
+}
+
+#event-changelog-table tbody tr.undisclosed td.date,
+#event-changelog-table tbody tr.undisclosed td.user {
+ font-style: italic;
+}
+
+#event-changelog-table .diff {
+ width: 4em;
+ padding: 2px;
+}
+
+#event-changelog-table .revision {
+ width: 5em;
+}
+
+#event-changelog-table .date {
+ width: 11em;
+}
+
+#event-changelog-table .user {
+ width: auto;
+}
+
+#event-changelog-table .operation {
+ width: 15%;
+}
+
+#event-changelog-table .actions {
+ width: 50px;
+ text-align: right;
+ padding: 4px;
+}
+
+#event-changelog-table td a.iconbutton.restore,
+#event-changelog-table td a.iconbutton.preview {
+ background-image: url(images/calendars.png);
+ background-position: 1px -147px;
+}
+
+#event-changelog-table td a.iconbutton.restore {
+ background-image: url(images/calendars.png);
+ background-position: 1px -167px;
+}
+
+#event-changelog-table tr.first td a.iconbutton {
+ opacity: 0.3;
+ cursor: default;
+}
+
#event-partstat .changersvp {
cursor: pointer;
color: #333;
@@ -745,7 +852,7 @@ a.miniColors-trigger {
}
div.form-section,
-#eventshow div.event-section,
+.calendarmain .eventdialog div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.6em;
@@ -757,7 +864,7 @@ div.form-section,
border-bottom: 2px solid #fafafa;
}
-#eventshow label,
+.calendarmain .eventdialog label,
#eventedit label,
.form-section label {
display: inline-block;
@@ -830,7 +937,7 @@ td.topalign {
#event-rsvp,
#edit-attendees-notify {
- margin: 0.3em 0;
+ margin: 0.6em 0 0.3em 0;
padding: 0.5em;
}
@@ -1186,13 +1293,13 @@ td.topalign {
}
a.dropdown-link {
- font-size: 12px;
+ font-size: 11px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' â¼';
- font-size: 11px;
+ font-size: 10px;
color: #666;
}
@@ -1201,7 +1308,10 @@ a.dropdown-link:after {
}
.ui-dialog-buttonset a.dropdown-link {
- margin-right: 1em;
+ position: relative;
+ top: 2px;
+ margin: 0 1em;
+ color: #333;
}
#calendarsidebar .ui-datepicker-calendar {
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index bf84f3a..5e53cb6 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 4f3228c..50ad616 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -77,7 +77,7 @@
</ul>
</div>
-<div id="eventshow" class="uidialog" aria-hidden="true">
+<div id="eventshow" class="uidialog eventdialog" aria-hidden="true">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
@@ -136,10 +136,109 @@
<label><roundcube:label name="attachments" /></label>
<div class="event-text"></div>
</div>
+ <div class="event-line" id="event-created-changed">
+ <label><roundcube:label name="calendar.created" /></label>
+ <span class="event-text event-created"></span>
+ <label><roundcube:label name="calendar.changed" /></label>
+ <span class="event-text event-changed"></span>
+ </div>
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" class="event-dialog-message" style="display:none" />
</div>
+<div id="eventoptionsmenu" class="popupmenu" aria-hidden="true">
+ <h3 id="aria-label-eventoptions" class="voice"><roundcube:label name="calendar.eventoptions" /></h3>
+ <ul id="eventoptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-eventoptions">
+ <li role="menuitem"><roundcube:button command="event-download" label="download" classAct="active" /></li>
+ <li role="menuitem"><roundcube:button command="event-sendbymail" label="send" classAct="active" /></li>
+ <roundcube:if condition="env:calendar_driver == 'kolab' && config:kolab_bonnie_api" />
+ <li role="menuitem"><roundcube:button command="event-history" type="link" label="calendar.eventhistory" classAct="active" /></li>
+ <roundcube:endif />
+ </ul>
+</div>
+
+<div id="eventdiff" class="uidialog eventdialog" aria-hidden="true">
+ <h1 class="event-title">Event Title</h1>
+ <h1 class="event-title-new event-text-new"></h1>
+ <div class="event-section event-date"></div>
+ <div class="event-section event-location">
+ <h5 class="label"><roundcube:label name="calendar.location" /></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-description">
+ <h5 class="label"><roundcube:label name="calendar.description" /></h5>
+ <div class="event-text-diff" style="white-space:pre-wrap"></div>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-url">
+ <h5 class="label"><roundcube:label name="calendar.url" /></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-recurrence">
+ <h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-alarms">
+ <h5 class="label"><roundcube:label name="calendar.alarms" /><span class="index"></span></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-line event-start">
+ <label><roundcube:label name="calendar.start" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-end">
+ <label><roundcube:label name="calendar.end" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-attendees">
+ <label><roundcube:label name="calendar.tabattendees" /><span class="index"></span></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-calendar">
+ <label><roundcube:label name="calendar.calendar" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-categories">
+ <label><roundcube:label name="calendar.category" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-status">
+ <label><roundcube:label name="calendar.status" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-free_busy">
+ <label><roundcube:label name="calendar.freebusy" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-priority">
+ <label><roundcube:label name="calendar.priority" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-sensitivity">
+ <label><roundcube:label name="calendar.sensitivity" /></label>
+ <span class="event-text-old"></span> ⇢
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-section event-attachments">
+ <label><roundcube:label name="attachments" /><span class="index"></span></label>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+</div>
+
<roundcube:include file="/templates/eventedit.html" />
<div id="eventresourcesdialog" class="uidialog" aria-hidden="true">
@@ -223,6 +322,11 @@
</div>
</div>
+<div id="eventhistory" class="uidialog" aria-hidden="true">
+ <roundcube:object name="plugin.event_changelog_table" id="event-changelog-table" class="records-table" />
+ <div class="compare-button"><input type="button" class="button" value="â³ <roundcube:label name='calendar.compare' />" /></div>
+</div>
+
<div id="calendarform" class="uidialog" aria-hidden="true">
<roundcube:label name="loading" />
</div>
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 9c507a3..d267da5 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -746,7 +746,17 @@ class libcalendaring extends rcube_plugin
$until = $this->gettext('forever');
}
- return rtrim($freq . $details . ', ' . $until);
+ $except = '';
+ if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
+ $format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
+ $exdates = array_map(
+ function($dt) use ($format) { return format_date($dt, $format); },
+ array_slice($rrule['EXDATE'], 0, 10)
+ );
+ $except = '; ' . $this->gettext('except') . ' ' . join(', ');
+ }
+
+ return rtrim($freq . $details . ', ' . $until . $except);
}
/**
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7e8c717..26e4ae4 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -66,6 +66,7 @@ $labels['fourth'] = 'fourth';
$labels['last'] = 'last';
$labels['dayofmonth'] = 'Day of month';
$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'except';
// itip related labels
$labels['itipinvitation'] = 'Invitation to';
commit f3b31c863d0f28596377120106f621e21f892cf3
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Jul 29 14:34:03 2014 +0200
Add utility function to render a nice diff of two texts using the FineDiff library by Raymond Hill
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index c32d65a..052724c 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -124,4 +124,15 @@ class libkolab extends rcube_plugin
return $request;
}
+
+ /**
+ * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
+ */
+ public static function html_diff($from, $to)
+ {
+ include_once __dir__ . '/vendor/finediff.php';
+
+ $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
+ return $diff->renderDiffToHTML();
+ }
}
diff --git a/plugins/libkolab/vendor/finediff.php b/plugins/libkolab/vendor/finediff.php
new file mode 100644
index 0000000..b3c416c
--- /dev/null
+++ b/plugins/libkolab/vendor/finediff.php
@@ -0,0 +1,688 @@
+<?php
+/**
+* FINE granularity DIFF
+*
+* Computes a set of instructions to convert the content of
+* one string into another.
+*
+* Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
+*
+* Licensed under The MIT License
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in
+* all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*
+* @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
+* @link http://www.raymondhill.net/finediff/
+* @version 0.6
+* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+*/
+
+/**
+* Usage (simplest):
+*
+* include 'finediff.php';
+*
+* // for the stock stack, granularity values are:
+* // FineDiff::$paragraphGranularity = paragraph/line level
+* // FineDiff::$sentenceGranularity = sentence level
+* // FineDiff::$wordGranularity = word level
+* // FineDiff::$characterGranularity = character level [default]
+*
+* $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
+* // store opcodes for later use...
+*
+* ...
+*
+* // restore $to_text from $from_text + $opcodes
+* include 'finediff.php';
+* $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
+*
+* ...
+*/
+
+/**
+* Persisted opcodes (string) are a sequence of atomic opcode.
+* A single opcode can be one of the following:
+* c | c{n} | d | d{n} | i:{c} | i{length}:{s}
+* 'c' = copy one character from source
+* 'c{n}' = copy n characters from source
+* 'd' = skip one character from source
+* 'd{n}' = skip n characters from source
+* 'i:{c} = insert character 'c'
+* 'i{n}:{s}' = insert string s, which is of length n
+*
+* Do not exist as of now, under consideration:
+* 'm{n}:{o} = move n characters from source o characters ahead.
+* It would be essentially a shortcut for a delete->copy->insert
+* command (swap) for when the inserted segment is exactly the same
+* as the deleted one, and with only a copy operation in between.
+* TODO: How often this case occurs? Is it worth it? Can only
+* be done as a postprocessing method (->optimize()?)
+*/
+abstract class FineDiffOp {
+ abstract public function getFromLen();
+ abstract public function getToLen();
+ abstract public function getOpcode();
+ }
+
+class FineDiffDeleteOp extends FineDiffOp {
+ public function __construct($len) {
+ $this->fromLen = $len;
+ }
+ public function getFromLen() {
+ return $this->fromLen;
+ }
+ public function getToLen() {
+ return 0;
+ }
+ public function getOpcode() {
+ if ( $this->fromLen === 1 ) {
+ return 'd';
+ }
+ return "d{$this->fromLen}";
+ }
+ }
+
+class FineDiffInsertOp extends FineDiffOp {
+ public function __construct($text) {
+ $this->text = $text;
+ }
+ public function getFromLen() {
+ return 0;
+ }
+ public function getToLen() {
+ return strlen($this->text);
+ }
+ public function getText() {
+ return $this->text;
+ }
+ public function getOpcode() {
+ $to_len = strlen($this->text);
+ if ( $to_len === 1 ) {
+ return "i:{$this->text}";
+ }
+ return "i{$to_len}:{$this->text}";
+ }
+ }
+
+class FineDiffReplaceOp extends FineDiffOp {
+ public function __construct($fromLen, $text) {
+ $this->fromLen = $fromLen;
+ $this->text = $text;
+ }
+ public function getFromLen() {
+ return $this->fromLen;
+ }
+ public function getToLen() {
+ return strlen($this->text);
+ }
+ public function getText() {
+ return $this->text;
+ }
+ public function getOpcode() {
+ if ( $this->fromLen === 1 ) {
+ $del_opcode = 'd';
+ }
+ else {
+ $del_opcode = "d{$this->fromLen}";
+ }
+ $to_len = strlen($this->text);
+ if ( $to_len === 1 ) {
+ return "{$del_opcode}i:{$this->text}";
+ }
+ return "{$del_opcode}i{$to_len}:{$this->text}";
+ }
+ }
+
+class FineDiffCopyOp extends FineDiffOp {
+ public function __construct($len) {
+ $this->len = $len;
+ }
+ public function getFromLen() {
+ return $this->len;
+ }
+ public function getToLen() {
+ return $this->len;
+ }
+ public function getOpcode() {
+ if ( $this->len === 1 ) {
+ return 'c';
+ }
+ return "c{$this->len}";
+ }
+ public function increase($size) {
+ return $this->len += $size;
+ }
+ }
+
+/**
+* FineDiff ops
+*
+* Collection of ops
+*/
+class FineDiffOps {
+ public function appendOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+ $edits[] = new FineDiffCopyOp($from_len);
+ }
+ else if ( $opcode === 'd' ) {
+ $edits[] = new FineDiffDeleteOp($from_len);
+ }
+ else /* if ( $opcode === 'i' ) */ {
+ $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
+ }
+ }
+ public $edits = array();
+ }
+
+/**
+* FineDiff class
+*
+* TODO: Document
+*
+*/
+class FineDiff {
+
+ /**------------------------------------------------------------------------
+ *
+ * Public section
+ *
+ */
+
+ /**
+ * Constructor
+ * ...
+ * The $granularityStack allows FineDiff to be configurable so that
+ * a particular stack tailored to the specific content of a document can
+ * be passed.
+ */
+ public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
+ // setup stack for generic text documents by default
+ $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
+ $this->edits = array();
+ $this->from_text = $from_text;
+ $this->doDiff($from_text, $to_text);
+ }
+
+ public function getOps() {
+ return $this->edits;
+ }
+
+ public function getOpcodes() {
+ $opcodes = array();
+ foreach ( $this->edits as $edit ) {
+ $opcodes[] = $edit->getOpcode();
+ }
+ return implode('', $opcodes);
+ }
+
+ public function renderDiffToHTML() {
+ $in_offset = 0;
+ $html = '';
+ foreach ( $this->edits as $edit ) {
+ $n = $edit->getFromLen();
+ if ( $edit instanceof FineDiffCopyOp ) {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffDeleteOp ) {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffInsertOp ) {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ $in_offset += $n;
+ }
+ return $html;
+ }
+
+ /**------------------------------------------------------------------------
+ * Return an opcodes string describing the diff between a "From" and a
+ * "To" string
+ */
+ public static function getDiffOpcodes($from, $to, $granularities = null) {
+ $diff = new FineDiff($from, $to, $granularities);
+ return $diff->getOpcodes();
+ }
+
+ /**------------------------------------------------------------------------
+ * Return an iterable collection of diff ops from an opcodes string
+ */
+ public static function getDiffOpsFromOpcodes($opcodes) {
+ $diffops = new FineDiffOps();
+ FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
+ return $diffops->edits;
+ }
+
+ /**------------------------------------------------------------------------
+ * Re-create the "To" string from the "From" string and an "Opcodes" string
+ */
+ public static function renderToTextFromOpcodes($from, $opcodes) {
+ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+ * Render the diff to an HTML string
+ */
+ public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+ * Generic opcodes parser, user must supply callback for handling
+ * single opcode
+ */
+ public static function renderFromOpcodes($from, $opcodes, $callback) {
+ if ( !is_callable($callback) ) {
+ return '';
+ }
+ $out = '';
+ $opcodes_len = strlen($opcodes);
+ $from_offset = $opcodes_offset = 0;
+ while ( $opcodes_offset < $opcodes_len ) {
+ $opcode = substr($opcodes, $opcodes_offset, 1);
+ $opcodes_offset++;
+ $n = intval(substr($opcodes, $opcodes_offset));
+ if ( $n ) {
+ $opcodes_offset += strlen(strval($n));
+ }
+ else {
+ $n = 1;
+ }
+ if ( $opcode === 'c' ) { // copy n characters from source
+ $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else if ( $opcode === 'd' ) { // delete n characters from source
+ $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+ $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+ $opcodes_offset += 1 + $n;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Stock granularity stacks and delimiters
+ */
+
+ const paragraphDelimiters = "\n\r";
+ public static $paragraphGranularity = array(
+ FineDiff::paragraphDelimiters
+ );
+ const sentenceDelimiters = ".\n\r";
+ public static $sentenceGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters
+ );
+ const wordDelimiters = " \t.\n\r";
+ public static $wordGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters,
+ FineDiff::wordDelimiters
+ );
+ const characterDelimiters = "";
+ public static $characterGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters,
+ FineDiff::wordDelimiters,
+ FineDiff::characterDelimiters
+ );
+
+ public static $textStack = array(
+ ".",
+ " \t.\n\r",
+ ""
+ );
+
+ /**------------------------------------------------------------------------
+ *
+ * Private section
+ *
+ */
+
+ /**
+ * Entry point to compute the diff.
+ */
+ private function doDiff($from_text, $to_text) {
+ $this->last_edit = false;
+ $this->stackpointer = 0;
+ $this->from_text = $from_text;
+ $this->from_offset = 0;
+ // can't diff without at least one granularity specifier
+ if ( empty($this->granularityStack) ) {
+ return;
+ }
+ $this->_processGranularity($from_text, $to_text);
+ }
+
+ /**
+ * This is the recursive function which is responsible for
+ * handling/increasing granularity.
+ *
+ * Incrementally increasing the granularity is key to compute the
+ * overall diff in a very efficient way.
+ */
+ private function _processGranularity($from_segment, $to_segment) {
+ $delimiters = $this->granularityStack[$this->stackpointer++];
+ $has_next_stage = $this->stackpointer < count($this->granularityStack);
+ foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
+ // increase granularity
+ if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
+ $this->_processGranularity(
+ substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
+ $fragment_edit->getText()
+ );
+ }
+ // fuse copy ops whenever possible
+ else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
+ $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
+ $this->from_offset += $fragment_edit->getFromLen();
+ }
+ else {
+ /* $fragment_edit instanceof FineDiffCopyOp */
+ /* $fragment_edit instanceof FineDiffDeleteOp */
+ /* $fragment_edit instanceof FineDiffInsertOp */
+ $this->edits[] = $this->last_edit = $fragment_edit;
+ $this->from_offset += $fragment_edit->getFromLen();
+ }
+ }
+ $this->stackpointer--;
+ }
+
+ /**
+ * This is the core algorithm which actually perform the diff itself,
+ * fragmenting the strings as per specified delimiters.
+ *
+ * This function is naturally recursive, however for performance purpose
+ * a local job queue is used instead of outright recursivity.
+ */
+ private static function doFragmentDiff($from_text, $to_text, $delimiters) {
+ // Empty delimiter means character-level diffing.
+ // In such case, use code path optimized for character-level
+ // diffing.
+ if ( empty($delimiters) ) {
+ return FineDiff::doCharDiff($from_text, $to_text);
+ }
+
+ $result = array();
+
+ // fragment-level diffing
+ $from_text_len = strlen($from_text);
+ $to_text_len = strlen($to_text);
+ $from_fragments = FineDiff::extractFragments($from_text, $delimiters);
+ $to_fragments = FineDiff::extractFragments($to_text, $delimiters);
+
+ $jobs = array(array(0, $from_text_len, 0, $to_text_len));
+
+ $cached_array_keys = array();
+
+ while ( $job = array_pop($jobs) ) {
+
+ // get the segments which must be diff'ed
+ list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+
+ // catch easy cases first
+ $from_segment_length = $from_segment_end - $from_segment_start;
+ $to_segment_length = $to_segment_end - $to_segment_start;
+ if ( !$from_segment_length || !$to_segment_length ) {
+ if ( $from_segment_length ) {
+ $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
+ }
+ else if ( $to_segment_length ) {
+ $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
+ }
+ continue;
+ }
+
+ // find longest copy operation for the current segments
+ $best_copy_length = 0;
+
+ $from_base_fragment_index = $from_segment_start;
+
+ $cached_array_keys_for_current_segment = array();
+
+ while ( $from_base_fragment_index < $from_segment_end ) {
+ $from_base_fragment = $from_fragments[$from_base_fragment_index];
+ $from_base_fragment_length = strlen($from_base_fragment);
+ // performance boost: cache array keys
+ if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
+ if ( !isset($cached_array_keys[$from_base_fragment]) ) {
+ $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
+ }
+ else {
+ $to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
+ }
+ // get only indices which falls within current segment
+ if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
+ $to_fragment_indices = array();
+ foreach ( $to_all_fragment_indices as $to_fragment_index ) {
+ if ( $to_fragment_index < $to_segment_start ) { continue; }
+ if ( $to_fragment_index >= $to_segment_end ) { break; }
+ $to_fragment_indices[] = $to_fragment_index;
+ }
+ $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
+ }
+ else {
+ $to_fragment_indices = $to_all_fragment_indices;
+ }
+ }
+ else {
+ $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
+ }
+ // iterate through collected indices
+ foreach ( $to_fragment_indices as $to_base_fragment_index ) {
+ $fragment_index_offset = $from_base_fragment_length;
+ // iterate until no more match
+ for (;;) {
+ $fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
+ if ( $fragment_from_index >= $from_segment_end ) {
+ break;
+ }
+ $fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
+ if ( $fragment_to_index >= $to_segment_end ) {
+ break;
+ }
+ if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
+ break;
+ }
+ $fragment_length = strlen($from_fragments[$fragment_from_index]);
+ $fragment_index_offset += $fragment_length;
+ }
+ if ( $fragment_index_offset > $best_copy_length ) {
+ $best_copy_length = $fragment_index_offset;
+ $best_from_start = $from_base_fragment_index;
+ $best_to_start = $to_base_fragment_index;
+ }
+ }
+ $from_base_fragment_index += strlen($from_base_fragment);
+ // If match is larger than half segment size, no point trying to find better
+ // TODO: Really?
+ if ( $best_copy_length >= $from_segment_length / 2) {
+ break;
+ }
+ // no point to keep looking if what is left is less than
+ // current best match
+ if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
+ break;
+ }
+ }
+
+ if ( $best_copy_length ) {
+ $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
+ $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
+ $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
+ }
+ else {
+ $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
+ }
+ }
+
+ ksort($result, SORT_NUMERIC);
+ return array_values($result);
+ }
+
+ /**
+ * Perform a character-level diff.
+ *
+ * The algorithm is quite similar to doFragmentDiff(), except that
+ * the code path is optimized for character-level diff -- strpos() is
+ * used to find out the longest common subequence of characters.
+ *
+ * We try to find a match using the longest possible subsequence, which
+ * is at most the length of the shortest of the two strings, then incrementally
+ * reduce the size until a match is found.
+ *
+ * I still need to study more the performance of this function. It
+ * appears that for long strings, the generic doFragmentDiff() is more
+ * performant. For word-sized strings, doCharDiff() is somewhat more
+ * performant.
+ */
+ private static function doCharDiff($from_text, $to_text) {
+ $result = array();
+ $jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
+ while ( $job = array_pop($jobs) ) {
+ // get the segments which must be diff'ed
+ list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+ $from_segment_len = $from_segment_end - $from_segment_start;
+ $to_segment_len = $to_segment_end - $to_segment_start;
+
+ // catch easy cases first
+ if ( !$from_segment_len || !$to_segment_len ) {
+ if ( $from_segment_len ) {
+ $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
+ }
+ else if ( $to_segment_len ) {
+ $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
+ }
+ continue;
+ }
+ if ( $from_segment_len >= $to_segment_len ) {
+ $copy_len = $to_segment_len;
+ while ( $copy_len ) {
+ $to_copy_start = $to_segment_start;
+ $to_copy_start_max = $to_segment_end - $copy_len;
+ while ( $to_copy_start <= $to_copy_start_max ) {
+ $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
+ if ( $from_copy_start !== false ) {
+ $from_copy_start += $from_segment_start;
+ break 2;
+ }
+ $to_copy_start++;
+ }
+ $copy_len--;
+ }
+ }
+ else {
+ $copy_len = $from_segment_len;
+ while ( $copy_len ) {
+ $from_copy_start = $from_segment_start;
+ $from_copy_start_max = $from_segment_end - $copy_len;
+ while ( $from_copy_start <= $from_copy_start_max ) {
+ $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
+ if ( $to_copy_start !== false ) {
+ $to_copy_start += $to_segment_start;
+ break 2;
+ }
+ $from_copy_start++;
+ }
+ $copy_len--;
+ }
+ }
+ // match found
+ if ( $copy_len ) {
+ $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
+ $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
+ $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
+ }
+ // no match, so delete all, insert all
+ else {
+ $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
+ }
+ }
+ ksort($result, SORT_NUMERIC);
+ return array_values($result);
+ }
+
+ /**
+ * Efficiently fragment the text into an array according to
+ * specified delimiters.
+ * No delimiters means fragment into single character.
+ * The array indices are the offset of the fragments into
+ * the input string.
+ * A sentinel empty fragment is always added at the end.
+ * Careful: No check is performed as to the validity of the
+ * delimiters.
+ */
+ private static function extractFragments($text, $delimiters) {
+ // special case: split into characters
+ if ( empty($delimiters) ) {
+ $chars = str_split($text, 1);
+ $chars[strlen($text)] = '';
+ return $chars;
+ }
+ $fragments = array();
+ $start = $end = 0;
+ for (;;) {
+ $end += strcspn($text, $delimiters, $end);
+ $end += strspn($text, $delimiters, $end);
+ if ( $end === $start ) {
+ break;
+ }
+ $fragments[$start] = substr($text, $start, $end - $start);
+ $start = $end;
+ }
+ $fragments[$start] = '';
+ return $fragments;
+ }
+
+ /**
+ * Stock opcode renderers
+ */
+ private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' || $opcode === 'i' ) {
+ return substr($from, $from_offset, $from_len);
+ }
+ return '';
+ }
+
+ private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+ return htmlentities(substr($from, $from_offset, $from_len));
+ }
+ else if ( $opcode === 'd' ) {
+ $deletion = substr($from, $from_offset, $from_len);
+ if ( strcspn($deletion, " \n\r") === 0 ) {
+ $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+ }
+ return '<del>' . htmlentities($deletion) . '</del>';
+ }
+ else /* if ( $opcode === 'i' ) */ {
+ return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
+ }
+ return '';
+ }
+ }
+
diff --git a/plugins/libkolab/vendor/finediff_modifications.diff b/plugins/libkolab/vendor/finediff_modifications.diff
new file mode 100644
index 0000000..3a9ad5c
--- /dev/null
+++ b/plugins/libkolab/vendor/finediff_modifications.diff
@@ -0,0 +1,121 @@
+--- finediff.php.orig 2014-07-29 14:24:10.000000000 +0200
++++ finediff.php 2014-07-29 14:30:38.000000000 +0200
+@@ -234,25 +234,25 @@
+
+ public function renderDiffToHTML() {
+ $in_offset = 0;
+- ob_start();
++ $html = '';
+ foreach ( $this->edits as $edit ) {
+ $n = $edit->getFromLen();
+ if ( $edit instanceof FineDiffCopyOp ) {
+- FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffDeleteOp ) {
+- FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffInsertOp ) {
+- FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+- FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+- FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ $in_offset += $n;
+ }
+- return ob_get_clean();
++ return $html;
+ }
+
+ /**------------------------------------------------------------------------
+@@ -277,18 +277,14 @@
+ * Re-create the "To" string from the "From" string and an "Opcodes" string
+ */
+ public static function renderToTextFromOpcodes($from, $opcodes) {
+- ob_start();
+- FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+- return ob_get_clean();
++ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+ * Render the diff to an HTML string
+ */
+ public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+- ob_start();
+- FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+- return ob_get_clean();
++ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+@@ -297,8 +293,9 @@
+ */
+ public static function renderFromOpcodes($from, $opcodes, $callback) {
+ if ( !is_callable($callback) ) {
+- return;
++ return '';
+ }
++ $out = '';
+ $opcodes_len = strlen($opcodes);
+ $from_offset = $opcodes_offset = 0;
+ while ( $opcodes_offset < $opcodes_len ) {
+@@ -312,18 +309,19 @@
+ $n = 1;
+ }
+ if ( $opcode === 'c' ) { // copy n characters from source
+- call_user_func($callback, 'c', $from, $from_offset, $n, '');
++ $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else if ( $opcode === 'd' ) { // delete n characters from source
+- call_user_func($callback, 'd', $from, $from_offset, $n, '');
++ $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+- call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
++ $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+ $opcodes_offset += 1 + $n;
+ }
+ }
++ return $out;
+ }
+
+ /**
+@@ -665,24 +663,26 @@
+ */
+ private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' || $opcode === 'i' ) {
+- echo substr($from, $from_offset, $from_len);
++ return substr($from, $from_offset, $from_len);
+ }
++ return '';
+ }
+
+ private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+- echo htmlentities(substr($from, $from_offset, $from_len));
++ return htmlentities(substr($from, $from_offset, $from_len));
+ }
+ else if ( $opcode === 'd' ) {
+ $deletion = substr($from, $from_offset, $from_len);
+ if ( strcspn($deletion, " \n\r") === 0 ) {
+ $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+ }
+- echo '<del>', htmlentities($deletion), '</del>';
++ return '<del>' . htmlentities($deletion) . '</del>';
+ }
+ else /* if ( $opcode === 'i' ) */ {
+- echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
++ return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
+ }
++ return '';
+ }
+ }
+
commit 4d9522a6548419b399090249e121c7f8b9fe4fa9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Jul 28 12:19:02 2014 +0200
Implement a JSON-RPC based client to the Bonnie service API (#3093)
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index fd8ac84..b043bb7 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -51,4 +51,11 @@ $rcmail_config['kolab_users_id_attrib'] = null;
// Use these attributes when searching users in LDAP
$rcmail_config['kolab_users_search_attrib'] = array('cn','mail','alias');
-
+// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects
+$rcmail_config['kolab_bonnie_api'] = array(
+ 'uri' => 'https://<kolab-hostname>:8080/api/rpc',
+ 'user' => 'webclient',
+ 'pass' => 'Welcome2KolabSystems',
+ 'secret' => '8431f191707fffffff00000000cccc',
+ 'debug' => true, // logs requests/responses to <log-dir>/bonnie
+);
diff --git a/plugins/libkolab/lib/kolab_bonnie_api.php b/plugins/libkolab/lib/kolab_bonnie_api.php
new file mode 100644
index 0000000..732d29b
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_bonnie_api.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Provider class for accessing historic groupware object data through the Bonnie service
+ *
+ * API Specification at https://wiki.kolabsys.com/User:Bruederli/Draft:Bonnie_Client_API
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_bonnie_api
+{
+ public $ready = false;
+
+ private $config = array();
+ private $client = null;
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($config)
+ {
+ $this->config = $confg;
+
+ $this->client = new kolab_bonnie_api_client($config['uri'], $config['timeout'] ?: 5, (bool)$config['debug']);
+
+ $this->client->set_secret($config['secret']);
+ $this->client->set_authentication($config['user'], $config['pass']);
+ $this->client->set_request_user(rcube::get_instance()->get_user_name());
+
+ $this->ready = !empty($config['secret']) && !empty($config['user']) && !empty($config['pass']);
+ }
+
+ /**
+ * Wrapper function for <object>.changelog() API call
+ */
+ public function changelog($type, $uid, $folder=null)
+ {
+ return $this->client->execute($type.'.changelog', array('uid' => $uid, 'folder' => $folder));
+ }
+
+ /**
+ * Wrapper function for <object>.diff() API call
+ */
+ public function diff($type, $uid, $rev, $folder=null)
+ {
+ return $this->client->execute($type.'.diff', array('uid' => $uid, 'rev' => $rev, 'folder' => $folder));
+ }
+
+ /**
+ * Wrapper function for <object>.get() API call
+ */
+ public function get($type, $uid, $rev, $folder=null)
+ {
+ return $this->client->execute($type.'.get', array('uid' => $uid, 'rev' => intval($rev), 'folder' => $folder));
+ }
+
+ /**
+ * Generic wrapper for direct API calls
+ */
+ public function _execute($method, $params = array())
+ {
+ return $this->client->execute($method, $params);
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_bonnie_api_client.php b/plugins/libkolab/lib/kolab_bonnie_api_client.php
new file mode 100644
index 0000000..bc209f4
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_bonnie_api_client.php
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * JSON-RPC client class with some extra features for communicating with the Bonnie API service.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_bonnie_api_client
+{
+ /**
+ * URL of the RPC endpoint
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * HTTP client timeout in seconds
+ * @var integer
+ */
+ protected $timeout;
+
+ /**
+ * Debug flag
+ * @var bool
+ */
+ protected $debug;
+
+ /**
+ * Username for authentication
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * Password for authentication
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * Secret key for request signing
+ * @var string
+ */
+ protected $secret;
+
+ /**
+ * Default HTTP headers to send to the server
+ * @var array
+ */
+ protected $headers = array(
+ 'Connection' => 'close',
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ );
+
+ /**
+ * Constructor
+ *
+ * @param string $url Server URL
+ * @param integer $timeout Request timeout
+ * @param bool $debug Enabled debug logging
+ * @param array $headers Custom HTTP headers
+ */
+ public function __construct($url, $timeout = 5, $debug = false, $headers = array())
+ {
+ $this->url = $url;
+ $this->timeout = $timeout;
+ $this->debug = $debug;
+ $this->headers = array_merge($this->headers, $headers);
+ }
+
+ /**
+ * Setter for secret key for request signing
+ */
+ public function set_secret($secret)
+ {
+ $this->secret = $secret;
+ }
+
+ /**
+ * Setter for the X-Request-User header
+ */
+ public function set_request_user($username)
+ {
+ $this->headers['X-Request-User'] = $username;
+ }
+
+ /**
+ * Set authentication parameters
+ *
+ * @param string $username Username
+ * @param string $password Password
+ */
+ public function set_authentication($username, $password)
+ {
+ $this->username = $username;
+ $this->password = $password;
+ }
+
+ /**
+ * Automatic mapping of procedures
+ *
+ * @param string $method Procedure name
+ * @param array $params Procedure arguments
+ * @return mixed
+ */
+ public function __call($method, $params)
+ {
+ return $this->execute($method, $params);
+ }
+
+ /**
+ * Execute an RPC command
+ *
+ * @param string $method Procedure name
+ * @param array $params Procedure arguments
+ * @return mixed
+ */
+ public function execute($method, array $params = array())
+ {
+ $id = mt_rand();
+
+ $payload = array(
+ 'jsonrpc' => '2.0',
+ 'method' => $method,
+ 'id' => $id,
+ );
+
+ if (!empty($params)) {
+ $payload['params'] = $params;
+ }
+
+ $result = $this->send_request($payload, $method != 'system.keygen');
+
+ if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) {
+ return $result['result'];
+ }
+ else if (isset($result['error'])) {
+ $this->_debug('ERROR', $result);
+ }
+
+ return null;
+ }
+
+ /**
+ * Do the HTTP request
+ *
+ * @param string $payload Data to send
+ */
+ protected function send_request($payload, $sign = true)
+ {
+ try {
+ $payload_ = json_encode($payload);
+
+ // add request signature
+ if ($sign && !empty($this->secret)) {
+ $this->headers['X-Request-Sign'] = $this->request_signature($payload_);
+ }
+ else if ($this->headers['X-Request-Sign']) {
+ unset($this->headers['X-Request-Sign']);
+ }
+
+ $this->_debug('REQUEST', $payload, $this->headers);
+ $request = libkolab::http_request($this->url, 'POST', array('timeout' => $this->timeout));
+ $request->setHeader($this->headers);
+ $request->setAuth($this->username, $this->password);
+ $request->setBody($payload_);
+
+ $response = $request->send();
+
+ if ($response->getStatus() == 200) {
+ $result = json_decode($response->getBody(), true);
+ $this->_debug('RESPONSE', $result);
+ }
+ else {
+ throw new Exception(sprintf("HTTP %d %s", $response->getStatus(), $response->getReasonPhrase()));
+ }
+ }
+ catch (Exception $e) {
+ rcube::raise_error(array(
+ 'code' => 500,
+ 'type' => 'php',
+ 'message' => "Bonnie API request failed: " . $e->getMessage(),
+ ), true);
+
+ return array('id' => $payload['id'], 'error' => $e->getMessage(), 'code' => -32000);
+ }
+
+ return is_array($result) ? $result : array();
+ }
+
+ /**
+ * Compute the hmac signature for the current event payload using
+ * the secret key configured for this API client
+ *
+ * @param string $data The request payload data
+ * @return string The request signature
+ */
+ protected function request_signature($data)
+ {
+ // TODO: get the session key with a system.keygen call
+ return hash_hmac('sha256', $this->headers['X-Request-User'] . ':' . $data, $this->secret);
+ }
+
+ /**
+ * Write debug log
+ */
+ protected function _debug(/* $message, $data1, data2, ...*/)
+ {
+ if (!$this->debug)
+ return;
+
+ $args = func_get_args();
+
+ $msg = array();
+ foreach ($args as $arg) {
+ $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
+ }
+
+ rcube::write_log('bonnie', join(";\n", $msg));
+ }
+
+}
\ No newline at end of file
commit 04718ed0ef44892efa5c0b135d7a3042fa496b48
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon Jul 28 11:44:26 2014 +0200
Use generic date conversion method from Rondcube utils
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5a1a8b0..9c507a3 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -145,7 +145,7 @@ class libcalendaring extends rcube_plugin
if (is_numeric($dt))
$dt = new DateTime('@'.$dt);
else if (is_string($dt))
- $dt = new DateTime($dt);
+ $dt = rcube_utils::anytodatetime($dt);
if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
$dt->setTimezone($this->timezone);
More information about the commits
mailing list