286 lines
12 KiB
PHP
286 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* HAL API Class - Handles communication with HAL API
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class Thalim_HAL_API {
|
|
|
|
// Fields to retrieve from HAL (expanded for import preview)
|
|
private const FIELDS = 'halId_s,title_s,docType_s,authFullName_s,authIdHal_s,publicationDate_s,producedDate_s,submittedDate_s,journalTitle_s,bookTitle_s,uri_s,fileMain_s,abstract_s,keyword_s,publisher_s,modifiedDate_s,doiId_s,citationFull_s,conferenceTitle_s,city_s,country_s,defenseDate_s';
|
|
|
|
/**
|
|
* Fetch publications from THALIM structure
|
|
*
|
|
* @param int $rows Number of results to fetch
|
|
* @param int $start Offset
|
|
* @param string $sort Solr sort expression
|
|
* @param string $date_from Filter start date (YYYY-MM-DD), empty = no lower bound
|
|
* @param string $date_to Filter end date (YYYY-MM-DD), empty = no upper bound
|
|
*/
|
|
public function fetch_publications($rows = 100, $start = 0, $sort = 'producedDate_tdate desc', $date_from = '', $date_to = '', $author_hal_id = '') {
|
|
$url = $this->build_url($rows, $start, $sort, $date_from, $date_to, $author_hal_id);
|
|
return $this->request($url);
|
|
}
|
|
|
|
/**
|
|
* Fetch full HAL docs by a list of hal_ids (batched).
|
|
* Uses Solr fq=halId_s:(id1 OR id2 OR ...) syntax. No structId filter —
|
|
* fetch by halId exact, regardless of structure.
|
|
*
|
|
* @param string[] $hal_ids HAL IDs to fetch.
|
|
* @param int $batch Batch size (default 100).
|
|
* @return array|WP_Error Array keyed by halId_s, or WP_Error on failure.
|
|
*/
|
|
public function fetch_by_hal_ids(array $hal_ids, int $batch = 100) {
|
|
$docs = [];
|
|
|
|
// HAL Solr's halId_s is the canonical ID without a version suffix
|
|
// (e.g. "hal-03583975", not "hal-03583975v2"). Some legacy SPIP entries
|
|
// carry a version suffix, so strip it before querying and keep a map
|
|
// to re-key the result under the original caller-supplied ID.
|
|
$originals = array_values(array_unique($hal_ids));
|
|
$stripped_map = []; // stripped_id => [original_id, ...]
|
|
foreach ($originals as $orig) {
|
|
$stripped = preg_replace('/v\d+$/', '', $orig);
|
|
$stripped_map[$stripped][] = $orig;
|
|
}
|
|
$query_ids = array_keys($stripped_map);
|
|
|
|
$chunks = array_chunk($query_ids, $batch);
|
|
foreach ($chunks as $chunk) {
|
|
$filter = 'halId_s:(' . implode(' OR ', $chunk) . ')';
|
|
$params = [
|
|
'q=' . urlencode('*:*'),
|
|
'fq=' . urlencode($filter),
|
|
'rows=' . count($chunk),
|
|
'fl=' . urlencode(self::FIELDS),
|
|
'wt=json',
|
|
];
|
|
$url = THALIM_HAL_API_BASE . '?' . implode('&', $params);
|
|
$data = $this->request($url);
|
|
if (is_wp_error($data)) return $data;
|
|
foreach ($data['response']['docs'] ?? [] as $doc) {
|
|
$canonical = $doc['halId_s'] ?? '';
|
|
if ($canonical === '') continue;
|
|
// Key the doc under every original ID that stripped to this canonical form
|
|
foreach ($stripped_map[$canonical] ?? [$canonical] as $orig) {
|
|
$docs[$orig] = $doc;
|
|
}
|
|
}
|
|
// Be polite with HAL if we have multiple chunks
|
|
if (count($chunks) > 1) usleep(250000);
|
|
}
|
|
return $docs;
|
|
}
|
|
|
|
/**
|
|
* Check which HAL IDs exist in HAL's author referential (ref/author).
|
|
* Returns map [normalized_hal_id => bool|null] — null when the API errored
|
|
* (treated as "unknown", not "invalid").
|
|
*/
|
|
public function validate_hal_ids(array $hal_ids) {
|
|
$result = [];
|
|
$clean = [];
|
|
foreach ($hal_ids as $id) {
|
|
$id = trim((string) $id);
|
|
if ($id !== '') $clean[strtolower($id)] = $id;
|
|
}
|
|
if (empty($clean)) return $result;
|
|
|
|
// Default all to false; flip to true when found
|
|
foreach ($clean as $norm => $_) $result[$norm] = false;
|
|
|
|
$endpoint = 'https://api.archives-ouvertes.fr/ref/author/';
|
|
$chunks = array_chunk(array_values($clean), 100);
|
|
foreach ($chunks as $chunk) {
|
|
// Solr OR with quoted values to tolerate dashes/dots in slugs
|
|
$quoted = array_map(fn($id) => '"' . str_replace('"', '', $id) . '"', $chunk);
|
|
$params = [
|
|
'q=' . urlencode('idHal_s:(' . implode(' OR ', $quoted) . ')'),
|
|
'fl=' . urlencode('idHal_s'),
|
|
'rows=' . count($chunk),
|
|
'wt=json',
|
|
];
|
|
$data = $this->request($endpoint . '?' . implode('&', $params));
|
|
if (is_wp_error($data)) {
|
|
foreach ($chunk as $id) $result[strtolower($id)] = null;
|
|
continue;
|
|
}
|
|
foreach ($data['response']['docs'] ?? [] as $doc) {
|
|
if (!empty($doc['idHal_s'])) {
|
|
$result[strtolower($doc['idHal_s'])] = true;
|
|
}
|
|
}
|
|
if (count($chunks) > 1) usleep(250000);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Expand a set of idHAL slugs to ALL valid idHAL forms of the same people.
|
|
*
|
|
* HAL frequently registers a person under several PREFERRED idHAL forms
|
|
* (e.g. "xavier-garnier" AND "xaviergarnier"). A publication may be tagged
|
|
* with any one of them, so matching a WP profile against a single stored
|
|
* form silently misses those tagged under an alternate form.
|
|
*
|
|
* ref/author exposes every form under the same person, sharing the same
|
|
* numeric root of `docid` ("6951-1022", "6951-180497" => person 6951).
|
|
* We resolve each input slug to its person root(s), then collect every
|
|
* idHAL form attached to those roots.
|
|
*
|
|
* NOTE: this hits the network. It is meant to be called from the background
|
|
* cron resolver (see Thalim_HAL_Forms_Cache), never from the UI/import path.
|
|
*
|
|
* @param string[] $idhals Slugs to expand (a member's stored identifiant_hal).
|
|
* @return array Map [input_slug_lowercased => [form1, form2, ...]].
|
|
* On API error a slug maps to just itself.
|
|
*/
|
|
public function expand_hal_id_forms(array $idhals) {
|
|
$out = [];
|
|
$clean = [];
|
|
foreach ($idhals as $id) {
|
|
$id = strtolower(trim((string) $id));
|
|
if ($id !== '') { $clean[$id] = true; $out[$id] = [$id]; }
|
|
}
|
|
if (empty($clean)) return $out;
|
|
|
|
$endpoint = 'https://api.archives-ouvertes.fr/ref/author/';
|
|
|
|
// Step 1: for each slug, find the person root(s) of its docid.
|
|
$slug_roots = []; // slug => [root => true]
|
|
$all_roots = []; // root => true
|
|
foreach (array_chunk(array_keys($clean), 50) as $chunk) {
|
|
$quoted = array_map(fn($id) => '"' . str_replace('"', '', $id) . '"', $chunk);
|
|
$params = [
|
|
'q=' . urlencode('idHal_s:(' . implode(' OR ', $quoted) . ')'),
|
|
'fl=' . urlencode('docid,idHal_s'),
|
|
'rows=' . (count($chunk) * 5),
|
|
'wt=json',
|
|
];
|
|
$data = $this->request($endpoint . '?' . implode('&', $params));
|
|
if (is_wp_error($data)) continue;
|
|
foreach ($data['response']['docs'] ?? [] as $doc) {
|
|
$slug = strtolower($doc['idHal_s'] ?? '');
|
|
$root = isset($doc['docid']) ? explode('-', $doc['docid'])[0] : '';
|
|
if ($slug !== '' && $root !== '') {
|
|
$slug_roots[$slug][$root] = true;
|
|
$all_roots[$root] = true;
|
|
}
|
|
}
|
|
usleep(250000);
|
|
}
|
|
if (empty($all_roots)) return $out;
|
|
|
|
// Step 2: collect every idHAL form attached to those person roots.
|
|
// NB: do NOT quote the "root-*" terms — quoting disables the Solr wildcard.
|
|
// Roots are numeric, so they are safe to inline; keep only digits defensively.
|
|
$root_forms = []; // root => [form => true]
|
|
foreach (array_chunk(array_keys($all_roots), 50) as $chunk) {
|
|
$terms = array_map(fn($r) => preg_replace('/\D/', '', $r) . '-*', $chunk);
|
|
$params = [
|
|
'q=' . urlencode('docid:(' . implode(' OR ', $terms) . ')'),
|
|
'fl=' . urlencode('docid,idHal_s'),
|
|
'rows=' . (count($chunk) * 10),
|
|
'wt=json',
|
|
];
|
|
$data = $this->request($endpoint . '?' . implode('&', $params));
|
|
if (is_wp_error($data)) continue;
|
|
foreach ($data['response']['docs'] ?? [] as $doc) {
|
|
$root = isset($doc['docid']) ? explode('-', $doc['docid'])[0] : '';
|
|
$form = strtolower($doc['idHal_s'] ?? '');
|
|
if ($root !== '' && $form !== '') $root_forms[$root][$form] = true;
|
|
}
|
|
usleep(250000);
|
|
}
|
|
|
|
// Step 3: map each input slug to the union of forms across its roots.
|
|
foreach ($clean as $slug => $_) {
|
|
$forms = [$slug => true];
|
|
foreach (array_keys($slug_roots[$slug] ?? []) as $root) {
|
|
foreach (array_keys($root_forms[$root] ?? []) as $f) $forms[$f] = true;
|
|
}
|
|
$out[$slug] = array_keys($forms);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Test API connection
|
|
*/
|
|
public function test_connection() {
|
|
$result = $this->fetch_publications(5);
|
|
if (is_wp_error($result)) return $result;
|
|
return [
|
|
'success' => true,
|
|
'total' => $result['response']['numFound'] ?? 0,
|
|
'sample' => $result['response']['docs'] ?? []
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Build API URL with proper fq parameter handling
|
|
*
|
|
* @param int $rows
|
|
* @param int $start
|
|
* @param string $sort
|
|
* @param string $date_from YYYY-MM-DD or empty
|
|
* @param string $date_to YYYY-MM-DD or empty
|
|
*/
|
|
private function build_url($rows = 5, $start = 0, $sort = 'modifiedDate_tdate desc', $date_from = '', $date_to = '', $author_hal_id = '') {
|
|
$doc_types = implode(' OR ', THALIM_HAL_DOC_TYPES);
|
|
|
|
$from = $date_from ? $date_from . 'T00:00:00Z' : '*';
|
|
$to = $date_to ? $date_to . 'T23:59:59Z' : '*';
|
|
|
|
$params = [
|
|
'q=' . urlencode('*:*'),
|
|
'fq=' . urlencode('structId_i:' . THALIM_HAL_STRUCT_ID),
|
|
'fq=' . urlencode('docType_s:(' . $doc_types . ')'),
|
|
];
|
|
|
|
if ($from !== '*' || $to !== '*') {
|
|
$params[] = 'fq=' . urlencode('producedDate_tdate:[' . $from . ' TO ' . $to . ']');
|
|
}
|
|
|
|
if ($author_hal_id !== '') {
|
|
$params[] = 'fq=' . urlencode('authIdHal_s:' . $author_hal_id);
|
|
}
|
|
|
|
$params = array_merge($params, [
|
|
'rows=' . intval($rows),
|
|
'start=' . intval($start),
|
|
'sort=' . urlencode($sort),
|
|
'fl=' . urlencode(self::FIELDS),
|
|
'wt=json'
|
|
]);
|
|
|
|
return THALIM_HAL_API_BASE . '?' . implode('&', $params);
|
|
}
|
|
|
|
/**
|
|
* Get API URL for debugging display
|
|
*/
|
|
public function get_api_url($rows = 5) {
|
|
return $this->build_url($rows, 0);
|
|
}
|
|
|
|
/**
|
|
* Make HTTP request
|
|
*/
|
|
private function request($url) {
|
|
$response = wp_remote_get($url, ['timeout' => 30, 'headers' => ['Accept' => 'application/json']]);
|
|
if (is_wp_error($response)) return $response;
|
|
|
|
$code = wp_remote_retrieve_response_code($response);
|
|
if ($code !== 200) return new WP_Error('api_error', "HTTP $code");
|
|
|
|
$data = json_decode(wp_remote_retrieve_body($response), true);
|
|
return json_last_error() === JSON_ERROR_NONE ? $data : new WP_Error('json_error', 'Invalid JSON');
|
|
}
|
|
}
|