diff --git a/admin/inc/class.admin_ui.inc.php b/admin/inc/class.admin_ui.inc.php
index 3f1beb9278..6c94305b50 100644
--- a/admin/inc/class.admin_ui.inc.php
+++ b/admin/inc/class.admin_ui.inc.php
@@ -77,19 +77,7 @@ class admin_ui
);
$sel_options['tree'] = $this->tree_data();
- $sel_options['filter'] = array('' => lang('All groups'));
- foreach(self::$accounts->search(array(
- 'type' => 'groups',
- 'start' => false,
- 'order' => 'account_lid',
- 'sort' => 'ASC',
- )) as $data)
- {
- $sel_options['filter'][$data['account_id']] = empty($data['account_description']) ? $data['account_lid'] : array(
- 'label' => $data['account_lid'],
- 'title' => $data['account_description'],
- );
- }
+ $sel_options['filter'] = array_merge([['value' => '', 'label' => lang('All groups')]], Etemplate\Widget\Select::groups());
$sel_options['filter2'] = array(
'' => 'All',
@@ -103,7 +91,7 @@ class admin_ui
$tpl->setElementAttribute('tree', 'actions', self::tree_actions());
// switching between iframe and nm/accounts-list depending on load parameter
- // important for first time load eg. from an other application calling it's site configuration
+ // important for first time load e.g. from another application calling it's site configuration
$tpl->setElementAttribute('iframe', 'disabled', empty($_GET['load']));
$content['iframe'] = 'about:blank'; // we show accounts-list be default now
if (!empty($_GET['load']))
@@ -633,73 +621,19 @@ class admin_ui
if (!empty($data['tooltip'])) $data['tooltip'] = lang($data['tooltip']);
// make sure keys are unique, as we overwrite tree entries otherwise
if (isset($parent[$data[Tree::ID]])) $data[Tree::ID] .= md5($data['link']);
- $parent[$data[Tree::ID]] = self::fix_userdata($data);
+ $parent[$data[Tree::ID]] = Tree::fixUserdata($data);
}
}
}
elseif ($root == '/groups')
{
- foreach($GLOBALS['egw']->accounts->search(array(
- 'type' => 'groups',
- 'order' => 'account_lid',
- 'sort' => 'ASC',
- )) as $group)
- {
- $tree[Tree::CHILDREN][] = self::fix_userdata(array(
- Tree::LABEL => $group['account_lid'],
- Tree::TOOLTIP => $group['account_description'],
- Tree::ID => $root.'/'.$group['account_id'],
- ));
- }
+ $tree[Tree::CHILDREN] = Tree::groups($root);
}
- self::strip_item_keys($tree[Tree::CHILDREN]);
+ Tree::stripChildrenKeys($tree[Tree::CHILDREN]);
//_debug_array($tree); exit;
return $tree;
}
- /**
- * Fix userdata as understood by tree
- *
- * @param array $data
- * @return array
- */
- private static function fix_userdata(array $data)
- {
- if(!$data[Tree::LABEL])
- {
- $data[Tree::LABEL] = $data['text'];
- }
- // store link as userdata, maybe we should store everything not directly understood by tree this way ...
- foreach(array_diff_key($data, array_flip(array(
- Tree::ID,Tree::LABEL,Tree::TOOLTIP,'im0','im1','im2','item','child','select','open','call',
- ))) as $name => $content)
- {
- $data['userdata'][] = array(
- 'name' => $name,
- 'content' => $content,
- );
- unset($data[$name]);
- }
- return $data;
- }
-
- /**
- * Attribute 'item' has to be an array
- *
- * @param array $items
- */
- private static function strip_item_keys(array &$items)
- {
- $items = array_values($items);
- foreach($items as &$item)
- {
- if (is_array($item) && isset($item['item']))
- {
- self::strip_item_keys($item['item']);
- }
- }
- }
-
public static $hook_data = array();
/**
* Return data from regular admin hook calling display_section() instead of returning it
diff --git a/admin/lang/egw_de.lang b/admin/lang/egw_de.lang
index 2a5e29942b..14142d16b7 100644
--- a/admin/lang/egw_de.lang
+++ b/admin/lang/egw_de.lang
@@ -95,8 +95,8 @@ add new application admin de Neue Anwendung hinzufügen
add new email address: admin de Neue E-Mail-Adresse hinzufügen:
add peer server admin de Server zu Serververbund hinzufügen
add profile admin de Profil hinzufügen
-add to group admin de Zu Gruppe hinzufügen
add sub-category admin de Unterkategorie hinzufügen
+add to group admin de Zu Gruppe hinzufügen
add user admin de Neuen Benutzer erstellen
add user or group admin de Benutzer oder Gruppen eingeben
added admin de Hinzugefügt
@@ -196,6 +196,7 @@ calendar recurrence horizont in days (default 1000) admin de Kalender Wiederholu
can be used by application admin de Kann von folgender Anwendung verwendet werden
can be used by group admin de Kann von folgender Gruppe verwendet werden
can be used by user admin de Kann von folgendem Benutzer verwendet werden
+can be used to show groups by container, if enabled admin de Kann benutzt werden um Gruppen in Containern anzuzeigen, wenn eingeschaltet
can change password admin de Darf Passwort ändern
can not change users into groups, same sign required! admin de Das Ändern von Benutzern in Gruppen ist nicht möglich. Gleiches Kennzeichen wird benötigt!
cancel changes admin de Änderungen abbrechen
@@ -244,6 +245,7 @@ configuration admin de Konfiguration
configuration saved. admin de Die Konfiguration wurde erfolgreich gespeichert.
connection dropped by imap server. admin de Verbindung von IMAP-Server beendet.
connection is not secure! everyone can read eg. your credentials. admin de Die Verbindung ist NICHT sicher! Jeder kann z. B. Ihr Passwort lesen.
+container admin de Container
continue admin de Weiter
cookie domain (default empty means use full domain name, for sitemgr eg. ".domain.com" allows to use the same cookie for egw.domain.com and www.domain.com) admin de Cookie Domain
(Vorgabe 'leer' bedeutet den kompletten Domainnamen, für SiteMgr erlaubt z.B. ".domain.com" das gleiche Cookie für egw.domain.com und www.domain.com zu verwenden).
cookie path (allows multiple egw sessions with different directories, has problemes with sitemgr!) admin de Cookie Pfad (erlaubt mehrere EGw-Sitzungen mit unterschiedlichen Verzeichnissen, hat Probleme mit SiteMgr!).
@@ -418,10 +420,10 @@ enter a passphrase if you would like to protect your private key by password. ad
enter some random text for app_session
encryption (requires mcrypt) admin de Zufälligen Text für app_session
Verschlüsselung (benötigt mcrypt)
enter the background color for the login page admin de Hintergrundfarbe für die Anmeldeseite
enter the background color for the site title admin de Hintergrundfarbe für den Titel der Installation
-enter the full path for temporary files.
examples: /tmp, c:\temp admin de Vollständiger Pfad für temporäre Dateien.
Beispiel: /tmp, C:\TEMP
enter the full path for temporary files.
examples: /tmp, c:temp admin de Vollständiger Pfad für temporäre Dateien.
Beispiel: /tmp, C:\TEMP
-enter the full path for users and group files.
examples: /files, e:\files admin de Vollständiger Pfad für Benutzer- und Gruppen-Dateien.
Beispiel: /files, E:\Files
+enter the full path for temporary files.
examples: /tmp, c:\temp admin de Vollständiger Pfad für temporäre Dateien.
Beispiel: /tmp, C:\TEMP
enter the full path for users and group files.
examples: /files, e:files admin de Vollständiger Pfad für Benutzer- und Gruppen-Dateien.
Beispiel: /files, E:\Files
+enter the full path for users and group files.
examples: /files, e:\files admin de Vollständiger Pfad für Benutzer- und Gruppen-Dateien.
Beispiel: /files, E:\Files
enter the hostname of the machine on which this server is running admin de Hostname des Computers auf dem der Server läuft
enter the location of egroupware's url.
example: http://www.domain.com/egroupware or /egroupware
no trailing slash admin de URL zur EGroupware-Installation.
Beispiel: https://egw.domain.com/egroupware or /egroupware
keinen nachfolgenden Slash /
enter the search string. to show all entries, empty this field and press the submit button again admin de Geben Sie Ihren Suchbegriff ein. Um alle Einträge anzuzeigen geben Sie keinen Begriff ein und drücken Sie den Suchen-Knopf noch einmal
@@ -517,6 +519,7 @@ group excepted from above export limit (admins are always excepted) admin de Gru
group has been added common de Gruppe wurde hinzugefügt.
group has been deleted common de Gruppe wurde gelöscht.
group has been updated common de Gruppe wurde aktualisiert.
+group hierarchy admin de Gruppen Hierarchie
group list admin de Liste der Gruppen
group manager admin de Gruppenmanager
group name admin de Gruppenname
@@ -778,6 +781,7 @@ quota size in mbyte admin de Quota-Größe in MByte
re-enter password admin de Passwort wiederholen
read this list of methods. admin de Diese Liste der Methoden lesen.
register application hooks admin de Registrieren der "Hooks" der Anwendungen
+regular expression to find part to use as container admin de Regulärer Ausdruck um den Teil zu finden, der als Container verwendet wird
reject passwords containing part of username or full name (3 or more characters long) admin de Passwörter zurückweisen die einen Teil des Benutzernamen oder vollständigen Namens beinhalten (3 oder mehr Zeichen lang)
relay access checked admin de Nicht angemeldetes Senden überprüft
remark admin de Bemerkung
@@ -860,6 +864,7 @@ show as optional, but required once user has it setup admin de Als optional anze
show as required, but only once user has it setup admin de Als benötigt anzeigen, aber nur benötigt, wenn Benutzer sie eingerichtet hat
show current action admin de Aktuelle Aktion anzeigen
show error log admin de Fehlerprotokoll anzeigen
+show groups in container based on admin de Zeige Gruppen in Container basierend auf
show members admin de Mitglieder dieser Gruppe anzeigen
show phpinfo() admin de phpinfo() anzeigen
show session ip address admin de IP-Adresse der Sitzung anzeigen
diff --git a/admin/lang/egw_en.lang b/admin/lang/egw_en.lang
index a023b53325..5abe2b3f8e 100644
--- a/admin/lang/egw_en.lang
+++ b/admin/lang/egw_en.lang
@@ -190,6 +190,7 @@ calendar recurrence horizont in days (default 1000) admin en Calendar recurrence
can be used by application admin en Can be used by application
can be used by group admin en Can be used by group
can be used by user admin en Can be used by user
+can be used to show groups by container, if enabled admin en Can be used to show groups by container, if enabled
can change password admin en Can change password
can not change users into groups, same sign required! admin en Can NOT change users into groups, same sign required!
cancel changes admin en Cancel changes
@@ -238,6 +239,7 @@ configuration admin en Configuration
configuration saved. admin en Configuration saved.
connection dropped by imap server. admin en Connection dropped by IMAP server.
connection is not secure! everyone can read eg. your credentials. admin en Connection is NOT secure! Everyone can read eg. your credentials.
+container admin en Container
continue admin en Continue
cookie domain (default empty means use full domain name, for sitemgr eg. ".domain.com" allows to use the same cookie for egw.domain.com and www.domain.com) admin en Cookie domain. Default empty uses full domain name. E.g. in Site Manager ".domain.com" allows to use the same cookie for egw.domain.com and www.domain.com.
cookie path (allows multiple egw sessions with different directories, has problemes with sitemgr!) admin en Cookie path. Allows multiple EGroupware sessions with different directories.
@@ -409,10 +411,10 @@ enter a passphrase if you would like to protect your private key by password. ad
enter some random text for app_session
encryption (requires mcrypt) admin en Enter some random text for app_session
encryption, requires mcrypt
enter the background color for the login page admin en Enter the background colour for the login page
enter the background color for the site title admin en Enter the background colour for the site title
-enter the full path for temporary files.
examples: /tmp, c:\temp admin en Enter the full path for temporary files.
Examples: /tmp, C:\TEMP
enter the full path for temporary files.
examples: /tmp, c:temp admin en Enter the full path for temporary files.
Examples: /tmp, C:\TEMP
-enter the full path for users and group files.
examples: /files, e:\files admin en Enter the full path for users and group files.
Examples: /files, E:\FILES
+enter the full path for temporary files.
examples: /tmp, c:\temp admin en Enter the full path for temporary files.
Examples: /tmp, C:\TEMP
enter the full path for users and group files.
examples: /files, e:files admin en Enter the full path for users and group files.
Examples: /files, E:\FILES
+enter the full path for users and group files.
examples: /files, e:\files admin en Enter the full path for users and group files.
Examples: /files, E:\FILES
enter the hostname of the machine on which this server is running admin en Enter the host name of the machine on which this server is running
enter the location of egroupware's url.
example: http://www.domain.com/egroupware or /egroupware
no trailing slash admin en Enter the location of EGroupware's URL.
Example: https://egw.domain.com/egroupware or /egroupware
No trailing slash
enter the search string. to show all entries, empty this field and press the submit button again admin en Enter the search string. To show all entries, empty this field and press the SUBMIT button again
@@ -503,6 +505,7 @@ group excepted from above export limit (admins are always excepted) admin en Gro
group has been added common en Group has been added.
group has been deleted common en Group has been deleted.
group has been updated common en Group has been updated.
+group hierarchy admin en Group hierarchy
group list admin en Group list
group manager admin en Group manager
group name admin en Group name
@@ -763,6 +766,7 @@ quota size in mbyte admin en Quota size in MByte
re-enter password admin en Re-enter password
read this list of methods. admin en Read this list of methods.
register application hooks admin en Register application hooks
+regular expression to find part to use as container admin en Regular expression to find part to use as container
reject passwords containing part of username or full name (3 or more characters long) admin en Reject passwords containing part of username or full name (3 or more characters long)
relay access checked admin en Relay access checked
remark admin en Remark
@@ -844,6 +848,7 @@ show as optional, but required once user has it setup admin en Show as optional,
show as required, but only once user has it setup admin en Show as required, but only once user has it setup
show current action admin en Show current action
show error log admin en Show error log
+show groups in container based on admin en Show groups in container based on
show members admin en Show members
show phpinfo() admin en Show phpinfo()
show session ip address admin en Show session IP address
diff --git a/admin/src/Groups.php b/admin/src/Groups.php
index 0aaeb6f0fb..f813725cd5 100644
--- a/admin/src/Groups.php
+++ b/admin/src/Groups.php
@@ -250,6 +250,9 @@ class Groups
{
$readonlys['button[save]'] = $readonlys['button[apply]'] = true;
}
+ // disable DN for LDAP, AD or synced groups were the real DN is stored here
+ $content['disable_dn'] = $GLOBALS['egw_info']['server']['account_repository'] !== 'sql' ||
+ !empty($GLOBALS['egw_info']['server']['account_import_source']) && $GLOBALS['egw_info']['server']['account_import_type'] !== 'users';
$tpl->exec('admin.'.self::class.'.edit', $content, $sel_options, $readonlys, $content, 2);
}
diff --git a/admin/templates/default/config.xet b/admin/templates/default/config.xet
index f2b9319631..8f12aa67bb 100644
--- a/admin/templates/default/config.xet
+++ b/admin/templates/default/config.xet
@@ -401,8 +401,7 @@
-
-
+
@@ -415,4 +414,4 @@
-
\ No newline at end of file
+
diff --git a/admin/templates/default/group.edit.xet b/admin/templates/default/group.edit.xet
index c22b2e4d6b..ae5d481612 100644
--- a/admin/templates/default/group.edit.xet
+++ b/admin/templates/default/group.edit.xet
@@ -43,6 +43,10 @@
+
+
+
+
diff --git a/admin/templates/default/site-config.xet b/admin/templates/default/site-config.xet
index 4d096f4916..96b242cdab 100644
--- a/admin/templates/default/site-config.xet
+++ b/admin/templates/default/site-config.xet
@@ -2,26 +2,26 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/api/src/Accounts/Sql.php b/api/src/Accounts/Sql.php
index 39589e3692..2b04fea945 100644
--- a/api/src/Accounts/Sql.php
+++ b/api/src/Accounts/Sql.php
@@ -565,7 +565,7 @@ class Sql
foreach($this->contacts->search($criteria,
array_merge(array(1,'n_given','n_family','id','created','modified','files',$this->table.'.account_id AS account_id'),$email_cols),
$order, "account_lid,account_type,account_status,account_expires,account_primary_group,account_description".
- ",account_lastlogin,account_lastloginfrom,account_lastpwd_change",
+ ",account_lastlogin,account_lastloginfrom,account_lastpwd_change,account_uuid,account_dn",
$wildcard,false,$query[0] == '!' ? 'AND' : 'OR',
!empty($param['offset']) ? array($param['start'], $param['offset']) : $param['start'] ?? false,
$filter,$join) ?? [] as $contact)
@@ -575,6 +575,8 @@ class Sql
$account_id = ($contact['account_type'] == 'g' ? -1 : 1) * $contact['account_id'];
$accounts[$account_id] = array(
'account_id' => $account_id,
+ 'account_dn' => $contact['account_dn'],
+ 'account_uuid' => $contact['account_uuid'],
'account_lid' => $contact['account_lid'],
'account_type' => $contact['account_type'],
'account_firstname' => $contact['n_given'],
diff --git a/api/src/Etemplate/Widget.php b/api/src/Etemplate/Widget.php
index 0d69e04579..136e18025f 100644
--- a/api/src/Etemplate/Widget.php
+++ b/api/src/Etemplate/Widget.php
@@ -1128,7 +1128,7 @@ class Widget
/**
* disables all cells with name == $name
*
- * @param sting $name cell-name
+ * @param string $name cell-name
* @param boolean $disabled =true disable or enable a cell, default true=disable
* @return reference to attribute
*/
diff --git a/api/src/Etemplate/Widget/Select.php b/api/src/Etemplate/Widget/Select.php
index eaab7e2bbb..579abccd63 100644
--- a/api/src/Etemplate/Widget/Select.php
+++ b/api/src/Etemplate/Widget/Select.php
@@ -448,9 +448,9 @@ class Select extends Etemplate\Widget
}
/**
- * Fix already html-encoded options, eg. "&nbps" AND optinal re-index array to keep order
+ * Fix already html-encoded options, e.g. "&nbps;" AND optional re-index array to keep order
*
- * Get run automatic for everything in $sel_options by etemplate_new::exec / etemplate_new::fix_sel_options
+ * Get run automatic for everything in $sel_options by Api\Etemplate::exec / Api\Etemplate::fix_sel_options
*
* @param array $options
* @param boolean $use_array_of_objects Re-indexes options, making everything more complicated
@@ -1104,6 +1104,44 @@ class Select extends Etemplate\Widget
$response = Api\Json\Response::get();
$response->data($options);
}
+
+ /**
+ * Get groups including container, if enabled
+ *
+ * Internally using Tree::groups() to not implement container logic again.
+ *
+ * @param array|null $tree
+ * @param string $prefix to use container as prefix instead of not working opt-groups
+ * @return array[]
+ * @todo fix options here or select-widget to understand and show opt-groups, incl. icons
+ */
+ public static function groups(?array $tree=null, $prefix='')
+ {
+ if (!isset($tree)) $tree = Tree::groups('');
+ $options = [];
+ foreach($tree as $group)
+ {
+ if (isset($group[Tree::CHILDREN]))
+ {
+ $options[/*$group[Tree::LABEL]*/] = [
+ 'value' => $group[Tree::LABEL],
+ 'label' => self::groups($group[Tree::CHILDREN], $group[Tree::LABEL]),
+ 'title' => $group[Tree::TOOLTIP] ?? null,
+ 'icon' => $group[Tree::IMAGE_FOLDER_CLOSED],
+ ];
+ }
+ else
+ {
+ $options[/*$group[Tree::ID]*/] = [
+ 'value' => $group[Tree::ID],
+ 'label' => ($prefix ? $prefix.': ' : '').$group[Tree::LABEL],
+ 'title' => $group[Tree::TOOLTIP],
+ 'icon' => $group[Tree::IMAGE_LEAF],
+ ];
+ }
+ }
+ return $options;
+ }
}
Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\Select', array('et2-select', 'selectbox', 'listbox', 'select',
diff --git a/api/src/Etemplate/Widget/Tree.php b/api/src/Etemplate/Widget/Tree.php
index dc8dde0104..6c8b4cf008 100644
--- a/api/src/Etemplate/Widget/Tree.php
+++ b/api/src/Etemplate/Widget/Tree.php
@@ -20,28 +20,28 @@ use EGroupware\Api;
*
* Example initialisation of tree via $sel_options array:
*
- * use Api\Etemplate\Widget\Tree as tree;
+ * use Api\Etemplate\Widget\Tree;
*
* $sel_options['tree'] = array(
- * tree::ID => 0, tree::CHILDREN => array( // ID of root has to be 0!
+ * Tree::ID => 0, Tree::CHILDREN => array( // ID of root has to be 0!
* array(
- * tree::ID => '/INBOX',
- * tree::LABEL => 'INBOX', tree::TOOLTIP => 'Your inbox',
- * tree::OPEN => 1, tree::IMAGE_FOLDER_OPEN => 'kfm_home.png', tree::IMAGE_FOLDER_CLOSED => 'kfm_home.png',
- * tree::CHILDREN => array(
- * array(tree::ID => '/INBOX/sub', tree::LABEL => 'sub', tree::IMAGE_LEAF => 'folderClosed.gif'),
- * array(tree::ID => '/INBOX/sub2', tree::LABEL => 'sub2', tree::IMAGE_LEAF => 'folderClosed.gif'),
+ * Tree::ID => '/INBOX',
+ * Tree::LABEL => 'INBOX', Tree::TOOLTIP => 'Your inbox',
+ * Tree::OPEN => 1, Tree::IMAGE_FOLDER_OPEN => 'kfm_home.png', Tree::IMAGE_FOLDER_CLOSED => 'kfm_home.png',
+ * Tree::CHILDREN => array(
+ * array(Tree::ID => '/INBOX/sub', Tree::LABEL => 'sub', Tree::IMAGE_LEAF => 'folderClosed.gif'),
+ * array(Tree::ID => '/INBOX/sub2', Tree::LABEL => 'sub2', Tree::IMAGE_LEAF => 'folderClosed.gif'),
* ),
- * tree::CHECKED => true,
+ * Tree::CHECKED => true,
* ),
* array(
- * tree::ID => '/user',
- * tree::LABEL => 'user',
- * tree::CHILDREN => array(
- * array(tree::ID => '/user/birgit', tree::LABEL => 'birgit', tree::IMAGE_LEAF => 'folderClosed.gif'),
- * array(tree::ID => '/user/ralf', tree::LABEL => 'ralf', tree::AUTOLOAD_CHILDREN => 1),
+ * Tree::ID => '/user',
+ * Tree::LABEL => 'user',
+ * Tree::CHILDREN => array(
+ * array(Tree::ID => '/user/birgit', Tree::LABEL => 'birgit', Tree::IMAGE_LEAF => 'folderClosed.gif'),
+ * array(Tree::ID => '/user/ralf', Tree::LABEL => 'ralf', Tree::AUTOLOAD_CHILDREN => 1),
* ),
- * tree::NOCHECKBOX => true
+ * Tree::NOCHECKBOX => true
* ),
* ));
*
@@ -550,4 +550,113 @@ class Tree extends Etemplate\Widget
}
return $category;
}
+
+ /**
+ * Fix userdata as understood by tree
+ *
+ * @param array $data
+ * @return array
+ */
+ public static function fixUserdata(array $data)
+ {
+ // store link as userdata, maybe we should store everything not directly understood by tree this way ...
+ foreach(array_diff_key($data, array_flip([
+ self::ID, self::LABEL, self::TOOLTIP, self::IMAGE_LEAF, self::IMAGE_FOLDER_OPEN, self::IMAGE_FOLDER_CLOSED,
+ 'item', self::AUTOLOAD_CHILDREN, 'select', self::OPEN, 'call',
+ ])) as $name => $content)
+ {
+ $data['userdata'][] = array(
+ 'name' => $name,
+ 'content' => $content,
+ );
+ unset($data[$name]);
+ }
+ return $data;
+ }
+
+
+ /**
+ * Get list of all groups as tree, taking container into account, if enabled
+ *
+ * @param string $root root for building tree-IDs, "" for just using IDs, no path
+ * @return array[] with tree-children, groups have IDs $root/$account_id (independent of container!), while container use $root/md5($container_name)
+ */
+ public static function groups(string $root='/groups')
+ {
+ if ($root) $root = rtrim($root, '/').'/';
+ $group_container_attr = $GLOBALS['egw_info']['server']['group_container_attribute'] ?? '';
+ static $default_regexp = [
+ 'account_lid' => '/^([^ ]+) /',
+ 'account_dn' => '/,CN=([^,]+),/i',
+ ];
+ $group_container_regexp = $GLOBALS['egw_info']['server']['group_container_regexp'] ?? $default_regexp[$group_container_attr] ?? null;
+ $group_container_replace = $GLOBALS['egw_info']['server']['group_container_replace'] ?? '$1';
+
+ $children = [];
+ foreach(Api\Accounts::getInstance()->search(array(
+ 'type' => 'groups',
+ 'order' => 'account_lid',
+ 'sort' => 'ASC',
+ 'start' => false, // to NOT limit number of returned groups
+ )) as $group)
+ {
+ if ($group_container_attr && !empty($group[$group_container_attr]) &&
+ preg_match($group_container_regexp, $group[$group_container_attr], $matches) &&
+ ($container_name = ucfirst($matches[substr($group_container_replace, 1)] ?? '')))
+ {
+ foreach($children as &$container)
+ {
+ if ($container[Tree::LABEL] === $container_name) break;
+ }
+ if ($container[Tree::LABEL] !== $container_name)
+ {
+ $children[] = self::fixUserdata([
+ Tree::LABEL => $container_name,
+ Tree::ID => $root.md5($container_name),
+ Tree::IMAGE_FOLDER_OPEN => Api\Image::find('api', 'dhtmlxtree/folderOpen'),
+ Tree::IMAGE_FOLDER_CLOSED => Api\Image::find('api', 'dhtmlxtree/folderClosed'),
+ Tree::CHILDREN => [],
+ ]);
+ $container =& $children[count($children)-1];
+ }
+ $container[Tree::CHILDREN][] = self::fixUserdata([
+ Tree::LABEL => $group['account_lid'],
+ Tree::TOOLTIP => $group['account_description'],
+ Tree::ID => $root.$group['account_id'],
+ Tree::IMAGE_LEAF => Api\Image::find('addressbook', 'group'),
+ ]);
+ }
+ else
+ {
+ $children[] = self::fixUserdata([
+ Tree::LABEL => $group['account_lid'],
+ Tree::TOOLTIP => $group['account_description'],
+ Tree::ID => $root.$group['account_id'],
+ Tree::IMAGE_LEAF => Api\Image::find('addressbook', 'group'),
+ ]);
+ }
+ }
+ // we need to sort (again), otherwise the containers would not be alphabetic sorted (Groups are already)
+ uasort($children, static function ($a, $b) {
+ return strnatcasecmp($a[Tree::LABEL], $b[Tree::LABEL]);
+ });
+ return $children;
+ }
+
+ /**
+ * Attribute Tree::Children='item' has to be an array (keys: 0, 1, ...), not object/associate array
+ *
+ * @param array $items
+ */
+ public static function stripChildrenKeys(array &$items)
+ {
+ $items = array_values($items);
+ foreach($items as &$item)
+ {
+ if (is_array($item) && isset($item[self::CHILDREN]))
+ {
+ self::stripChildrenKeys($item[self::CHILDREN]);
+ }
+ }
+ }
}
\ No newline at end of file