+ // TODO: This might need to be configurable or discovered somehow
+ $path = '/principals/user/';
+ if ($host_path = parse_url($this->dav->url, PHP_URL_PATH)) {
+ $path = '/' . trim($host_path, '/') . $path;
+ }
+
+ $request = [];
+
+ foreach ($sharees as $principal => $sharee) {
+ // Convert a user identifier into a principal href or mailto: href
+ if (strpos($principal, '@')) {
+ $principal = 'mailto:' . $principal;
+ } else {
+ $principal = $path . $principal;
+ }
+
+ $request[$principal] = $sharee;
+ }
+
+ return $this->dav->shareResource($this->href, $request);
+ }
+
/**
* Return folder name as string representation of this object
*
diff --git a/plugins/libkolab/lib/kolab_utils.php b/plugins/libkolab/lib/kolab_utils.php
--- a/plugins/libkolab/lib/kolab_utils.php
+++ b/plugins/libkolab/lib/kolab_utils.php
@@ -27,14 +27,17 @@
{
public static function folder_form($form, $folder, $domain, $hidden_fields = [], $no_acl = false)
{
- $rcmail = rcube::get_instance();
+ $rcmail = rcmail::get_instance();
// add folder ACL tab
- if (!$no_acl && is_string($folder) && strlen($folder)) {
- $form['sharing'] = [
- 'name' => rcube::Q($rcmail->gettext('libkolab.tabsharing')),
- 'content' => self::folder_acl_form($folder),
- ];
+ if (!$no_acl && $folder) {
+ if (($folder instanceof kolab_storage_dav_folder) || (is_string($folder) && strlen($folder))) {
+ $sharing_content = self::folder_acl_form($folder);
+ $form['sharing'] = [
+ 'name' => rcube::Q($rcmail->gettext('libkolab.tabsharing')),
+ 'content' => $sharing_content,
+ ];
+ }
}
$form_html = '';
@@ -71,10 +74,18 @@
/**
* Handler for ACL form template object
+ *
+ * @param string|kolab_storage_dav_folder $folder DAV folder object or IMAP folder name
+ *
+ * @return ?string HTML content
*/
- public static function folder_acl_form($folder)
+ private static function folder_acl_form($folder)
{
- $rcmail = rcube::get_instance();
+ if ($folder instanceof kolab_storage_dav_folder) {
+ return self::folder_dav_acl_form($folder);
+ }
+
+ $rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$options = $storage->folder_info($folder);
@@ -93,4 +104,29 @@
return html::div('hint', $rcmail->gettext('libkolab.aclnorights'));
}
+
+ /**
+ * Handler for DAV ACL form template object
+ *
+ * @param kolab_storage_dav_folder $folder DAV folder object
+ *
+ * @return ?string HTML content
+ */
+ private static function folder_dav_acl_form($folder)
+ {
+ $rcmail = rcmail::get_instance();
+ $sharing = $rcmail->config->get('kolab_dav_sharing');
+
+ if (!$sharing) {
+ return null;
+ }
+
+ $class = 'kolab_dav_' . $sharing;
+
+ if ($form = $class::form($folder)) {
+ return $form;
+ }
+
+ return html::div('hint', rcmail::get_instance()->gettext('libkolab.aclnorights'));
+ }
}
diff --git a/plugins/libkolab/libkolab.js b/plugins/libkolab/libkolab.js
--- a/plugins/libkolab/libkolab.js
+++ b/plugins/libkolab/libkolab.js
@@ -25,7 +25,7 @@
* for the JavaScript code in this file.
*/
-var libkolab_audittrail = {}, libkolab = {};
+var libkolab_audittrail = {}, libkolab = {}, libkolab_invitations = {};
libkolab_audittrail.quote_html = function(str)
{
@@ -306,6 +306,8 @@
// render the results for folderlist search
function render_search_results(results)
{
+ libkolab_invitations = {};
+
if (results.length) {
// create treelist widget to present the search results
if (!search_results_widget) {
@@ -340,17 +342,28 @@
e.stopPropagation();
e.bubbles = false;
+ // Share invitation
+ if (search_results[id] && 'share_invitation' in search_results[id] && search_results[id].share_invitation) {
+ libkolab_invitations[id] = { li: li, list: me };
+ rcmail.http_post(
+ 'plugin.share-invitation',
+ { id: id, invitation: search_results[id].share_invitation, status: 'accepted' },
+ rcmail.set_busy(true, 'libkolab.invitation-accepting')
+ );
+ return false;
+ }
+
// activate + subscribe
if ($(e.target).hasClass('subscribed')) {
search_results[id].subscribed = true;
$(e.target).attr('aria-checked', 'true');
li.children().first()
.toggleClass('subscribed')
- .find('input[type=checkbox]').get(0).checked = true;
+ .find('input[type=checkbox]').prop('checked', true);
if (has_children && search_results[id].group == 'other user') {
li.find('ul li > div').addClass('subscribed')
- .find('a.subscribed').attr('aria-checked', 'true');;
+ .find('a.subscribed').attr('aria-checked', 'true');
}
}
else if (!this.checked) {
@@ -416,6 +429,15 @@
item.find('a.subscribed, span.subscribed').hide();
}
+ // disable click on shared invitations (it will be fixed back in add_result2list)
+ if ('share_invitation' in prop && prop.share_invitation) {
+ var elem = item.find('a.listname').first();
+ if (elem.length) {
+ elem.data('onclick', elem.attr('onclick'))
+ .attr('onclick', 'return false');
+ }
+ }
+
prop.li = item.parent().get(0);
me.triggerEvent('add-item', prop);
}
@@ -427,12 +449,15 @@
// helper method to (recursively) add a search result item to the main list widget
function add_result2list(id, li, active)
{
- var node = search_results_widget.get_node(id),
+ var cl, data,
+ childs = [],
+ node = search_results_widget.get_node(id),
prop = search_results[id],
+ classes = prop.group || '',
parent_id = prop.parent || null,
has_children = node.children && node.children.length,
dom_node = has_children ? li.children().first().clone(true, true) : li.children().first(),
- childs = [];
+ name_node = dom_node.find('a.listname');
// find parent node and insert at the right place
if (parent_id && me.get_node(parent_id)) {
@@ -447,6 +472,41 @@
dom_node.children('span,a').first().html(Q(prop.name));
}
+ if (name_node && (data = name_node.data('onclick'))) {
+ name_node.attr('onclick', data).removeData('onclick');
+ }
+
+ // Handle id change (switch IDs for various elements/properties of the list row)
+ if (prop.id && prop.id != id) {
+ if (cl = dom_node.attr('class')) {
+ dom_node.attr('class', cl.replace(id, prop.id));
+ } else {
+ // for addressbook copy 'class' attribute
+ if (cl = dom_node.parent().attr('class')) {
+ classes += ' ' + cl;
+ }
+ // and remove the checkbox
+ dom_node.children(':not(a)').hide();
+ }
+ dom_node.children('a').each(function() {
+ if (this.id && this.id.includes(id)) {
+ this.id = this.id.replace(id, prop.id);
+ }
+ });
+ dom_node.find('input[type=checkbox]').each(function() {
+ if (this.value == id) {
+ this.value = prop.id;
+ }
+ });
+ if (data) {
+ name_node.attr({
+ onclick: data.replace(id, prop.id),
+ href: name_node.attr('href').replace(id, prop.id),
+ rel: name_node.attr('rel').replace(id, prop.id),
+ });
+ }
+ }
+
// replace virtual node with a real one
if (me.get_node(id)) {
$(me.get_item(id, true)).children().first()
@@ -465,8 +525,8 @@
// move this result item to the main list widget
me.insert({
- id: id,
- classes: [ prop.group || '' ],
+ id: prop.id || id,
+ classes: [ classes ],
virtual: prop.virtual,
html: dom_node,
level: node.level,
@@ -477,7 +537,7 @@
delete prop.html;
prop.active = active;
- me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+ me.triggerEvent('insert-item', { id: prop.id || id, data: prop, item: li });
// register childs, too
if (childs.length) {
@@ -508,6 +568,26 @@
}
}
+ this.accept_invitation = function (id, prop) {
+ var li = libkolab_invitations[id].li;
+
+ search_results[id] = prop;
+
+ if (prop.active) {
+ li.find('input[type=checkbox]').prop('disabled', false).prop('checked', true);
+ }
+
+ if (prop.listname) {
+ li.find('a.calname').text(prop.listname);
+ }
+
+ li.find('a.quickview').show();
+
+ add_result2list(id, li, prop.active)
+
+ delete libkolab_invitations[id];
+ }
+
// do some magic when search is performed on the widget
this.addEventListener('search', function(search) {
// hide search results
@@ -623,6 +703,312 @@
kolab_folderlist.prototype = rcube_treelist_widget.prototype;
}
+// =============== ACL UI ===============
+
+// Display new-entry form
+rcube_webmail.prototype.acl_create = function () {
+ this.acl_init_form();
+};
+
+// Display ACL edit form
+rcube_webmail.prototype.acl_edit = function () {
+ var id = this.acl_list.get_single_selection();
+ if (id) {
+ this.acl_init_form(id);
+ }
+};
+
+// ACL entry delete
+rcube_webmail.prototype.acl_delete = function () {
+ var users = this.acl_get_usernames();
+
+ if (users && users.length) {
+ this.confirm_dialog(this.get_label('libkolab.deleteconfirm'), 'delete', function () {
+ rcmail.http_post('plugin.davacl', {
+ _act: 'delete',
+ _user: users.join(','),
+ _target: rcmail.env.acl_target,
+ }, rcmail.set_busy(true, 'libkolab.deleting'));
+ });
+ }
+};
+
+// Save ACL data
+rcube_webmail.prototype.acl_save = function () {
+ var data, type, rights = [], user = $('#acluser', this.acl_form).val();
+
+ $('#rights :checkbox', this.acl_form).map(function () {
+ if (this.checked) {
+ rights.push(this.value);
+ }
+ });
+
+ if (type = $('input:checked[name=usertype]', this.acl_form).val()) {
+ if (type != 'user') {
+ user = type;
+ }
+ }
+
+ if (!user) {
+ this.alert_dialog(this.get_label('libkolab.nouser'));
+ return;
+ }
+
+ if (!rights.length) {
+ this.alert_dialog(this.get_label('libkolab.norights'));
+ return;
+ }
+
+ data = {
+ _act: 'save',
+ _user: user,
+ _acl: rights.join(','),
+ _target: this.env.acl_target,
+ };
+
+ if (this.acl_id) {
+ data._old = this.acl_id;
+ }
+
+ this.http_post('plugin.davacl', data, this.set_busy(true, 'libkolab.saving'));
+};
+
+// Cancel/Hide the form
+rcube_webmail.prototype.acl_cancel = function () {
+ this.ksearch_blur();
+ this.acl_popup.dialog('close');
+};
+
+// Update data after save (and hide form)
+rcube_webmail.prototype.acl_update = function (o) {
+ // delete old row
+ if (o.old) {
+ this.acl_remove_row(o.old);
+ }
+ // make sure the same ID doesn't exist
+ else if (this.env.acl[o.id]) {
+ this.acl_remove_row(o.id);
+ }
+
+ // add new row
+ this.acl_add_row(o, true);
+ // hide autocomplete popup
+ this.ksearch_blur();
+ // hide form
+ this.acl_popup.dialog('close');
+};
+
+// ACL table initialization
+rcube_webmail.prototype.acl_list_init = function () {
+ this.acl_list = new rcube_list_widget(this.gui_objects.acltable,
+ { multiselect: true, draggable: false, keyboard: true });
+
+ this.acl_list
+ .addEventListener('select', function (list) {
+ rcmail.enable_command('acl-delete', list.get_selection().length > 0);
+ rcmail.enable_command('acl-edit', list.get_selection().length == 1);
+ list.focus();
+ })
+ .addEventListener('dblclick', function (list) {
+ rcmail.acl_edit();
+ })
+ .addEventListener('keypress', function (list) {
+ if (list.key_pressed == list.ENTER_KEY) {
+ rcmail.command('acl-edit');
+ } else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY) {
+ if (!rcmail.acl_form || !rcmail.acl_form.is(':visible')) {
+ rcmail.command('acl-delete');
+ }
+ }
+ })
+ .init();
+};
+
+// Returns names of users in selected rows
+rcube_webmail.prototype.acl_get_usernames = function () {
+ var users = [], n, len, id, row,
+ list = this.acl_list,
+ selection = list.get_selection();
+
+ for (n = 0, len = selection.length; n < len; n++) {
+ if ((row = list.rows[selection[n]]) && (id = $(row.obj).data('userid'))) {
+ users.push(id);
+ }
+ }
+
+ return users;
+};
+
+// Removes ACL table row
+rcube_webmail.prototype.acl_remove_row = function (id) {
+ var list = this.acl_list;
+
+ list.remove_row(id);
+ list.clear_selection();
+
+ // we don't need it anymore (remove id conflict)
+ $('#rcmrow' + id).remove();
+ this.env.acl[id] = null;
+
+ this.enable_command('acl-delete', list.get_selection().length > 0);
+ this.enable_command('acl-edit', list.get_selection().length == 1);
+};
+
+// Adds ACL table row
+rcube_webmail.prototype.acl_add_row = function (o, sel) {
+ var n, len, ids = [], spec = [], id = o.id, list = this.acl_list,
+ table = this.gui_objects.acltable,
+ row = $('thead > tr', table).clone();
+
+ // Update new row
+ $('th', row).map(function () {
+ var td = $(''),
+ title = $(this).attr('title'),
+ cl = this.className.replace(/^acl/, '');
+
+ if (title) {
+ td.attr('title', title);
+ }
+
+ if (cl == 'user') {
+ td.addClass(cl).attr('title', o.title).append($('').text(o.display));
+ } else {
+ cl = $.inArray(cl, o.acl) >= 0 ? ' enabled' : ' disabled';
+ td.addClass(this.className + cl).html('');
+ }
+
+ $(this).replaceWith(td);
+ });
+
+ row = row.attr({ id: 'rcmrow' + id, 'data-userid': o.username }).get(0);
+
+ this.env.acl[id] = o.acl;
+
+ // sorting... (create an array of user identifiers, then sort it)
+ for (n in this.env.acl) {
+ if (this.env.acl[n]) {
+ if (this.env.acl_specials.length && $.inArray(n, this.env.acl_specials) >= 0) {
+ spec.push(n);
+ } else {
+ ids.push(n);
+ }
+ }
+ }
+
+ ids.sort();
+ // specials on the top
+ ids = spec.concat(ids);
+
+ // find current id
+ for (n = 0, len = ids.length; n < len; n++) {
+ if (ids[n] == id) {
+ break;
+ }
+ }
+
+ // add row
+ if (!$('tbody > tr', table).length) {
+ list.insert_row(row);
+ } else {
+ if (n) {
+ $('#rcmrow' + ids[n - 1]).after(row);
+ } else {
+ $('#rcmrow' + ids[n + 1]).before(row);
+ }
+ list.init_row(row);
+ list.rowcount++;
+ }
+
+ if (sel) {
+ list.select_row(o.id);
+ }
+};
+
+// Initializes and shows ACL create/edit form
+rcube_webmail.prototype.acl_init_form = function (id) {
+ var row, td, val = '', type = 'user',
+ ul = $('#rights'),
+ checkboxes = $(':checkbox', ul),
+ name_input = $('#acluser'),
+ type_list = $('#usertype');
+
+ if (!this.acl_form) {
+ var fn = function () {
+ $(this).closest('li').find('[type=radio]').prop('checked', true);
+ };
+ name_input.click(fn).keypress(fn);
+
+ checkboxes.on('input', function (event) {
+ if (event.target.checked) {
+ checkboxes.each(function (i, box) {
+ if (box == event.target) {
+ return false;
+ }
+
+ box.checked = true;
+ });
+ }
+ });
+ }
+
+ this.acl_form = $('#aclform');
+
+ if (id && (row = this.acl_list.rows[id])) {
+ row = row.obj;
+ checkboxes.each(function () {
+ td = $('td.' + this.id, row);
+ this.checked = td.length && td.hasClass('enabled');
+ });
+
+ if (!this.env.acl_specials.length || $.inArray(id, this.env.acl_specials) < 0) {
+ val = $(row).data('userid');
+ } else {
+ type = id;
+ }
+ } else {
+ // mark read rights by default
+ checkboxes.prop('checked', false).filter('#aclread').prop('checked', true).trigger('input');
+ }
+
+ name_input.val(val);
+ $('input[type=radio][value=' + type + ']').prop('checked', true);
+
+ this.acl_id = id;
+
+ var buttons = {}, me = this, body = document.body;
+
+ buttons[this.get_label('save')] = function (e) {
+ me.command('acl-save');
+ };
+ buttons[this.get_label('cancel')] = function (e) {
+ me.command('acl-cancel');
+ };
+
+ // display it as popup
+ this.acl_popup = this.show_popup_dialog(
+ this.acl_form.show(),
+ id ? this.get_label('libkolab.editperms') : this.get_label('libkolab.newuser'),
+ buttons,
+ {
+ button_classes: ['mainaction submit', 'cancel'],
+ modal: true,
+ closeOnEscape: true,
+ close: function (e, ui) {
+ (me.is_framed() ? parent.rcmail : me).ksearch_hide();
+ me.acl_form.appendTo(body).hide();
+ $(this).remove();
+ window.focus(); // focus iframe
+ },
+ }
+ );
+
+ if (type == 'user') {
+ name_input.focus();
+ } else {
+ $('input:checked', type_list).focus();
+ }
+};
+
window.rcmail && rcmail.addEventListener('init', function(e) {
var loading_lock;
@@ -683,4 +1069,37 @@
}, true);
}
}
+
+ if (rcmail.gui_objects.acltable) {
+ rcmail.acl_list_init();
+
+ rcmail.enable_command('acl-create', 'acl-save', 'acl-cancel', true);
+ rcmail.enable_command('acl-delete', 'acl-edit', false);
+
+ // enable autocomplete on user input
+ if (rcmail.env.kolab_autocomplete) {
+ var inst = rcmail.is_framed() ? parent.rcmail : rcmail;
+ inst.init_address_input_events($('#acluser'), { action: 'settings/plugin.acl-autocomplete' });
+
+ // pass config settings and localized texts to autocomplete context
+ inst.set_env({ autocomplete_max: rcmail.env.autocomplete_max, autocomplete_min_length: rcmail.env.autocomplete_min_length });
+ inst.add_label('autocompletechars', rcmail.labels.autocompletechars);
+ inst.add_label('autocompletemore', rcmail.labels.autocompletemore);
+
+ // fix inserted value
+ inst.addEventListener('autocomplete_insert', function (e) {
+ if (e.field.id != 'acluser') {
+ return;
+ }
+
+ e.field.value = e.insert.replace(/[ ,;]+$/, '');
+ });
+ }
+ }
+
+ rcmail.addEventListener('plugin.share-invitation', function(data) {
+ if (data.id in libkolab_invitations) {
+ libkolab_invitations[data.id].list.accept_invitation(data.id, data.source);
+ }
+ });
});
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -35,6 +35,8 @@
*/
public function init()
{
+ $rcmail = rcube::get_instance();
+
// load local config
$this->load_config();
$this->require_plugin('libcalendaring');
@@ -50,7 +52,13 @@
// For Chwala
$this->add_hook('folder_mod', ['kolab_storage', 'folder_mod']);
- $rcmail = rcube::get_instance();
+ // For DAV ACL
+ if ($sharing = $rcmail->config->get('kolab_dav_sharing')) {
+ $class = 'kolab_dav_' . $sharing;
+ $this->register_action('plugin.davacl', "$class::actions");
+ $this->register_action('plugin.davacl-autocomplete', "$class::autocomplete");
+ }
+
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
} catch (Exception $e) {
diff --git a/plugins/libkolab/localization/en_US.inc b/plugins/libkolab/localization/en_US.inc
--- a/plugins/libkolab/localization/en_US.inc
+++ b/plugins/libkolab/localization/en_US.inc
@@ -30,3 +30,35 @@
$labels['objectchangelognotavailable'] = 'History is not available for this object.';
$labels['tabsharing'] = 'Sharing';
$labels['aclnorights'] = 'You do not have administrator rights for this folder.';
+
+// Folder ACL (Sharing tab)
+$labels['add'] = 'Add';
+$labels['all'] = 'Anyone';
+$labels['authenticated'] = 'Authenticated users';
+$labels['identifier'] = 'Identifier';
+$labels['newuser'] = 'Add entry';
+$labels['actions'] = 'Access right actions...';
+$labels['username'] = 'User:';
+$labels['myrights'] = 'Access Rights';
+$labels['editperms'] = 'Edit permissions';
+$labels['aclall'] = 'All';
+$labels['aclread'] = 'Read';
+$labels['aclwrite'] = 'Write';
+$labels['aclread-free-busy'] = 'Free-Busy';
+$labels['acllongall'] = 'All (administration)';
+
+$labels['ariasummaryacltable'] = 'List of access rights';
+$labels['arialabelaclactions'] = 'List actions';
+$labels['arialabelaclform'] = 'Access rights form';
+
+$messages['deleting'] = 'Deleting access rights...';
+$messages['saving'] = 'Saving access rights...';
+$messages['updatesuccess'] = 'Successfully changed access rights';
+$messages['deletesuccess'] = 'Successfully deleted access rights';
+$messages['createsuccess'] = 'Successfully added access rights';
+$messages['updateerror'] = 'Unable to update access rights';
+$messages['deleteerror'] = 'Unable to delete access rights';
+$messages['createerror'] = 'Unable to add access rights';
+$messages['deleteconfirm'] = 'Are you sure, you want to remove access rights of selected user(s)?';
+$messages['norights'] = 'No rights has been specified!';
+$messages['nouser'] = 'No username has been specified!';
diff --git a/plugins/libkolab/skins/elastic/templates/acl.html b/plugins/libkolab/skins/elastic/templates/acl.html
new file mode 100644
--- /dev/null
+++ b/plugins/libkolab/skins/elastic/templates/acl.html
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/plugins/libkolab/skins/larry/images/enabled.png b/plugins/libkolab/skins/larry/images/enabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@ .user {
+ width: 30%;
+ border-left: none;
+}
+
+#acltable.advanced thead tr > .user {
+ width: 25%;
+}
+
+#acltable tbody td.user {
+ text-align: left;
+}
+
+#acltable tbody td.partial {
+ background-image: url(images/partial.png);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+#acltable tbody td.enabled {
+ background-image: url(images/enabled.png);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+#acltable tbody tr.selected td.partial {
+ background-color: #019bc6;
+ background-image: url(images/partial.png), -moz-linear-gradient(top, #019bc6 0%, #017cb4 100%);
+ background-image: url(images/partial.png), linear-gradient(top, #019bc6 0%, #017cb4 100%);
+}
+
+#acltable tbody tr.selected td.enabled {
+ background-color: #019bc6;
+ background-image: url(images/enabled.png), linear-gradient(top, #019bc6 0%, #017cb4 100%);
+}
+
+#aclform {
+ display: none;
+}
+
+#aclform div {
+ padding: 0;
+ text-align: center;
+ clear: both;
+}
+
+#aclform ul {
+ list-style: none;
+ margin: 0.2em;
+ padding: 0;
+}
+
+#aclform ul li label {
+ margin-left: 0.5em;
+}
+
+ul.toolbarmenu li span.delete {
+ background-position: 0 -1509px;
+}
diff --git a/plugins/libkolab/skins/larry/templates/acl.html b/plugins/libkolab/skins/larry/templates/acl.html
new file mode 100644
--- /dev/null
+++ b/plugins/libkolab/skins/larry/templates/acl.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/libkolab/tests/LibkolabTest.php b/plugins/libkolab/tests/LibkolabTest.php
new file mode 100644
--- /dev/null
+++ b/plugins/libkolab/tests/LibkolabTest.php
@@ -0,0 +1,76 @@
+
+ *
+ * Copyright (C) Apheleia IT
+ *
+ * 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 LibkolabTest extends PHPUnit\Framework\TestCase
+{
+ public function test_html_diff_plain()
+ {
+ // Empty input
+ $text1 = '';
+ $text2 = '';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('', $diff);
+
+ $text1 = 'test plain text';
+ $text2 = '';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text', $diff);
+
+ $text1 = '';
+ $text2 = 'test plain text';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text', $diff);
+
+ $text1 = 'test plain text';
+ $text2 = 'test plain text';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text', $diff);
+
+ // TODO: more cases e.g. multiline
+ }
+
+ public function test_html_diff_html()
+ {
+ $text1 = 'test plain text ';
+ $text2 = '';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text ', $diff);
+
+ $text1 = '';
+ $text2 = 'test plain text ';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text ', $diff);
+
+ $text1 = 'test plain text ';
+ $text2 = 'test plain text';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text ', $diff);
+
+ $text1 = 'test plain text ';
+ $text2 = 'test ';
+ $diff = libkolab::html_diff($text1, $text2);
+ $this->assertSame('test plain text ', $diff);
+
+ // TODO: more cases e.g. multiline
+ }
+}
diff --git a/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php b/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
--- a/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
+++ b/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
@@ -96,16 +96,15 @@
$alarms = !isset($folder->attributes['alarms']) || $folder->attributes['alarms'];
} else {
$alarms = false;
- $rights = 'lr';
$editable = false;
- if ($myrights = $folder->get_myrights()) {
- $rights = $myrights;
- if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
- $editable = strpos($rights, 'i') !== false;
- }
+ $rights = $folder->get_myrights();
+ $editable = strpos($rights, 'i') !== false;
+ $norename = strpos($rights, 'x') === false;
+
+ if (!empty($folder->attributes['invitation'])) {
+ $invitation = $folder->attributes['invitation'];
+ $active = true;
}
- $info = $folder->get_folder_info();
- $norename = !$editable || $info['norename'] || $info['protected'];
}
$list_id = $folder->id;
@@ -113,14 +112,14 @@
return [
'id' => $list_id,
'name' => $folder->get_name(),
- 'listname' => $folder->get_foldername(),
+ 'listname' => $folder->get_name(),
'editname' => $folder->get_foldername(),
'color' => $folder->get_color('0000CC'),
'showalarms' => $prefs[$list_id]['showalarms'] ?? $alarms,
'editable' => $editable,
'rights' => $rights,
'norename' => $norename,
- 'active' => !isset($prefs[$list_id]['active']) || !empty($prefs[$list_id]['active']),
+ 'active' => $active ?? (!isset($prefs[$list_id]['active']) || !empty($prefs[$list_id]['active'])),
'owner' => $folder->get_owner(),
'parentfolder' => $folder->get_parent(),
'default' => $folder->default,
@@ -133,6 +132,7 @@
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
'caldavuid' => '', // $folder->get_uid(),
'history' => !empty($this->bonnie_api),
+ 'share_invitation' => $invitation ?? null,
];
}
@@ -416,49 +416,32 @@
*/
public function search_lists($query, $source)
{
- /*
- $this->search_more_results = false;
- $this->lists = $this->folders = array();
-
- // find unsubscribed IMAP folders that have "event" type
- if ($source == 'folders') {
- foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) {
- $this->folders[$folder->id] = $folder;
- $this->lists[$folder->id] = $this->folder_props($folder, array());
- }
- }
- // search other user's namespace via LDAP
- else if ($source == 'users') {
- $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
- foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
- $folders = array();
- // search for tasks folders shared by this user
- foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) {
- $folders[] = new kolab_storage_folder($foldername, 'task');
- }
+ $this->search_more_results = false;
+ $this->lists = $this->folders = [];
- if (count($folders)) {
- $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
- $this->folders[$userfolder->id] = $userfolder;
- $this->lists[$userfolder->id] = $this->folder_props($userfolder, array());
+ // find unsubscribed IMAP folders that have "event" type
+ if ($source == 'folders') {
+ foreach ((array) $this->storage->search_folders('task', $query, ['other']) as $folder) {
+ $this->folders[$folder->id] = $folder;
+ $this->lists[$folder->id] = $this->folder_props($folder, []);
+ }
+ }
+ // find other user's calendars (invitations)
+ elseif ($source == 'users') {
+ // we have slightly more space, so display twice the number
+ $limit = $this->rc->config->get('autocomplete_max', 15) * 2;
- foreach ($folders as $folder) {
- $this->folders[$folder->id] = $folder;
- $this->lists[$folder->id] = $this->folder_props($folder, array());
- $count++;
- }
- }
+ foreach ($this->storage->get_share_invitations('task', $query) as $invitation) {
+ $this->folders[$invitation->id] = $invitation;
+ $this->lists[$invitation->id] = $this->folder_props($invitation, []);
- if ($count >= $limit) {
- $this->search_more_results = true;
- break;
- }
- }
+ if (count($this->lists) > $limit) {
+ $this->search_more_results = true;
}
+ }
+ }
- return $this->get_lists();
- */
- return [];
+ return $this->get_lists();
}
/**
@@ -1434,6 +1417,31 @@
return false;
}
+ /**
+ * Accept an invitation to a shared folder
+ *
+ * @param string $href Invitation location href
+ *
+ * @return array|false
+ */
+ public function accept_share_invitation($href)
+ {
+ $folder = $this->storage->accept_share_invitation('task', $href);
+
+ if ($folder === false) {
+ return false;
+ }
+
+ // Activate the folder
+ $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', []);
+ $prefs['kolab_tasklists'][$folder->id]['active'] = true;
+
+ $tasklist = $this->folder_props($folder, $prefs['kolab_tasklists']);
+
+ $this->rc->user->save_prefs($prefs);
+
+ return $tasklist;
+ }
/**
* Get attachment properties
@@ -1568,7 +1576,8 @@
$this->_read_lists();
if (!empty($list['id']) && ($list = $this->lists[$list['id']])) {
- $folder_name = $this->get_folder($list['id'])->name;
+ $folder = $this->get_folder($list['id']);
+ $folder_name = $folder->name;
} else {
$folder_name = '';
}
@@ -1591,6 +1600,6 @@
$form['properties']['fields'][$f] = $fieldprop[$f];
}
- return kolab_utils::folder_form($form, $folder_name, 'tasklist', $hidden_fields, true);
+ return kolab_utils::folder_form($form, $folder ?? null, 'tasklist', $hidden_fields);
}
}
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -286,6 +286,18 @@
return false;
}
+ /**
+ * Accept an invitation to a shared folder
+ *
+ * @param string $href Invitation location href
+ *
+ * @return array|false
+ */
+ public function accept_share_invitation($href)
+ {
+ return false;
+ }
+
/**
* Get attachment properties
*
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -177,7 +177,7 @@
});
tasklists_widget.addEventListener('select', function(node) {
var id = $(this).data('id');
- rcmail.enable_command('list-edit', me.has_permission(me.tasklists[node.id], 'wa'));
+ rcmail.enable_command('list-edit', me.has_permission(me.tasklists[node.id], 'xwa'));
rcmail.enable_command('list-delete', me.has_permission(me.tasklists[node.id], 'xa'));
rcmail.enable_command('list-import', me.has_permission(me.tasklists[node.id], 'i'));
rcmail.enable_command('list-remove', me.tasklists[node.id] && me.tasklists[node.id].removable);
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -129,6 +129,8 @@
$this->register_action('itip-delegate', [$this, 'mail_itip_delegate']);
$this->add_hook('refresh', [$this, 'refresh']);
+ $this->rc->plugins->register_action('plugin.share-invitation', $this->ID, [$this, 'share_invitation']);
+
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
} elseif ($args['task'] == 'mail') {
if ($args['action'] == 'show' || $args['action'] == 'preview') {
@@ -1461,6 +1463,18 @@
return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
}
+ /**
+ * Handle invitations to a shared folder
+ */
+ public function share_invitation()
+ {
+ $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
+ $invitation = rcube_utils::get_input_value('invitation', rcube_utils::INPUT_POST);
+
+ if ($tasklist = $this->driver->accept_share_invitation($invitation)) {
+ $this->rc->output->command('plugin.share-invitation', ['id' => $id, 'source' => $tasklist]);
+ }
+ }
/******* UI functions ********/
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -273,7 +273,7 @@
if (!empty($prop['virtual'])) {
$classes[] = 'virtual';
- } elseif (empty($prop['editable'])) {
+ } elseif (strpos($prop['rights'], 'i') === false && strpos($prop['rights'], 'w') === false) {
$classes[] = 'readonly';
}
if (!empty($prop['subscribed'])) {
@@ -285,30 +285,50 @@
if (!$activeonly || !empty($prop['active'])) {
$label_id = 'tl:' . $id;
+ $listname = !empty($prop['listname']) ? $prop['listname'] : $prop['name'];
+ $actions = '';
+
$chbox = html::tag('input', [
'type' => 'checkbox',
'name' => '_list[]',
'value' => $id,
- 'checked' => !empty($prop['active']),
+ 'checked' => !empty($prop['active']) && empty($prop['share_invitation']),
'title' => $this->plugin->gettext('activate'),
'aria-labelledby' => $label_id,
]);
- $actions = '';
if (!empty($prop['removable'])) {
$actions .= html::a(['href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')], ' ');
}
- $actions .= html::a(['href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'], ' ');
+
+ $actions .= html::a(
+ [
+ 'href' => '#',
+ 'class' => 'quickview',
+ 'title' => $this->plugin->gettext('focusview'),
+ 'role' => 'checkbox',
+ 'aria-checked' => 'false',
+ 'style' => !empty($prop['share_invitation']) ? 'display:none' : null,
+ ],
+ ' '
+ );
+
if (isset($prop['subscribed'])) {
- $actions .= html::a(['href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'], ' ');
+ $actions .= html::a(
+ [
+ 'href' => '#',
+ 'class' => 'subscribed',
+ 'title' => $this->plugin->gettext('tasklistsubscribe'),
+ 'role' => 'checkbox',
+ 'aria-checked' => $prop['subscribed'] ? 'true' : 'false',
+ ],
+ ' '
+ );
}
return html::div(
implode(' ', $classes),
- html::a(
- ['class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id],
- !empty($prop['listname']) ? $prop['listname'] : $prop['name']
- )
+ html::a(['class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id], $listname)
. (!empty($prop['virtual']) ? '' : $chbox . html::span('actions', $actions))
);
}
@@ -352,7 +372,7 @@
}
foreach ((array) $this->plugin->driver->get_lists() as $id => $prop) {
- if (!empty($prop['editable']) || strpos($prop['rights'], 'i') !== false) {
+ if (!empty($prop['rights']) && strpos($prop['rights'], 'i') !== false) {
$select->add($prop['name'], $id);
if (!$default || !empty($prop['default'])) {
$default = $id;
|