From 9224c543043670103df476b4ad23801e16af33aa Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Wed, 25 Sep 2024 17:10:36 +0200 Subject: [PATCH] select_object & LSselect: Improve perfs to allow to handle large related objects collection --- .../class/class.LSattr_html_select_object.php | 243 ++++++++++-------- .../class.LSformElement_select_object.php | 3 +- src/includes/class/class.LSldap.php | 13 +- src/includes/class/class.LSldapObject.php | 46 +++- src/includes/class/class.LSselect.php | 71 +++-- src/includes/functions.php | 9 + .../LSformElement_select_object_field.tpl | 10 +- 7 files changed, 254 insertions(+), 141 deletions(-) diff --git a/src/includes/class/class.LSattr_html_select_object.php b/src/includes/class/class.LSattr_html_select_object.php index 29490b67..ba2d3ef9 100644 --- a/src/includes/class/class.LSattr_html_select_object.php +++ b/src/includes/class/class.LSattr_html_select_object.php @@ -174,8 +174,8 @@ class LSattr_html_select_object extends LSattr_html{ * * @param mixed $values array|null Array of the input values () * @param boolean $fromDNs boolean If true, considered provided values as DNs (default: false) - * @param boolean $retrieveAttrValues boolean If true, attribute values will be returned instead - * of selected objects info (default: false) + * @param boolean $retrieveAttrValues boolean If true, final attribute values will be returned + * instead of selected objects info (default: false) * * @author Benjamin Renard * @@ -193,139 +193,172 @@ class LSattr_html_select_object extends LSattr_html{ self :: log_warning('getFormValues(): $values is not array'); return false; } + if (!LSsession :: loadLSclass("LSsearch")) + return false; // Retrieve/check selectable objects config - $objs = array(); + $objs = []; $confs = $this -> getSelectableObjectsConfig($objs); if (!is_array($confs) || empty($confs)) { self :: log_warning('getFormValues(): invalid selectable objects config'); return false; } - $selected_objects = array(); - $unrecognizedValues = array(); - $found_values = array(); + $selected_objects = []; + $found_values = []; + $unrecognizedValues = []; foreach ($confs as $conf) { + $common_search_params = [ + "filter" => $conf['filter'], + "attributes" => ( + $retrieveAttrValues && !in_array($conf['value_attribute'], ["dn", "%{dn}"])? + [$conf['value_attribute']]: + [$objs[$conf['object_type']]->rdn_attr] + ), + "displayFormat" => ( + $conf['display_name_format']? + $conf['display_name_format']: + $objs[$conf['object_type']] -> getDisplayNameFormat() + ), + ]; foreach($values as $value) { - // If we already mark its value as unrecognized, pass - if (in_array($value, $unrecognizedValues)) + // Ignore empty value and value already marked as unrecognized + if (empty($value) || in_array($value, $unrecognizedValues)) continue; - // Ignore empty value - if (empty($value)) - continue; - - // Determine value attribute: DN/attribute valued (or force form DNs) - if(($conf['value_attribute']=='dn') || ($conf['value_attribute']=='%{dn}') || $fromDNs) { - // Construct resulting list from DN values - if ($conf['onlyAccessible']) { - if (!LSsession :: canAccess($conf['object_type'], $value)) { - self :: log_debug("getFormValues(): ".$conf['object_type']."($value): not accessible, pass"); - continue; - } - } - - // Load object data (with custom filter if defined) - if(!$objs[$conf['object_type']] -> loadData($value, $conf['filter'])) { - self :: log_debug("getFormValues(): ".$conf['object_type']."($value): not found, pass"); + // Compute search params based on value attribute type (DN or attribute valued) and $fromDNs + if($fromDNs || in_array($conf['value_attribute'], ['dn', '%{dn}'])) { + if (!checkDn($value)) { + self :: log_warning( + "getFormValues(): value '$value' is not a valid DN, pass" + ); continue; } - self :: log_debug("getFormValues(): ".$conf['object_type']."($value): found"); - - // Check if it's the first this value match with an object - if (isset($found_values[$value])) { - // DN match with multiple object type - LSerror :: addErrorCode('LSattr_html_select_object_03',array('val' => $value, 'attribute' => $this -> name)); - $unrecognizedValues[] = $value; - unset($selected_objects[$found_values[$value]]); - break; + if ($conf['onlyAccessible'] && !LSsession :: canAccess($conf['object_type'], $value)) { + self :: log_debug( + "getFormValues(): object {$conf['object_type']} '$value' is not accessible, pass" + ); + continue; } - $found_values[$value] = $value; + $search_params = array_merge( + $common_search_params, + ["basedn" => $value, "scope" => "base"] + ); + } + else { + $filter = Net_LDAP2_Filter::create($conf['value_attribute'], 'equals', $value); + $search_params = array_merge( + $common_search_params, + [ + "filter" => ( + $common_search_params["filter"]? + LSldap::combineFilters('and', [$common_search_params["filter"], $filter]): + $filter + ), + ] + ); + } - if ($retrieveAttrValues) { - // Retrieve attribute value case: $selected_objects[dn] = attribute value - if(($conf['value_attribute']=='dn') || ($conf['value_attribute']=='%{dn}')) { - $selected_objects[$value] = $value; - } - else { - $val = $objs[$conf['object_type']] -> getValue($conf['value_attribute']); - if (!empty($val)) { - $selected_objects[$value] = $val[0]; - } - else { - LSerror :: addErrorCode( - 'LSattr_html_select_object_06', - array( - 'name' => $objs[$conf['object_type']] -> getDisplayName($conf['display_name_format']), - 'attr' => $this -> name - ) - ); - } - } + // Search object + $LSsearch = new LSsearch( + $conf['object_type'], + 'LSattr_html_select_object::getFormValues', + $search_params, + true + ); + + if(!$LSsearch -> run(false)) { + self :: log_warning( + "getFormValues(): error during search of object(s) {$conf['object_type']} ". + "for value '$value', pass" + ); + continue; + } + + $entries = $LSsearch -> listEntries(); + if (!is_array($entries) || empty($entries)) { + self :: log_debug( + "getFormValues(): value '$value' not found as {$conf['object_type']}, pass" + ); + continue; + } + + if (count($entries) > 1) { + self :: log_warning( + "getFormValues(): ".count($entries)." objects {$conf['object_type']} found ". + "for value '$value', pass: ".implode(" / ", array_keys($entries)) + ); + if (array_key_exists($value, $selected_objects)) + unset($selected_objects[$value]); + $unrecognizedValues[] = $value; + $found_values[$value] = ( + array_key_exists($value, $found_values)? + array_merge($found_values[$value], array_keys($entries)): + array_keys($entries) + ); + break; + } + $entry = $entries[key($entries)]; + self :: log_debug( + "getFormValues(): value '$value' found as {$conf['object_type']}: {$entry->dn}" + ); + + // Check if it's the first this value match with an object + if (array_key_exists($value, $found_values)) { + // DN match with multiple object type + LSerror :: addErrorCode( + 'LSattr_html_select_object_03', + ['val' => $value, 'attribute' => $this -> name] + ); + unset($selected_objects[$value]); + $unrecognizedValues[] = $value; + $found_values[$value][] = $entry->dn; + break; + } + $found_values[$value] = [$entry->dn]; + + if ($retrieveAttrValues) { + // Retrieve attribute value case: $selected_objects[dn] = attribute value + if(($conf['value_attribute']=='dn') || ($conf['value_attribute']=='%{dn}')) { + $selected_objects[$entry->dn] = $entry->dn; } else { - // General case: $selected_objects[dn] = array(name + object_type) - $selected_objects[$value] = array( - 'name' => $objs[$conf['object_type']] -> getDisplayName($conf['display_name_format']), - 'object_type' => $conf['object_type'], - ); - self :: log_debug("getFormValues(): ".$conf['object_type']."($value): ".varDump($selected_objects[$value])); + $val = ensureIsArray($entry -> get($conf['value_attribute'])); + if (!empty($val)) { + $selected_objects[$entry->dn] = $val[0]; + } + else { + LSerror :: addErrorCode( + 'LSattr_html_select_object_06', + array( + 'name' => $entry -> displayName, + 'attr' => $this -> name + ) + ); + } } } else { - // Construct resulting list from attributes values - $filter = Net_LDAP2_Filter::create($conf['value_attribute'], 'equals', $value); - if (isset($conf['filter'])) - $filter = LSldap::combineFilters('and', array($filter, $conf['filter'])); - $sparams = array(); - $sparams['onlyAccessible'] = $conf['onlyAccessible']; - $listobj = $objs[$conf['object_type']] -> listObjectsName( - $filter, - NULL, - $sparams, - $conf['display_name_format'] + // General case: $selected_objects[dn] = array(name + object_type) + $selected_objects[$entry->dn] = array( + 'name' => $entry -> displayName, + 'object_type' => $conf['object_type'], + ); + self :: log_debug( + "getFormValues(): object {$conf['object_type']} info for value '$value': ". + varDump($selected_objects[$entry->dn]) ); - if (count($listobj)==1) { - if (isset($found_values[$value])) { - // Value match with multiple object type - LSerror :: addErrorCode('LSattr_html_select_object_03',array('val' => $value, 'attribute' => $this -> name)); - $unrecognizedValues[] = $value; - unset($selected_objects[$found_values[$value]]); - break; - } - $dn = key($listobj); - $selected_objects[$dn] = array( - 'name' => $listobj[$dn], - 'object_type' => $conf['object_type'], - ); - $found_values[$value] = $dn; - } - else if(count($listobj) > 1) { - LSerror :: addErrorCode('LSattr_html_select_object_03',array('val' => $value, 'attribute' => $this -> name)); - if (!in_array($value, $unrecognizedValues)) - $unrecognizedValues[] = $value; - break; - } } } } - // Check if all values have been found (or already considered as unrecognized) - foreach ($values as $value) { - if (!isset($found_values[$value]) && !in_array($value, $unrecognizedValues)) { - self :: log_debug("getFormValues(): value '$value' not recognized"); - $unrecognizedValues[] = $value; - } - } - // Retrieve attribute values case: return forged array values (list of attribute values) if ($retrieveAttrValues) return array_values($selected_objects); // General case - self :: log_debug("getFormValues(): unrecognizedValues=".varDump($unrecognizedValues)); - $this -> unrecognizedValues = $unrecognizedValues; - + $this -> unrecognizedValues = array_diff($values, array_keys($found_values)); + self :: log_debug("getFormValues(): unrecognizedValues=".varDump($this -> unrecognizedValues)); self :: log_debug("getFormValues(): final values=".varDump($selected_objects)); return $selected_objects; } @@ -347,7 +380,7 @@ class LSattr_html_select_object extends LSattr_html{ /** - * Return array of atttribute values form array of form values + * Return array of attribute values form array of form values * * @param mixed $values Array of form values * diff --git a/src/includes/class/class.LSformElement_select_object.php b/src/includes/class/class.LSformElement_select_object.php index fddd912b..c8702b76 100644 --- a/src/includes/class/class.LSformElement_select_object.php +++ b/src/includes/class/class.LSformElement_select_object.php @@ -140,7 +140,8 @@ class LSformElement_select_object extends LSformElement { $this -> attr_html -> getLSselectId(), $select_conf, boolval($this -> getParam('multiple', 0, 'int')), - $this -> values + $this -> values, + false ); return True; } diff --git a/src/includes/class/class.LSldap.php b/src/includes/class/class.LSldap.php index b1a3a555..31c84627 100644 --- a/src/includes/class/class.LSldap.php +++ b/src/includes/class/class.LSldap.php @@ -462,8 +462,17 @@ class LSldap extends LSlog_staticLoggerClass { * * @return boolean True if entry exists, false otherwise */ - public static function exists($dn) { - return is_a(self :: getLdapEntry($dn), 'Net_LDAP2_Entry'); + public static function exists($dn, $filter=null) { + $entry = self :: search( + $filter?$filter:"(objectClass=*)", + $dn, + [ + "scope" => "base", + "attronly" => true, + "attributes" => ["objectClass"], + ] + ); + return boolval($entry); } /** diff --git a/src/includes/class/class.LSldapObject.php b/src/includes/class/class.LSldapObject.php index ff385a19..22c2cabb 100644 --- a/src/includes/class/class.LSldapObject.php +++ b/src/includes/class/class.LSldapObject.php @@ -149,6 +149,23 @@ class LSldapObject extends LSlog_staticLoggerClass { } } + /** + * Check if the specified object exists + * @param string $dn Object's DN + * @param string|Net_LDAP2_Filter|null $filter Extra LDAP that object must match (optional, default: null) + * @return bool + */ + public static function exists($dn, $filter=null) { + return LSldap::exists( + $dn, + ( + $filter? + LSldap::combineFilters("and", [static :: _getObjectFilter(), $filter]): + static :: _getObjectFilter() + ) + ); + } + /** * Load object data from LDAP * @@ -901,40 +918,43 @@ class LSldapObject extends LSlog_staticLoggerClass { } /** - * Retourne le filtre correpondants aux objetcClass de l'objet courant + * Return LDAP filter string of the current object type * * @author Benjamin Renard * - * @return Net_LDAP2_Filter le filtre ldap correspondant au type de l'objet + * @return Net_LDAP2_Filter LDAP filter as a Net_LDAP2_Filter object, or false in case of error */ public function getObjectFilter() { return self :: _getObjectFilter($this -> type_name); } /** - * Retourne le filtre correpondants aux objetcClass de l'objet + * Return object type LDAP filter string + * + * @param string|null $type Object type (optional, default: called class) * * @author Benjamin Renard * - * @return Net_LDAP2_Filter|false le filtre ldap correspondant au type de l'objet, ou false + * @return Net_LDAP2_Filter|false LDAP filter as a Net_LDAP2_Filter object, or false in case of error */ - public static function _getObjectFilter($type) { - $oc=LSconfig::get("LSobjects.$type.objectclass"); - if(!is_array($oc)) return false; - $filters=array(); + public static function _getObjectFilter($type=null) { + $type = $type?$type:get_called_class(); + $oc = LSconfig::get("LSobjects.$type.objectclass"); + if(!is_array($oc) || !$oc) return false; + $filters = []; foreach ($oc as $class) { - $filters[]=Net_LDAP2_Filter::create('objectClass','equals',$class); + $filters[] = Net_LDAP2_Filter::create('objectClass', 'equals', $class); } - $filter=LSconfig::get("LSobjects.$type.filter"); + $filter = LSconfig::get("LSobjects.$type.filter"); if ($filter) { - $filters[]=Net_LDAP2_Filter::parse($filter); + $filters[] = Net_LDAP2_Filter::parse($filter); } - $filter = LSldap::combineFilters('and',$filters); + $filter = LSldap::combineFilters('and', $filters); if ($filter) return $filter; - LSerror :: addErrorCode('LSldapObject_30',$type); + LSerror :: addErrorCode('LSldapObject_30', $type); return false; } diff --git a/src/includes/class/class.LSselect.php b/src/includes/class/class.LSselect.php index 4f12b8f5..5f47fb38 100644 --- a/src/includes/class/class.LSselect.php +++ b/src/includes/class/class.LSselect.php @@ -51,9 +51,10 @@ class LSselect extends LSlog_staticLoggerClass { * @param boolean $multiple True if this selection permit to select more than one object, False otherwise (optional, * default: false) * @param array|null $current_selected_objects Array of current selected objects (optional, see setSelectedObjects for format specification) + * @param bool $check_objects Check selected objects (optional, default: true) * @return void */ - public static function init($id, $LSobjects, $multiple=false, $current_selected_objects=null) { + public static function init($id, $LSobjects, $multiple=false, $current_selected_objects=null, $check_objects=true) { if ( !isset($_SESSION['LSselect']) || !is_array($_SESSION['LSselect'])) $_SESSION['LSselect'] = array(); $_SESSION['LSselect'][$id] = array ( @@ -62,8 +63,11 @@ class LSselect extends LSlog_staticLoggerClass { 'selected_objects' => array(), ); if (is_array($current_selected_objects)) - self :: setSelectedObjects($id, $current_selected_objects); - self :: log_debug("Initialized with id=$id: multiple=".($multiple?'yes':'no')." ".count($_SESSION['LSselect'][$id]['selected_objects'])." selected object(s)."); + self :: setSelectedObjects($id, $current_selected_objects, $check_objects); + self :: log_debug( + "Initialized with id=$id: multiple=".($multiple?'yes':'no')." ". + count($_SESSION['LSselect'][$id]['selected_objects'])." selected object(s)." + ); } /** @@ -216,6 +220,7 @@ class LSselect extends LSlog_staticLoggerClass { * @param array $selected_objects Array of selectable object info with objects's DN * as key and array of object's info as value. Objects's * info currently contains only the object type (key=object_type). + * @param bool $check_objects Check selected objects (optional, default: true) * * @return array|false Array of selectable object info with objects's DN as key * and array of object's info as value. Objects's info returned @@ -224,24 +229,37 @@ class LSselect extends LSlog_staticLoggerClass { * * @return void */ - public static function setSelectedObjects($id, $selected_objects) { + public static function setSelectedObjects($id, $selected_objects, $check_objects=true) { if (!self :: exists($id)) return; if (!is_array($selected_objects)) return; - $_SESSION['LSselect'][$id]['selected_objects'] = array(); + $previously_selected = $_SESSION['LSselect'][$id]['selected_objects']; + $_SESSION['LSselect'][$id]['selected_objects'] = []; foreach($selected_objects as $dn => $info) { if (!is_array($info) || !isset($info['object_type'])) { self :: log_warning("setSelectedObjects($id): invalid object info for dn='$dn'"); continue; } - if (self :: checkObjectIsSelectable($id, $info['object_type'], $dn)) - $_SESSION['LSselect'][$id]['selected_objects'][$dn] = $info; - else { - self :: log_warning("setSelectedObjects($id): object type='".$info['object_type']."' and dn='$dn' is not selectable".varDump($_SESSION['LSselect'][$id])); + if ( + $check_objects + && !( + in_array($dn, $previously_selected) + || self :: checkObjectIsSelectable($id, $info['object_type'], $dn) + ) + ) { + self :: log_warning( + "setSelectedObjects($id): object type='{$info['object_type']}' and dn='$dn' is not ". + "selectable" + ); + continue; } + $_SESSION['LSselect'][$id]['selected_objects'][$dn] = $info; } - self :: log_debug("id=$id: updated with ".count($_SESSION['LSselect'][$id]['selected_objects'])." selected object(s)."); + self :: log_debug( + "setSelectedObjects($id): updated with ". + count($_SESSION['LSselect'][$id]['selected_objects'])." selected object(s)." + ); } /** @@ -250,29 +268,46 @@ class LSselect extends LSlog_staticLoggerClass { * @param string $id The LSselect ID * @param string $object_type The object type * @param string $object_dn The object DN + * @param bool $check_exists Check if object exists in LDAP (optional, default: true) * * @return boolean True if object is selectable, false otherwise */ - public static function checkObjectIsSelectable($id, $object_type, $object_dn) { + public static function checkObjectIsSelectable($id, $object_type, $object_dn, $check_exists=true) { if (!self :: exists($id)) { - self :: log_warning("checkObjectIsSelectable($id, $object_type, $object_dn): LSselect $id doesn't exists"); + self :: log_warning( + "checkObjectIsSelectable($id, $object_type, $object_dn): LSselect $id doesn't exists" + ); return false; } if (!array_key_exists($object_type, $_SESSION['LSselect'][$id]['LSobjects'])) { - self :: log_warning("checkObjectIsSelectable($id, $object_type, $object_dn): object type $object_type not selectabled"); + self :: log_warning( + "checkObjectIsSelectable($id, $object_type, $object_dn): object type $object_type not ". + "selectable" + ); return false; } // Load LSobject type if ( !LSsession :: loadLSobject($object_type) ) { - self :: log_warning("checkObjectIsSelectable($id, $object_type, $object_dn): fail to load object type $object_type"); + self :: log_warning( + "checkObjectIsSelectable($id, $object_type, $object_dn): fail to load object type ". + $object_type + ); return false; } - // Instanciate object and load object data from DN - $object = new $object_type(); - if (!$object -> loadData($object_dn, self :: getConfig($id, "LSobjects.$object_type.filter", null))) { - self :: log_warning("checkObjectIsSelectable($id, $object_type, $object_dn): object $object_dn not found (or does not match with selection filter)"); + // Check object exists + if ( + $check_exists + && !$object_type :: exists( + $object_dn, + self :: getConfig($id, "LSobjects.$object_type.filter", null, "string") + ) + ) { + self :: log_warning( + "checkObjectIsSelectable($id, $object_type, $object_dn): object $object_dn not found ". + "(or does not match with selection filter)" + ); return false; } diff --git a/src/includes/functions.php b/src/includes/functions.php index 7c131e03..14376ac7 100644 --- a/src/includes/functions.php +++ b/src/includes/functions.php @@ -329,6 +329,15 @@ function LSdebugDefined() { ); } +/** + * Check specified value is a valid DN + * @param mixed $dn + * @return bool + */ +function checkDn($dn) { + return is_string($dn) && boolval(@ldap_explode_dn($dn, 0)); +} + /** * VĂ©rifie la compatibilite des DN * diff --git a/src/templates/default/LSformElement_select_object_field.tpl b/src/templates/default/LSformElement_select_object_field.tpl index 1b2551b8..9c574ecb 100644 --- a/src/templates/default/LSformElement_select_object_field.tpl +++ b/src/templates/default/LSformElement_select_object_field.tpl @@ -1,6 +1,12 @@ {if $dn} - {$info.name|escape:"htmlall"} - {if !$freeze}{/if} + + {$info.name|escape:"htmlall"} + + {if !$freeze} + + {/if} {else} {$noValueTxt|escape:"htmlall"} {/if}