From 2db4d0fbae90cf2e3cfbecc364dd6136b1e557d4 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Thu, 28 Mar 2024 12:06:59 +0100 Subject: [PATCH] Add possibily to make global search using API --- doc/src/api/index.md | 168 ++++++++++++++++++- src/includes/class/class.LSsearch.php | 9 +- src/includes/routes.php | 229 ++++++++++++++++++++++++++ 3 files changed, 402 insertions(+), 4 deletions(-) diff --git a/doc/src/api/index.md b/doc/src/api/index.md index 1daa3300..dc557119 100644 --- a/doc/src/api/index.md +++ b/doc/src/api/index.md @@ -176,7 +176,8 @@ HTTP 404 sera générée. Permet de réclamer un résultat de recherche dans lequel, la clé `objects` sera une liste et non un dictionnaire. Dans ce cas, le DN de l'objet est fourni dans la clé `dn` des détails - des objets. + des objets. Seul la présence de ce paramètre suffit à activer ce comportement, sa valeur n'a pas + d'importance. - `withoutCache` @@ -560,3 +561,168 @@ HTTP 404 sera générée. ] } ``` + +- `/api/1.0/search` + + Cette méthode permet d'effectuer une recherche sur plusieurs types d'objets de l'annuaire à la + fois. Par mimétisme du comportement de l'interface web, la recherche est paginée et accepte des + paramètres similaires en plus de paramètre plus appropriés à un fonctionnement programmatique. + + Les paramètres acceptés par cette méthode sont sensiblement les mêmes que ceux acceptés par la + méthode de recherche d'un type d'objet de l'annuaire en particulier et ils ne seront donc pas + tous redocumentés ici : + + - `filter` + + - `predefinedFilter` + + - `pattern` + + - `approx` + + - `basedn` + + - `subDn` + + - `scope` + + - `recursive` + + - `displayFormat` + + - `extraDisplayedColumns` + + - `attributes` + + - `attributesDetails` + + - `page` + + - `all` + + - `as_list` + + - `withoutCache` + + - `keepParamsBetweenSearches` + + - `nbObjectsByPage` + + Permet de préciser le nombre maximum d'objets retournés par type d'objet ET par page du résultat + de recherche. + + - `types` + + Permet de limiter les types d'objets à inclure dans le résultat de recherche. Par défaut, tous + les types d'objets auxquels l'utilisateur à accès et dont la recherche globale n'est pas + désactivée seront inclus. + + - `splited_result` + + Permet de faire en sorte que les objets inclus dans le résultat de recherche soient séparés par + type dans des sous-clés de `objects`. Par défaut, tous les objets sont retournés dans la clé + `objects` et une sous-clé `type` est ajouté à chacun d'eux pour les distinguer. Seul la présence + de ce paramètre suffit à activer ce comportement, sa valeur n'a pas d'importance. + +!!! important + + **Pour chaque type d'objets inclus dans la recherche, un filtre et/ou un mot clé de recherche + doit être spécifié.** Cette méthode n'a pas vocation à permettre de lister tous les objets de + l'annuaire. + + + **Exemple :** + + ``` + # curl -u username:secret 'https://ldapsaisie/api/api/1.0/search?pattern=LdapSaisie&pretty' + { + "success": true, + "objects": { + "uid=s.ldapsaisie,ou=people,o=ls": { + "name": "Secretariat LdapSaisie", + "type": "LSpeople", + "Mail": "secretariat@ldapsaisie.biz" + }, + "uid=ls,ou=people,o=ls": { + "name": "LdapSaisie", + "type": "LSpeople", + "Mail": "ldap.saisie@ls.com" + }, + "uid=erwpa,ou=people,o=ls": { + "name": "Erwan PAGE", + "type": "LSpeople", + "Mail": "erwan.page@ldapsaisie.biz" + }, + "uid=invite,ou=people,o=ls": { + "name": "Utilisateur de passage", + "type": "LSpeople", + "Mail": "invite@ldapsaisie.biz" + }, + "uid=demo,ou=people,o=ls": { + "name": "Demonstration LdapSaisie", + "type": "LSpeople", + "Mail": "demo@ls.com" + }, + "uid=admin,ou=people,o=ls": { + "name": "Administration LdapSaisie", + "type": "LSpeople", + "Mail": "admin@ls.com" + }, + "uid=admin3,ou=people,o=ls": { + "name": "ZAdministration LdapSaisie", + "type": "LSpeople", + "Mail": "admin@ls.com" + }, + "uid=ldapsaisie,ou=sysaccounts,o=ls": { + "name": "ldapsaisie", + "type": "LSsysaccount" + } + }, + "total": null, + "params": { + "keepParamsBetweenSearches": false, + "LSpeople": { + "filter": null, + "pattern": "LdapSaisie", + "predefinedFilter": false, + "basedn": null, + "scope": null, + "sizelimit": 0, + "attronly": false, + "approx": false, + "recursive": true, + "attributes": [], + "onlyAccessible": true, + "sortDirection": null, + "sortBy": null, + "sortlimit": 0, + "displayFormat": "%{cn}", + "nbObjectsByPage": 25, + "withoutCache": false, + "extraDisplayedColumns": true + }, + "LSsysaccount": { + "filter": null, + "pattern": "LdapSaisie", + "predefinedFilter": false, + "basedn": null, + "scope": null, + "sizelimit": 0, + "attronly": false, + "approx": false, + "recursive": false, + "attributes": [], + "onlyAccessible": true, + "sortDirection": null, + "sortBy": null, + "sortlimit": 0, + "displayFormat": "%{uid}", + "nbObjectsByPage": 30, + "withoutCache": false, + "extraDisplayedColumns": true + } + }, + "page": 1, + "nbPages": 1 + } + ``` diff --git a/src/includes/class/class.LSsearch.php b/src/includes/class/class.LSsearch.php index b63c07e0..c88c3ddb 100644 --- a/src/includes/class/class.LSsearch.php +++ b/src/includes/class/class.LSsearch.php @@ -658,11 +658,13 @@ class LSsearch extends LSlog_staticLoggerClass { } /** - * Define search parameters by reading request data ($_REQUEST) + * Define search parameters by reading request data + * + * @param array|null $request The request (optional, default: $_REQUEST) * * @return boolean True if all parameters found in request data are handled, False otherwise */ - public function setParamsFromRequest() { + public function setParamsFromRequest($request=null) { $allowedParams = array( 'pattern', 'approx', 'recursive', 'extraDisplayedColumns', 'nbObjectsByPage', 'attributes', 'sortBy', 'sortDirection', 'withoutCache', 'predefinedFilter', @@ -670,8 +672,9 @@ class LSsearch extends LSlog_staticLoggerClass { 'filter', 'basedn', 'subDn', 'scope', 'attributes', 'displayFormat', ); $data = array(); + $request = $request?$request:$_REQUEST; - foreach($_REQUEST as $key => $value) { + foreach($request as $key => $value) { if (!in_array($key, $allowedParams)) continue; switch($key) { diff --git a/src/includes/routes.php b/src/includes/routes.php index 966946f1..2614197a 100644 --- a/src/includes/routes.php +++ b/src/includes/routes.php @@ -1560,6 +1560,235 @@ function get_LSobject_from_API_request($request, $instanciate=true, $check_acces return get_LSobject_from_request($request, $instanciate, $check_access, true); } +/** + * Handle API global search request + * @param LSurlRequest $request The request + * @return void + */ +function handle_api_global_search($request) { + // Check global search is enabled + if (!LSsession :: globalSearch()) { + LSurl :: error_404($request); + return; + } + + if (!LSsession :: loadLSclass('LSsearch')) { + LSerror :: addErrorCode('LSsession_05','LSsearch'); + LSsession :: displayAjaxReturn(); + return; + } + + if (!LSsession :: loadLSclass('LSform')) { + LSerror :: addErrorCode('LSsession_05','LSform'); + LSsession :: displayAjaxReturn(); + return; + } + + $onlyLSobjects = (isset($_REQUEST['types'])?ensureIsArray($_REQUEST['types']):[]); + $keepParamsBetweenSearches = ( + isset($_REQUEST['keepParamsBetweenSearches'])? + boolval($_REQUEST['keepParamsBetweenSearches']): + false + ); + $all = isset($_REQUEST['all']); + $page_nb = (isset($_REQUEST['page'])?(int)$_REQUEST['page']:1); + $allowedParams = array( + 'pattern', 'approx', 'recursive', 'extraDisplayedColumns', 'nbObjectsByPage', + 'attributes', 'withoutCache', 'predefinedFilter', 'filter', 'basedn', 'subDn', + 'scope', 'attributes', 'displayFormat', + ); + + // Handle JSON output + $data = array( + 'success' => true, + 'objects' => array(), + 'total' => 0, + 'params' => array( + 'keepParamsBetweenSearches' => $keepParamsBetweenSearches, + ), + ); + if (!$all) { + $data['page'] = $page_nb; + $data['nbPages'] = 1; + } + + foreach (LSsession :: getLSaccess() as $LSobject => $label) { + if ( $LSobject == "SELF" || !LSsession :: loadLSobject($LSobject) ) + continue; + if (!LSconfig::get("LSobjects.$LSobject.globalSearch", true, 'bool')) + continue; + if ($onlyLSobjects && !in_array($LSobject, $onlyLSobjects)) + continue; + + $object = new $LSobject(); + + $search = new LSsearch( + $LSobject, + 'api', + null, + !$keepParamsBetweenSearches + ); + $search -> setParam( + 'extraDisplayedColumns', + LSconfig::get("LSobjects.$LSobject.globalSearch_extraDisplayedColumns", true, 'bool') + ); + $search -> setParam('onlyAccessible', True); + $params = []; + foreach($_REQUEST as $key => $value) + if (in_array($key, $allowedParams)) + $params[$key] = $value; + if (isset($_REQUEST['type_params']) && isset($_REQUEST['type_params'][$LSobject])) + foreach(ensureIsArray($_REQUEST['type_params'][$LSobject]) as $key => $value) + if (in_array($key, $allowedParams)) + $params[$key] = $value; + + if ( + (!isset($params['pattern']) || !$params['pattern']) + && (!isset($params['filter']) || !$params['filter']) + ) { + LSerror :: addErrorCode( + false, + _("No pattern or filter provided for $LSobject (required in global search).") + ); + LSsession :: displayAjaxReturn(); + return; + } + + if (!$search -> setParamsFromRequest($params)) { + LSerror :: addErrorCode(false, "Invalid search parameters for $LSobject."); + LSsession :: displayAjaxReturn(); + return; + } + + // Run search + if (!$search -> run()) + LSlog :: fatal("Fail to run search on $LSobject."); + + if ($search -> total <= 0) + continue; + + $data['total'] += $search -> total; + + if ($all) { + $entries = $search -> listEntries(); + if (!is_array($entries)) + LSlog :: fatal("Fail to retrieve search result for $LSobject."); + } + else { + // Retrieve page + $page = $search -> getPage($page_nb); + + /* + * $page = array( + * 'nb' => $page, + * 'nbPages' => 1, + * 'list' => array(), + * 'total' => $this -> total + * ); + */ + + // Check page + if (!is_array($page)) + LSlog :: fatal("Fail to retrieve page #$page_nb for $LSobject."); + + if ($page['nb'] >= $data['page']) + $data['page'] = $page['nb']; + if ($page['nbPages'] >= $data['nbPages']) + $data['nbPages'] = $page['nbPages']; + } + + // Export search parameters + $exportedParams = array( + 'filter', 'pattern', 'predefinedFilter', 'basedn', 'scope', 'sizelimit', 'attronly', + 'approx', 'recursive', 'attributes', 'onlyAccessible', 'sortDirection', 'sortBy', 'sortlimit', + 'displayFormat', 'nbObjectsByPage', 'withoutCache', 'extraDisplayedColumns' + ); + if (LSsession :: subDnIsEnabled()) + $exportedParams = array_merge($exportedParams, array('displaySubDn', 'subDn')); + $data['params'][$LSobject] = []; + foreach ($exportedParams as $param) { + $data['params'][$LSobject][$param] = $search->getParam($param); + if ($param == 'filter' && $data['params'][$LSobject][$param]) + $data['params'][$LSobject][$param] = $data['params'][$LSobject][$param] -> as_string(); + } + + // Instanciate LSform export to handle custom requested attributes + $object = new $LSobject(); + $export = new LSform($object, 'export'); + foreach ($search -> attributes as $attr) { + if (array_key_exists($attr, $object -> attrs)) + $object -> attrs[$attr] -> addToExport($export); + } + + // Reset & increase time limit: allow one seconds by object to handle, + // with a minimum of 30 seconds + $timeout = count($all?$entries:$page['list']); // @phpstan-ignore-line + set_time_limit($timeout>30?$timeout:30); + + // Handle objects + $data['objects'][$LSobject] = []; + foreach(($all?$entries:$page['list']) as $obj) { // @phpstan-ignore-line + $data['objects'][$LSobject][$obj -> dn] = array( + 'name' => $obj -> displayName, + ); + // When as_list enabled, put object DN in object details (otherwise, it's the key) + if (isset($_REQUEST['as_list'])) + $data['objects'][$LSobject][$obj -> dn]['dn'] = $obj -> dn; + // When splited_result is disabled, put object type in object details (otherwise, present as key) + if (!isset($_REQUEST['splited_result'])) + $data['objects'][$LSobject][$obj -> dn]['type'] = $LSobject; + if ($search -> displaySubDn) + $data['objects'][$LSobject][$obj -> dn][$search -> label_level] = $obj -> subDn; + if ($search -> extraDisplayedColumns) { + foreach ($search -> visibleExtraDisplayedColumns as $cid => $conf) { + $data['objects'][$LSobject][$obj -> dn][$conf['label']] = $obj -> $cid; + } + } + foreach ($search -> attributes as $attr) { + if (!LSsession :: canAccess($LSobject, $obj -> dn, 'r', $attr)) + continue; + $export -> elements[$attr] -> setValue( + $object -> attrs[$attr] -> html -> refreshForm( + $object -> attrs[$attr] -> getFormVal($obj -> $attr) + ) + ); + $data['objects'][$LSobject][$obj -> dn][$attr] = $export -> elements[$attr] -> getApiValue( + isset($_REQUEST['attributesDetails']) + ); + } + } + + $search -> afterUsingResult(); + } + + if (!$all && $data['page'] > $data['nbPages']) { + LSerror :: addErrorCode( + false, + "Requested page too hight ({$data['page']} > {$data['nbPages']})." + ); + LSsession :: displayAjaxReturn(); + return; + } + + // Handle as_list parameter + if (isset($_REQUEST['as_list'])) + foreach(array_keys($data['objects']) as $LSobject) + $data['objects'][$LSobject] = array_values($data['objects'][$LSobject]); + + // Handle splited_result parameter + if (!isset($_REQUEST['splited_result'])) { + $objects = []; + foreach(array_keys($data['objects']) as $LSobject) { + $objects = array_merge($objects, $data['objects'][$LSobject]); + unset($data['objects'][$LSobject]); + } + $data['objects'] = $objects; + } + + LSsession :: displayAjaxReturn($data); +} +LSurl :: add_handler('#^api/1.0/search/?$#', 'handle_api_global_search', true, false, true); + /* * Handle API LSobject search *