From 32ebf533a2f82571c029a908c5fee27f4febf21d Mon Sep 17 00:00:00 2001 From: alec Date: Fri, 13 May 2011 10:17:44 +0000 Subject: - ACL plugin (initial commit) git-svn-id: https://svn.roundcube.net/trunk@4757 208e9e7b-5314-0410-a742-e7e81cd9613c --- plugins/acl/acl.js | 268 +++++++++++++++ plugins/acl/acl.php | 458 +++++++++++++++++++++++++ plugins/acl/config.inc.php | 10 + plugins/acl/config.inc.php.dist | 13 + plugins/acl/localization/en_US.inc | 79 +++++ plugins/acl/localization/pl_PL.inc | 79 +++++ plugins/acl/skins/default/acl.css | 89 +++++ plugins/acl/skins/default/images/flagged.png | Bin 0 -> 674 bytes plugins/acl/skins/default/templates/table.html | 54 +++ 9 files changed, 1050 insertions(+) create mode 100644 plugins/acl/acl.js create mode 100644 plugins/acl/acl.php create mode 100644 plugins/acl/config.inc.php create mode 100644 plugins/acl/config.inc.php.dist create mode 100644 plugins/acl/localization/en_US.inc create mode 100644 plugins/acl/localization/pl_PL.inc create mode 100644 plugins/acl/skins/default/acl.css create mode 100644 plugins/acl/skins/default/images/flagged.png create mode 100644 plugins/acl/skins/default/templates/table.html (limited to 'plugins') diff --git a/plugins/acl/acl.js b/plugins/acl/acl.js new file mode 100644 index 000000000..0bf194804 --- /dev/null +++ b/plugins/acl/acl.js @@ -0,0 +1,268 @@ +/** + * ACL plugin script + * + * @version 0.1 + * @author Aleksander Machniak + */ + +if (window.rcmail) { + rcmail.addEventListener('init', function() { + if (rcmail.gui_objects.acltable) + rcmail.acl_list_init(); + + rcmail.enable_command('acl-create', 'acl-save', 'acl-cancel', 'acl-mode-switch', true); + rcmail.enable_command('acl-delete', 'acl-edit', false); + }); +} + +// Display new-entry form +rcube_webmail.prototype.acl_create = function() +{ + this.acl_init_form(); +} + +// Display ACL edit form +rcube_webmail.prototype.acl_edit = function() +{ + // @TODO: multi-row edition + 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 && confirm(this.get_label('acl.deleteconfirm'))) { + this.http_request('plugin.acl', '_act=delete&_user='+urlencode(users.join(',')) + + '&_mbox='+urlencode(this.env.mailbox), + this.set_busy(true, 'acl.deleting')); + } +} + +// Save ACL data +rcube_webmail.prototype.acl_save = function() +{ + var user = $('#acluser').val(), rights = ''; + + $(':checkbox', this.env.acl_advanced ? $('#advancedrights') : sim_ul = $('#simplerights')).map(function() { + if (this.checked) + rights += this.value; + }); + + if (!user) { + alert(this.get_label('acl.nouser')); + return; + } + if (!rights) { + alert(this.get_label('acl.norights')); + return; + } + + this.http_request('plugin.acl', '_act=save' + + '&_user='+urlencode(user) + + '&_acl=' +rights + + '&_mbox='+urlencode(this.env.mailbox) + + (this.acl_id ? '&_old='+this.acl_id : ''), + this.set_busy(true, 'acl.saving')); +} + +// Cancel/Hide form +rcube_webmail.prototype.acl_cancel = function() +{ + this.acl_form.hide(); +} + +// Update data after save (and hide form) +rcube_webmail.prototype.acl_update = function(o) +{ + // delete row + if (o.old) + this.acl_remove_row(o.old); + + // add new row + this.acl_add_row(o, true); + // hide form + this.acl_form.hide(); +} + +// Switch table display mode +rcube_webmail.prototype.acl_mode_switch = function(elem) +{ + this.env.acl_advanced = !this.env.acl_advanced; + this.enable_command('acl-delete', 'acl-edit', false); + this.http_request('plugin.acl', '_act=list' + + '&_mode='+(this.env.acl_advanced ? 'advanced' : 'simple') + + '&_mbox='+urlencode(this.env.mailbox), + this.set_busy(true, 'loading')); +} + +// 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, toggleselect:true}); + this.acl_list.addEventListener('select', function(o) { rcmail.acl_list_select(o); }); + this.acl_list.addEventListener('dblclick', function(o) { rcmail.acl_list_dblclick(o); }); + this.acl_list.addEventListener('keypress', function(o) { rcmail.acl_list_keypress(o); }); + this.acl_list.init(); +} + +// ACL table row selection handler +rcube_webmail.prototype.acl_list_select = function(list) +{ + rcmail.enable_command('acl-delete', list.selection.length > 0); + rcmail.enable_command('acl-edit', list.selection.length == 1); + list.focus(); +} + +// ACL table double-click handler +rcube_webmail.prototype.acl_list_dblclick = function(list) +{ + this.acl_edit(); +} + +// ACL table keypress handler +rcube_webmail.prototype.acl_list_keypress = function(list) +{ + if (list.key_pressed == list.ENTER_KEY) + this.command('acl-edit'); + else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY) + this.command('acl-delete'); +} + +// Reloads ACL table +rcube_webmail.prototype.acl_list_update = function(html) +{ + $(this.gui_objects.acltable).html(html); + this.acl_list_init(); +} + +// Returns names of users in selected rows +rcube_webmail.prototype.acl_get_usernames = function() +{ + var users = [], n, len, cell, row, + list = this.acl_list, + selection = list.get_selection(); + + for (n=0, len=selection.length; n tr', table).clone(); + + // Update new row + $('td', row).map(function() { + var cl = this.className.replace(/^acl/, ''); + + if (items && items[cl]) + cl = items[cl]; + + if (cl == 'user') + $(this).text(o.username); + else + $(this).addClass(String(o.acl).match(RegExp(cl)) ? 'enabled' : 'disabled').text(''); + }); + + row.attr('id', 'rcmrow'+id); + row = row.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]) + ids.push(n); + ids.sort(); + + // find current id + for (n=0, len=ids.length; n= mw) + this.acl_form.css({left: parseInt((bw - mw)/2)+'px'}); + + // display it + this.acl_form.show(); + name_input.focus(); +} diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php new file mode 100644 index 000000000..8222d2449 --- /dev/null +++ b/plugins/acl/acl.php @@ -0,0 +1,458 @@ + + * + * + * Copyright (C) 2011, Kolab Systems AG + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +class acl extends rcube_plugin +{ + public $task = 'settings'; + + private $rc; + private $supported = null; + private $mbox; + + /** + * Plugin initialization + */ + function init() + { + $this->rc = rcmail::get_instance(); + + $this->require_plugin('jqueryui'); + + // Register hooks + $this->add_hook('folder_form', array($this, 'folder_form')); + // Plugin actions + $this->register_action('plugin.acl', array($this, 'acl_actions')); + } + + /** + * Handler for plugin actions (AJAX) + */ + function acl_actions() + { + $action = trim(get_input_value('_act', RCUBE_INPUT_GPC)); + + // Connect to IMAP + $this->rc->imap_init(); + $this->rc->imap_connect(); + + // Load localization and configuration + $this->add_texts('localization/'); + $this->load_config(); + + if ($action == 'save') { + $this->action_save(); + } + else if ($action == 'delete') { + $this->action_delete(); + } + else if ($action == 'list') { + $this->action_list(); + } + + // Only AJAX actions + $this->rc->output->send(); + } + + /** + * Handler for 'folder_form' hook + * + * @param array $args Hook arguments array (form data) + * + * @return array Hook arguments array + */ + function folder_form($args) + { + // Edited folder name (empty in create-folder mode) + $mbox_imap = $args['options']['name']; + if (!strlen($mbox_imap)) { + return $args; + } + + // Do nothing on protected folders (?) + if ($args['options']['protected']) { + return $args; + } + + // Do nothing if no ACL support + if (!$this->rc->imap->get_capability('ACL')) { + return $args; + } + + // Get MYRIGHTS + if (!($myrights = $args['options']['rights'])) { + return $args; + } + + // Load localization and include scripts + $this->load_config(); + $this->add_texts('localization/', array('deleteconfirm', 'norights', + 'nouser', 'deleting', 'saving')); + $this->include_script('acl.js'); + $this->rc->output->include_script('list.js'); + $this->include_stylesheet($this->local_skin_path().'/acl.css'); + + // Display folder rights to 'Info' fieldset + $args['form']['props']['fieldsets']['info']['content']['myrights'] = array( + 'label' => Q($this->gettext('myrights')), + 'value' => $this->acl2text($myrights) + ); + + // Return if not folder admin + if (!in_array('a', $myrights)) { + return $args; + } + + // The 'Sharing' tab + $this->mbox = $mbox_imap; + $this->rc->output->set_env('mailbox', $mbox_imap); + $this->rc->output->add_handlers(array( + 'acltable' => array($this, 'templ_table'), + 'aclrights' => array($this, 'templ_rights'), + )); + + $args['form']['sharing'] = array( + 'name' => Q($this->gettext('sharing')), + 'content' => $this->rc->output->parse('acl.table', false, false), + ); + + return $args; + } + + /** + * Creates ACL rights table + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + function templ_table($attrib) + { + if (empty($attrib['id'])) + $attrib['id'] = 'acl-table'; + + $out = $this->list_rights($attrib); + + $this->rc->output->add_gui_object('acltable', $attrib['id']); + + return $out; + } + + /** + * Creates ACL rights form + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + function templ_rights($attrib) + { + // Get supported rights + $supported = $this->rights_supported(); + + // depending on server capability either use 'te' or 'd' for deleting msgs + $deleteright = implode(array_intersect(str_split('ted'), $supported)); + + $out = ''; + $ul = ''; + $input = new html_checkbox(); + + // Advanced rights + $attrib['id'] = 'advancedrights'; + foreach ($supported as $val) { + $id = "acl$val"; + $ul .= html::tag('li', null, + $input->show('', array( + 'name' => "acl[$val]", 'value' => $val, 'id' => $id)) + . html::label(array('for' => $id, 'title' => $this->gettext('longacl'.$val)), + $this->gettext('acl'.$val))); + } + + $out = html::tag('ul', $attrib, $ul, html::$common_attrib); + + // Simple rights + $ul = ''; + $attrib['id'] = 'simplerights'; + $items = array( + 'read' => 'lrs', + 'write' => 'wi', + 'delete' => $deleteright, + 'full' => implode($supported), + ); + + foreach ($items as $key => $val) { + $id = "acl$key"; + $ul .= html::tag('li', null, + $input->show('', array( + 'name' => "acl[$val]", 'value' => $val, 'id' => $id)) + . html::label(array('for' => $id, 'title' => $this->gettext('longacl'.$key)), + $this->gettext('acl'.$key))); + } + + $out .= "\n" . html::tag('ul', $attrib, $ul, html::$common_attrib); + + $this->rc->output->set_env('acl_items', $items); + + return $out; + } + + /** + * Creates ACL rights table + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + private function list_rights($attrib=array()) + { + // Get ACL for the folder + $acl = $this->rc->imap->get_acl($this->mbox); + + if (!is_array($acl)) { + $acl = array(); + } + + // Sort the list by username + uksort($acl, 'strnatcasecmp'); + + // Get supported rights and build column names + $supported = $this->rights_supported(); + + // depending on server capability either use 'te' or 'd' for deleting msgs + $deleteright = implode(array_intersect(str_split('ted'), $supported)); + + // Use advanced or simple (grouped) rights + $advanced = $this->rc->config->get('acl_advanced_mode'); + + if ($advanced) { + $items = array(); + foreach ($supported as $sup) { + $items[$sup] = $sup; + } + } + else { + $items = array( + 'read' => 'lrs', + 'write' => 'wi', + 'delete' => $deleteright, + 'full' => implode($supported), + ); + } + + // Create the table + $attrib['noheader'] = true; + $table = new html_table($attrib); + + // Create table header + $table->add_header('user', $this->gettext('username')); + foreach (array_keys($items) as $key) { + $table->add_header('acl'.$key, $this->gettext('shortacl'.$key)); + } + + $i = 1; + $js_table = array(); + foreach ($acl as $user => $rights) { + if ($this->rc->imap->conn->user == $user) { + continue; + } + + // filter out virtual rights (c or d) the server may return + $userrights = array_intersect($rights, $supported); + $userid = html_identifier($user); + + $table->add_row(array('id' => 'rcmrow'.$userid)); + $table->add('user', Q($user)); + + foreach ($items as $key => $right) { + $in = $this->acl_compare($userrights, $right); + $table->add('acl'.$key . ' ' . ($in ? 'enabled' : 'disabled'), ''); + } + + $js_table[$userid] = implode($userrights); + } + + $this->rc->output->set_env('acl', $js_table); + $this->rc->output->set_env('acl_advanced', $advanced); + + $out = $table->show(); + + return $out; + } + + /** + * Handler for ACL update/create action + */ + private function action_save() + { + $mbox = trim(get_input_value('_mbox', RCUBE_INPUT_GPC, true, 'UTF7-IMAP')); + $user = trim(get_input_value('_user', RCUBE_INPUT_GPC)); + $acl = trim(get_input_value('_acl', RCUBE_INPUT_GPC)); + $oldid = trim(get_input_value('_old', RCUBE_INPUT_GPC)); + + $acl = array_intersect(str_split($acl), $this->rights_supported()); + + if (!strpos($user, '@') && ($realm = $this->rc->config->get('acl_username_realm'))) { + $user .= '@' . rcube_idn_to_ascii(preg_replace('/^@/', '', $realm)); + } + + if ($acl && $user && strlen($mbox) + && $this->rc->imap->set_acl($mbox, $user, $acl) + ) { + $ret = array('id' => html_identifier($user), + 'username' => $user, 'acl' => implode($acl), 'old' => $oldid); + $this->rc->output->command('acl_update', $ret); + $this->rc->output->show_message($oldid ? 'acl.updatesuccess' : 'acl.createsuccess', 'confirmation'); + } + else { + $this->rc->output->show_message($oldid ? 'acl.updateerror' : 'acl.createerror', 'error'); + } + } + + /** + * Handler for ACL delete action + */ + private function action_delete() + { + $mbox = trim(get_input_value('_mbox', RCUBE_INPUT_GPC, true, 'UTF7-IMAP')); + $user = trim(get_input_value('_user', RCUBE_INPUT_GPC)); + + $user = explode(',', $user); + + foreach ($user as $u) { + if ($this->rc->imap->delete_acl($mbox, $u)) { + $this->rc->output->command('acl_remove_row', html_identifier($u)); + } + else { + $error = true; + } + } + + if (!$error) { + $this->rc->output->show_message('acl.deletesuccess', 'confirmation'); + } + else { + $this->rc->output->show_message('acl.deleteerror', 'error'); + } + } + + /** + * Handler for ACL list update action (with display mode change) + */ + private function action_list() + { + if (in_array('acl_advanced_mode', (array)$this->rc->config->get('dont_override'))) { + return; + } + + $this->mbox = trim(get_input_value('_mbox', RCUBE_INPUT_GPC, true, 'UTF7-IMAP')); + $advanced = trim(get_input_value('_mode', RCUBE_INPUT_GPC)); + $advanced = $advanced == 'advanced' ? true : false; + + // Save state in user preferences + $this->rc->user->save_prefs(array('acl_advanced_mode' => $advanced)); + + $out = $this->list_rights(); + + $out = preg_replace(array('/^]+>/', '/<\/table>$/'), '', $out); + + $this->rc->output->command('acl_list_update', $out); + } + + /** + * Creates
    list with descriptive access rights + * + * @param array $rights MYRIGHTS result + * + * @return string HTML content + */ + function acl2text($rights) + { + if (empty($rights)) { + return ''; + } + + $supported = $this->rights_supported(); + $list = array(); + $attrib = array( + 'name' => 'rcmyrights', + 'style' => 'padding: 0 15px;', + ); + + foreach ($supported as $right) { + if (in_array($right, $rights)) { + $list[] = html::tag('li', null, Q($this->gettext('acl' . $right))); + } + } + + if (count($list) == count($supported)) + return Q($this->gettext('aclfull')); + + return html::tag('ul', $attrib, implode("\n", $list)); + } + + /** + * Compares two ACLs (according to supported rights) + * + * @param array $acl1 ACL rights array (or string) + * @param array $acl2 ACL rights array (or string) + * + * @param boolean Comparision result + */ + function acl_compare($acl1, $acl2) + { + if (!is_array($acl1)) $acl1 = str_split($acl1); + if (!is_array($acl2)) $acl2 = str_split($acl2); + + $rights = $this->rights_supported(); + + $acl1 = array_intersect($acl1, $rights); + $acl2 = array_intersect($acl2, $rights); + $res = array_intersect($acl1, $acl2); + + return count($res) == count($acl2); + } + + /** + * Get list of supported access rights (according to RIGHTS capability) + * + * @return array List of supported access rights abbreviations + */ + function rights_supported() + { + if ($this->supported !== null) { + return $this->supported; + } + + $capa = $this->rc->imap->get_capability('RIGHTS'); + + if (is_array($capa)) { + $rights = strtolower($capa[0]); + } + else { + $rights = 'cd'; + } + + return $this->supported = str_split('lrswi' . $rights . 'pa'); + } +} diff --git a/plugins/acl/config.inc.php b/plugins/acl/config.inc.php new file mode 100644 index 000000000..c634c1af1 --- /dev/null +++ b/plugins/acl/config.inc.php @@ -0,0 +1,10 @@ + diff --git a/plugins/acl/config.inc.php.dist b/plugins/acl/config.inc.php.dist new file mode 100644 index 000000000..6cb3093f3 --- /dev/null +++ b/plugins/acl/config.inc.php.dist @@ -0,0 +1,13 @@ + diff --git a/plugins/acl/localization/en_US.inc b/plugins/acl/localization/en_US.inc new file mode 100644 index 000000000..7f1655f40 --- /dev/null +++ b/plugins/acl/localization/en_US.inc @@ -0,0 +1,79 @@ + diff --git a/plugins/acl/localization/pl_PL.inc b/plugins/acl/localization/pl_PL.inc new file mode 100644 index 000000000..cba12d927 --- /dev/null +++ b/plugins/acl/localization/pl_PL.inc @@ -0,0 +1,79 @@ + diff --git a/plugins/acl/skins/default/acl.css b/plugins/acl/skins/default/acl.css new file mode 100644 index 000000000..8731a6503 --- /dev/null +++ b/plugins/acl/skins/default/acl.css @@ -0,0 +1,89 @@ +#aclmanager +{ + position: relative; + border: 1px solid #999; + min-height: 302px; +} + +#aclcontainer +{ + overflow-x: auto; +} + +#acltable +{ + width: 100%; + border-collapse: collapse; + background-color: #F9F9F9; +} + +#acltable td +{ + width: 1%; + white-space: nowrap; +} + +#acltable thead td +{ + padding: 0 4px 0 2px; +} + +#acltable tbody td +{ + text-align: center; + padding: 2px; + border-bottom: 1px solid #999999; + cursor: default; +} + +#acltable tbody td.user +{ + width: 96%; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; +} + +#acltable tbody td.enabled +{ + background: url(images/flagged.png) center no-repeat; +} + +#acltable tr.selected td +{ + color: #FFFFFF; + background-color: #CC3333; +} + +#acladvswitch +{ + position: absolute; + right: 4px; + text-align: right; + line-height: 22px; +} + +#acladvswitch input +{ + vertical-align: middle; +} + +#acladvswitch span +{ + display: block; +} + +#aclform +{ + top: 100px; + width: 400px; + padding: 10px; +} + +#aclform div +{ + padding: 0; + text-align: center; + clear: both; +} diff --git a/plugins/acl/skins/default/images/flagged.png b/plugins/acl/skins/default/images/flagged.png new file mode 100644 index 000000000..98215f68c Binary files /dev/null and b/plugins/acl/skins/default/images/flagged.png differ diff --git a/plugins/acl/skins/default/templates/table.html b/plugins/acl/skins/default/templates/table.html new file mode 100644 index 000000000..8caafe7dc --- /dev/null +++ b/plugins/acl/skins/default/templates/table.html @@ -0,0 +1,54 @@ + + +
    +
    + +
    +
    + + + + + +
    +
    + +
    +
      +
    • +
    • +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    + + -- cgit v1.2.3