diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js index b9402501..fb280b1d 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.js +++ b/plugins/kolab_addressbook/kolab_addressbook.js @@ -126,6 +126,17 @@ if (window.rcmail) { rcmail.display_message(rcmail.gettext('noaddressbooksfound','kolab_addressbook'), 'info'); }); } + + // append button to show contact audit trail + if (rcmail.env.action == 'show' && rcmail.env.kolab_audit_trail && rcmail.env.cid) { + $('' + rcmail.get_label('kolab_addressbook.showhistory') + '') + .click(function(e) { + var rc = rcmail.is_framed() && parent.rcmail.contact_history_dialog ? parent.rcmail : rcmail; + rc.contact_history_dialog(); + return false; + }) + .appendTo($('
').addClass('formbuttons-secondary-kolab').appendTo('.formbuttons')); + } }); rcmail.addEventListener('listupdate', function() { @@ -139,7 +150,6 @@ if (window.rcmail) { source = rcmail.env.source ? rcmail.env.address_sources[rcmail.env.source] : null; if (selected && source.kolab) { - console.log('select', source.rights) rcmail.enable_command('delete', 'move', selected && source.rights.indexOf('t') >= 0); } }); @@ -150,7 +160,7 @@ if (window.rcmail) { rcube_webmail.prototype.set_book_actions = function() { var source = !this.env.group ? this.env.source : null, - sources = this.env.address_sources; + sources = this.env.address_sources || {}; var props = source && sources[source] && sources[source].kolab ? sources[source] : { removable: false, rights: '' } this.enable_command('book-create', true); @@ -344,6 +354,176 @@ rcube_webmail.prototype.book_realname = function() return source != '' && sources[source] && sources[source].realname ? sources[source].realname : ''; }; +// open dialog to show the current contact's changelog +rcube_webmail.prototype.contact_history_dialog = function() +{ + var $dialog, rec = { cid: this.get_single_cid(), source: rcmail.env.source }, + source = this.env.address_sources ? this.env.address_sources[rcmail.env.source] || {} : {}; + + if (!rec.cid || !window.libkolab_audittrail || !source.audittrail) { + return false; + } + + // render dialog + $dialog = libkolab_audittrail.object_history_dialog({ + module: 'kolab_addressbooks', + container: '#contacthistory', + title: rcmail.gettext('objectchangelog','kolab_addressbook'), + + // callback function for list actions + listfunc: function(action, rev) { + var rec = $dialog.data('rec'); + console.log(action, rev, rec) + //rcmail.loading_lock = rcmail.set_busy(true, 'loading', this.loading_lock); + //rcmail.http_post('action', { _do: action, _data: { uid: rec.uid, list:rec.list, rev: rev } }, saving_lock); + }, + + // callback function for comparing two object revisions + comparefunc: function(rev1, rev2) { + var rec = $dialog.data('rec'); + rcmail.kab_loading_lock = rcmail.set_busy(true, 'loading', rcmail.kab_loading_lock); + rcmail.http_post('plugin.contact-diff', { cid: rec.cid, source: rec.source, rev1: rev1, rev2: rev2 }, rcmail.kab_loading_lock); + } + }); + + $dialog.data('rec', rec); + + // fetch changelog data + this.kab_loading_lock = rcmail.set_busy(true, 'loading', this.kab_loading_lock); + this.http_post('plugin.contact-changelog', rec, this.kab_loading_lock); +}; + +// callback for displaying a contact's change history +rcube_webmail.prototype.contact_render_changelog = function(data) +{ + var $dialog = $('#contacthistory'), + rec = $dialog.data('rec'); + + if (data === false || !data.length || !event) { + // display 'unavailable' message + $('
' + rcmail.gettext('objectchangelognotavailable','kolab_addressbook') + '
') + .insertBefore($dialog.find('.changelog-table').hide()); + return; + } + + source = this.env.address_sources[rec.source] || {} + // source.editable = !source.readonly + + data.module = 'kolab_addressbook'; + libkolab_audittrail.render_changelog(data, rec, source); + + // set dialog size according to content + // dialog_resize($dialog.get(0), $dialog.height(), 600); +}; + +// callback for rendering a diff view of two contact revisions +rcube_webmail.prototype.contact_show_diff = function(data) +{ + var $dialog = $('#contactdiff'), + rec = {}, namediff = { 'old': '', 'new': '', 'set': false }; + + if (this.contact_list && this.contact_list.data[data.cid]) { + rec = this.contact_list.data[data.cid]; + } + + $dialog.find('div.form-section, h2.contact-names-new').hide().data('set', false); + $dialog.find('div.form-section.clone').remove(); + + var name_props = ['prefix','firstname','middlename','surname','suffix']; + + // Quote HTML entities + var Q = function(str){ + return String(str).replace(//g, '>').replace(/"/g, '"'); + }; + + // show each property change + $.each(data.changes, function(i, change) { + var prop = change.property, r2, html = !!change.ishtml, + row = $('div.contact-' + prop, $dialog).first(); + + // special case: names + if ($.inArray(prop, name_props) >= 0) { + namediff['old'] += change['old'] + ' '; + namediff['new'] += change['new'] + ' '; + namediff['set'] = true; + return true; + } + + // 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; + } + + // render photo as image with data: url + if (prop == 'photo') { + row.children('.diff-img-old').attr('src', change['old'] ? 'data:' + (change['old'].mimetype || 'image/gif') + ';base64,' + change['old'].base64 : 'data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7'); + row.children('.diff-img-new').attr('src', change['new'] ? 'data:' + (change['new'].mimetype || 'image/gif') + ';base64,' + change['new'].base64 : 'data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7'); + } + else if (change.diff_) { + row.children('.diff-text-diff').html(change.diff_); + row.children('.diff-text-old, .diff-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('.diff-text-old').html(change.old_ || change['old'] || '--').show(); + row.children('.diff-text-new').html(change.new_ || change['new'] || '--').show(); + } + + // display index number + if (typeof change.index != 'undefined') { + row.find('.index').html('(' + change.index + ')'); + } + + row.show().data('set', true); + }); + + // always show name + if (namediff.set) { + $('.contact-names', $dialog).html($.trim(namediff['old'] || '--')).addClass('diff-text-old').show(); + $('.contact-names-new', $dialog).html($.trim(namediff['new'] || '--')).show(); + } + else { + $('.contact-names', $dialog).text(rec.name).removeClass('diff-text-old').show(); + } + + // open jquery UI dialog + $dialog.dialog({ + modal: false, + resizable: true, + closeOnEscape: true, + title: rcmail.gettext('objectdiff','kolab_addressbook').replace('$rev1', data.rev1).replace('$rev2', data.rev2), + 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: 400, + width: 480 + }).show(); + + // set dialog size according to content + // dialog_resize($dialog.get(0), $dialog.height(), rcmail.gui_containers.notedetailview.width() - 40); +}; + + function kolab_addressbook_contextmenu() { if (!window.rcm_callbackmenu_init) { diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php index 6a2aaa6e..35908bbf 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.php +++ b/plugins/kolab_addressbook/kolab_addressbook.php @@ -11,7 +11,7 @@ * @author Thomas Bruederli * @author Aleksander Machniak * - * Copyright (C) 2011, Kolab Systems AG + * Copyright (C) 2011-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 @@ -36,6 +36,8 @@ class kolab_addressbook extends rcube_plugin private $rc; private $ui; + public $bonnie_api = false; + const GLOBAL_FIRST = 0; const PERSONAL_FIRST = 1; const GLOBAL_ONLY = 2; @@ -69,6 +71,15 @@ class kolab_addressbook extends rcube_plugin $this->register_action('plugin.book-search', array($this, 'book_search')); $this->register_action('plugin.book-subscribe', array($this, 'book_subscribe')); + $this->register_action('plugin.contact-changelog', array($this, 'contact_changelog')); + $this->register_action('plugin.contact-diff', array($this, 'contact_diff')); + $this->register_action('plugin.contact-show', array($this, 'contact_show')); + + // get configuration for the Bonnie API + if ($bonnie_config = $this->rc->config->get('kolab_bonnie_api', false)) { + $this->bonnie_api = new kolab_bonnie_api($bonnie_config); + } + // Load UI elements if ($this->api->output->type == 'html') { $this->load_config(); @@ -168,8 +179,9 @@ class kolab_addressbook extends rcube_plugin 'group' => $abook->get_namespace(), 'subscribed' => $abook->is_subscribed(), 'carddavurl' => $abook->get_carddav_url(), - 'removable' => true, - 'kolab' => true, + 'removable' => true, + 'kolab' => true, + 'audittrail' => !empty($this->bonnie_api), ); } } @@ -497,10 +509,213 @@ class kolab_addressbook extends rcube_plugin */ } + if ($this->bonnie_api && $this->rc->action == 'show') { + $this->rc->output->set_env('kolab_audit_trail', true); + } + return $p; } + /** + * Handler for contact audit trail changelog requests + */ + public function contact_changelog() + { + if (empty($this->bonnie_api)) { + return false; + } + $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); + $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); + + list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source); + + $result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null; + if (is_array($result) && $result['uid'] == $uid) { + if (is_array($result['changes'])) { + $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); + array_walk($result['changes'], function(&$change) use ($dtformat) { + if ($change['date']) { + $dt = rcube_utils::anytodatetime($change['date']); + if ($dt instanceof DateTime) { + $change['date'] = $this->rc->format_date($dt, $dtformat); + } + } + }); + } + $this->rc->output->command('contact_render_changelog', $result['changes']); + } + else { + $this->rc->output->command('contact_render_changelog', false); + } + + $this->rc->output->send(); + } + + /** + * Handler for audit trail diff view requests + */ + public function contact_diff() + { + if (empty($this->bonnie_api)) { + return false; + } + + $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); + $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); + $rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST); + $rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST); + + list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source); + + $result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid); + if (is_array($result) && $result['uid'] == $uid) { + $result['rev1'] = $rev1; + $result['rev2'] = $rev2; + $result['cid'] = $contact; + + // convert some properties, similar to rcube_kolab_contacts::_to_rcube_contact() + $keymap = array( + 'lastmodified-date' => 'changed', + 'additional' => 'middlename', + 'fn' => 'name', + 'tel' => 'phone', + 'url' => 'website', + 'bday' => 'birthday', + 'note' => 'notes', + 'role' => 'profession', + 'title' => 'jobtitle', + ); + + $propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number'); + $date_format = $this->rc->config->get('date_format', 'Y-m-d'); + + // map kolab object properties to keys and values the client expects + array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) { + if (array_key_exists($change['property'], $keymap)) { + $change['property'] = $keymap[$change['property']]; + } + + // format date-time values + if ($change['property'] == 'created' || $change['property'] == 'changed') { + if ($old_ = rcube_utils::anytodatetime($change['old'])) { + $change['old_'] = $this->rc->format_date($old_); + } + if ($new_ = rcube_utils::anytodatetime($change['new'])) { + $change['new_'] = $this->rc->format_date($new_); + } + } + // format dates + else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') { + if ($old_ = rcube_utils::anytodatetime($change['old'])) { + $change['old_'] = $this->rc->format_date($old_, $date_format); + } + if ($new_ = rcube_utils::anytodatetime($change['new'])) { + $change['new_'] = $this->rc->format_date($new_, $date_format); + } + } + // convert email, website, phone values + else if (array_key_exists($change['property'], $propmap)) { + $propname = $propmap[$change['property']]; + foreach (array('old','new') as $k) { + $k_ = $k . '_'; + if (!empty($change[$k])) { + $change[$k_] = html::quote($change[$k][$propname] ?: '--'); + if ($change[$k]['type']) { + $change[$k_] .= ' ' . html::span('subtype', rcmail_get_type_label($change[$k]['type'])); + } + $change['ishtml'] = true; + } + } + } + // serialize address structs + if ($change['property'] == 'address') { + foreach (array('old','new') as $k) { + $k_ = $k . '_'; + $change[$k]['zipcode'] = $change[$k]['code']; + $template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}'); + $composite = array(); + foreach ($change[$k] as $p => $val) { + if (strlen($val)) + $composite['{'.$p.'}'] = $val; + } + $change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite)); + if ($change[$k]['type']) { + $change[$k_] .= html::div('subtype', rcmail_get_type_label($change[$k]['type'])); + } + $change['ishtml'] = true; + } + + $change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true); + } + // localize gender values + else if ($change['property'] == 'gender') { + if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']); + if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']); + } + // translate 'key' entries in individual properties + else if ($change['property'] == 'key') { + $p = $change['old'] ?: $change['new']; + $t = $p['type']; + $change['property'] = $t . 'publickey'; + $change['old'] = $change['old'] ? $change['old']['key'] : ''; + $change['new'] = $change['new'] ? $change['new']['key'] : ''; + } + // compute a nice diff of notes + else if ($change['property'] == 'notes') { + $change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false); + } + }); + + $this->rc->output->command('contact_show_diff', $result); + } + else { + $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); + } + + $this->rc->output->send(); + } + + /** + * Handler for audit trail revision view requests + */ + public function contact_show() + { + + $this->rc->output->send(); + } + + + /** + * Helper method to resolved the given contact identifier into uid and mailbox + * + * @return array (uid,mailbox,msguid) tuple + */ + private function _resolve_contact_identity($id, $abook) + { + $mailbox = $msguid = null; + + $source = $this->get_address_book(array('id' => $abook)); + if ($source['instance']) { + $uid = $source['instance']->id2uid($id); + $list = kolab_storage::id_decode($abook); + } + else { + return array(null, $mailbox, $msguid); + } + + // get resolve message UID and mailbox identifier + if ($folder = kolab_storage::get_folder($list)) { + $mailbox = $folder->get_mailbox_id(); + $msguid = $folder->cache->uid2msguid($uid); + } + + return array($uid, $mailbox, $msguid); + } + + /** + * + */ private function _sort_form_fields($contents, $source) { $block = array(); diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php index 65476488..e20cf14d 100644 --- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php +++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php @@ -43,7 +43,7 @@ class kolab_addressbook_ui */ private function init_ui() { - if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action)) { + if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action) && $this->rc->action != 'show') { return; } @@ -105,6 +105,33 @@ class kolab_addressbook_ui 'kolab_addressbook.noaddressbooksfound', 'kolab_addressbook.foldersubscribe', 'resetsearch'); + + + if ($this->plugin->bonnie_api) { + $this->plugin->api->include_script('libkolab/js/audittrail.js'); + + $this->rc->output->add_label( + 'kolab_addressbook.showhistory', + 'kolab_addressbook.compare', + 'kolab_addressbook.objectchangelog', + 'kolab_addressbook.objectdiff', + 'kolab_addressbook.showrevision', + 'kolab_addressbook.actionappend', + 'kolab_addressbook.actionmove', + 'kolab_addressbook.actiondelete', + 'kolab_addressbook.objectdiffnotavailable', + 'kolab_addressbook.objectchangelognotavailable', + 'close' + ); + + $this->plugin->add_hook('render_page', array($this, 'render_audittrail_page')); + $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); + } + } + // include stylesheet for audit trail + else if ($this->rc->action == 'show' && $this->plugin->bonnie_api) { + $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); + $this->rc->output->add_label('kolab_addressbook.showhistory'); } // book create/edit form else { @@ -247,6 +274,20 @@ class kolab_addressbook_ui return $out; } + /** + * + */ + public function render_audittrail_page($p) + { + // append audit trail UI elements to contact page + if ($p['template'] === 'addressbook' && !$p['kolab-audittrail']) { + $this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.audittrail', false, false)); + $p['kolab-audittrail'] = true; + } + + return $p; + } + private function get_form_part($form) { diff --git a/plugins/kolab_addressbook/localization/en_US.inc b/plugins/kolab_addressbook/localization/en_US.inc index b636d4d5..a65f46c9 100644 --- a/plugins/kolab_addressbook/localization/en_US.inc +++ b/plugins/kolab_addressbook/localization/en_US.inc @@ -51,6 +51,20 @@ $labels['foldersubscribe'] = 'List permanently'; $labels['nraddressbooksfound'] = '$nr address books found'; $labels['noaddressbooksfound'] = 'No address books found'; +// history dialog +$labels['showhistory'] = 'Show History'; +$labels['compare'] = 'Compare'; +$labels['objectchangelog'] = 'Change History'; +$labels['objectdiff'] = 'Changes from $rev1 to $rev2'; +$labels['actionappend'] = 'Saved'; +$labels['actionmove'] = 'Moved'; +$labels['actiondelete'] = 'Deleted'; +$labels['showrevision'] = 'Show this version'; +$labels['restore'] = 'Restore this version'; +$labels['objectnotfound'] = 'Failed to load contact data'; +$labels['objectchangelognotavailable'] = 'Change history is not available for this contact'; +$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions'; + $messages['bookdeleteconfirm'] = 'Do you really want to delete the selected address book and all contacts in it?'; $messages['bookdeleting'] = 'Deleting address book...'; $messages['booksaving'] = 'Saving address book...'; diff --git a/plugins/kolab_addressbook/skins/classic/templates/audittrail.html b/plugins/kolab_addressbook/skins/classic/templates/audittrail.html new file mode 100644 index 00000000..e69de29b diff --git a/plugins/kolab_addressbook/skins/larry/folder_icons.png b/plugins/kolab_addressbook/skins/larry/folder_icons.png index b9c59b5a..19b8cfab 100644 Binary files a/plugins/kolab_addressbook/skins/larry/folder_icons.png and b/plugins/kolab_addressbook/skins/larry/folder_icons.png differ diff --git a/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css index 5484807e..2d2d8795 100644 --- a/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css +++ b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css @@ -139,3 +139,108 @@ #directorylist a.contextRow { background-color: #C7E3EF; } + +#contactdiff, +#contacthistory { + display: none; +} + +.formbuttons .formbuttons-secondary-kolab { + display: block; + margin: 2px 2px 0 0; + text-align: center; +} + +.formbuttons .btn-contact-history { + display: inline-block; + padding: 1px; + color: #333; + text-decoration: none; +} + +.formbuttons .btn-contact-history:hover { + text-decoration: underline; +} + +.formbuttons .btn-contact-history:before { + content: ""; + display: inline-block; + position: relative; + top: 5px; + width: 16px; + height: 16px; + margin-right: 3px; + background: url('folder_icons.png') 0px -200px no-repeat; +} + +#contactdiff .contact-names, +#contactdiff .contact-names-new { + margin-top: 0; +} + +#contactdiff .contact-names.diff-text-old { + margin-bottom: 0; +} + +#contactdiff .diff-text-diff del, +#contactdiff .diff-text-diff ins { + text-decoration: none; + color: inherit; +} + +#contactdiff .diff-img-old, +#contactdiff .diff-text-old, +#contactdiff .diff-text-diff del { + background-color: #fdd; + text-decoration: line-through; +} + +#contactdiff .diff-text-new, +#contactdiff .diff-img-new, +#contactdiff .diff-text-diff ins, +#contactdiff .diff-text-diff .diffmod img { + background-color: #dfd; +} + +#contactdiff .diff-img-old, +#contactdiff .diff-img-new { + min-width: 48px; + max-width: 112px; +} + +#contactdiff label { + color: #666; + font-weight: bold; +} + +#contactdiff label span.index { + vertical-align: inherit; + margin-left: 0.6em; + font-weight: normal; +} + +#contactdiff .contact-name { + font-size: 120%; + font-weight: bold; +} + +#contactdiff .subtype { + color: #666; +} + +#contactdiff span.subtype { + margin-left: 0.5em; +} + +#contactdiff div.subtype { + margin-top: 0.2em; +} + +#contactdiff .subtype:before { + content: "("; +} + +#contactdiff .subtype:after { + content: ")"; +} + diff --git a/plugins/kolab_addressbook/skins/larry/templates/audittrail.html b/plugins/kolab_addressbook/skins/larry/templates/audittrail.html new file mode 100644 index 00000000..9411b03a --- /dev/null +++ b/plugins/kolab_addressbook/skins/larry/templates/audittrail.html @@ -0,0 +1,144 @@ + + + \ No newline at end of file diff --git a/plugins/libkolab/js/audittrail.js b/plugins/libkolab/js/audittrail.js index 42cdbf0c..87340b3f 100644 --- a/plugins/libkolab/js/audittrail.js +++ b/plugins/libkolab/js/audittrail.js @@ -70,7 +70,7 @@ libkolab_audittrail.object_history_dialog = function(p) minWidth: 450, width: 650, height: 350, - minHeight: 200, + minHeight: 200 }) .show().children('.compare-button').hide();