['id' => int, 'name' => string]
private $contributor_idhal = null; // Set by check_contributor_idhal_gate() — forced filter in contributor mode
// Document type labels
private const DOC_TYPE_LABELS = [
'ART' => 'Article',
'COUV' => "Chapitre d'ouvrage",
'OUV' => 'Ouvrage',
'COMM' => 'Communication',
'ISSUE' => 'Direction de numéro',
'PROCEEDINGS' => 'Colloque',
'THESE' => 'Thèse',
'HDR' => 'HDR',
'SON' => 'Son',
'VIDEO' => 'Vidéo',
];
public function __construct() {
$this->api = new Thalim_HAL_API();
}
public function render() {
if (!current_user_can('edit_posts')) {
wp_die('Unauthorized');
}
$is_contributor = $this->is_contributor_mode();
// Contributor precondition: must have a valid idHAL on their profile
if ($is_contributor) {
$gate = $this->check_contributor_idhal_gate();
if ($gate !== true) {
echo '
Importer depuis HAL
';
echo $gate;
echo '';
return;
}
}
$this->handle_actions();
$title = $is_contributor ? 'Importer mes publications HAL' : 'THALIM HAL Importer';
echo '' . esc_html($title) . '
';
$this->render_styles();
$this->render_message();
if ($is_contributor) {
$this->render_contributor_notice();
}
if (self::CONFIG_PANEL_ENABLED && !$is_contributor) {
$this->render_config();
}
$this->render_preview();
if (self::CSV_IMPORT_ENABLED && !$is_contributor) {
$this->render_csv_import();
}
echo '';
}
private function is_contributor_mode(): bool {
return current_user_can('edit_posts') && !current_user_can('edit_others_posts');
}
/**
* Returns true if the current contributor can use the page, or an HTML
* notice string explaining why not. Also caches the resolved idHAL into
* $this->contributor_idhal for downstream forcing.
*/
private function check_contributor_idhal_gate() {
$user_id = get_current_user_id();
$idhal = trim((string) get_user_meta($user_id, 'identifiant_hal', true));
$profile_url = get_edit_user_link($user_id);
if ($idhal === '') {
return sprintf(
'Votre identifiant HAL n\'est pas renseigné.
'
. '
Pour utiliser cet outil, ajoutez votre identifiant_hal (idHAL) à votre profil.
'
. '
Modifier mon profil
',
esc_url($profile_url)
);
}
$validity = $this->get_hal_ids_validity([strtolower($idhal)]);
$is_valid = $validity[strtolower($idhal)] ?? null;
if ($is_valid === false) {
return sprintf(
'Votre identifiant HAL (%s) est introuvable dans le référentiel HAL.
'
. '
Vérifiez l\'orthographe sur votre profil. La validation est mise en cache 24h.
'
. '
Modifier mon profil
',
esc_html($idhal), esc_url($profile_url)
);
}
// null = API error → graceful degradation, on laisse passer
$this->contributor_idhal = $idhal;
return true;
}
private function render_contributor_notice() {
?>
À compléter avant publication :
- Axe(s) thématique(s) — obligatoire pour la publication.
- Autres membres THALIM co-auteurs (champ autre_membres) si applicable.
- Image à la une (illustration).
- Programme(s) de recherche associé(s) si pertinent.
Les publications sont importées en statut En attente. Un éditeur les validera après votre complément.
message = ['error', 'Security check failed.'];
return;
}
$action = sanitize_text_field($_POST['thalim_hal_action']);
if (self::CONFIG_PANEL_ENABLED && $action === 'test_api') {
$this->handle_test_api();
}
if ($action === 'refresh') {
// Clear all preview transients (they are keyed by date range hash)
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_thalim_hal_preview_%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_thalim_hal_preview_%'");
$this->message = ['success', 'Preview data refreshed from HAL API.'];
}
if ($action === 'import_pending') {
$this->handle_import();
}
if ($action === 'import_single') {
$this->handle_import_single();
}
if (self::CSV_IMPORT_ENABLED) {
if ($action === 'csv_upload') $this->handle_csv_upload();
if ($action === 'csv_batch') $this->handle_csv_batch();
if ($action === 'csv_cancel') $this->handle_csv_cancel();
if ($action === 'csv_download_report') $this->handle_csv_download_report();
}
}
/**
* Handle bulk import of ready publications as pending posts.
* Uses cached raw HAL docs to avoid a second outbound API call.
*/
private function handle_import() {
$date_from = sanitize_text_field($_POST['hal_date_from'] ?? '');
$date_to = sanitize_text_field($_POST['hal_date_to'] ?? '');
$author_hal_id = sanitize_text_field($_POST['hal_author_id'] ?? '');
$is_contributor = $this->is_contributor_mode();
if ($is_contributor) {
$author_hal_id = $this->contributor_idhal; // force, ignore POST
}
$force_author = $is_contributor ? get_current_user_id() : null;
// Reuse the cached preview data — raw_docs are stored alongside processed docs
$preview = $this->get_preview_data($date_from, $date_to, $author_hal_id);
if (is_wp_error($preview)) {
$this->message = ['error', 'API Error: ' . $preview->get_error_message()];
return;
}
$raw_docs = $preview['raw_docs'] ?? [];
if (empty($raw_docs)) {
$this->message = ['warning', 'Aucune publication dans le cache. Utilisez Filtrer pour charger les données d\'abord.'];
return;
}
$this->load_wp_users_hal_ids();
$importer = new Thalim_HAL_Importer_Logic();
$imported = 0;
$skipped = 0;
$errors = [];
$cache_updates = []; // hal_id => ['id', 'status'] for cache mutation
foreach ($raw_docs as $doc) {
$hal_id = $doc['halId_s'] ?? '';
$author_hal_ids = $doc['authIdHal_s'] ?? [];
$matched_users = $this->match_authors_to_users($author_hal_ids);
if (empty($matched_users) || $importer->is_imported($hal_id)) {
$skipped++;
continue;
}
$post_id = $importer->import($doc, $this->wp_users_by_hal_id, 'pending', false, [], $force_author);
if (is_wp_error($post_id)) {
$errors[] = $hal_id . ': ' . $post_id->get_error_message();
} else {
$imported++;
$cache_updates[$hal_id] = ['id' => $post_id, 'status' => 'pending'];
}
}
$this->update_preview_cache_after_import($date_from, $date_to, $author_hal_id, $cache_updates);
$msg = sprintf('%d publication(s) importée(s) en statut "En attente".', $imported);
if ($skipped) $msg .= sprintf(' %d ignorée(s) (déjà importées ou sans membre THALIM correspondant).', $skipped);
if (!empty($errors)) $msg .= ' Erreurs : ' . implode('; ', $errors);
$this->message = [empty($errors) ? 'success' : 'warning', $msg];
}
/**
* Import a single ready publication. hal_id + filter values are POSTed
* from the per-row form so the cache lookup hits the right preview entry.
*/
private function handle_import_single() {
$hal_id = sanitize_text_field($_POST['hal_id'] ?? '');
$date_from = sanitize_text_field($_POST['hal_date_from'] ?? '');
$date_to = sanitize_text_field($_POST['hal_date_to'] ?? '');
$author_hal_id = sanitize_text_field($_POST['hal_author_id'] ?? '');
$is_contributor = $this->is_contributor_mode();
if ($is_contributor) {
$author_hal_id = $this->contributor_idhal; // force, ignore POST
}
if (!$hal_id) {
$this->message = ['error', 'hal_id manquant.'];
return;
}
$preview = $this->get_preview_data($date_from, $date_to, $author_hal_id);
if (is_wp_error($preview)) {
$this->message = ['error', 'API Error: ' . $preview->get_error_message()];
return;
}
$doc = null;
foreach ($preview['raw_docs'] ?? [] as $d) {
if (($d['halId_s'] ?? '') === $hal_id) { $doc = $d; break; }
}
if (!$doc) {
$this->message = ['warning', "Publication $hal_id introuvable dans le tableau (cache expiré ?). Rafraîchir et réessayer."];
return;
}
// Critical security check: in contributor mode, the requested hal_id MUST
// be a publication where the contributor is an author. Defends against a
// forged POST that tries to import another member's publication.
if ($is_contributor) {
$doc_authors = array_map('strtolower', array_map('trim', $doc['authIdHal_s'] ?? []));
if (!in_array(strtolower($this->contributor_idhal), $doc_authors, true)) {
$this->message = ['error', "Vous n'êtes pas auteur de la publication $hal_id."];
return;
}
}
$importer = new Thalim_HAL_Importer_Logic();
if ($importer->is_imported($hal_id)) {
$this->message = ['warning', "Publication $hal_id déjà importée."];
return;
}
$this->load_wp_users_hal_ids();
if (empty($this->match_authors_to_users($doc['authIdHal_s'] ?? []))) {
$this->message = ['error', "Aucun membre THALIM identifié pour $hal_id."];
return;
}
$force_author = $is_contributor ? get_current_user_id() : null;
$post_id = $importer->import($doc, $this->wp_users_by_hal_id, 'pending', false, [], $force_author);
if (is_wp_error($post_id)) {
$this->message = ['error', "Erreur import : " . $post_id->get_error_message()];
return;
}
$this->update_preview_cache_after_import($date_from, $date_to, $author_hal_id, [
$hal_id => ['id' => $post_id, 'status' => 'pending'],
]);
$this->message = ['success', sprintf('Publication %s importée (post #%d, en attente).', $hal_id, $post_id)];
}
private function render_styles() {
?>
message) return;
printf('',
esc_attr($this->message[0]), esc_html($this->message[1]));
}
private function render_preview() {
$is_contributor = $this->is_contributor_mode();
// Read filters from POST (after submit) or GET (page reload with state)
$date_from = sanitize_text_field($_POST['hal_date_from'] ?? $_GET['hal_date_from'] ?? '');
$date_to = sanitize_text_field($_POST['hal_date_to'] ?? $_GET['hal_date_to'] ?? '');
$author_hal_id = sanitize_text_field($_POST['hal_author_id'] ?? $_GET['hal_author_id'] ?? '');
// Contributor mode: ignore any POSTed author filter, force their own idHAL.
if ($is_contributor) {
$author_hal_id = $this->contributor_idhal;
}
// Users must be loaded before rendering the dropdown (admins/editors only)
$this->load_wp_users_hal_ids();
$preview = $this->get_preview_data($date_from, $date_to, $author_hal_id);
$ready_count = is_wp_error($preview) ? 0 : $preview['stats']['ready'];
$import_label = $is_contributor
? sprintf('Importer mes %d publication(s) (En attente)', $ready_count)
: sprintf('Importer %d publication(s) (En attente)', $ready_count);
?>
render_wp_users_debug(); ?>
render_summary($preview['stats']); ?>
render_preview_table($preview['docs'], [
'date_from' => $date_from,
'date_to' => $date_to,
'author_hal_id' => $author_hal_id,
]); ?>
render_legend(); ?>
Total in HAL
Already Imported
Ready to Import
No Matched User
No publications found.';
return;
}
?>
| Statut |
HAL ID |
Titre |
Type |
Auteurs |
IDs HAL auteurs |
Date |
Membres THALIM |
Lien HAL |
Action |
| render_status_cell($doc); ?> |
|
|
|
|
wp_users_by_hal_id[$normalized]);
?>
aucun
|
|
Aucun
|
Voir sur HAL
|
|
Légende :
✓ Importé
★ Prêt Membre THALIM identifié
✗ Bloqué Aucun membre THALIM ne correspond aux IDs auteurs HAL
true|false|null] (null = API error).
*/
private function get_hal_ids_validity(array $hal_ids) {
sort($hal_ids);
$cache_key = 'thalim_hal_ids_validity_' . md5(implode('|', $hal_ids));
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
$result = $this->api->validate_hal_ids($hal_ids);
set_transient($cache_key, $result, DAY_IN_SECONDS);
return $result;
}
private function render_wp_users_debug() {
$this->load_wp_users_hal_ids();
if (empty($this->wp_users_by_hal_id)) {
echo 'Aucun utilisateur WordPress n\'a le champ identifiant_hal renseigné.
';
return;
}
$validity = $this->get_hal_ids_validity(array_keys($this->wp_users_by_hal_id));
$invalid_count = count(array_filter($validity, fn($v) => $v === false));
?>
Utilisateurs WordPress avec identifiant HAL (wp_users_by_hal_id); ?> utilisateurs 0) echo ', ' . $invalid_count . ' invalide(s)';
?>) — Cliquer pour déplier
| Utilisateur | Identifiant HAL | Validité HAL | Debug (brut) | Modifier |
wp_users_by_hal_id as $hal_id => $user):
$valid = $validity[$hal_id] ?? null;
$row_style = $valid === false ? 'background:#f8d7da' : '';
?>
| (ID : ) |
|
✓ valide
✗ invalide
— inconnu
|
"" ( car.) |
Modifier |
is_contributor_mode() ? ('u' . get_current_user_id() . '|') : '';
return 'thalim_hal_preview_' . md5($scope . $date_from . '|' . $date_to . '|' . $author_hal_id);
}
/**
* Mute the cached preview in place to reflect newly-imported posts —
* avoids re-hitting the HAL API just to refresh statuses after an import.
*
* @param array $imports Map hal_id => ['id' => int, 'status' => string]
*/
private function update_preview_cache_after_import($date_from, $date_to, $author_hal_id, array $imports) {
if (empty($imports)) return;
$cache_key = $this->get_preview_cache_key($date_from, $date_to, $author_hal_id);
$preview = get_transient($cache_key);
if (!$preview) return;
foreach ($preview['docs'] as &$d) {
if (isset($imports[$d['hal_id']]) && empty($d['is_imported'])) {
$d['is_imported'] = true;
$d['imported_post_id'] = $imports[$d['hal_id']]['id'];
$d['imported_post_status'] = $imports[$d['hal_id']]['status'];
$preview['stats']['ready']--;
$preview['stats']['imported']++;
}
}
unset($d);
set_transient($cache_key, $preview, 300);
}
private function get_preview_data($date_from = '', $date_to = '', $author_hal_id = '') {
// Server-side override: in contributor mode, force the author filter
// to the contributor's own idHAL regardless of what was POSTed.
if ($this->is_contributor_mode() && $this->contributor_idhal) {
$author_hal_id = $this->contributor_idhal;
}
$cache_key = $this->get_preview_cache_key($date_from, $date_to, $author_hal_id);
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
$rows = ($date_from || $date_to || $author_hal_id) ? 200 : 50;
$result = $this->api->fetch_publications($rows, 0, 'producedDate_tdate desc', $date_from, $date_to, $author_hal_id);
if (is_wp_error($result)) return $result;
$importer = new Thalim_HAL_Importer_Logic();
$this->load_wp_users_hal_ids();
$preview = [
'stats' => [
'total' => $result['response']['numFound'] ?? 0,
'imported' => 0,
'ready' => 0,
'blocked' => 0
],
'docs' => [],
'raw_docs' => [], // Raw HAL docs kept for import, avoids a second API call
];
foreach ($result['response']['docs'] ?? [] as $doc) {
$hal_id = $doc['halId_s'] ?? '';
$imported_post = $importer->get_imported_post($hal_id);
$is_imported = $imported_post !== null;
$author_hal_ids = $doc['authIdHal_s'] ?? [];
$matched_users = $this->match_authors_to_users($author_hal_ids);
$has_match = !empty($matched_users);
// Update stats
if ($is_imported) {
$preview['stats']['imported']++;
} elseif ($has_match) {
$preview['stats']['ready']++;
} else {
$preview['stats']['blocked']++;
}
$preview['docs'][] = [
'hal_id' => $hal_id,
'title' => $doc['title_s'][0] ?? 'N/A',
'type' => $doc['docType_s'] ?? '',
'authors' => $doc['authFullName_s'] ?? [],
'author_hal_ids' => $author_hal_ids,
'publication_date' => $doc['publicationDate_s'] ?? '',
'produced_date' => $doc['submittedDate_s'] ?? '',
'journal' => $doc['journalTitle_s'] ?? $doc['bookTitle_s'] ?? '',
'url' => $doc['uri_s'] ?? '',
'is_imported' => $is_imported,
'imported_post_id' => $imported_post['id'] ?? null,
'imported_post_status' => $imported_post['status'] ?? null,
'matched_users' => $matched_users,
'has_match' => $has_match,
];
$preview['raw_docs'][] = $doc; // Full HAL doc kept for import
}
set_transient($cache_key, $preview, 300);
return $preview;
}
/**
* Load all WordPress users with HAL IDs into cache.
* Stores: normalized_hal_id => ['id' => int, 'name' => string]
*/
private function load_wp_users_hal_ids() {
if ($this->wp_users_by_hal_id !== null) return;
$this->wp_users_by_hal_id = [];
$users = get_users([
'meta_key' => 'identifiant_hal',
'meta_compare' => 'EXISTS'
]);
foreach ($users as $user) {
$hal_id = get_user_meta($user->ID, 'identifiant_hal', true);
if (!empty($hal_id)) {
$normalized = strtolower(trim($hal_id));
$this->wp_users_by_hal_id[$normalized] = [
'id' => $user->ID,
'name' => $user->display_name,
'hal_id' => trim($hal_id), // original value for API filter
];
}
}
}
/**
* Match HAL author IDs to WordPress users.
* Returns array of display names (for preview display).
*/
private function match_authors_to_users($author_hal_ids) {
$matched = [];
foreach ($author_hal_ids as $hal_id) {
$normalized = strtolower(trim($hal_id));
if (isset($this->wp_users_by_hal_id[$normalized])) {
$matched[] = $this->wp_users_by_hal_id[$normalized]['name'];
}
}
return $matched;
}
private function get_row_class($doc) {
if ($doc['is_imported']) return 'hal-status-imported';
if ($doc['has_match']) return 'hal-status-ready';
return 'hal-status-blocked';
}
private function render_status_cell($doc) {
if ($doc['is_imported']) {
$status = $doc['imported_post_status'] ?? '';
$is_pending = $status === 'pending';
$label = $is_pending ? 'En attente' : ($status === 'publish' ? 'Publié' : $status);
?>
✓
Modifier
★';
return;
}
echo '✗';
}
}