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

Aleksander Machniak machniak at kolabsys.com
Tue Mar 4 14:45:41 CET 2014


 lib/Auth/LDAP.php                            |    8 +
 lib/api/kolab_api_service_ou.php             |    8 +
 lib/client/kolab_client_task_ou.php          |  159 +++++++++++++++++++++++++++
 lib/ext/Net/LDAP3.php                        |   41 ++++--
 lib/kolab_api_service.php                    |    9 +
 lib/kolab_client_task.php                    |   49 ++++----
 lib/kolab_form.php                           |    2 
 lib/kolab_html.php                           |    2 
 lib/locale/en_US.php                         |    1 
 public_html/js/kolab_admin.js                |   41 ++++++
 public_html/skins/default/images/buttons.png |binary
 public_html/skins/default/style.css          |   22 +++
 12 files changed, 301 insertions(+), 41 deletions(-)

New commits:
commit 2a8ac19a13e682bd1e52b2364702414031f36767
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Tue Mar 4 14:43:24 2014 +0100

    Add support for organizational unit hierarchy

diff --git a/lib/Auth/LDAP.php b/lib/Auth/LDAP.php
index 25bef97..83eb859 100644
--- a/lib/Auth/LDAP.php
+++ b/lib/Auth/LDAP.php
@@ -723,7 +723,13 @@ class LDAP extends Net_LDAP3 {
 
     public function organizationalunit_add($attrs, $typeid = null)
     {
-        $base_dn = $this->entry_base_dn('ou', $typeid);
+        if (!empty($attrs['base_dn'])) {
+            $base_dn = $attrs['base_dn'];
+            unset($attrs['base_dn']);
+        }
+        else {
+            $base_dn = $this->entry_base_dn('ou', $typeid);
+        }
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]ou_rdn_attr
diff --git a/lib/api/kolab_api_service_ou.php b/lib/api/kolab_api_service_ou.php
index 015d736..7e9eddc 100644
--- a/lib/api/kolab_api_service_ou.php
+++ b/lib/api/kolab_api_service_ou.php
@@ -167,9 +167,15 @@ class kolab_api_service_ou extends kolab_api_service
         $result = $auth->organizationalunit_info($getdata['id']);
 
         // normalize result
-        $result = $this->parse_result_attributes('ou', $result);
+        $result = $this->parse_result_attributes('ou', $result, $dn);
 
         if ($result) {
+            // get base_dn "attribute" for the API client
+            $dn = substr($dn, strlen($result['ou']) + 4);
+            if (strpos($dn, 'ou=') === 0) {
+                $result['base_dn'] = $dn;
+            }
+
             return $result;
         }
 
diff --git a/lib/client/kolab_client_task_ou.php b/lib/client/kolab_client_task_ou.php
index 67c1df3..e1d80a8 100644
--- a/lib/client/kolab_client_task_ou.php
+++ b/lib/client/kolab_client_task_ou.php
@@ -78,6 +78,55 @@ class kolab_client_task_ou extends kolab_client_task
         $this->output->set_object('taskcontent', $output);
     }
 
+    /**
+     * OU list result handler, tree list builder
+     */
+    protected function list_result_handler($result, $head, $foot, $table_class)
+    {
+        $result = $this->ou_list_sort($result);
+        $result = $this->ou_list_build_tree($result, $max_tree_level);
+
+        foreach ($result as $idx => $item) {
+            $class = array('selectable');
+            $tree  = '';
+            $style = '';
+
+            if ($item['level']) {
+                $tree .= '<span class="level" style="width:' . ($item['level'] * 16) . 'px"></span>';
+                $style = 'display:none';
+            }
+
+            if ($item['has_children']) {
+                $tree .= '<span class="expando"></span>';
+            }
+            else if ($max_tree_level) {
+                $tree .= '<span class="spacer"></span>';
+            }
+
+            $i++;
+            $cells = array();
+            $cells[] = array(
+                'class'   => 'name',
+                'body'    => $tree . kolab_html::escape($item['name']),
+                'onclick' => "kadm.command('ou.info', '$idx')",
+            );
+
+            $rows[] = array(
+                'id'    => $i,
+                'class' => implode(' ', $class),
+                'cells' => $cells,
+                'style' => $style,
+                'data-level' => !empty($item['level']) ? $item['level'] : '',
+            );
+        }
+
+        if ($max_tree_level) {
+            $this->output->command('tree_list_init');
+        }
+
+        return array($rows, $head, '', $table_class . ' tree');
+    }
+
     private function ou_form($attribs, $data = array())
     {
         if (empty($attribs['id'])) {
@@ -95,6 +144,7 @@ class kolab_client_task_ou extends kolab_client_task
             'type_id'                   => 'system',
             'type_id_name'              => 'system',
             'ou'                        => 'system',
+            'base_dn'                   => 'system',
             'description'               => 'system',
         );
 
@@ -122,6 +172,17 @@ class kolab_client_task_ou extends kolab_client_task
             $fields['type_id']['type'] = kolab_form::INPUT_HIDDEN;
         }
 
