lib/api lib/Auth lib/client lib/ext lib/kolab_api_service.php lib/kolab_client_task.php lib/kolab_form.php lib/locale public_html/js public_html/skins

Aleksander Machniak machniak at kolabsys.com
Fri Mar 21 16:07:07 CET 2014


 lib/Auth/LDAP.php                          |    3 
 lib/api/kolab_api_service_form_value.php   |   16 
 lib/api/kolab_api_service_ou.php           |    4 
 lib/api/kolab_api_service_resource.php     |    2 
 lib/api/kolab_api_service_sharedfolder.php |    2 
 lib/api/kolab_api_service_user.php         |    8 
 lib/client/kolab_client_task_ou.php        |   21 
 lib/client/kolab_client_task_settings.php  |    4 
 lib/ext/Net/LDAP3.php                      |    2 
 lib/kolab_api_service.php                  |   71 -
 lib/kolab_client_task.php                  |   23 
 lib/kolab_form.php                         |   12 
 lib/locale/en_US.php                       |   51 +
 public_html/js/kolab_admin.js              | 1240 ++++++++++++++++++++++++-----
 public_html/skins/default/style.css        |  171 +++
 public_html/skins/default/ui.js            |    6 
 16 files changed, 1374 insertions(+), 262 deletions(-)

