diff --git a/plugins/libkolab/js/audittrail.js b/plugins/libkolab/js/audittrail.js index 641fa23e..b680114e 100644 --- a/plugins/libkolab/js/audittrail.js +++ b/plugins/libkolab/js/audittrail.js @@ -1,212 +1,261 @@ /** * Kolab groupware audit trail utilities * * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this file. * * Copyright (C) 2015, Kolab Systems AG * * 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 . * * @licend The above is the entire license notice * for the JavaScript code in this file. */ var libkolab_audittrail = {} libkolab_audittrail.quote_html = function(str) { return String(str).replace(//g, '>').replace(/"/g, '"'); }; // show object changelog in a dialog libkolab_audittrail.object_history_dialog = function(p) { // render dialog var $dialog = $(p.container); // close show dialog first if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); // hide and reset changelog table $dialog.find('div.notfound-message').remove(); $dialog.find('.changelog-table').show().children('tbody') - .html('' + rcmail.gettext('loading') + ''); + .html('' + rcmail.gettext('loading') + ''); // open jquery UI dialog $dialog.dialog({ modal: false, resizable: true, closeOnEscape: true, title: p.title, open: function() { $dialog.attr('aria-hidden', 'false'); }, close: function() { $dialog.dialog('destroy').attr('aria-hidden', 'true').hide(); }, buttons: [ { text: rcmail.gettext('close'), click: function() { $dialog.dialog('close'); }, autofocus: true } ], minWidth: 450, width: 650, height: 350, minHeight: 200 }) .show().children('.compare-button').hide(); // initialize event handlers for history dialog UI elements if (!$dialog.data('initialized')) { // compare button $dialog.find('.compare-button input').click(function(e) { var rev1 = $dialog.find('.changelog-table input.diff-rev1:checked').val(), rev2 = $dialog.find('.changelog-table input.diff-rev2:checked').val(); if (rev1 && rev2 && rev1 != rev2) { // swap revisions if the user got it wrong if (rev1 > rev2) { var tmp = rev2; rev2 = rev1; rev1 = tmp; } if (p.comparefunc) { p.comparefunc(rev1, rev2); } } else { alert('Invalid selection!') } if (!rcube_event.is_keyboard(e) && this.blur) { this.blur(); } return false; }); // delegate handlers for list actions $dialog.find('.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('revisionrestoreconfirm', p.module).replace('$rev', rev))) { return false; } if (p.listfunc) { p.listfunc(action, rev); } if (!rcube_event.is_keyboard(e) && this.blur) { this.blur(); } return false; }) .on('click', 'input.diff-rev1', function(e) { if (!this.checked) return true; var rev1 = this.value, selection_valid = false; $dialog.find('.changelog-table input.diff-rev2').each(function(i, elem) { $(elem).prop('disabled', elem.value <= rev1); if (elem.checked && elem.value > rev1) { selection_valid = true; } }); if (!selection_valid) { $dialog.find('.changelog-table input.diff-rev2:not([disabled])').last().prop('checked', true); } }); $dialog.addClass('changelog-dialog').data('initialized', true); } return $dialog; }; // callback from server with changelog data libkolab_audittrail.render_changelog = function(data, object, folder) { var Q = libkolab_audittrail.quote_html; var $dialog = $('.changelog-dialog') if (data === false || !data.length) { return false; } var i, change, accessible, op_append, first = data.length - 1, last = 0, is_writeable = !!folder.editable, - op_labels = { APPEND: 'actionappend', MOVE: 'actionmove', DELETE: 'actiondelete' }, + op_labels = { RECEIVE: 'actionreceive', APPEND: 'actionappend', MOVE: 'actionmove', DELETE: 'actiondelete', READ: 'actionread', FLAGSET: 'actionflagset', FLAGCLEAR: 'actionflagclear' }, actions = ' ' + (is_writeable ? '' : ''), tbody = $dialog.find('.changelog-table tbody').html(''); for (i=first; i >= 0; i--) { change = data[i]; accessible = change.date && change.user; if (change.op == 'MOVE' && change.mailbox) { op_append = ' ⇢ ' + change.mailbox; } + else if ((change.op == 'FLAGSET' || change.op == 'FLAGCLEAR') && change.flags) { + op_append = ': ' + change.flags; + } else { op_append = ''; } $('') .append('' + (accessible && change.op != 'DELETE' ? ' '+ '' : '')) .append('' + Q(i+1) + '') .append('' + Q(change.date || '') + '') .append('' + Q(change.user || 'undisclosed') + '') - .append('' + Q(rcmail.gettext(op_labels[change.op] || '', data.module) + (op_append ? ' ...' : '')) + '') + .append('' + Q(rcmail.gettext(op_labels[change.op] || '', data.module) + op_append) + '') .append('' + (accessible && change.op != 'DELETE' ? actions.replace(/\{rev\}/g, change.rev) : '') + '') .appendTo(tbody); } if (first > 0) { $dialog.find('.compare-button').fadeIn(200); $dialog.find('.changelog-table tr.last input.diff-rev1').click(); } // set dialog size according to content libkolab_audittrail.dialog_resize($dialog.get(0), $dialog.height() + 15, 600); return $dialog; }; // resize and reposition (center) the dialog window libkolab_audittrail.dialog_resize = function(id, height, width) { var win = $(window), w = win.width(), h = win.height(); $(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) }) .dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?) }; + + +// register handlers for mail message history +window.rcmail && rcmail.addEventListener('init', function(e) { + var loading_lock; + + if (rcmail.env.task == 'mail') { + rcmail.register_command('kolab-mail-history', function() { + var dialog, uid = rcmail.get_single_uid(), rec = { uid: uid, mbox: rcmail.get_message_mailbox(uid) }; + if (!uid || !window.libkolab_audittrail) { + return false; + } + + // render dialog + $dialog = libkolab_audittrail.object_history_dialog({ + module: 'libkolab', + container: '#mailmessagehistory', + title: rcmail.gettext('objectchangelog','libkolab') + }); + + $dialog.data('rec', rec); + + // fetch changelog data + loading_lock = rcmail.set_busy(true, 'loading', loading_lock); + rcmail.http_post('plugin.message-changelog', { _uid: rec.uid, _mbox: rec.mbox }, loading_lock); + + }, rcmail.env.action == 'show'); + + rcmail.addEventListener('plugin.message_render_changelog', function(data) { + var $dialog = $('#mailmessagehistory'), + rec = $dialog.data('rec'); + + if (data === false || !data.length || !event) { + // display 'unavailable' message + $('
' + rcmail.gettext('objectchangelognotavailable','libkolab') + '
') + .insertBefore($dialog.find('.changelog-table').hide()); + return; + } + + data.module = 'libkolab'; + libkolab_audittrail.render_changelog(data, rec, {}); + }); + + rcmail.env.message_commands.push('kolab-mail-history'); + } +}); diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php index 4abb288f..1dede0d3 100644 --- a/plugins/libkolab/libkolab.php +++ b/plugins/libkolab/libkolab.php @@ -1,206 +1,303 @@ * * Copyright (C) 2012-2015, Kolab Systems AG * * 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 . */ class libkolab extends rcube_plugin { static $http_requests = array(); + static $bonnie_api = false; /** * Required startup method of a Roundcube plugin */ public function init() { // load local config $this->load_config(); // extend include path to load bundled lib classes $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path'); set_include_path($include_path); $this->add_hook('storage_init', array($this, 'storage_init')); $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders')); $rcmail = rcube::get_instance(); try { kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT')); } catch (Exception $e) { rcube::raise_error($e, true); kolab_format::$timezone = new DateTimeZone('GMT'); } + + $this->add_texts('localization/', $rcmail->output->type == 'html' && $rcmail->task == 'mail'); + + // embed scripts and templates for email message audit trail + if ($rcmail->task == 'mail' && self::get_bonnie_api()) { + if ($rcmail->output->type == 'html') { + $this->add_hook('render_page', array($this, 'bonnie_render_page')); + + $this->include_script('js/audittrail.js'); + $this->include_stylesheet($this->local_skin_path() . '/libkolab.css'); + + // add 'Show history' item to message menu + $this->api->add_content(html::tag('li', null, + $this->api->output->button(array( + 'command' => 'kolab-mail-history', + 'label' => 'libkolab.showhistory', + 'type' => 'link', + 'classact' => 'icon history active', + 'class' => 'icon history', + 'innerclass' => 'icon history', + ))), + 'messagemenu'); + } + + $this->register_action('plugin.message-changelog', array($this, 'message_changelog')); + } } /** * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers */ function storage_init($p) { $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION'); return $p; } + /** + * Getter for a singleton instance of the Bonnie API + * + * @return mixed kolab_bonnie_api instance if configured, false otherwise + */ + public static function get_bonnie_api() + { + // get configuration for the Bonnie API + if (!self::$bonnie_api && ($bonnie_config = rcube::get_instance()->config->get('kolab_bonnie_api', false))) { + self::$bonnie_api = new kolab_bonnie_api($bonnie_config); + } + + return self::$bonnie_api; + } + + /** + * Hook to append the message history dialog template to the mail view + */ + function bonnie_render_page($p) + { + if (($p['template'] === 'mail' || $p['template'] === 'message') && !$p['kolab-audittrail']) { + // append a template for the audit trail dialog + $this->api->output->add_footer( + html::div(array('id' => 'mailmessagehistory', 'class' => 'uidialog', 'aria-hidden' => 'true', 'style' => 'display:none'), + self::object_changelog_table(array('class' => 'records-table changelog-table')) + ) + ); + $this->api->output->set_env('kolab_audit_trail', true); + $p['kolab-audittrail'] = true; + } + + return $p; + } + + /** + * Handler for message audit trail changelog requests + */ + public function message_changelog() + { + if (!self::$bonnie_api) { + return false; + } + + $rcmail = rcube::get_instance(); + $msguid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST, true); + $mailbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); + + $result = $msguid && $mailbox ? self::$bonnie_api->changelog('mail', null, $mailbox, $msguid) : null; + if (is_array($result)) { + if (is_array($result['changes'])) { + $dtformat = $rcmail->config->get('date_format') . ' ' . $rcmail->config->get('time_format'); + array_walk($result['changes'], function(&$change) use ($dtformat, $rcmail) { + if ($change['date']) { + $dt = rcube_utils::anytodatetime($change['date']); + if ($dt instanceof DateTime) { + $change['date'] = $rcmail->format_date($dt, $dtformat); + } + } + }); + } + $this->api->output->command('plugin.message_render_changelog', $result['changes']); + } + else { + $this->api->output->command('plugin.message_render_changelog', false); + } + + $this->api->output->send(); + } + /** * Wrapper function to load and initalize the HTTP_Request2 Object * * @param string|Net_Url2 Request URL * @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT') * @param array Configuration for this Request instance, that will be merged * with default configuration * * @return HTTP_Request2 Request object */ public static function http_request($url = '', $method = 'GET', $config = array()) { $rcube = rcube::get_instance(); $http_config = (array) $rcube->config->get('kolab_http_request'); // deprecated configuration options if (empty($http_config)) { foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) { $value = $rcube->config->get('kolab_' . $option, true); if (is_bool($value)) { $http_config[$option] = $value; } } } if (!empty($config)) { $http_config = array_merge($http_config, $config); } // force CURL adapter, this allows to handle correctly // compressed responses with SplObserver registered (kolab_files) (#4507) $http_config['adapter'] = 'HTTP_Request2_Adapter_Curl'; $key = md5(serialize($http_config)); if (!($request = self::$http_requests[$key])) { // load HTTP_Request2 require_once 'HTTP/Request2.php'; try { $request = new HTTP_Request2(); $request->setConfig($http_config); } catch (Exception $e) { rcube::raise_error($e, true, true); } // proxy User-Agent string $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']); self::$http_requests[$key] = $request; } // cleanup try { $request->setBody(''); $request->setUrl($url); $request->setMethod($method); } catch (Exception $e) { rcube::raise_error($e, true, true); } return $request; } /** * Table oultine for object changelog display */ public static function object_changelog_table($attrib = array()) { $rcube = rcube::get_instance(); + $attrib += array('domain' => 'libkolab'); $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0)); $table->add_header('diff', ''); $table->add_header('revision', $rcube->gettext('revision', $attrib['domain'])); $table->add_header('date', $rcube->gettext('date', $attrib['domain'])); $table->add_header('user', $rcube->gettext('user', $attrib['domain'])); $table->add_header('operation', $rcube->gettext('operation', $attrib['domain'])); $table->add_header('actions', ' '); return $table->show($attrib); } /** * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill */ public static function html_diff($from, $to, $is_html = null) { // auto-detect text/html format if ($is_html === null) { $from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '') > 0); $to_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '') > 0); $is_html = $from_html || $to_html; // ensure both parts are of the same format if ($is_html && !$from_html) { $converter = new rcube_text2html($from, false, array('wrap' => true)); $from = $converter->get_html(); } if ($is_html && !$to_html) { $converter = new rcube_text2html($to, false, array('wrap' => true)); $to = $converter->get_html(); } } // compute diff from HTML if ($is_html) { include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php'; include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php'; include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php'; // replace data: urls with a transparent image to avoid memory problems $from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from); $to = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to); $diff = new Caxy\HtmlDiff\HtmlDiff($from, $to); $diffhtml = $diff->build(); // remove empty inserts (from tables) return preg_replace('!\s*!Uims', '', $diffhtml); } else { include_once __dir__ . '/vendor/finediff.php'; $diff = new FineDiff($from, $to, FineDiff::$wordGranularity); return $diff->renderDiffToHTML(); } } /** * Return a date() format string to render identifiers for recurrence instances * * @param array Hash array with event properties * @return string Format string */ public static function recurrence_id_format($event) { return $event['allday'] ? 'Ymd' : 'Ymd\THis'; } } diff --git a/plugins/libkolab/localization/en_US.inc b/plugins/libkolab/localization/en_US.inc new file mode 100644 index 00000000..c74d0040 --- /dev/null +++ b/plugins/libkolab/localization/en_US.inc @@ -0,0 +1,32 @@ +