+        $ou_roots = array_merge(array(''), $this->ou_list());
+
+        // Add OU root selector
+        $fields['base_dn'] = array(
+            'section' => 'system',
+            'type'    => kolab_form::INPUT_SELECT,
+            'options' => $ou_roots,
+            'escaped' => true,
+            'default' => $data['base_dn'],
+        );
+
         // Create mode
         if ($add_mode) {
             // Page title
@@ -146,4 +207,102 @@ class kolab_client_task_ou extends kolab_client_task
 
         return $form->output();
     }
+
+    private function ou_list()
+    {
+        $result = $this->api_get('ous.list', null, array('page_size' => 999));
+        $list   = (array) $result->get('list');
+
+        $sorted = $this->ou_list_sort($list);
+
+        return $this->ou_list_build($sorted);
+    }
+
+    private function ou_list_sort($list)
+    {
+        // build the list for sorting
+        foreach (array_keys($list) as $dn) {
+            $name = kolab_utils::dn2ufn($dn);
+            $name = explode(',', $name);
+
+            $sort_name = array_pop($name) . ',' . implode(',', array_reverse($name));
+
+            $list[$dn] = mb_strtolower($sort_name);
+        }
+
+        // sort
+        asort($list, SORT_LOCALE_STRING);
+
+        return $list;
+    }
+
+    private function ou_list_build($list)
+    {
+        // @TODO: this code assumes parent always exist
+        foreach (array_keys($list) as $dn) {
+            $item      = $this->parse_dn($dn);
+            $list[$dn] = str_repeat('   ', $item['level']) . kolab_html::escape($item['name']);
+        }
+
+        return $list;
+    }
+
+    /**
+     * Builds a tree from OUs list
+     */
+    private function ou_list_build_tree($list, &$max_level)
+    {
+        $last   = '';
+        $result = array();
+
+        foreach (array_keys($list) as $dn) {
+            $item = $this->parse_dn($dn);
+
+            if ($item['level']) {
+                // create a parent unit entry if it does not exist (search result)
+                $parent = $this->domain_dn($item['domain']);
+
+                foreach ($item['path'] as $sub) {
+                    $parent = "ou=$sub,$parent";
+                    if (!isset($result[$parent])) {
+                        $result[$parent] = $this->parse_dn($parent);
+                        $last = $parent;
+                    }
+
+                    $result[$parent]['has_children'] = true;
+                }
+            }
+
+            $result[$dn] = $item;
+            $last        = $dn;
+            $max_level   = max($max_level, $item['level']);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Parse OU DN string into an array
+     */
+    private function parse_dn($dn)
+    {
+        $path   = kolab_utils::dn2ufn($dn);
+        $path   = explode(', ', $path);
+        $domain = array_pop($path); // remove domain
+
+        return array(
+            'name'   => array_shift($path),
+            'path'   => array_reverse($path),
+            'level'  => count($path),
+            'domain' => $domain,
+        );
+    }
+
+    /**
+     * Converts domain name into DN string
+     */
+    private function domain_dn($domain)
+    {
+        return "dc=" . implode(',dc=', explode('.', $domain));
+    }
 }
diff --git a/lib/ext/Net/LDAP3.php b/lib/ext/Net/LDAP3.php
index 52d8626..674f261 100644
--- a/lib/ext/Net/LDAP3.php
+++ b/lib/ext/Net/LDAP3.php
@@ -1228,8 +1228,7 @@ class Net_LDAP3
         // TODO: Get $rdn_attr - we have type_id in $new_attrs
         $dn_components  = ldap_explode_dn($subject_dn, 0);
         $rdn_components = explode('=', $dn_components[0]);
-
-        $rdn_attr = $rdn_components[0];
+        $rdn_attr       = $rdn_components[0];
 
         $this->_debug("Net_LDAP3::modify_entry() using rdn attribute: " . $rdn_attr);
 
@@ -1363,6 +1362,11 @@ class Net_LDAP3
         }
 
         foreach ($new_attrs as $attr => $value) {
+            // OU's parent base dn
+            if ($attr == 'base_dn') {
+                continue;
+            }
+
             if (is_array($value)) {
                 if (count($value) == 1) {
                     $new_attrs[$attr] = $value[0];
@@ -1420,21 +1424,28 @@ class Net_LDAP3
             $old_ou = implode(',', $subject_dn_components);
         }
 
-        if ((!empty($old_ou) || !empty($new_ou)) && strtolower($old_ou) !== strtolower($new_ou)) {
-            // object is an organizational unit
-            if (strpos($subject_dn, 'ou=' . $old_ou) === 0) {
-                $mod_array['rename']['new_parent'] = substr($subject_dn, strlen('ou=' . $old_ou) + 1);
-                if (empty($mod_array['rename']['dn']) || empty($mod_array['rename']['new_rdn'])) {
-                    $mod_array['rename']['dn']      = $subject_dn;
-                    $mod_array['rename']['new_rdn'] = 'ou=' . $new_ou;
+        // object is an organizational unit
+        if (strpos($subject_dn, 'ou=' . $old_ou) === 0) {
+            $root = substr($subject_dn, strlen($old_ou) + 4); // remove ou=*,
+
+            if ((!empty($new_attrs['base_dn']) && strtolower($new_attrs['base_dn']) !== strtolower($root))
+                || (strtolower($old_ou) !== strtolower($new_ou))
+            ) {
+                if (!empty($new_attrs['base_dn'])) {
+                    $root = $new_attrs['base_dn'];
                 }
+
+                $mod_array['rename']['new_parent'] = $root;
+                $mod_array['rename']['dn']         = $subject_dn;
+                $mod_array['rename']['new_rdn']    = 'ou=' . $new_ou;
             }
-            else {
-                $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;
-                    $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . $new_attrs[$rdn_attr];
-                }
+        }
+        // not OU object, but changed ou attribute
+        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;
+                $mod_array['rename']['new_rdn'] = $rdn_attr . '=' . $new_attrs[$rdn_attr];
             }
         }
 
diff --git a/lib/kolab_api_service.php b/lib/kolab_api_service.php
index 0e01a9e..4733347 100644
--- a/lib/kolab_api_service.php
+++ b/lib/kolab_api_service.php
@@ -347,6 +347,12 @@ abstract class kolab_api_service
             }
         }
 
+        // OU's parent attribute
+        if ($object_name == 'ou' && !empty($attribs['base_dn'])) {
+            // @TODO: validate?
+            $result['base_dn'] = $attribs['base_dn'];
+        }
+
         $result = array_merge($result, $special_attr_validate);
 
         Log::trace("parse_input_attributes result (merge of \$result and \$special_attr_validate)", $result);
@@ -452,10 +458,11 @@ 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())
+    protected function parse_result_attributes($object_name, $attrs = array(), &$dn = null)
     {
         //console("parse_result_attributes($object_name, \$attrs = ", $attrs);
 
diff --git a/lib/kolab_client_task.php b/lib/kolab_client_task.php
index c9bcae9..35eec94 100644
--- a/lib/kolab_client_task.php
+++ b/lib/kolab_client_task.php
@@ -1298,7 +1298,7 @@ class kolab_client_task
                     $field['suffix'] = kolab_html::escape($this->translate($field['suffix']));
                 }
 */