New commits:
commit 5a62c7e3594c60b7f37c417d5a004010ffb8c767
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Mar 21 16:05:35 2014 +0100

    Add ACI form widget (Request #1782)

diff --git a/lib/Auth/LDAP.php b/lib/Auth/LDAP.php
index 83eb859..bfba321 100644
--- a/lib/Auth/LDAP.php
+++ b/lib/Auth/LDAP.php
@@ -1026,6 +1026,7 @@ class LDAP extends Net_LDAP3 {
         // additional special attributes that aren't in LDAP schema
         $additional_attributes = array(
             'top' => array('nsRoleDN'),
+            '*'   => array('aci'),
         );
 
         if (!empty($attributes)) {
@@ -1034,6 +1035,8 @@ class LDAP extends Net_LDAP3 {
                     $attributes['may'] = array_merge($attributes['may'], $attrs);
                 }
             }
+
+            $attributes['may'] = array_merge($attributes['may'], $additional_attributes['*']);
         }
 
         return $attributes;
diff --git a/lib/api/kolab_api_service_form_value.php b/lib/api/kolab_api_service_form_value.php
index fec6372..58b30a1 100644
--- a/lib/api/kolab_api_service_form_value.php
+++ b/lib/api/kolab_api_service_form_value.php
@@ -950,6 +950,22 @@ class kolab_api_service_form_value extends kolab_api_service
 
     private function select_options_attribute($postdata, $attribs = array())
     {
+        // if objectClasses aren't specified we'll use all classes already in use
+        // not all classes in LDAP
+        if (empty($postdata['classes'])) {
+            $postdata['classes'] = array();
+
+            foreach ($this->supported_types as $type) {
+                foreach ($this->object_types($type) as $obj_type) {
+                    if ($obj_type['attributes'] && $obj_type['attributes']['fields']) {
+                        $postdata['classes'] = array_merge($postdata['classes'], (array) $obj_type['attributes']['fields']['objectclass']);
+                    }
+                }
+            }
+        }
+
+        $postdata['classes'] = array_unique($postdata['classes']);
+
         $auth = Auth::get_instance();
         $list = $auth->schema_attributes($postdata['classes']);
 
diff --git a/lib/api/kolab_api_service_ou.php b/lib/api/kolab_api_service_ou.php
index 7e9eddc..b7c902e 100644
--- a/lib/api/kolab_api_service_ou.php
+++ b/lib/api/kolab_api_service_ou.php
@@ -167,11 +167,11 @@ class kolab_api_service_ou extends kolab_api_service
         $result = $auth->organizationalunit_info($getdata['id']);
 
         // normalize result
-        $result = $this->parse_result_attributes('ou', $result, $dn);
+        $result = $this->parse_result_attributes('ou', $result);
 
         if ($result) {
             // get base_dn "attribute" for the API client
-            $dn = substr($dn, strlen($result['ou']) + 4);
+            $dn = substr($result['entrydn'], strlen($result['ou']) + 4);
             if (strpos($dn, 'ou=') === 0) {
                 $result['base_dn'] = $dn;
             }
diff --git a/lib/api/kolab_api_service_resource.php b/lib/api/kolab_api_service_resource.php
index ac04f3b..f5d095f 100644
--- a/lib/api/kolab_api_service_resource.php
+++ b/lib/api/kolab_api_service_resource.php
@@ -165,8 +165,6 @@ class kolab_api_service_resource extends kolab_api_service
         // normalize result
         $result = $this->parse_result_attributes('resource', $result);
 
-        //console($result);
-
         if ($result) {
             return $result;
         }
diff --git a/lib/api/kolab_api_service_sharedfolder.php b/lib/api/kolab_api_service_sharedfolder.php
index 23518b3..d14c4fc 100644
--- a/lib/api/kolab_api_service_sharedfolder.php
+++ b/lib/api/kolab_api_service_sharedfolder.php
@@ -165,8 +165,6 @@ class kolab_api_service_sharedfolder extends kolab_api_service
         // normalize result
         $result = $this->parse_result_attributes('sharedfolder', $result);
 
-        //console($result);
-
         if ($result) {
             return $result;
         }
diff --git a/lib/api/kolab_api_service_user.php b/lib/api/kolab_api_service_user.php
index 6cf4483..acdaec0 100644
--- a/lib/api/kolab_api_service_user.php
+++ b/lib/api/kolab_api_service_user.php
@@ -178,11 +178,11 @@ class kolab_api_service_user extends kolab_api_service
         $result = $this->parse_result_attributes('user', $result);
 
         if (empty($result['ou'])) {
-            $_dn = ldap_explode_dn($result_dn, 0);
+            $dn = ldap_explode_dn($result['entrydn'], 0);
             // pop the count and rdn
-            unset($_dn['count']);
-            unset($_dn[0]);
-            $result['ou'] = implode(',', $_dn);
+            unset($dn['count']);
+            unset($dn[0]);
+            $result['ou'] = implode(',', $dn);
         }
 
         Log::trace("user.info on " . $getdata['id'] . " parsed result: " . var_export($result, TRUE));
diff --git a/lib/client/kolab_client_task_ou.php b/lib/client/kolab_client_task_ou.php
index e1d80a8..1cddeff 100644
--- a/lib/client/kolab_client_task_ou.php
+++ b/lib/client/kolab_client_task_ou.php
@@ -70,10 +70,10 @@ class kolab_client_task_ou extends kolab_client_task
      */
     public function action_info()
     {
-        $id         = $this->get_input('id', 'POST');
-        $result     = $this->api_get('ou.info', array('id' => $id));
-        $resource   = $result->get();
-        $output     = $this->ou_form(null, $resource);
+        $id     = $this->get_input('id', 'POST');
+        $result = $this->api_get('ou.info', array('id' => $id));
+        $unit   = $result->get();
+        $output = $this->ou_form(null, $unit);
 
         $this->output->set_object('taskcontent', $output);
     }
@@ -136,16 +136,19 @@ class kolab_client_task_ou extends kolab_client_task
         // Form sections
         $sections = array(
             'system' => 'ou.system',
+            'aci'    => 'ou.aci',
             'other'  => 'ou.other',
         );
 
         // field-to-section map and fields order
         $fields_map = array(
-            'type_id'                   => 'system',
-            'type_id_name'              => 'system',
-            'ou'                        => 'system',
-            'base_dn'                   => 'system',
-            'description'               => 'system',
+            'type_id'         => 'system',
+            'type_id_name'    => 'system',
+            'ou'              => 'system',
+            'base_dn'         => 'system',
+            'description'     => 'system',
+
+            'aci'             => 'aci',
         );
 
         // Prepare fields
diff --git a/lib/client/kolab_client_task_settings.php b/lib/client/kolab_client_task_settings.php
index f1e37ac..56feb97 100644
--- a/lib/client/kolab_client_task_settings.php
+++ b/lib/client/kolab_client_task_settings.php
@@ -33,7 +33,7 @@ class kolab_client_task_settings extends kolab_client_task
 
     protected $form_element_types = array(
         'text', 'select', 'multiselect', 'list', 'list-autocomplete', 'checkbox', 'password', 'ldap_url',
-        'text-quota',
+        'text-quota', 'aci',
     );
 
 
@@ -763,7 +763,7 @@ class kolab_client_task_settings extends kolab_client_task
             ),
             'options' => array(
                 'type'      => kolab_form::INPUT_TEXTAREA,
-                'data-type' => kolab_form::TYPE_LIST,
+                'data-type' => 'list',
             ),
             'maxcount' => array(
                 'type' => kolab_form::INPUT_TEXT,
diff --git a/lib/ext/Net/LDAP3.php b/lib/ext/Net/LDAP3.php
index 674f261..550cebd 100644
--- a/lib/ext/Net/LDAP3.php
+++ b/lib/ext/Net/LDAP3.php
@@ -1441,7 +1441,7 @@ class Net_LDAP3
             }
         }
         // not OU object, but changed ou attribute
-        else if ((!empty($old_ou) || !empty($new_ou)) && strtolower($old_ou) !== strtolower($new_ou)) {
+        else if ((!empty($old_ou) && !empty($new_ou)) && strtolower($old_ou) !== strtolower($new_ou)) {
             $mod_array['rename']['new_parent'] = $new_ou;
             if (empty($mod_array['rename']['dn']) || empty($mod_array['rename']['new_rdn'])) {
                 $mod_array['rename']['dn']      = $subject_dn;
diff --git a/lib/kolab_api_service.php b/lib/kolab_api_service.php
index 4733347..752c243 100644
--- a/lib/kolab_api_service.php
+++ b/lib/kolab_api_service.php
@@ -67,22 +67,7 @@ abstract class kolab_api_service
             return array();
         }
 
-        // get list of object types
-        if ($object_name == 'domain') {
-            $object_types = array(
-                '1' => array(
-                    'key'        => 'default',
-                    'attributes' => kolab_api_service_domain_types::$DEFAULT_TYPE_ATTRS,
-                ),
-            );
-            $object_types['1']['attributes']['form_fields']['aci'] = array(
-                'type'     => 'list',
-                'optional' => true,
-            );
-        }
-        else {
-            $object_types = $this->object_types($object_name);
-        }
+        $object_types = $this->object_types($object_name);
 
         if (empty($type_id)) {
             if (count($object_types) == 1) {
@@ -224,7 +209,7 @@ abstract class kolab_api_service
      */
     protected function object_types($object_name)
     {
-        if (!$object_name || !in_array($object_name, $this->supported_types_db)) {
+        if (!$object_name || !in_array($object_name, $this->supported_types)) {
             return array();
         }
 
@@ -238,29 +223,43 @@ abstract class kolab_api_service
             }
         }
 
-        $sql_result   = $this->db->query("SELECT * FROM {$object_name}_types ORDER BY name");
-        $object_types = array();
-
-        while ($row = $this->db->fetch_assoc($sql_result)) {
-            $object_types[$row['id']] = array();
-
-            foreach ($row as $key => $value) {
-                if ($key != "id") {
-                    if ($key == "attributes") {
-                        $object_types[$row['id']][$key] = json_decode($value, true);
-                    }
-                    else {
-                        $object_types[$row['id']][$key] = $value;
+        // get list of object types
+        if ($object_name == 'domain') {
+            $object_types = array(
+                '1' => array(
+                    'key'        => 'default',
+                    'attributes' => kolab_api_service_domain_types::$DEFAULT_TYPE_ATTRS,
+                ),
+            );
+            $object_types['1']['attributes']['form_fields']['aci'] = array(
+                'type'     => 'list',
+                'optional' => true,
+            );
+        }
+        else {
+            $sql_result   = $this->db->query("SELECT * FROM {$object_name}_types ORDER BY name");
+            $object_types = array();
+
+            while ($row = $this->db->fetch_assoc($sql_result)) {
+                $object_types[$row['id']] = array();
+
+                foreach ($row as $key => $value) {
+                    if ($key != "id") {
+                        if ($key == "attributes") {
+                            $object_types[$row['id']][$key] = json_decode($value, true);
+                        }
+                        else {
+                            $object_types[$row['id']][$key] = $value;
+                        }
                     }
                 }
             }
         }
 
-        //console("Object types for " . $object_name, $object_types);
-
         if ($devel_mode == null) {
             return $this->cache['object_types'][$object_name] = $object_types;
-        } else {
+        }
+        else {
             return $object_types;
         }
 
@@ -458,11 +457,10 @@ abstract class kolab_api_service
      *
      * @param string $object_name  Name of the object (user, group, etc.)
      * @param array  $attrs        Entry attributes
-     * @param string $dn           Will be filled with object base DN
      *
      * @return array Entry attributes
      */
-    protected function parse_result_attributes($object_name, $attrs = array(), &$dn = null)
+    protected function parse_result_attributes($object_name, $attrs = array())
     {
         //console("parse_result_attributes($object_name, \$attrs = ", $attrs);
 
@@ -523,6 +521,9 @@ abstract class kolab_api_service
         // add object type id to the result
         $attrs['type_id'] = $type_id;
 
+        // always return entrydn
+        $attrs['entrydn'] = $dn;
+
         return $attrs;
     }
 
diff --git a/lib/kolab_client_task.php b/lib/kolab_client_task.php
index 35eec94..2c016e6 100644
--- a/lib/kolab_client_task.php
+++ b/lib/kolab_client_task.php
@@ -849,7 +849,7 @@ class kolab_client_task
 
         case 'list':
             $result['type']      = kolab_form::INPUT_TEXTAREA;
-            $result['data-type'] = kolab_form::TYPE_LIST;
+            $result['data-type'] = 'list';
 
             if (!empty($field['maxlength'])) {
                 $result['data-maxlength'] = $field['maxlength'];
@@ -879,6 +879,24 @@ class kolab_client_task
             $result['default'] = $field['default'];
             break;
 
+        case 'aci':
+            $result['type']      = kolab_form::INPUT_TEXTAREA;
+            $result['data-type'] = 'aci';
+
+            $this->output->add_translation('aci.new', 'aci.edit', 'aci.remove',
+                'aci.users', 'aci.rights', 'aci.targets', 'aci.aciname',
+                'aci.read', 'aci.compare', 'aci.search', 'aci.write', 'aci.selfwrite',
+                'aci.delete', 'aci.add', 'aci.proxy', 'aci.all', 'aci.allow', 'aci.deny',
+                'aci.typeusers', 'aci.typegroups', 'aci.typeroles', 'aci.typeadmins', 'aci.typespecials',
+                'aci.ldap-all', 'aci.ldap-anyone', 'aci.ldap-self', 'aci.ldap-parent',
+                'aci.usersearch', 'aci.usersearchresult', 'aci.selected', 'aci.other',
+                'aci.userselected', 'aci.useradd', 'aci.userremove', 'aci.thisentry',
+                'aci.rights.target', 'aci.rights.filter', 'aci.rights.attrs', 'aci.checkall', 'aci.checknone',
+                'aci.error.noname', 'aci.error.exists', 'aci.error.nousers',
+                'button.cancel', 'button.ok'
+            );
+            break;
+
         default:
             $result['type'] = kolab_form::INPUT_TEXT;
 
@@ -1260,7 +1278,7 @@ class kolab_client_task
                     $value = $data[$idx];
 
                     // Convert data for the list field with autocompletion
-                    if ($field['data-type'] == kolab_form::TYPE_LIST) {
+                    if ($field['data-type'] == 'list') {
                         if (!is_array($value)) {
                             if (!empty($field['data-autocomplete'])) {
                                 $value = array($value => $value);
@@ -1363,6 +1381,7 @@ class kolab_client_task
         $this->output->set_env('assoc_fields', $assoc_fields);
         $this->output->set_env('required_fields', $req_fields);
         $this->output->set_env('autocomplete_min_length', $ac_min_len);
+        $this->output->set_env('entrydn', $data['entrydn']);
         $this->output->add_translation('form.required.empty', 'form.maxcount.exceeded',
             $name . '.add.success', $name . '.edit.success', $name . '.delete.success',
             $name . '.delete.confirm', $name . '.delete.force',
diff --git a/lib/kolab_form.php b/lib/kolab_form.php
index 6ce7332..9ebf900 100644
--- a/lib/kolab_form.php
+++ b/lib/kolab_form.php
@@ -40,8 +40,6 @@ class kolab_form
     const INPUT_CONTENT  = 20;
     const INPUT_TEXTQUOTA = 30;
 
-    const TYPE_LIST = 1;
-
     private $attribs  = array();
     private $elements = array();
     private $sections = array();
@@ -301,16 +299,6 @@ class kolab_form
                 $attribs['cols'] = 50;
             }
 
-            if (!empty($attribs['data-type'])) {
-                switch ($attribs['data-type']) {
-                    case self::TYPE_LIST:
-                        $attribs['data-type'] = 'list';
-                    break;
-                    default:
-                        unset($attribs['data-type']);
-                }
-            }
-
             $content = kolab_html::textarea($attribs, true);
             break;
 
diff --git a/lib/locale/en_US.php b/lib/locale/en_US.php
index 35dc51c..bc74504 100644
--- a/lib/locale/en_US.php
+++ b/lib/locale/en_US.php
@@ -7,6 +7,55 @@ $LANG['about.support'] = 'Professional support is available from <a href="http:/
 $LANG['about.technology'] = 'Technology';
 $LANG['about.warranty'] = 'It comes with absolutely <b>no warranties</b> and is typically run entirely self supported. You can find help & information on the community <a href="http://kolab.org">web site</a> & <a href="http://wiki.kolab.org">wiki</a>.';
 
+$LANG['aci.new'] = 'New...';
+$LANG['aci.edit'] = 'Edit...';
+$LANG['aci.remove'] = 'Remove';
+$LANG['aci.users'] = 'Users';
+$LANG['aci.rights'] = 'Rights';
+$LANG['aci.targets'] = 'Targets';
+$LANG['aci.aciname'] = 'ACI name:';
+$LANG['aci.hosts'] = 'Hosts';
+$LANG['aci.times'] = 'Times';
+$LANG['aci.name'] = 'Name';
+$LANG['aci.userid'] = 'User ID';
+$LANG['aci.email'] = 'E-mail';
+$LANG['aci.read'] = 'Read';
+$LANG['aci.compare'] = 'Compare';
+$LANG['aci.search'] = 'Search';
+$LANG['aci.write'] = 'Write';
+$LANG['aci.selfwrite'] = 'Self-write';
+$LANG['aci.delete'] = 'Delete';
+$LANG['aci.add'] = 'Add';
+$LANG['aci.proxy'] = 'Proxy';
+$LANG['aci.all'] = 'All rights';
+$LANG['aci.allow'] = 'Allow';
+$LANG['aci.deny'] = 'Deny';
+$LANG['aci.typeusers'] = 'Users';
+$LANG['aci.typegroups'] = 'Groups';
+$LANG['aci.typeroles'] = 'Roles';
+$LANG['aci.typeadmins'] = 'Administrators';
+$LANG['aci.typespecials'] = 'Special Rights';
+$LANG['aci.ldap-self'] = 'Self';
+$LANG['aci.ldap-anyone'] = 'All users';
+$LANG['aci.ldap-all'] = 'All authenticated users';
+$LANG['aci.ldap-parent'] = 'Parent';
+$LANG['aci.usersearch'] = 'Search for:';
+$LANG['aci.usersearchresult'] = 'Search results:';
+$LANG['aci.userselected'] = 'Selected users/groups/roles:';
+$LANG['aci.useradd'] = 'Add';
+$LANG['aci.userremove'] = 'Remove';
+$LANG['aci.error.noname'] = 'ACI rule name is required!';
+$LANG['aci.error.exists'] = 'ACI rule with specified name already exists!';
+$LANG['aci.error.nousers'] = 'At least one user entry is required!';
+$LANG['aci.rights.target'] = 'Target entry:';
+$LANG['aci.rights.filter'] = 'Filter:';
+$LANG['aci.rights.attrs'] = 'Attributes:';
+$LANG['aci.checkall'] = 'Check all';
+$LANG['aci.checknone'] = 'Check none';
+$LANG['aci.thisentry'] = 'This entry';
+$LANG['aci.selected'] = 'all selected';
+$LANG['aci.other'] = 'all except selected';
+
 $LANG['add'] = 'Add';
 
 $LANG['api.notypeid'] = 'No object type ID specified!';
@@ -36,6 +85,7 @@ $LANG['attribute.validate.extended'] = 'extended';
 
 $LANG['button.cancel'] = 'Cancel';
 $LANG['button.delete'] = 'Delete';
+$LANG['button.ok'] = 'OK';
 $LANG['button.save'] = 'Save';
 $LANG['button.submit'] = 'Submit';
 
@@ -136,6 +186,7 @@ $LANG['modifiersname'] = 'Modified by';
 $LANG['password.generate'] = 'Generate password';
 $LANG['reqtime'] = 'Request time: $1 sec.';
 
+$LANG['ou.aci'] = 'Access Rights';
 $LANG['ou.add'] = 'Add Unit';
 $LANG['ou.add.success'] = 'Unit created successfully.';
 $LANG['ou.ou'] = 'Unit Name';
diff --git a/public_html/js/kolab_admin.js b/public_html/js/kolab_admin.js
index 171ae01..44c3227 100644
--- a/public_html/js/kolab_admin.js
+++ b/public_html/js/kolab_admin.js
@@ -2,7 +2,7 @@
  +--------------------------------------------------------------------------+
  | This file is part of the Kolab Web Admin Panel                           |
  |                                                                          |
- | Copyright (C) 2011-2012, Kolab Systems AG                                |
+ | Copyright (C) 2011-2014, 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 |
@@ -23,7 +23,7 @@
 
 function kolab_admin()
 {
-  var ref = this;
+  var self = this;
 
   this.env = {};
   this.translations = {};
@@ -34,8 +34,8 @@ function kolab_admin()
   // set jQuery ajax options
   $.ajaxSetup({
     cache: false,
-    error: function(request, status, err) { ref.http_error(request, status, err); },
-    beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
+    error: function(request, status, err) { self.http_error(request, status, err); },
+    beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', self.env.token); }
   });
 
   /*********************************************************/
@@ -130,7 +130,7 @@ function kolab_admin()
 
     // set timer for requests
     if (a && this.env.request_timeout)
-      this.request_timer = window.setTimeout(function() { ref.request_timed_out(); }, this.request_timeout * 1000);
+      this.request_timer = window.setTimeout(function() { self.request_timed_out(); }, this.request_timeout * 1000);
   };
 
   // called when a request timed out
@@ -190,7 +190,7 @@ function kolab_admin()
   // display a system message (types: loading, notice, error)
   this.display_message = function(msg, type, timeout)
   {
-    var obj, ref = this;
+    var obj;
 
     if (!type)
       type = 'notice';
@@ -209,11 +209,11 @@ function kolab_admin()
 
     if (type != 'loading') {
       msg = '<div><span>' + msg + '</span></div>';
-      obj.addClass(type).click(function() { return ref.hide_message(); });
+      obj.addClass(type).click(function() { return self.hide_message(); });
     }
 
     if (timeout > 0)
-      window.setTimeout(function() { ref.hide_message(type, type != 'loading'); }, timeout);
+      window.setTimeout(function() { self.hide_message(type, type != 'loading'); }, timeout);
 
     obj.attr('id', type == 'loading' ? 'loading' : 'message')
       .appendTo('body').html(msg).show();
@@ -234,6 +234,103 @@ function kolab_admin()
       $('#'+id).html(this.env.watermark);
   }
 
+  // modal dialog popup
+  this.modal_dialog = function(content, buttons, opts)
+  {
+    var settings = {btns: {}},
+      body = $('<div class="modal"></div>'),
+      head, foot, footer = [];
+
+    // title bar
+    if (opts && opts.title)
+      $('<div class="modal_header"></div>')
+        .append($('<span>').text(opts.title))
+        .appendTo(body);
+
+    // dialog content
+    if (typeof content != 'object')
+      content = $('<div></div>').html(content);
+
+    content.addClass('modal_msg').appendTo(body);
+
+    // buttons
+    $.each(buttons, function(i, v) {
+      var n = i.replace(/[^a-z0-9_]/ig, '');
+      settings.btns[n] = v;
+      footer.push({name: n, label: self.t(i)});
+    });
+
+//    if (!settings.btns.cancel && (!opts || !opts.no_cancel))
+//      settings.btns.cancel = function() { this.hide(); };
+
+    if (footer.length) {
+      foot = $('<div class="modal_btns"></div>');
+      $.each(footer, function() {
+        $('<div></div>').addClass('modal_btn_' + this.name).text(this.label).appendTo(foot);
+      });
+
+      body.append(foot);
+    }
+
+    // configure and display dialog
+    body.wModal(settings).wModal('show');
+  };
+
+  this.tree_list_init = function()
+  {
+    $('table.list.tree span.expando').click(function() {
+      var tr = $(this).parents('table.list.tree tr'),
+        expanded = tr.hasClass('expanded'),
+        level = tr.data('level') || 0,
+        row = tr[0],
+        found = false;
+
+      tr[expanded ? 'removeClass' : 'addClass']('expanded');
+
+      $('tr', tr.parent()).each(function() {
+        if (this === row) {
+          found = true;
+          return;
+        }
+
+        if (!found)
+          return;
+
+        var r = $(this), l = r.data('level') || 0;
+
+        if (l <= level)
+          return false;
+
+        if (!expanded && l == level+1)
+          r.show();
+        else if (expanded && l > level)
+          r.hide().removeClass('expanded');
+      });
+
+      return false;
+    });
+  };
+
+  // position and display popup
+  this.popup_show = function(e, popup)
+  {
+    var popup = $(popup),
+      pos = this.mouse_pos(e),
+      win = $(window),
+      w = popup.width(),
+      h = popup.height(),
+      left = pos.left - w,
+      top = pos.top;
+
+    if (top + h > win.height())
+      top -= h;
+    if (left + w > win.width())
+      left -= w;
+
+    popup.css({left: left + 'px', top: top + 'px'}).show();
+    e.stopPropagation();
+  };
+
 
   /********************************************************/
   /*********        Remote request methods        *********/
@@ -369,93 +466,6 @@ function kolab_admin()
   };
 
 
-  /********************************************************/
-  /*********            Helper methods            *********/
-  /********************************************************/
-
-  // disable/enable all fields of a form
-  this.lock_form = function(form, lock)
-  {
-    if (!form || !form.elements)
-      return;
-
-    var n, len, elm;
-
-    if (lock)
-      this.disabled_form_elements = [];
-
-    for (n=0, len=form.elements.length; n<len; n++) {
-      elm = form.elements[n];
-
-      if (elm.type == 'hidden')
-        continue;
-      // remember which elem was disabled before lock
-      if (lock && elm.disabled)
-        this.disabled_form_elements.push(elm);
-      // check this.disabled_form_elements before inArray() as a workaround for FF5 bug
-      // http://bugs.jquery.com/ticket/9873
-      else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
-        elm.disabled = lock;
-    }
-  };
-
-  this.set_request_time = function()
-  {
-    this.env.request_time = (new Date()).getTime();
-  };
-
-  // Update request time element
-  this.update_request_time = function()
-  {
-    if (this.env.request_time) {
-      var t = ((new Date()).getTime() - this.env.request_time)/1000,
-        el = $('#reqtime');
-      el.text(el.text().replace(/[0-9.,]+/, t));
-    }
-  };
-
-  // position and display popup
-  this.popup_show = function(e, popup)
-  {
-    var popup = $(popup),
-      pos = this.mouse_pos(e),
-      win = $(window),
-      w = popup.width(),
-      h = popup.height(),
-      left = pos.left - w,
-      top = pos.top;
-
-    if (top + h > win.height())
-      top -= h;
-    if (left + w > win.width())
-      left -= w;
-
-    popup.css({left: left + 'px', top: top + 'px'}).show();
-    e.stopPropagation();
-  };
-
-  // Return absolute mouse position of an event
-  this.mouse_pos = function(e)
-  {
-    if (!e) e = window.event;
-
-    var mX = (e.pageX) ? e.pageX : e.clientX,
-      mY = (e.pageY) ? e.pageY : e.clientY;
-
-    if (document.body && document.all) {
-      mX += document.body.scrollLeft;
-      mY += document.body.scrollTop;
-    }
-
-    if (e._offset) {
-      mX += e._offset.left;
-      mY += e._offset.top;
-    }
-
-    return { left:mX, top:mY };
-  };
-
-
   /*********************************************************/
   /*********     keyboard autocomplete methods     *********/
   /*********************************************************/
@@ -708,8 +718,10 @@ function kolab_admin()
   // Form initialization
   this.form_init = function(id)
   {
-    var form = $('#'+id);
+    var form = $('#'+id),
+      aci_fields = $('textarea[data-type="aci"]', form);
 
+    this.aci = {};
     this.trigger_event('form-load', id);
 
     // replace some textarea fields with pretty/smart input lists
@@ -721,6 +733,10 @@ function kolab_admin()
     // create LDAP URL fields
     $('input[data-type="ldap_url"]:not(:disabled):not([readonly])', form)
       .each(function() { kadm.form_url_element_wrapper(this); });
+    // create ACI fields
+    aci_fields.each(function() { kadm.form_aci_element_wrapper(this); });
+    if (aci_fields.length)
+      this.form_aci_init();
   };
 
   // Form serialization
@@ -761,6 +777,11 @@ function kolab_admin()
       data.json = kadm.form_url_element_submit(this.name, data.json, form);
     });
 
+    // ACI fields
+    $('textarea[data-type="aci"]:not(:disabled):not([readonly])', form).each(function() {
+      data.json = kadm.form_aci_element_submit(this.name, data.json, form);
+    });
+
     // quota inputs
     $('input[data-type="quota"]', form).each(function() {
       var unit = $('select[name="' + this.name + '-unit"]').val();
@@ -1068,82 +1089,640 @@ function kolab_admin()
         else if (e.which == 38 || e.which == 40) {
           options = options.not(':hidden');
 
-          if (options.length <= 1)
-            return;
+          if (options.length <= 1)
+            return;
+
+          var focused,
+            selected = options.filter('.selected'),
+            index = options.index(selected);
+
+          if (e.which == 40) {
+            if (!(focused = options.get(index+1)))
+              focused = options.get(index-1);
+          }
+          else {
+            if (!(focused = options.get(index-1)))
+              focused = options.get(index+1);
+          }
+
+          if (focused) {
+            focused = $(focused);
+            selected.removeClass('selected');
+            focused.addClass('selected');
+
+            var parent = focused.parent(),
+              parent_height = parent.height(),
+              parent_top = parent.get(0).scrollTop,
+              top = focused.offset().top - parent.offset().top,
+              height = focused.height();
+
+            if (top < 0)
+              parent.get(0).scrollTop = 0;
+            else if (top >= parent_height)
+              parent.get(0).scrollTop = top - parent_height + height + parent_top;
+          }
+
+          return;
+        }
+
+        if (!s) {
+          options.show().removeClass('selected');
+          return;
+        }
+
+        options.each(function() {
+          var o = $(this), v = o.data('value');
+          o[v.indexOf(s) != -1 ? 'show' : 'hide']().removeClass('selected');
+        });
+
+        options = options.not(':hidden');
+        if (options.length == 1)
+          options.addClass('selected');
+      });
+
+    // add option rows
+    $.each(list, function(i, v) {
+      var elem = kadm.form_select_option_element(form, {value: v, key: v, element: e});
+      elem.appendTo(content);
+    });
+  };
+
+  // Creates option element for smart select
+  this.form_select_option_element = function(form, data)
+  {
+    // build element content
+    var elem = $('<span class="listelement"></span>')
+      .data('value', data.key).text(data.value)
+      .click(function(e) {
+        var val = $(this).data('value'),
+          elem = $(data.element),
+          old_val = elem.val();
+
+        $('span.link', elem.parent()).text(val);
+        elem.val(val);
+        if (val != old_val)
+          elem.change();
+      });
+
+    return elem;
+  };
+
+  // initialize ACI fields in form
+  this.form_aci_init = function()
+  {
+    // get list of ldap attributes for ACI form
+    if (!this.ldap_attributes) {
+      this.api_post('form_value.select_options', {attributes: ['attribute']}, 'form_aci_init_response');
+    }
+  };
+
+  this.form_aci_init_response = function(response)
+  {
+    if (!this.api_response(response))
+      return;
+
+    this.ldap_attributes = response.result.attribute ? response.result.attribute.list : [];
+  };
+
+  // Replaces form element with ACI element
+  this.form_aci_element_wrapper = function(form_element)
+  {
+    var i, e = $(form_element),
+      form = form_element.form,
+      name = form_element.name,
+      div = $('<div class="aci"></div>'),
+      select = $('<select multiple="multiple" size="8"></select>'),
+      table = $('<table class="acltable"><tr><td class="list"></td><td class="buttons"></td></tr></table>'),
+      buttons = [
+        $('<input type="button" />').attr({value: this.t('aci.new')}),
+        $('<input type="button" />').attr({value: this.t('aci.edit'), disabled: true}),
+        $('<input type="button" />').attr({value: this.t('aci.remove'), disabled: true})
+      ],
+      aci = this.parse_aci(e.val()) || [];
+    this.aci[name] = aci;
+    e.hide();
+
+    // this.log(e.val());
+    // this.log(aci);
+
+    $.each(aci, function(i, entry) {
+      $('<option></option>').val(i).text(entry.name).appendTo(select)
+        .on('dblclick', function () { self.form_aci_dialog(name, this.value); });
+    });
+
+    select.attr('id', 'aci'+name).on('change', function() {
+      var selected = $(this).val() || [];
+
+      buttons[1].prop('disabled', selected.length != 1);
+      buttons[2].prop('disabled', selected.length == 0);
+    });
+
+    // click on 'new' and 'edit' button
+    buttons[0].on('click', function() { self.form_aci_dialog(name); });
+    buttons[1].on('click', function() {
+      var selected = select.val();
+      self.form_aci_dialog(name, selected && selected.length ? selected[0] : null);
+    });
+
+    // click on 'remove' button
+    buttons[2].on('click', function() {
+      $.each(select.val() || [], function(i, v) {
+        self.aci[name][v] = null;
+        $('option[value="' + v + '"]', select).remove();
+      });
+      buttons[1].prop('disabled', true);
+      buttons[2].prop('disabled', true);
+    });
+
+    $('.buttons', table).append(buttons);
+    $('.list', table).append(select);
+    div.append(table)
+
+    $(form_element).parent().append(div);
+  };
+
+  // updates form data with ACI (on form submit)
+  this.form_aci_element_submit = function(name, data, form)
+  {
+    data[name] = this.build_aci(this.aci[name]);
+
+    return data;
+  };
+
+  // display aci dialog
+  this.form_aci_dialog = function(name, id)
+  {
+    var aci = id ? this.aci[name][id] : {};
+
+    this.aci_dialog_aci  = aci;
+    this.aci_dialog_name = name;
+    this.aci_dialog_id   = id;
+
+    this.modal_dialog(this.form_aci_dialog_content(aci), this.form_aci_dialog_buttons());
+
+    window.setTimeout(function() { $('#aci-name').focus(); }, 100);
+  };
+
+  // return aci dialog buttons
+  this.form_aci_dialog_buttons = function()
+  {
+    var buttons = {
+      'button.ok': function() {
+        if (self.form_aci_dialog_submit()) {
+          this.hide();
+          $('#aci-dialog').remove();
+        }
+      },
+      'button.cancel': function() {
+        this.hide();
+        $('#aci-dialog').remove();
+      },
+    };
+
+    return buttons;
+  };
+
+  // build and return aci dialog content
+  this.form_aci_dialog_content = function(aci)
+  {
+    var dialog = $('#aci-dialog');
+
+    if (!dialog.length) {
+      var i, tabs = [
+        $('<fieldset></fieldset>').attr('id', 'aci-tab-users')
+          .append($('<legend></legend>').text(this.t('aci.users')))
+          .append(this.form_aci_dialog_tab_users()),
+        $('<fieldset></fieldset>').attr('id', 'aci-tab-rights')
+          .append($('<legend></legend>').text(this.t('aci.rights')))
+          .append(this.form_aci_dialog_tab_rights()),
+        $('<fieldset></fieldset>').attr('id', 'aci-tab-targets')
+          .append($('<legend></legend>').text(this.t('aci.targets')))
+          .append(this.form_aci_dialog_tab_targets())
+      ];
+
+      dialog = $('<div id="aci-dialog"><label></label><input id="aci-name" type="text" size="40" />'
+          + '<form id="aci-form"></form></div>')
+        .hide().appendTo('body');
+
+      dialog.children('label').text(this.t('aci.aciname'));
+
+      $('#aci-form').append(tabs);
+
+      this.trigger_event('form-load', 'aci-form');
+    }
+
+    // reset form elements
+    this.form_aci_dialog_reset(aci);
+
+    return dialog.show();
+  };
+
+  this.form_aci_dialog_reset = function(aci)
+  {
+    var users = $('#aci-users').html(''),
+      rights = aci.perms ? aci.perms[0].rights : [],
+      inputs = $('#aci-rights input').prop('checked', false),
+      target = $('#aci-targets-target').val(''),
+      target_filter = $('#aci-targets-filter').val(''),
+      target_operator = $('#aci-targets input[name="attr-operator"]'),
+      rule = aci.perms ? aci.perms[0].type : 'userdn';
+
+    $.each(rights, function(i, v) {
+      $('#aci-rights-' + v).click();
+    });
+
+    $('#aci-name').val(aci.name);
+    $('#aci-rights-type').val(aci.perms ? aci.perms[0].type : '');
+    $('#aci-users-button-remove').prop('disabled', true);
+    target_operator.filter('[value="="]').prop('checked', true);
+    target_operator.filter('[value="!="]').prop('checked', false);
+
+    $.each(aci.perms ? aci.perms : [], function(i, perm) {
+      $.each(perm.rules || [], function(n, rule) {
+        // these permission rules we do not support here
+        if (!/^(userdn|groupdn|roledn)$/i.test(rule.keyword) || rule.operator != '=')
+          return;
+
+        $.each(rule.expression || [], function(x, v) {
+          if (v.substr(0, 8) == 'ldap:///') {
+            v = v.substr(8);
+          }
+
+          var t = v;
+          if (t == 'all' || t == 'self' || t == 'anyone' || t == 'parent')
+            t = self.t('aci.ldap-' + t);
+          else if (/^cn=([^,]+)/.test(t))
+            t = RegExp.$1;
+          // @TODO: resolve users DN with user names
+
+          $('<option></option>').attr({value: rule.keyword + ':' + v}).text(t).appendTo(users);
+        });
+      });
+    });
+
+    $.each(aci.targets || [], function(i, v) {
+      switch (v.type) {
+        case 'targetfilter':
+          target_filter.val(v.expression);
+          break;
+
+        case 'targetattr':
+          if (v.expression[0] == '*')
+            $('#aci-targets-attr option').prop('selected', true);
+          else
+            $('#aci-targets-attr').val(v.expression);
+
+          target_operator.filter('[value="="]').prop('checked', v.operator == '=');
+          target_operator.filter('[value="!="]').prop('checked', v.operator == '!=');
+          break;
+
+        case 'target':
+          target.val(v.expression);
+          break;
+      }
+    });
+  };
+
+  // submits aci dialog, updates aci definition in form
+  this.form_aci_dialog_submit = function()
+  {
+    var val, rules = [], rights = [],
+      name_input = $('#aci-name'),
+      name = name_input.val(),
+      rights_type = $('#aci-rights-type').val(),
+      aci_list = $('#aci' + this.aci_dialog_name),
+      exists = false,
+      aci = {perms: [], targets: [], version: '3.0', name: name};
+
+    // sanity checks
+    if (!name) {
+      alert(this.t('aci.error.noname'));
+      name_input.focus();
+      return false;
+    }
+
+    $.each(this.aci[self.aci_dialog_name] || [], function(i, v) {
+      if (v && v.name == name && (!self.aci_dialog_id || self.aci_dialog_id != i)) {
+        exists = true;
+        return false;
+      }
+    });
+
+    if (exists) {
+      alert(this.t('aci.error.exists'));
+      name_input.focus();
+      return false;
+    }
+
+    // permissions
+    $('#aci-users option').each(function() {
+      var keyword, value = this.value;
+
+      /^([a-z]+):/.test(value);
+      keyword = RegExp.$1;
+      value   = value.substr(keyword.length + 1);
+
+      rules.push({
+        join: 'or',
+        operator: '=',
+        keyword: keyword,
+        expression: ['ldap:///' + value]
+      });
+    });
+
+    if (!rules.length) {
+      alert(this.t('aci.error.nousers'));
+      return false;
+    }
+
+    $('#aci-rights input').each(function() {
+      if (this.checked) {
+        if (this.value == 'all') {
+          rights = ['all'];
+          return false;
+        }
+
+        rights.push(this.value);
+      }
+    });
+
+    if (!rights.length) {
+      rights = ['all'];
+      rights_type = rights_type == 'allow' ? 'deny' : 'allow';
+    }
+
+    aci.perms.push({
+      rights: rights,
+      type: rights_type,
+      rules: rules
+    });
+
+    // targets
+    if ((v = $('#aci-targets-attr').val() || []).length)
+      aci.targets.push({
+        type: 'targetattr',
+        expression: v.length == $('#aci-targets-attr option').length ? ['*'] : v,
+        operator: $('#aci-targets input[name="attr-operator"][value="!="]').is(':checked') ? '!=' : '=',
+      });
+
+    if (v = $('#aci-targets-target').val())
+      aci.targets.push({
+        type: 'target',
+        expression: v,
+        operator: '='
+      });
+
+    if (v = $('#aci-targets-filter').val())
+      aci.targets.push({
+        type: 'targetfilter',
+        expression: v,
+        operator: '=' // @TODO,
+      });
+
+    // this.log(aci);
+    // this.log(this.build_aci([aci]));
+
+    if (this.aci_dialog_id) {
+      this.aci[this.aci_dialog_name][this.aci_dialog_id] = aci;
+      $('option[value="' + this.aci_dialog_id + '"]', aci_list).text(aci.name);
+    }
+    else {
+      this.aci[this.aci_dialog_name].push(aci);
+      $('<option></option>').val(this.aci[this.aci_dialog_name].length-1)
+        .text(aci.name)
+        .appendTo(aci_list)
+        .on('dblclick', function () { self.form_aci_dialog(self.aci_dialog_name, this.value); });
+    }
+
+    return true;
+  };
+
+  // tab Users in aci dialog
+  this.form_aci_dialog_tab_users = function()
+  {
+    var select = $('<select id="aci-users" multiple="multiple" size="8"></select>'),
+      table = $('<table class="acltable"><tr><td class="list"></td><td class="buttons"></td></tr></table>'),
+      buttons = [
+        $('<input type="button" />').attr({value: this.t('aci.new')}),
+        $('<input type="button" id="aci-users-button-remove" />').attr({value: this.t('aci.remove'), disabled: true})
+      ];
+
+    select.on('change', function() {
+      var selected = $(this).val() || [];
+      buttons[1].attr('disabled', selected.length == 0);
+    });
+
+    // click on 'new' button
+    buttons[0].on('click', function() { self.form_aci_user_dialog(); });
+
+    // click on 'remove' button
+    buttons[1].on('click', function() {
+      $.each(select.val() || [], function(i, v) {
+        $('option[value="' + v + '"]', select).remove();
+      });
+
+      $(this).prop('disabled', true);
+    });
+
+    $('.buttons', table).append(buttons);
+    $('.list', table).append(select);
+
+    return table;
+  };
+
+  // tab Rights in aci dialog
+  this.form_aci_dialog_tab_rights = function()
+  {
+    var div = $('<div id="aci-rights"></div>'),
+      select = $('<select id="aci-rights-type"></select>'),
+      types = ['allow', 'deny'],
+      rights = ['read', 'compare', 'search', 'write', 'selfwrite', 'delete', 'add', 'proxy', 'all'],
+      inputs = [];
 
-          var focused,
-            selected = options.filter('.selected'),
-            index = options.index(selected);
+    $.each(rights, function(i, v) {
+      var input = $('<input type="checkbox" name="aci-right[]" />').attr({value: v, id: 'aci-rights-' + v});
+      inputs.push($('<label for="aci-rights-' + v + '"></label>').text(self.t('aci.' + v)).prepend(input));
 
-          if (e.which == 40) {
-            if (!(focused = options.get(index+1)))
-              focused = options.get(index-1);
-          }
-          else {
-            if (!(focused = options.get(index-1)))
-              focused = options.get(index+1);
-          }
+      if (v == 'all')
+        input.on('change', function() {
+          var list = $('input:not(#aci-rights-all)', div);
 
-          if (focused) {
-            focused = $(focused);
-            selected.removeClass('selected');
-            focused.addClass('selected');
+          if (this.checked)
+            list.prop({checked: true, disabled: true});
+          else
+            list.prop({disabled: false});
+        });
+    });
 
-            var parent = focused.parent(),
-              parent_height = parent.height(),
-              parent_top = parent.get(0).scrollTop,
-              top = focused.offset().top - parent.offset().top,
-              height = focused.height();
+    $.each(types, function(i, v) {
+      $('<option></option>').attr({value: v}).text(self.t('aci.' + v)).appendTo(select);
+    });
 
-            if (top < 0)
-              parent.get(0).scrollTop = 0;
-            else if (top >= parent_height)
-              parent.get(0).scrollTop = top - parent_height + height + parent_top;
-          }
+    return div.append(select).append(inputs);
+  };
+
+  // tab Targets in aci dialog
+  this.form_aci_dialog_tab_targets = function()
+  {
+    var opts = [],
+      content = $('<div id="aci-targets"></div>'),
+      target = $('<input id="aci-targets-target" type="text" size="40" />'),
+      filter = $('<input id="aci-targets-filter" type="text" size="40" />'),
+      button = $('<input type="button" id="aci-targets-targetbtn" />').val(this.t('aci.thisentry'))
+        .on('click', function() { target.val(self.env.entrydn) }),
+      select = $('<select id="aci-targets-attr" multiple="multiple" size="8"></select>'),
+      radio = [
+        $('<label>').text(this.t('aci.selected')).prepend($('<input type="radio" name="attr-operator" value="=" />')),
+        $('<label>').text(this.t('aci.other')).prepend($('<input type="radio" name="attr-operator" value="!=" />'))
+      ];
+
+    $.each(this.ldap_attributes, function(i, v) {
+      var o = document.createElement('option');
+      o.value = v.toLowerCase();
+      $(o).text(v);
+      opts.push(o);
+    });
 
-          return;
+    if (opts.length)
+      select.append(opts);
+
+    content.append([
+      $('<label>').text(this.t('aci.rights.target')), $('<div>').append([target, button]),
+      $('<label>').text(this.t('aci.rights.filter')), $('<div>').append(filter),
+      $('<label>').text(this.t('aci.rights.attrs')), $('<div>').append([select, radio[0], radio[1]])
+    ]);
+
+    return content;
+  };
+
+  this.form_aci_user_dialog = function()
+  {
+    var dialog = $('#aci-dialog'),
+      content = $('<div id="aci-users-dialog"></div>'),
+      search = $('<input id="aci-users-search" type="text" />'),
+      search_btn = $('<input id="aci-users-search-button" type="button" />')
+        .val(this.t('aci.search')).on('click', function() { self.form_aci_user_search(); }),
+      results = $('<select id="aci-users-results" multiple="multiple" size="6"></select>'),
+      selected = $('<select id="aci-users-selected" multiple="multiple" size="6"></select>'),
+      groups = $('<select id="aci-users-group"></select>')
+        .on('change', function() {
+          results.html('');
+          if (this.value == 'specials')
+            $.each(['self', 'all', 'anyone', 'parent'], function(i, v) {
+              if (!$('option[value="userdn:' + v + '"]', selected).length)
+                $('<option></option>').attr({value: 'userdn:' + v}).text(self.t('aci.ldap-' + v))
+                  .appendTo(results)
+                  .on('dblclick', function() { self.form_aci_user_option_dblclick(this); });
+            });
+        }),
+      options = ['users', 'groups', 'roles', /* 'admins', */ 'specials'],
+      buttons = {
+        'button.ok': function() {
+          self.form_aci_user_dialog_submit();
+          this.hide();
+          $('#aci-users-dialog').remove();
+          // bring back the main dialog
+          self.modal_dialog(dialog, self.form_aci_dialog_buttons());
+        },
+        'button.cancel': function() {
+          this.hide();
+          $('#aci-users-dialog').remove();
+          // bring back the main dialog
+          self.modal_dialog(dialog, self.form_aci_dialog_buttons());
         }
+      };
 
-        if (!s) {
-          options.show().removeClass('selected');
-          return;
-        }
+    $.each(options, function(i, v) {
+      $('<option></option>').attr({value: v}).text(self.t('aci.type' + v)).appendTo(groups);
+    });
 
-        options.each(function() {
-          var o = $(this), v = o.data('value');
-          o[v.indexOf(s) != -1 ? 'show' : 'hide']().removeClass('selected');
-        });
+    content.append([
+      $('<label>').text(this.t('aci.usersearch')), $('<div>').append([search, groups, search_btn]),
+      $('<label>').text(this.t('aci.usersearchresult')), $('<div>').append(results),
+      $('<label>').text(this.t('aci.userselected')), $('<div>').append(selected)
+    ]);
 
-        options = options.not(':hidden');
-        if (options.length == 1)
-          options.addClass('selected');
-      });
+    this.modal_dialog(content, buttons);
+  };
 
-    // add option rows
-    $.each(list, function(i, v) {
-      var elem = kadm.form_select_option_element(form, {value: v, key: v, element: e});
-      elem.appendTo(content);
+  this.form_aci_user_dialog_submit = function()
+  {
+    var user_list = $('#aci-users');
+
+    $('#aci-users-selected option').each(function() {
+      if (!$('option[value="' + this.value + '"]', user_list).length)
+        $('<option></option>').attr({value: this.value}).text($(this).text()).appendTo(user_list);
     });
   };
 
-  // Creates option element for smart select
-  this.form_select_option_element = function(form, data)
+  this.form_aci_user_search = function()
   {
-    // build element content
-    var elem = $('<span class="listelement"></span>')
-      .data('value', data.key).text(data.value)
-      .click(function(e) {
-        var val = $(this).data('value'),
-          elem = $(data.element),
-          old_val = elem.val();
+    var search = $('#aci-users-search').val(),
+      type = $('#aci-users-group').val(),
+      val, props, attrs = {
+        users: ['displayname', 'cn'],
+        groups: ['cn'],
+        roles: ['cn'],
+      };
 
-        $('span.link', elem.parent()).text(val);
-        elem.val(val);
-        if (val != old_val)
-          elem.change();
+    if (search == '')
+      return;
+
+    if (type == 'specials') {
+      $('#aci-users-results option').each(function() {
+        $(this)[$(this).text().indexOf(search) == -1 ? 'hide' : 'show'];
       });
+      return;
+    }
 
-    return elem;
+    // reset results select
+    $('#aci-users-results').html('');
+
+    // build search post parameters
+    val = {type: 'both', value: search};
+    props = {attributes: ['cn', 'mail'], page_size: 10, sort_by: 'cn', search: {}};
+    $.each(attrs[type], function(i, v) { props.search[v] = val; });
+
+    // call search
+    this.set_busy(true, 'searching');
+    this.api_post(type + '.list', props, 'form_aci_user_search_response');
+  };
+
+  this.form_aci_user_search_response = function(response)
+  {
+    if (!this.api_response(response))
+      return;
+
+    var results = $('#aci-users-results'),
+      selected = $('#aci-users-selected'),
+      type = $('#aci-users-group').val(),
+      prefixes = {users: 'userdn:', groups: 'groupdn:', roles: 'roledn:'},
+      prefix = prefixes[type] || prefixes.users;
+
+    $.each(response.result.list || {}, function(i, v) {
+      var value = prefix + i;
+      if (!$('option[value="' + value + '"]', selected).length) {
+        name = v.cn;
+
+        if (v.mail)
+          name += ' (' + v.mail + ')';
+
+        $('<option></option>').attr({value: value}).text(name)
+          .appendTo(results)
+          .on('dblclick', function() { self.form_aci_user_option_dblclick(this); });
+      }
+    });
+  };
+
+  this.form_aci_user_option_dblclick = function(elem)
+  {
+    var elem = $(elem), cloned = elem.clone(true),
+      target = $('#aci-users-' + (elem.parent().attr('id') == 'aci-users-results' ? 'selected' : 'results'));
+
+    if (!$('option[value="' + elem.val() + '"]', target).length) {
+      cloned.appendTo(target);
+      elem.remove();
+    }
   };
 
   // Replaces form element with LDAP URL element
@@ -1271,46 +1850,32 @@ function kolab_admin()
   /*********                 Forms                 *********/
   /*********************************************************/
 
-  this.tree_list_init = function()
+  // disable/enable all fields of a form
+  this.lock_form = function(form, lock)
   {
-    $('table.list.tree span.expando').click(function() {
-      var tr = $(this).parents('table.list.tree tr'),
-        expanded = tr.hasClass('expanded'),
-        level = tr.data('level') || 0,
-        row = tr[0],
-        found = false;
-
-      tr[expanded ? 'removeClass' : 'addClass']('expanded');
-
-      $('tr', tr.parent()).each(function() {
-        if (this === row) {
-          found = true;
-          return;
-        }
-
-        if (!found)
-          return;
+    if (!form || !form.elements)
+      return;
 
-        var r = $(this), l = r.data('level') || 0;
+    var n, len, elm;
 
-        if (l <= level)
-          return false;
+    if (lock)
+      this.disabled_form_elements = [];
 
-        if (!expanded && l == level+1)
-          r.show();
-        else if (expanded && l > level)
-          r.hide().removeClass('expanded');
-      });
+    for (n=0, len=form.elements.length; n<len; n++) {
+      elm = form.elements[n];
 
-      return false;
-    });
+      if (elm.type == 'hidden')
+        continue;
+      // remember which elem was disabled before lock
+      if (lock && elm.disabled)
+        this.disabled_form_elements.push(elm);
+      // check this.disabled_form_elements before inArray() as a workaround for FF5 bug
+      // http://bugs.jquery.com/ticket/9873
+      else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
+        elm.disabled = lock;
+    }
   };
 
-
-  /*********************************************************/
-  /*********                 Forms                 *********/
-  /*********************************************************/
-
   this.serialize_form = function(id)
   {
     var i, v, json = {},
@@ -2180,6 +2745,125 @@ function kolab_admin()
     $('input[name="' + f + '2"]').val(pass);
   };
 
+  // convert ACI string into object
+  this.parse_aci = function(aci_str)
+  {
+    var aci = [];
+
+    $.each((aci_str || '').split(/\r?\n/), function(i, str) {
+
+      var s, target, permission,
+        or_rx = /\s*\|\|\s*/,
+        entry = {targets: [], perms: []};
+
+      // Syntax: (target)(version 3.0; acl "name";permission bindRules;)
+
+      // target is optional and there can be many of it
+      while (/^\(target/.test(str)) {
+        if (s = str.match(/^\((target|targetattr|targetscope|targetcontrol|extop|targetfilter|targattrfilters)\s*([!=]+)\s*\"([^"]+)\"\)/)) {
+          target = {operator: s[2], type: s[1], expression: s[3]};
+
+          switch (target.type) {
+            case 'targetattr':
+            case 'targetcontrol':
+            case 'extop':
+              target.expression = target.expression.toLowerCase().split(or_rx);
+              break;
+          };
+
+          entry.targets.push(target);
+          str = str.substr(s[0].length);
+        }
+      }
+
+      // there must be one version and acl entry
+      if (s = str.match(/^\s*\(version\s*([0-9.]+)\s*;\s*acl\s*\"([^"]+)\";\s*/)) {
+        entry.version = s[1];
+        entry.name = s[2];
+
+        str = str.substr(s[0].length);
+
+        // there can be multiple permission/bindRule blocks
+        $.each(str.split(';'), function(i, perm) {
+          if (permission = self.parse_aci_permission(perm))
+            entry.perms.push(permission);
+        });
+      }
+
+      if (entry.name)
+        aci.push(entry);
+    });
+
+    return aci;
+  };
+
+  this.parse_aci_permission = function(perm)
+  {
+    var s, rule, permission;
+
+    if (s = perm.match(/^\s*(allow|deny)\s*\(([a-zA-Z, ]+)\)(.*)/)) {
+      permission = {type: s[1], rules: [], rights: s[2].toLowerCase().split(/\s*,\s*/)};
+
+      rule = s[3].replace(/^[ (]+/, '').replace(/[ )]+$/, '');
+
+      while (s = rule.match(/^\s*(or|and|or not|and not)?\s*(userdn|groupdn|roledn|userattr|ip|dns|timeofday|dayofweek|authmethod|ssf)\s*([<>!=]+)\s*\"([^"]+)\"/i)) {
+        permission.rules.push({
+          join: permission.rules.length ? (s[1] || '').toUpperCase() : null,
+          keyword: s[2],
+          operator: s[3],
+          expression: s[4].split(/\s*\|\|\s*/)
+        });
+
+        rule = rule.substr(s[0].length);
+      }
+    }
+
+    return permission;
+  };
+
+  // convert ACI object into ACI array (of strings)
+  this.build_aci = function(aci)
+  {
+    var result = [];
+
+    $.each(aci, function(i, entry) {
+      // skip removed entries
+      if (!entry)
+        return;
+
+      var acl = [], tokens = [];
+      $.each(entry.targets || [], function(i, target) {
+        var txt = target.expression;
+
+        if ($.isArray(txt))
+          txt = txt.join(' || ');
+
+        tokens.push('(' + target.type + ' ' + (target.operator || '=') + ' "' + txt + '")');
+      });
+
+      acl.push('version ' + entry.version);
+      acl.push('acl "' + entry.name + '"');
+
+      $.each(entry.perms || [], function(i, perm) {
+        var num = 0, text = perm.type + ' (' + perm.rights.join(',') + ')';
+
+        $.each(perm.rules || [], function(n, rule) {
+          if (rule.join && num)
+            text += ' ' + rule.join;
+          text += ' ' + rule.keyword + ' ' + (rule.operator || '=') + ' "' + rule.expression.join(' || ') + '"';
+          num++;
+        });
+
+        acl.push(text);
+      });
+
+      tokens.push('(' + acl.join('; ') + ';)');
+      result.push(tokens.join(''));
+    });
+
+    return result;
+  };
+
   // LDAP URL parser
   this.parse_ldap_url = function(url)
   {
@@ -2324,8 +3008,190 @@ function kolab_admin()
     return url;
   };
 
+  this.set_request_time = function()
+  {
+    this.env.request_time = (new Date()).getTime();
+  };
+
+  // Update request time element
+  this.update_request_time = function()
+  {
+    if (this.env.request_time) {
+      var t = ((new Date()).getTime() - this.env.request_time)/1000,
+        el = $('#reqtime');
+      el.text(el.text().replace(/[0-9.,]+/, t));
+    }
+  };
+
+  // Return absolute mouse position of an event
+  this.mouse_pos = function(e)
+  {
+    if (!e) e = window.event;
+
+    var mX = (e.pageX) ? e.pageX : e.clientX,
+      mY = (e.pageY) ? e.pageY : e.clientY;
+
+    if (document.body && document.all) {
+      mX += document.body.scrollLeft;
+      mY += document.body.scrollTop;
+    }
+
+    if (e._offset) {
+      mX += e._offset.left;
+      mY += e._offset.top;
+    }
+
+    return { left:mX, top:mY };
+  };
+
 };
 
+/**
+ * Modal dialogs
+ */
+(function($) {
+  $.fn.wModal = function(option, settings) {
+    if (typeof option === 'object') {
+      settings = option;
+    }
+    else if (typeof option === 'string') {
+      var values = [],
+        elements = this.each(function() {
+          var data = $(this).data('modal');
+
+          if (data) {
+            if (option === 'show')
+              data.show(settings || {});
+            else if (option === 'hide')
+              data.hide(settings || {});
+            else if ($.fn.wModal.defaultSettings[option] !== undefined) {
+              if (settings !== undefined)
+                data.settings[option] = settings;
+              else
+                values.push(data.settings[option]);
+            }
+          }
+        });
+
+      if (values.length === 1)
+        return values[0];
+      else if (values.length > 0)
+        return values;
+      else
+        return elements;
+    }
+
+    return this.each(function() {
+      var _settings = $.extend({}, $.fn.wModal.defaultSettings, settings || {}),
+        modal = new Modal(_settings, $(this)),
+        $el = modal.generate();
+
+      modal.pixel.append($el);
+
+      $(this).data('modal', modal);
+    });
+  }
+
+  $.fn.wModal.defaultSettings = {btns: {}, msg: null};
+
+  function Modal(settings, elem)
+  {
+    this.modal = null;
+    this.settings = settings;
+    this.elem = elem;
+    this.tempButtons = {};
+
+    return this;
+  }
+
+  Modal.prototype =
+  {
+    generate: function()
+    {
+      var _this = this;
+
+      if (this.modal) return this.modal;
+
+      // bg - check if bg already exists
+      if ($('#modal_bg').length) {
+        this.bg = $('#modal_bg');
+        this.pixel = $('#modal_pixel');
+      }
+      else {
+        this.bg = $('<div id="modal_bg"></div>').css({position:'fixed', left:'0', top:'0', display:'none'});
+        $('body').append(this.bg);
+        $(window).resize(function() { if(_this.bg.is(':visible')) _this.resetBg.apply(_this); });
+
+        // positioning pixel setting to body produces some weird effects with scrollbars when doing sliding effects
+        this.pixel = $('<div id="modal_pixel"></div>').css({position:'fixed', left:'0', top:'0', width:'0', height:'0', lineHeight:'0', fontSize:'0'});
+        $('body').append(this.pixel);
+      }
+
+      // modal
+      this.modal = $('<div class="modal_holder"></div>').css({position:'absolute', display:'none'});
+      this.modal.append(this.elem);
+
+      $(window).resize(function() { if(_this.modal.is(':visible')) _this.resetModal.apply(_this); });
+
+      this.resetBtns();
+
+      return this.modal;
+    },
+
+    resetModal: function()
+    {
+      var modalWidth = this.modal.outerWidth(true),
+        modalHeight = this.modal.outerHeight(true),
+        viewWidth = $(window).width(),
+        viewHeight = $(window).height(),
+        left = (viewWidth/2) - (modalWidth/2),
+        top = (viewHeight/2) - (modalHeight/2);
+
+      this.modal.css({left:(left > 0 ? left + 'px' : 'auto'), top:(top > 0 ? top + 'px' : 'auto'), bottom: 'auto', right: 'auto'});
+    },
+
+    resetBg: function()
+    {
+      this.bg.css({width:$(window).width(), height:$(window).height()});
+    },
+
+    resetBtns: function(btns)
+    {
+      var btns = btns || this.settings.btns,
+        _this = this;
+
+      for (var btn in btns) {
+        (function(btn) {
+          _this.modal.find('.modal_btn_' + btn).unbind('click');
+          _this.modal.find('.modal_btn_' + btn).click(function() {
+            if (_this.tempBtns[btn])
+              _this.tempBtns[btn].apply(_this);
+            else
+              btns[btn].apply(_this);
+          });
+        })(btn);
+      }
+    },
+
+    show: function(settings)
+    {
+      this.tempBtns = settings.btns || {};
+      this.resetBg();
+      this.resetModal();
+      this.pixel.children('.modal_holder').hide();
+
+      var _this = this;
+      this.bg.fadeIn(100, function(){ _this.modal.fadeIn(100); });
+    },
+
+    hide: function()
+    {
+      this.modal.hide().remove();
+      this.bg.hide();
+    }
+  }
+})(jQuery);
+
 // Add escape() method to RegExp object
 // http://dev.rubyonrails.org/changeset/7271
 RegExp.escape = function(str)
diff --git a/public_html/skins/default/style.css b/public_html/skins/default/style.css
index 7da1cc9..6b9ffef 100644
--- a/public_html/skins/default/style.css
+++ b/public_html/skins/default/style.css
@@ -772,7 +772,8 @@ span.form_error {
   padding-left: 5px;
 }
 
-.ldap_url {
+.ldap_url,
+.aci {
   background-color: #F5F5F5;
   border: 1px solid #D0D0D0;
   border-radius: 3px 3px 3px 3px;
@@ -825,6 +826,24 @@ table.form tr.required .ldap_url {
   height: 22px;
 }
 
+.aci {
+  padding: 5px;
+}
+
+.acltable select {
+  width: 400px;
+}
+
+.acltable .buttons {
+  vertical-align: top;
+  text-align: center;
+}
+
+.acltable .buttons input {
+  display: block;
+  width: 100px;
+}
+
 /*****   autocomplete list   *****/
 
 #autocompletepane
@@ -923,6 +942,156 @@ fieldset.tabbed
   border-top: none;
 }
 
+/*****  Dialog windows  *****/
+
+#modal_bg {
+  background-color: #000;
+  z-index: 10000;
+  opacity: 0.2;
+  filter: alpha(opacity=20);
+}
+
+#modal_pixel {
+  z-index: 10001;
+}
+
+.modal {
+  position: relative;
+  min-width: 350px;
+  overflow: hidden;
+  line-height: 15px;
+  background-color: #FFF;
+  color: #333;
+  border: 1px solid rgba(51, 51, 51, 0.5);
+  border-radius: 4px;
+  box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+}
+
+.modal_header {
+  padding: 10px;
+  font-size: 14px;
+  font-weight: bold;
+  border-bottom: solid 1px #DDD;
+}
+
+.modal_close {
+  position: absolute;
+  right: 10px;
+  top: 10px;
+  font-weight: normal;
+  font-size: 12px;
+  cursor: pointer;
+  color: #BABABA;
+}
+
+.modal_msg {
+  font-size: 12px;
+  padding: 20px;
+  color: #3A3A3A;
+  text-shadow: rgba(255, 255, 255, 0.75) 0 1px 1px;
+}
+
+.modal_btns {
+  padding: 10px;
+  font-size: 10px;
+  font-weight: bold;
+  border-top: solid 1px #DDD;
+  background-color: #EFEFEF;
+  text-align: right;
+  white-space: nowrap;
+}
+
+.modal_btns div {
+  display: inline-block;
+  min-width: 40px;
+  padding: 0 10px;
+  height: 25px;
+  line-height: 25px;
+  margin-left: 10px;
+  text-align: center;
+  cursor: pointer;
+  border-radius: 4px;
+  box-shadow: rgba(255, 255, 255, 0.2) 0px 1px 0px 0px inset, rgba(0, 0, 0, 0.0470588) 0px 1px 2px 0px;
+  text-shadow: rgba(255, 255, 255, 0.75) 0 1px 1px;
+  border: 1px solid rgba(0, 0, 0, 0.14902);
+  border-bottom-color: rgba(0, 0, 0, 0.247059);
+  background-color: #F5F5F5;
+  color: #333;
+}
+
+.modal_btns div:hover {
+  background-color: #E6E6E6;
+}
+
+.modal_btns div.default {
+  text-shadow: rgba(0, 0, 0, 0.247059) 0px -1px 0px;
+  border: 1px solid rgba(0, 0, 0, 0.0980392);
+  background-color: #006DCC;
+  color: #FFF;
+}
+
+.modal_btns div.default:hover {
+  background-color: #0044CC
+}
+
+/**** ACI widget ********/
+
+#aci-dialog,
+#aci-users-dialog {
+  width: 550px;
+  background-color: #F0F0F0;
+  border: 1px solid #D0D0D0;
+  border-radius: 4px;
+  margin: 10px;
+  padding: 10px;
+}
+
+#aci-users-dialog {
+  width: 400px;
+}
+
+#aci-dialog fieldset {
+  background-color: #f0f0f0;
+}
+
+#aci-dialog > label {
+  padding-right: 5px;
+}
+
+#aci-name {
+  width: 420px;
+}
+
+#aci-rights label,
+#aci-users-results,
+#aci-users-selected,
+#aci-targets-attr {
+  display: block;
+}
+
+#aci-rights-all {
+  padding-top: 5px;
+}
+
+#aci-targets-attr,
+#aci-users-results,
+#aci-users-selected,
+#aci-targets-target,
+#aci-targets-filter {
+  width: 400px;
+}
+
+#aci-users-dialog label,
+#aci-targets label {
+  font-size: 11px;
+  font-color: #606060;
+}
+
+#aci-users-dialog div,
+#aci-targets div {
+  margin-bottom: 10px;
+}
+
 /**** Login form elements ****/
 
 #login_form {
diff --git a/public_html/skins/default/ui.js b/public_html/skins/default/ui.js
index 11b513d..c0ed08c 100644
--- a/public_html/skins/default/ui.js
+++ b/public_html/skins/default/ui.js
@@ -97,7 +97,7 @@ function init_tabs(id, current)
     // create a tab
     a   = $('<a>').text(legend.text()).attr('href', '#');
     tab = $('<span>').attr({'id': 'tab'+idx, 'class': 'tablink'})
-        .click(function() { show_tab(id, idx); return false; })
+      .click(function() { show_tab(id, idx); return false; })
 
     // remove legend
     legend.remove();
@@ -114,13 +114,13 @@ function init_tabs(id, current)
 
 function show_tab(id, index)
 {
-  var fs = $('#'+id).children('fieldset');
+  var form = $('#'+id), fs = form.children('fieldset');
 
   fs.each(function(idx) {
     // Show/hide fieldset (tab content)
     $(this)[index == idx ? 'show' : 'hide']();
     // Select/unselect tab
-    $('#tab'+idx).toggleClass('tablink-selected', idx == index);
+    $('#tab'+idx, form).toggleClass('tablink-selected', idx == index);
   });
 };
 




More information about the commits mailing list