Accès contributeur scopé sur leurs propres publications

This commit is contained in:
2026-05-28 18:02:14 +02:00
parent d8053ac82e
commit f93fe77a12
4 changed files with 211 additions and 51 deletions

View File

@@ -20,6 +20,7 @@ class Thalim_HAL_Admin_Page {
private $api;
private $message = null;
private $wp_users_by_hal_id = null; // Cache: normalized_hal_id => ['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 = [
@@ -40,23 +41,97 @@ class Thalim_HAL_Admin_Page {
}
public function render() {
if (!current_user_can('edit_others_posts')) {
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 '<div class="wrap"><h1>Importer depuis HAL</h1>';
echo $gate;
echo '</div>';
return;
}
}
$this->handle_actions();
echo '<div class="wrap"><h1>THALIM HAL Importer</h1>';
$title = $is_contributor ? 'Importer mes publications HAL' : 'THALIM HAL Importer';
echo '<div class="wrap"><h1>' . esc_html($title) . '</h1>';
$this->render_styles();
$this->render_message();
if (self::CONFIG_PANEL_ENABLED) {
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) {
if (self::CSV_IMPORT_ENABLED && !$is_contributor) {
$this->render_csv_import();
}
echo '</div>';
}
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(
'<div class="notice notice-error"><p><strong>Votre identifiant HAL n\'est pas renseigné.</strong></p>'
. '<p>Pour utiliser cet outil, ajoutez votre <code>identifiant_hal</code> (idHAL) à votre profil.</p>'
. '<p><a href="%s" class="button button-primary">Modifier mon profil</a></p></div>',
esc_url($profile_url)
);
}
$validity = $this->get_hal_ids_validity([strtolower($idhal)]);
$is_valid = $validity[strtolower($idhal)] ?? null;
if ($is_valid === false) {
return sprintf(
'<div class="notice notice-error"><p><strong>Votre identifiant HAL (<code>%s</code>) est introuvable dans le référentiel HAL.</strong></p>'
. '<p>Vérifiez l\'orthographe sur votre profil. La validation est mise en cache 24h.</p>'
. '<p><a href="%s" class="button button-primary">Modifier mon profil</a></p></div>',
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() {
?>
<div class="notice notice-info" style="border-left-color:#2196f3">
<p><strong>À compléter avant publication :</strong></p>
<ul style="list-style:disc;padding-left:25px;margin:5px 0">
<li><strong>Axe(s) thématique(s)</strong> — obligatoire pour la publication.</li>
<li>Autres membres THALIM co-auteurs (champ <em>autre_membres</em>) si applicable.</li>
<li>Image à la une (illustration).</li>
<li>Programme(s) de recherche associé(s) si pertinent.</li>
</ul>
<p><small>Les publications sont importées en statut <strong>En attente</strong>. Un éditeur les validera après votre complément.</small></p>
</div>
<?php
}
private function handle_actions() {
if (!isset($_POST['thalim_hal_action'])) return;
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'thalim_hal_action')) {
@@ -102,6 +177,12 @@ class Thalim_HAL_Admin_Page {
$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)) {
@@ -132,7 +213,7 @@ class Thalim_HAL_Admin_Page {
continue;
}
$post_id = $importer->import($doc, $this->wp_users_by_hal_id);
$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 {
@@ -160,6 +241,11 @@ class Thalim_HAL_Admin_Page {
$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;
@@ -180,6 +266,17 @@ class Thalim_HAL_Admin_Page {
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."];
@@ -192,7 +289,8 @@ class Thalim_HAL_Admin_Page {
return;
}
$post_id = $importer->import($doc, $this->wp_users_by_hal_id);
$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;
@@ -240,19 +338,29 @@ class Thalim_HAL_Admin_Page {
}
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'] ?? '');
// Users must be loaded before rendering the dropdown
// 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);
?>
<div class="card" style="max-width:100%;margin-bottom:20px">
<h2>Import Preview</h2>
<h2><?php echo $is_contributor ? 'Mes publications HAL' : 'Import Preview'; ?></h2>
<form method="post" style="margin-bottom:20px;display:flex;align-items:center;gap:15px;flex-wrap:wrap">
<?php wp_nonce_field('thalim_hal_action'); ?>
@@ -265,17 +373,21 @@ class Thalim_HAL_Admin_Page {
<input type="date" name="hal_date_to" value="<?php echo esc_attr($date_to); ?>" style="width:auto">
</label>
<label style="font-weight:600">Auteur&nbsp;
<select name="hal_author_id" style="max-width:220px">
<option value="">— Tous —</option>
<?php foreach ($this->wp_users_by_hal_id as $user): ?>
<option value="<?php echo esc_attr($user['hal_id']); ?>"
<?php selected($author_hal_id, $user['hal_id']); ?>>
<?php echo esc_html($user['name']); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<?php if (!$is_contributor): ?>
<label style="font-weight:600">Auteur&nbsp;
<select name="hal_author_id" style="max-width:220px">
<option value="">— Tous —</option>
<?php foreach ($this->wp_users_by_hal_id as $user): ?>
<option value="<?php echo esc_attr($user['hal_id']); ?>"
<?php selected($author_hal_id, $user['hal_id']); ?>>
<?php echo esc_html($user['name']); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<?php else: ?>
<input type="hidden" name="hal_author_id" value="<?php echo esc_attr($author_hal_id); ?>">
<?php endif; ?>
<button class="button button-secondary" name="thalim_hal_action" value="filter">Filtrer</button>
<button class="button button-secondary" name="thalim_hal_action" value="refresh" style="margin-left:5px">Rafraîchir</button>
@@ -285,14 +397,14 @@ class Thalim_HAL_Admin_Page {
<button class="button button-primary" name="thalim_hal_action" value="import_pending"
<?php if ($ready_count === 0): ?>disabled title="Aucune publication prête à importer"<?php endif; ?>>
Importer <?php echo $ready_count; ?> publication(s) (En attente)
<?php echo esc_html($import_label); ?>
</button>
</form>
<?php if (is_wp_error($preview)): ?>
<div class="notice notice-error"><p><?php echo esc_html($preview->get_error_message()); ?></p></div>
<?php else: ?>
<?php $this->render_wp_users_debug(); ?>
<?php if (!$is_contributor) $this->render_wp_users_debug(); ?>
<?php $this->render_summary($preview['stats']); ?>
<?php $this->render_preview_table($preview['docs'], [
'date_from' => $date_from,
@@ -486,7 +598,11 @@ class Thalim_HAL_Admin_Page {
}
private function get_preview_cache_key($date_from, $date_to, $author_hal_id) {
return 'thalim_hal_preview_' . md5($date_from . '|' . $date_to . '|' . $author_hal_id);
// Scope the cache by user ID in contributor mode so two contributors
// don't share a cache entry (and don't collide with the admin cache
// that might use the same author_hal_id filter).
$scope = $this->is_contributor_mode() ? ('u' . get_current_user_id() . '|') : '';
return 'thalim_hal_preview_' . md5($scope . $date_from . '|' . $date_to . '|' . $author_hal_id);
}
/**
@@ -516,6 +632,11 @@ class Thalim_HAL_Admin_Page {
}
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;

View File

@@ -97,7 +97,8 @@ class Thalim_HAL_Importer_Logic {
array $wp_users_by_hal_id = [],
string $post_status = 'pending',
bool $backdate_post = false,
array $spip_context = []
array $spip_context = [],
?int $force_post_author = null
) {
$hal_id = $hal_doc['halId_s'] ?? '';
$doc_type = $hal_doc['docType_s'] ?? '';
@@ -118,7 +119,8 @@ class Thalim_HAL_Importer_Logic {
$matched_user_names[] = $user['name'];
}
}
$post_author = !empty($matched_user_ids) ? $matched_user_ids[0] : 1;
$post_author = $force_post_author
?? (!empty($matched_user_ids) ? $matched_user_ids[0] : 1);
// --- Create the post ---
$post_args = [