Add possibily to make global search using API

This commit is contained in:
Benjamin Renard 2024-03-28 12:06:59 +01:00
parent bd98a8b8ef
commit 2db4d0fbae
Signed by: bn8
GPG key ID: 3E2E1CE1907115BC
3 changed files with 402 additions and 4 deletions

View file

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

View file

@ -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<string,mixed>|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) {

View file

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