841 lines
28 KiB
PHP
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();
|