cgt-wp-theme/inc/class-cgt-confederal-feed.php
2026-11-12 21:27:56 +01:00

841 lines
28 KiB
PHP

<?php
/**
* Moteur de récupération et rendu de l'actualité confédérale depuis https://www.cgt.fr/
*
* - Interroge cgt.fr via wp_remote_get() (timeout 10s, User-Agent propre)
* - Parse le HTML Drupal via DOMDocument + DOMXPath sans polluer wp_posts
* - Extrait 1 article "À la une" + 3 articles "mais aussi..."
* - Enrichit les résumés via https://www.cgt.fr/actualites et, si besoin, depuis la page article
* - Cache Transient (3h) + sauvegarde de secours (autoload = false) + WP-Cron (2h)
*
* @package CGT_FHA
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class CGT_FHA_Confederal_Feed {
public const TRANSIENT_KEY = 'cgt_fha_confederal_feed';
public const BACKUP_OPTION = 'cgt_fha_confederal_feed_backup';
public const LOCK_TRANSIENT = 'cgt_fha_confederal_feed_lock';
public const CRON_HOOK = 'cgt_fha_refresh_confederal_feed';
public const CRON_INTERVAL = 'cgt_fha_two_hours';
/**
* Enregistre les hooks WP-Cron, Admin Bar et rafraîchissement manuel.
*/
public static function init(): void {
add_filter( 'cron_schedules', array( __CLASS__, 'register_cron_interval' ) );
add_action( 'init', array( __CLASS__, 'ensure_cron_scheduled' ) );
add_action( self::CRON_HOOK, array( __CLASS__, 'cron_refresh' ) );
add_action( 'switch_theme', array( __CLASS__, 'clear_cron' ) );
add_action( 'admin_bar_menu', array( __CLASS__, 'add_admin_bar_button' ), 95 );
add_action( 'admin_post_cgt_fha_refresh_feed', array( __CLASS__, 'handle_manual_refresh' ) );
}
/**
* Ajoute un intervalle WP-Cron de 2 heures.
*
* @param array<string, array<string, mixed>> $schedules Planifications existantes.
* @return array<string, array<string, mixed>>
*/
public static function register_cron_interval( array $schedules ): array {
if ( ! isset( $schedules[ self::CRON_INTERVAL ] ) ) {
$schedules[ self::CRON_INTERVAL ] = array(
'interval' => 2 * HOUR_IN_SECONDS,
'display' => 'Toutes les 2 heures (Actus Confédérales CGT)',
);
}
return $schedules;
}
/**
* Planifie la tâche WP-Cron si elle n'est pas déjà active.
*/
public static function ensure_cron_scheduled(): void {
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
wp_schedule_event( time() + 300, self::CRON_INTERVAL, self::CRON_HOOK );
}
}
/**
* Supprime la tâche WP-Cron lors du changement de thème.
*/
public static function clear_cron(): void {
$timestamp = wp_next_scheduled( self::CRON_HOOK );
while ( $timestamp ) {
wp_unschedule_event( $timestamp, self::CRON_HOOK );
$timestamp = wp_next_scheduled( self::CRON_HOOK );
}
}
/**
* Callback exécuté par WP-Cron toutes les 2 heures.
*/
public static function cron_refresh(): void {
self::get_feed( true );
}
/**
* Bouton « Actualiser les actus confédérales » dans la barre d'admin WordPress.
*/
public static function add_admin_bar_button( WP_Admin_Bar $wp_admin_bar ): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$url = wp_nonce_url(
admin_url( 'admin-post.php?action=cgt_fha_refresh_feed' ),
'cgt_fha_refresh_feed_action',
'cgt_fha_refresh_feed_nonce'
);
$wp_admin_bar->add_node(
array(
'id' => 'cgt-fha-refresh-confederal',
'title' => 'Actualiser les actus confédérales',
'href' => $url,
'meta' => array(
'title' => 'Synchroniser immédiatement les actualités depuis cgt.fr',
),
)
);
}
/**
* Traitement du rafraîchissement manuel sécurisé par nonce.
*/
public static function handle_manual_refresh(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Action non autorisée.', 'cgt-fha' ), 403 );
}
check_admin_referer( 'cgt_fha_refresh_feed_action', 'cgt_fha_refresh_feed_nonce' );
delete_transient( self::LOCK_TRANSIENT );
self::get_feed( true );
$redirect = add_query_arg(
array(
'page' => 'cgt-fha-settings',
'tab' => 'confederal',
'cgt_feed_refreshed' => '1',
),
admin_url( 'themes.php' )
);
wp_safe_redirect( $redirect );
exit;
}
/**
* Retourne les données du flux confédéral (cache -> réseau -> backup -> fallback).
*
* @param bool $force_refresh Force une requête réseau immédiate.
* @return array<string, mixed>
*/
public static function get_feed( bool $force_refresh = false ): array {
if ( ! $force_refresh ) {
$cached = get_transient( self::TRANSIENT_KEY );
if ( is_array( $cached ) && ! empty( $cached['featured'] ) ) {
return $cached;
}
}
// Anti-tempête : évite de relancer des requêtes HTTP bloquantes si cgt.fr vient d'échouer.
if ( $force_refresh || false === get_transient( self::LOCK_TRANSIENT ) ) {
$fresh = self::fetch_and_parse();
if ( ! empty( $fresh['featured']['title'] ) && ! empty( $fresh['latest'] ) ) {
set_transient( self::TRANSIENT_KEY, $fresh, 3 * HOUR_IN_SECONDS );
update_option( self::BACKUP_OPTION, $fresh, false );
delete_transient( self::LOCK_TRANSIENT );
return $fresh;
}
set_transient( self::LOCK_TRANSIENT, 1, 5 * MINUTE_IN_SECONDS );
}
$backup = get_option( self::BACKUP_OPTION, array() );
if ( is_array( $backup ) && ! empty( $backup['featured']['title'] ) ) {
return $backup;
}
return self::get_static_fallback();
}
/**
* Retourne l'état du cache pour l'onglet d'administration.
*
* @return array<string, mixed>
*/
public static function get_cache_status(): array {
$transient = get_transient( self::TRANSIENT_KEY );
$backup = get_option( self::BACKUP_OPTION, array() );
$data = is_array( $transient ) && ! empty( $transient ) ? $transient : ( is_array( $backup ) ? $backup : array() );
return array(
'has_transient' => is_array( $transient ) && ! empty( $transient ),
'fetched_at' => $data['fetched_at'] ?? 0,
'source' => $data['source'] ?? 'Fallback local',
'featured' => $data['featured'] ?? array(),
);
}
/**
* Télécharge et parse cgt.fr (avec enrichissement sur /actualites puis sur la page article si besoin).
*
* @return array<string, mixed>
*/
private static function fetch_and_parse(): array {
$home_html = self::remote_get_html( 'https://www.cgt.fr/' );
$featured = array();
$latest = array();
$source = 'https://www.cgt.fr/';
if ( '' !== $home_html ) {
$parsed = self::parse_homepage_html( $home_html );
$featured = $parsed['featured'];
$latest = $parsed['latest'];
}
$needs_actualites = empty( $featured['title'] )
|| '' === trim( (string) ( $featured['summary'] ?? '' ) )
|| empty( $featured['image_url'] )
|| count( $latest ) < 3;
if ( ! $needs_actualites ) {
foreach ( $latest as $item ) {
if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) || empty( $item['image_url'] ) ) {
$needs_actualites = true;
break;
}
}
}
if ( $needs_actualites ) {
$actus_html = self::remote_get_html( 'https://www.cgt.fr/actualites' );
if ( '' !== $actus_html ) {
$actus_items = self::parse_actualites_html( $actus_html );
if ( ! empty( $actus_items ) ) {
$by_path = array();
foreach ( $actus_items as $act ) {
$key = self::normalize_url_key( $act['url'] );
if ( '' !== $key ) {
$by_path[ $key ] = $act;
}
}
// Enrichit "featured" sans écraser une image haute définition déjà extraite.
if ( ! empty( $featured['url'] ) ) {
$f_key = self::normalize_url_key( $featured['url'] );
if ( isset( $by_path[ $f_key ] ) ) {
if ( '' === trim( (string) ( $featured['summary'] ?? '' ) ) && ! empty( $by_path[ $f_key ]['summary'] ) ) {
$featured['summary'] = $by_path[ $f_key ]['summary'];
}
if ( empty( $featured['image_url'] ) && ! empty( $by_path[ $f_key ]['image_url'] ) ) {
$featured['image_url'] = $by_path[ $f_key ]['image_url'];
}
if ( ( empty( $featured['category'] ) || 'Confédération CGT' === $featured['category'] ) && ! empty( $by_path[ $f_key ]['category'] ) ) {
$featured['category'] = $by_path[ $f_key ]['category'];
}
}
}
// Enrichit les 3 cartes "latest" depuis /actualites sans écraser image_a_la_une.
foreach ( $latest as $idx => $item ) {
$l_key = self::normalize_url_key( $item['url'] );
if ( isset( $by_path[ $l_key ] ) ) {
if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) && ! empty( $by_path[ $l_key ]['summary'] ) ) {
$latest[ $idx ]['summary'] = $by_path[ $l_key ]['summary'];
}
if ( empty( $item['image_url'] ) && ! empty( $by_path[ $l_key ]['image_url'] ) ) {
$latest[ $idx ]['image_url'] = $by_path[ $l_key ]['image_url'];
}
if ( ( empty( $item['category'] ) || 'Confédération CGT' === $item['category'] ) && ! empty( $by_path[ $l_key ]['category'] ) ) {
$latest[ $idx ]['category'] = $by_path[ $l_key ]['category'];
}
}
}
// Fallback complet si la page d'accueil a changé de structure.
if ( empty( $featured['title'] ) && count( $actus_items ) >= 2 ) {
$featured = array_shift( $actus_items );
$latest = array_slice( $actus_items, 0, 3 );
$source = 'https://www.cgt.fr/actualites (fallback)';
} elseif ( count( $latest ) < 3 ) {
$seen = array( self::normalize_url_key( (string) ( $featured['url'] ?? '' ) ) );
foreach ( $latest as $l ) {
$seen[] = self::normalize_url_key( (string) $l['url'] );
}
foreach ( $actus_items as $act ) {
if ( count( $latest ) >= 3 ) {
break;
}
$act_key = self::normalize_url_key( $act['url'] );
if ( '' !== $act_key && ! in_array( $act_key, $seen, true ) ) {
$latest[] = $act;
$seen[] = $act_key;
}
}
}
}
}
}
if ( empty( $featured['title'] ) || empty( $latest ) ) {
return array();
}
// Fallback final : complète un éventuel article épinglé absent de la page 1 d'/actualites.
if ( '' === trim( (string) ( $featured['summary'] ?? '' ) ) || empty( $featured['image_url'] ) ) {
$featured = self::enrich_from_article_page( $featured );
}
foreach ( $latest as $idx => $item ) {
if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) || empty( $item['image_url'] ) ) {
$latest[ $idx ] = self::enrich_from_article_page( $item );
}
}
return array(
'fetched_at' => time(),
'source' => $source,
'featured' => self::normalize_article( $featured, 240 ),
'latest' => array_map(
static fn( array $item ): array => self::normalize_article( $item, 190 ),
array_slice( $latest, 0, 3 )
),
);
}
/**
* Requête HTTP GET sécurisée avec timeout 10s.
*/
private static function remote_get_html( string $url ): string {
$response = wp_remote_get(
$url,
array(
'timeout' => 10,
'redirection' => 3,
'user-agent' => 'Mozilla/5.0 (compatible; CGT-FHA-Theme/1.1; +https://cgt-fha.fr/)',
'headers' => array(
'Accept' => 'text/html,application/xhtml+xml',
'Accept-Language' => 'fr-FR,fr;q=0.9',
),
)
);
if ( is_wp_error( $response ) ) {
return '';
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 400 ) {
return '';
}
return (string) wp_remote_retrieve_body( $response );
}
/**
* Parse la page d'accueil cgt.fr :
* - "À la une de la CGT" (#block-bluedrop-views-block-derniers-ajouts-block-2, .relative-wrapper)
* - "mais aussi..." (#block-views-block-derniers-ajouts-block-1, .views-row)
*
* @return array{featured: array<string, string>, latest: array<int, array<string, string>>}
*/
private static function parse_homepage_html( string $html ): array {
$xpath = self::create_xpath( $html );
if ( ! $xpath ) {
return array( 'featured' => array(), 'latest' => array() );
}
$featured = array();
$latest = array();
// 1. Bloc « à la une de la cgt » (ID Drupal exact + secours par titre de section).
$une_root = null;
$une_nodes = $xpath->query( '//section[@id="block-bluedrop-views-block-derniers-ajouts-block-2"]' );
if ( $une_nodes instanceof DOMNodeList && $une_nodes->length > 0 ) {
$une_root = $une_nodes->item( 0 );
} else {
$une_root = self::find_section_by_heading_text( $xpath, 'la une de la cgt' );
}
if ( $une_root ) {
$wrappers = $xpath->query( './/div[contains(@class,"relative-wrapper")]', $une_root );
$card_el = ( $wrappers instanceof DOMNodeList && $wrappers->length > 0 ) ? $wrappers->item( 0 ) : $une_root;
$parsed = self::extract_drupal_row( $xpath, $card_el );
if ( ! empty( $parsed['title'] ) && ! empty( $parsed['url'] ) ) {
$featured = $parsed;
}
}
// 2. Bloc « mais aussi... » (ID Drupal exact + secours par titre de section).
$mais_root = null;
$mais_nodes = $xpath->query( '//section[@id="block-views-block-derniers-ajouts-block-1"]' );
if ( $mais_nodes instanceof DOMNodeList && $mais_nodes->length > 0 ) {
$mais_root = $mais_nodes->item( 0 );
} else {
$mais_root = self::find_section_by_heading_text( $xpath, 'mais aussi' );
}
if ( $mais_root ) {
$rows = $xpath->query( './/div[contains(@class,"views-row")]', $mais_root );
if ( $rows instanceof DOMNodeList ) {
$seen = array( self::normalize_url_key( (string) ( $featured['url'] ?? '' ) ) );
foreach ( $rows as $row ) {
if ( count( $latest ) >= 3 ) {
break;
}
$item = self::extract_drupal_row( $xpath, $row );
$key = self::normalize_url_key( (string) ( $item['url'] ?? '' ) );
if ( ! empty( $item['title'] ) && '' !== $key && ! in_array( $key, $seen, true ) ) {
$latest[] = $item;
$seen[] = $key;
}
}
}
}
return array(
'featured' => $featured,
'latest' => $latest,
);
}
/**
* Parse la liste des articles de https://www.cgt.fr/actualites (jusqu'à 25 lignes .views-row).
*
* @return array<int, array<string, string>>
*/
private static function parse_actualites_html( string $html ): array {
$xpath = self::create_xpath( $html );
if ( ! $xpath || ! $xpath->document->documentElement ) {
return array();
}
$rows = $xpath->query( '//main//div[contains(@class,"views-row")]|//div[contains(@class,"view-content")]//div[contains(@class,"views-row")]' );
if ( ! ( $rows instanceof DOMNodeList ) || 0 === $rows->length ) {
return array();
}
$articles = array();
$seen = array();
foreach ( $rows as $row ) {
if ( count( $articles ) >= 25 ) {
break;
}
$item = self::extract_drupal_row( $xpath, $row );
$key = self::normalize_url_key( (string) ( $item['url'] ?? '' ) );
if ( ! empty( $item['title'] ) && '' !== $key && ! isset( $seen[ $key ] ) ) {
$seen[ $key ] = true;
$articles[] = $item;
}
}
return $articles;
}
/**
* Extrait proprement un article depuis un conteneur Drupal (bloc À la une ou .views-row).
*
* @return array<string, string>
*/
private static function extract_drupal_row( DOMXPath $xpath, DOMNode $row ): array {
$link_el = null;
$h_links = $xpath->query( './/h3//a[@href]|.//h2//a[@href]|.//h4//a[@href]', $row );
if ( $h_links instanceof DOMNodeList ) {
foreach ( $h_links as $cand ) {
$href = trim( $cand->getAttribute( 'href' ) );
if ( self::is_article_href( $href ) ) {
$link_el = $cand;
break;
}
}
}
if ( ! $link_el ) {
$fallback_links = $xpath->query( './/a[@href and not(contains(@href,"/terms/")) and not(contains(@href,"/taxonomy/")) and not(contains(@class,"taxonomy-style")) and not(contains(@class,"more-link"))]', $row );
if ( $fallback_links instanceof DOMNodeList ) {
foreach ( $fallback_links as $cand ) {
$href = trim( $cand->getAttribute( 'href' ) );
if ( self::is_article_href( $href ) ) {
$link_el = $cand;
break;
}
}
}
}
if ( ! $link_el ) {
return array();
}
$url = self::absolutize_url( $link_el->getAttribute( 'href' ) );
$title = self::normalize_space( $link_el->textContent );
if ( '' === $url || strlen( $title ) < 6 ) {
return array();
}
// Catégories (.taxonomy-style hors badge .highlighted).
$cats = array();
$cat_nodes = $xpath->query( './/*[contains(@class,"taxonomy-style")]', $row );
if ( $cat_nodes instanceof DOMNodeList ) {
foreach ( $cat_nodes as $cat_node ) {
if ( $cat_node instanceof DOMElement && false !== strpos( $cat_node->getAttribute( 'class' ), 'highlighted' ) ) {
continue;
}
$cat_txt = self::normalize_space( $cat_node->textContent );
if ( '' !== $cat_txt && strlen( $cat_txt ) < 45 && ! in_array( $cat_txt, $cats, true ) ) {
$cats[] = $cat_txt;
}
}
}
$category = ! empty( $cats ) ? implode( ' | ', array_slice( $cats, 0, 2 ) ) : 'Confédération CGT';
// Résumé (.field-type-text-long ou paragraphe dans .txt-container).
$summary = '';
$p_nodes = $xpath->query( './/*[contains(@class,"field-type-text-long")]|.//div[contains(@class,"txt-container")]//p|.//p', $row );
if ( $p_nodes instanceof DOMNodeList ) {
foreach ( $p_nodes as $p ) {
$txt = self::normalize_space( $p->textContent );
if ( strlen( $txt ) >= 10 && $txt !== $title ) {
$summary = $txt;
break;
}
}
}
// Image (.views-field-field-imagette-media img en priorité, sinon premier img).
$image_url = '';
$img_nodes = $xpath->query( './/div[contains(@class,"views-field-field-imagette-media")]//img|.//img', $row );
if ( $img_nodes instanceof DOMNodeList && $img_nodes->length > 0 ) {
$img = $img_nodes->item( 0 );
if ( $img instanceof DOMElement ) {
$src = $img->getAttribute( 'data-src' ) ?: $img->getAttribute( 'src' );
if ( '' === $src && $img->hasAttribute( 'srcset' ) ) {
$parts = explode( ',', $img->getAttribute( 'srcset' ) );
$first = trim( (string) $parts[0] );
$src = strtok( $first, ' ' ) ?: '';
}
$image_url = self::absolutize_url( $src );
}
}
return array(
'title' => $title,
'summary' => $summary,
'image_url' => $image_url,
'category' => $category,
'url' => $url,
);
}
/**
* Enrichit un article directement depuis sa page individuelle sur cgt.fr si résumé ou image manquent.
*
* @param array<string, string> $item Article partiel.
* @return array<string, string>
*/
private static function enrich_from_article_page( array $item ): array {
if ( empty( $item['url'] ) ) {
return $item;
}
$html = self::remote_get_html( $item['url'] );
if ( '' === $html ) {
return $item;
}
$xpath = self::create_xpath( $html );
if ( ! $xpath ) {
return $item;
}
if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) ) {
$candidates = array(
'.//*[contains(@class,"chapo") or contains(@class,"field-name-field-chapo")]',
'//meta[@property="og:description"]/@content',
'//meta[@name="description"]/@content',
'//main//article//p',
);
foreach ( $candidates as $query ) {
$nodes = $xpath->query( $query );
if ( $nodes instanceof DOMNodeList && $nodes->length > 0 ) {
foreach ( $nodes as $node ) {
$txt = self::normalize_space( $node instanceof DOMAttr ? $node->value : $node->textContent );
if ( strlen( $txt ) >= 15 && $txt !== ( $item['title'] ?? '' ) ) {
$item['summary'] = $txt;
break 2;
}
}
}
}
}
if ( empty( $item['image_url'] ) ) {
$og_img = $xpath->query( '//meta[@property="og:image"]/@content' );
if ( $og_img instanceof DOMNodeList && $og_img->length > 0 ) {
$src = trim( (string) $og_img->item( 0 )->nodeValue );
if ( '' !== $src ) {
$item['image_url'] = self::absolutize_url( $src );
}
}
}
return $item;
}
/**
* Localise la section ancêtre correspondant à un titre textuel donné.
*/
private static function find_section_by_heading_text( DOMXPath $xpath, string $needle ): ?DOMNode {
$headings = $xpath->query( '//h1|//h2|//h3' );
if ( ! ( $headings instanceof DOMNodeList ) ) {
return null;
}
foreach ( $headings as $heading ) {
$text = self::normalize_space( $heading->textContent );
$low = function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text );
if ( false !== strpos( $low, $needle ) ) {
$cur = $heading->parentNode;
while ( $cur && $cur instanceof DOMElement ) {
if ( 'section' === strtolower( $cur->tagName ) ) {
return $cur;
}
$cur = $cur->parentNode;
}
return $heading->parentNode;
}
}
return null;
}
/**
* Vérifie qu'un href pointe vers un article (et non une taxonomie ou l'index /actualites).
*/
private static function is_article_href( string $href ): bool {
if ( '' === $href || '#' === $href || 0 === strpos( $href, 'mailto:' ) || 0 === strpos( $href, 'tel:' ) ) {
return false;
}
if ( false !== strpos( $href, '/terms/' ) || false !== strpos( $href, '/taxonomy/' ) ) {
return false;
}
$path = untrailingslashit( (string) wp_parse_url( $href, PHP_URL_PATH ) );
return '' !== $path && '/actualites' !== $path && '/' !== $path;
}
/**
* Normalise une URL sur son chemin pour la correspondance fiable entre accueil et /actualites.
*/
private static function normalize_url_key( string $url ): string {
$path = (string) wp_parse_url( trim( $url ), PHP_URL_PATH );
$path = strtolower( untrailingslashit( $path ) );
return '' === $path || '/' === $path ? '' : $path;
}
/**
* Initialise un DOMXPath UTF-8 fiable sur PHP 8.x sans altérer les accents ni apostrophes typographiques.
*/
private static function create_xpath( string $html ): ?DOMXPath {
if ( '' === trim( $html ) || ! class_exists( 'DOMDocument' ) ) {
return null;
}
if ( function_exists( 'mb_encode_numericentity' ) ) {
$prepared = mb_encode_numericentity( $html, array( 0x80, 0x10FFFF, 0, ~0 ), 'UTF-8' );
} else {
$prepared = preg_replace( '/<head\b[^>]*>/i', '$0<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">', $html, 1 ) ?: $html;
}
$prev = libxml_use_internal_errors( true );
$dom = new DOMDocument( '1.0', 'UTF-8' );
$dom->loadHTML( $prepared, LIBXML_NONET | LIBXML_NOWARNING | LIBXML_NOERROR );
libxml_clear_errors();
libxml_use_internal_errors( $prev );
return new DOMXPath( $dom );
}
/**
* Convertit une URL relative cgt.fr en URL absolue.
*/
private static function absolutize_url( string $raw ): string {
$raw = trim( $raw );
if ( '' === $raw || 0 === strpos( $raw, 'data:' ) || 0 === strpos( $raw, 'javascript:' ) ) {
return '';
}
if ( 0 === strpos( $raw, '//' ) ) {
return 'https:' . $raw;
}
if ( preg_match( '#^https?://#i', $raw ) ) {
return esc_url_raw( $raw );
}
if ( '/' === $raw[0] ) {
return esc_url_raw( 'https://www.cgt.fr' . $raw );
}
return esc_url_raw( 'https://www.cgt.fr/' . ltrim( $raw, '/' ) );
}
/**
* Normalise les espaces blancs et entités HTML.
*/
private static function normalize_space( string $text ): string {
$decoded = html_entity_decode( wp_strip_all_tags( $text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
return trim( (string) preg_replace( '/\s+/u', ' ', $decoded ) );
}
/**
* Nettoie et tronque un article pour l'affichage sans phrase générique répétitive.
*
* @param array<string, string> $item Données brutes de l'article.
* @return array<string, string>
*/
private static function normalize_article( array $item, int $max_chars = 190 ): array {
$placeholder = CGT_FHA_URI . '/assets/img/placeholder.svg';
$summary = self::normalize_space( (string) ( $item['summary'] ?? '' ) );
if ( '' !== $summary && function_exists( 'mb_strlen' ) && mb_strlen( $summary, 'UTF-8' ) > $max_chars ) {
$summary = rtrim( mb_substr( $summary, 0, $max_chars - 1, 'UTF-8' ) ) . '…';
}
return array(
'title' => self::normalize_space( (string) ( $item['title'] ?? '' ) ),
'summary' => $summary,
'image_url' => ! empty( $item['image_url'] ) ? esc_url_raw( (string) $item['image_url'] ) : $placeholder,
'category' => self::normalize_space( (string) ( $item['category'] ?? 'Confédération CGT' ) ),
'url' => ! empty( $item['url'] ) ? esc_url_raw( (string) $item['url'] ) : 'https://www.cgt.fr/actualites',
);
}
/**
* Fallback statique si cgt.fr est inaccessible lors du premier démarrage.
*
* @return array<string, mixed>
*/
private static function get_static_fallback(): array {
$placeholder = CGT_FHA_URI . '/assets/img/placeholder.svg';
return array(
'fetched_at' => 0,
'source' => 'Fallback éditorial local',
'featured' => array(
'title' => 'Salaires, retraites, services publics : toutes et tous mobilisés avec la CGT',
'summary' => 'Retrouvez les dossiers nationaux, les revendications confédérales et les appels à l\'action portés par la CGT partout en France.',
'image_url' => $placeholder,
'category' => 'À la une',
'url' => 'https://www.cgt.fr/actualites',
),
'latest' => array(
array(
'title' => 'Pouvoir d\'achat : augmenter les salaires et le SMIC',
'summary' => 'Les propositions économiques de la CGT pour indexer les salaires sur l\'inflation et revaloriser les grilles.',
'image_url' => $placeholder,
'category' => 'Salaires',
'url' => 'https://www.cgt.fr/actualites',
),
array(
'title' => 'Conditions de travail & santé au travail',
'summary' => 'Prévenir l\'usure professionnelle, renforcer le rôle des élus CSE et défendre les droits des salarié·e·s.',
'image_url' => $placeholder,
'category' => 'Travail',
'url' => 'https://www.cgt.fr/actualites',
),
array(
'title' => 'Services publics de proximité dans nos territoires',
'summary' => 'Défendre l\'hôpital public, l\'école, les transports et les services publics essentiels en milieu rural.',
'image_url' => $placeholder,
'category' => 'Services publics',
'url' => 'https://www.cgt.fr/actualites',
),
),
);
}
/**
* Rendu HTML/CSS de la Section 4 (fidèle à la maquette).
*/
public static function render_section( string $section_title = 'ACTUALITÉ CONFÉDÉRALE' ): void {
$feed = self::get_feed();
$featured = $feed['featured'] ?? array();
$latest = $feed['latest'] ?? array();
if ( empty( $featured ) ) {
return;
}
?>
<section class="cgt-section cgt-section--soft" aria-labelledby="cgt-confederal-heading">
<div class="cgt-container">
<h2 id="cgt-confederal-heading" class="cgt-section__title"><?php echo esc_html( $section_title ); ?></h2>
<!-- Bloc supérieur « À LA UNE » -->
<article class="cgt-confederal-featured">
<div class="cgt-confederal-featured__media">
<img
src="<?php echo esc_url( $featured['image_url'] ); ?>"
alt="<?php echo esc_attr( $featured['title'] ); ?>"
class="cgt-confederal-featured__img"
loading="lazy"
decoding="async"
/>
<span class="cgt-confederal-featured__badge">À LA UNE</span>
</div>
<div class="cgt-confederal-featured__box">
<span class="cgt-confederal-featured__kicker"><?php echo esc_html( $featured['category'] ?: 'À LA UNE' ); ?></span>
<h3 class="cgt-confederal-featured__title"><?php echo esc_html( $featured['title'] ); ?></h3>
<?php if ( '' !== trim( (string) ( $featured['summary'] ?? '' ) ) ) : ?>
<p class="cgt-confederal-featured__summary"><?php echo esc_html( $featured['summary'] ); ?></p>
<?php endif; ?>
<a
href="<?php echo esc_url( $featured['url'] ); ?>"
class="cgt-btn cgt-btn--outline-dark"
target="_blank"
rel="noopener noreferrer"
>LIRE SUR CGT.FR ↗</a>
</div>
</article>
<!-- Grille inférieure : 3 articles -->
<?php if ( ! empty( $latest ) ) : ?>
<div class="cgt-confederal-grid">
<?php foreach ( $latest as $item ) : ?>
<article class="cgt-confederal-card">
<img
src="<?php echo esc_url( $item['image_url'] ); ?>"
alt="<?php echo esc_attr( $item['title'] ); ?>"
class="cgt-confederal-card__img"
loading="lazy"
decoding="async"
/>
<div class="cgt-confederal-card__box">
<h3 class="cgt-confederal-card__title"><?php echo esc_html( $item['title'] ); ?></h3>
<?php if ( '' !== trim( (string) ( $item['summary'] ?? '' ) ) ) : ?>
<p class="cgt-confederal-card__summary"><?php echo esc_html( $item['summary'] ); ?></p>
<?php endif; ?>
<div class="cgt-confederal-card__cta">
<a
href="<?php echo esc_url( $item['url'] ); ?>"
class="cgt-confederal-more"
target="_blank"
rel="noopener noreferrer"
>
<span class="cgt-confederal-more__label">Lire la suite</span>
<span class="cgt-confederal-more__arrow" aria-hidden="true">→</span>
</a>
</div>
</div>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php
}
}
CGT_FHA_Confederal_Feed::init();