* Admin: allow to show groups by container: e.g. LDAP DN or arbitrary part of name found by a regular expression

This commit is contained in:
ralf 2024-08-13 11:19:58 +02:00
parent 975a22bee1
commit 6432807096
11 changed files with 214 additions and 102 deletions

View File

@ -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

View File

@ -94,8 +94,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
@ -190,6 +190,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
@ -237,6 +238,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<br>(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. Ermöglicht mehrere EGroupware-Sitzungen mit unterschiedlichen Verzeichnissen..
@ -404,10 +406,10 @@ enter a passphrase if you would like to protect your private key by password. ad
enter some random text for app_session <br>encryption (requires mcrypt) admin de Zufälligen Text für app_session<br>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.<br>examples: /tmp, c:\temp admin de Vollständiger Pfad für temporäre Dateien.<br>Beispiel: /tmp, C:\TEMP
enter the full path for temporary files.<br>examples: /tmp, c:temp admin de Vollständiger Pfad für temporäre Dateien.<br>Beispiel: /tmp, C:\TEMP
enter the full path for users and group files.<br>examples: /files, e:\files admin de Vollständiger Pfad für Benutzer- und Gruppen-Dateien.<br>Beispiel: /files, E:\Files
enter the full path for temporary files.<br>examples: /tmp, c:\temp admin de Vollständiger Pfad für temporäre Dateien.<br>Beispiel: /tmp, C:\TEMP
enter the full path for users and group files.<br>examples: /files, e:files admin de Vollständiger Pfad für Benutzer- und Gruppen-Dateien.<br>Beispiel: /files, E:\Files
enter the full path for users and group files.<br>examples: /files, e:\files admin de Vollständiger Pfad für Benutzer- und Gruppen-Dateien.<br>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.<br>example: http://www.domain.com/egroupware &nbsp; or &nbsp; /egroupware<br><b>no trailing slash</b> admin de URL zur EGroupware-Installation.<br>Beispiel: https://egw.domain.com/egroupware or /egroupware<br><b>keinen nachfolgenden Slash /</b>
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
@ -498,6 +500,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
@ -754,6 +757,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
@ -833,6 +837,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

View File

@ -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
@ -237,6 +238,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.
@ -404,10 +406,10 @@ enter a passphrase if you would like to protect your private key by password. ad
enter some random text for app_session <br>encryption (requires mcrypt) admin en Enter some random text for app_session <br>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.<br>examples: /tmp, c:\temp admin en Enter the full path for temporary files.<br>Examples: /tmp, C:\TEMP
enter the full path for temporary files.<br>examples: /tmp, c:temp admin en Enter the full path for temporary files.<br>Examples: /tmp, C:\TEMP
enter the full path for users and group files.<br>examples: /files, e:\files admin en Enter the full path for users and group files.<br>Examples: /files, E:\FILES
enter the full path for temporary files.<br>examples: /tmp, c:\temp admin en Enter the full path for temporary files.<br>Examples: /tmp, C:\TEMP
enter the full path for users and group files.<br>examples: /files, e:files admin en Enter the full path for users and group files.<br>Examples: /files, E:\FILES
enter the full path for users and group files.<br>examples: /files, e:\files admin en Enter the full path for users and group files.<br>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.<br>example: http://www.domain.com/egroupware &nbsp; or &nbsp; /egroupware<br><b>no trailing slash</b> admin en Enter the location of EGroupware's URL.<br>Example: https://egw.domain.com/egroupware or /egroupware<br><b>No trailing slash</b>
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
@ -498,6 +500,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
@ -754,6 +757,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
@ -833,6 +837,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

View File

@ -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);
}

View File

@ -53,14 +53,26 @@
<option value="disabled">{No}</option>
</et2-select>
</row>
<!-- remove currently not supported/necessary development option debug_minify
<row>
<et2-description value="Disable minifying of javascript and CSS files" label="%s:"></et2-description>
<et2-select id="newsettings[debug_minify]">
<et2-description value="Group hierarchy" span="all" class="subHeader"></et2-description>
</row>
<row>
<et2-description value="Show groups in container based on" label="%s:"></et2-description>
<et2-select id="newsettings[group_container_attribute]">
<option value="">{No} - {Default}</option>
<option value="True">Yes</option>
<option value="account_lid">{Group name}: /^([^ ]+) / --> $1</option>
<option value="account_dn">LDAP DN: /,CN=([^,]+),/ --> $1</option>
</et2-select>
</row -->
</row>
<row>
<et2-description value="Regular expression to find part to use as container" label="%s:"></et2-description>
<et2-hbox>
<et2-textbox id="newsettings[group_container_regexp]" placeholder="/,CN=([^,]+),/"></et2-textbox>
<et2-description value="-->" style="position: relative; top: .5em"></et2-description>
<et2-textbox id="newsettings[group_container_replace]" placeholder="$1" width="100px"></et2-textbox>
</et2-hbox>
</row>
<row>
<et2-description value="Encryption" span="all" class="subHeader"></et2-description>
</row>
@ -390,7 +402,7 @@
</grid>
</template>
<template id="admin.config" template="" lang="" group="0" version="18.1">
<et2-tabbox id="tabs" width="100%">
<et2-tabbox id="tabs" width="100%" tabHeight="700">
<tabs>
<tab id="general" label="General"/>
<tab id="appearance" label="Appearance"/>

View File

@ -43,6 +43,10 @@
<et2-description value="Filesystem quota"></et2-description>
<et2-textbox id="quota" disabled="!@epl" placeholder="@default_quota"></et2-textbox>
</row>
<row>
<et2-description value="Container" for="account_dn"></et2-description>
<et2-textbox id="account_dn" maxlength="255" disabled="@disable_dn" statustext="Can be used to show groups by container, if enabled"></et2-textbox>
</row>
</rows>
</grid>
<et2-tabbox id="tabs" class="et2_nowrap" span="all" width="100%" tabHeight="250px">

View File

@ -14,7 +14,7 @@
<template template="@template" width="99%"/>
</row>
<row disabled="!@need_tab">
<et2-tabbox id="tabs2" width="100%">
<et2-tabbox id="tabs2" width="100%" tabHeight="700">
<tabs>
<tab id="config" label="Configuration"/>
</tabs>

View File

@ -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'],

View File

@ -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
*/

View File

@ -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',

View File

@ -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]);
}
}
}
}