-                if (!empty($field['options'])) {
+                if (!empty($field['options']) && empty($field['escaped'])) {
                     foreach ($field['options'] as $opt_idx => $option) {
                         if (is_array($option)) {
                             $field['options'][$opt_idx]['content'] = kolab_html::escape($this->translate($option['content']));
@@ -1527,6 +1527,8 @@ class kolab_client_task
         $cols = array('name');
         $i    = 0;
 
+        $table_class = 'list';
+
         // table header
         $head[0]['cells'][] = array('class' => 'name', 'body' => $this->translate($task . '.list'));
 
@@ -1555,29 +1557,34 @@ class kolab_client_task
 
         // table body
         if (!empty($result)) {
-            foreach ($result as $idx => $item) {
-                if (!is_array($item)) {
-                    continue;
-                }
+            if (method_exists($this, 'list_result_handler')) {
+                list($rows, $head, $foot, $table_class) = $this->list_result_handler($result, $head, $foot, $table_class);
+            }
+            else {
+                foreach ($result as $idx => $item) {
+                    if (!is_array($item)) {
+                        continue;
+                    }
 
-                $class = array('selectable');
+                    $class = array('selectable');
 
-                if (method_exists($this, 'list_item_handler')) {
-                    $item = $this->list_item_handler($item, $class);
-                }
-                else {
-                    $item = array_shift($item);
-                }
+                    if (method_exists($this, 'list_item_handler')) {
+                        $item = $this->list_item_handler($item, $class);
+                    }
+                    else {
+                        $item = array_shift($item);
+                    }
 
-                if (empty($item)) {
-                    continue;
-                }
+                    if (empty($item)) {
+                        continue;
+                    }
 
-                $i++;
-                $cells = array();
-                $cells[] = array('class' => 'name', 'body' => kolab_html::escape($item),
-                    'onclick' => "kadm.command('$task.info', '$idx')");
-                $rows[] = array('id' => $i, 'class' => implode(' ', $class), 'cells' => $cells);
+                    $i++;
+                    $cells = array();
+                    $cells[] = array('class' => 'name', 'body' => kolab_html::escape($item),
+                        'onclick' => "kadm.command('$task.info', '$idx')");
+                    $rows[] = array('id' => $i, 'class' => implode(' ', $class), 'cells' => $cells);
+                }
             }
         }
         else {
@@ -1588,7 +1595,7 @@ class kolab_client_task
 
         $table = kolab_html::table(array(
             'id'    => $task . 'list',
-            'class' => 'list',
+            'class' => $table_class,
             'head'  => $head,
             'body'  => $rows,
             'foot'  => $foot,
diff --git a/lib/kolab_form.php b/lib/kolab_form.php
index 2fe3e9c..6ce7332 100644
--- a/lib/kolab_form.php
+++ b/lib/kolab_form.php
@@ -319,7 +319,7 @@ class kolab_form
                 $attribs['size'] = 5;
             }
 
-            $content = kolab_html::select($attribs, true);
+            $content = kolab_html::select($attribs, empty($attribs['escaped']));
             break;
 
         case self::INPUT_CUSTOM:
diff --git a/lib/kolab_html.php b/lib/kolab_html.php
index 1107d4b..2317bd0 100644
--- a/lib/kolab_html.php
+++ b/lib/kolab_html.php
@@ -460,7 +460,7 @@ class kolab_html
             }
 
             // ignore empty values
-            if ($value === null) {
+            if ($value === null || $value === '') {
                 continue;
             }
 
diff --git a/lib/locale/en_US.php b/lib/locale/en_US.php
index c89fd2c..d4e28cd 100644
--- a/lib/locale/en_US.php
+++ b/lib/locale/en_US.php
@@ -147,6 +147,7 @@ $LANG['ou.list'] = 'Organizational Unit List';
 $LANG['ou.norecords'] = 'No organizational unit records found!';
 $LANG['ou.system'] = 'Details';
 $LANG['ou.type_id'] = 'Unit Type';
+$LANG['ou.base_dn'] = 'Parent Unit';
 
 $LANG['resource.add'] = 'Add Resource';
 $LANG['resource.add.success'] = 'Resource created successfully.';
diff --git a/public_html/js/kolab_admin.js b/public_html/js/kolab_admin.js
index e4741f2..171ae01 100644
--- a/public_html/js/kolab_admin.js
+++ b/public_html/js/kolab_admin.js
@@ -1266,6 +1266,47 @@ function kolab_admin()
     return url;
   };
 
+
+  /*********************************************************/
+  /*********                 Forms                 *********/
+  /*********************************************************/
+
+  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;
+    });
+  };
+
+
   /*********************************************************/
   /*********                 Forms                 *********/
   /*********************************************************/
diff --git a/public_html/skins/default/images/buttons.png b/public_html/skins/default/images/buttons.png
index 75e966f..fd814a5 100644
Binary files a/public_html/skins/default/images/buttons.png and b/public_html/skins/default/images/buttons.png differ
diff --git a/public_html/skins/default/style.css b/public_html/skins/default/style.css
index c6af986..7da1cc9 100644
--- a/public_html/skins/default/style.css
+++ b/public_html/skins/default/style.css
@@ -140,6 +140,28 @@ td.label {
   white-space: nowrap;
 }
 
+table.list.tree td span.expando {
+  background: url(images/buttons.png) -4px -161px no-repeat;
+  width: 16px;
+  height: 14px;
+  cursor: pointer;
+}
+
+table.list.tree tr.expanded td span.expando {
+  background-position: -4px -180px;
+}
+
+table.list.tree td span.expando,
+table.list.tree td span.spacer,
+table.list.tree td span.level {
+  display: inline-block;
+}
+
+table.list.tree td span.spacer {
+  width: 16px;
+}
+
+
 /**** Common UI elements ****/
 
 #topmenu {




More information about the commits mailing list