Refactoring : sécurité (XSS), découpage en modules inc/* et js/admin/*, IDs résolus par slug, perf (caches, cron Gravatar, assets auto-hébergés), tests

This commit is contained in:
2026-06-10 21:30:25 +02:00
parent e6b73df516
commit 9280c3b9ce
44 changed files with 3209 additions and 2907 deletions

305
inc/ajax.php Normal file
View File

@@ -0,0 +1,305 @@
<?php
/**
* Handlers AJAX du scroll infini (grille + agenda) et données des cartes agenda.
*
* Les deux handlers partagent la lecture des filtres POST et la construction
* de la requête (thalim_ajax_read_filters / thalim_ajax_build_query_args) —
* seuls divergent l'include_children de la clause catégorie, l'exclusion des
* épinglés (grille) et le calcul du today_offset (agenda).
*/
/**
* Lit et assainit les filtres communs envoyés en POST par infiniteScroll.js
* et agendaView.js.
*/
function thalim_ajax_read_filters(): array {
return [
'page' => intval( $_POST['page'] ?? 1 ),
'category' => isset( $_POST['category'] ) ? intval( $_POST['category'] ) : 0,
'axe' => isset( $_POST['axe'] ) ? intval( $_POST['axe'] ) : 0,
'date_from' => isset( $_POST['date_from'] ) ? sanitize_text_field( $_POST['date_from'] ) : '',
'date_to' => isset( $_POST['date_to'] ) ? sanitize_text_field( $_POST['date_to'] ) : '',
'taxonomy' => isset( $_POST['taxonomy'] ) ? sanitize_key( $_POST['taxonomy'] ) : '',
'term' => isset( $_POST['term'] ) ? intval( $_POST['term'] ) : 0,
'filter_cat' => isset( $_POST['filter_cat'] ) ? intval( $_POST['filter_cat'] ) : 0,
'filter_autres' => isset( $_POST['filter_autres'] ) ? intval( $_POST['filter_autres'] ) : 0,
'exclude_cats' => isset( $_POST['exclude_cats'] ) ? sanitize_text_field( $_POST['exclude_cats'] ) : '',
'search' => isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '',
];
}
/**
* Construit les arguments WP_Query communs aux deux handlers.
*
* @param array $f Filtres lus par thalim_ajax_read_filters().
* @param bool $include_children include_children de la clause catégorie principale.
*/
function thalim_ajax_build_query_args( array $f, bool $include_children ): array {
$query_args = [
'post_type' => 'post',
'post_status' => 'publish', // admin-ajax => is_admin() true: sans ça WP ajoute future/draft/pending et décale la pagination vs le front
'posts_per_page' => 12,
'paged' => $f['page'],
'orderby' => 'date',
'order' => 'DESC',
'thalim_event_date_order' => true,
];
if ( $f['search'] ) {
$query_args['s'] = $f['search'];
$query_args['relevanssi'] = true;
$query_args['orderby'] = 'relevance';
}
// Build tax_query — may combine category page filter, taxonomy term, and cat filter
$tax_clauses = [];
if ( $f['category'] ) {
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [ $f['category'] ],
'include_children' => $include_children,
];
}
if ( $f['taxonomy'] && $f['term'] ) {
$tax_clauses[] = [
'taxonomy' => $f['taxonomy'],
'field' => 'term_id',
'terms' => [ $f['term'] ],
];
// Exclure les séances de séminaire sur les pages de taxonomie
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [ thalim_cat_id( 'seance' ) ],
'operator' => 'NOT IN',
];
}
if ( $f['filter_cat'] ) {
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [ $f['filter_cat'] ],
'include_children' => ! $f['filter_autres'],
];
}
if ( $f['exclude_cats'] ) {
$ids = array_filter( array_map( 'intval', explode( ',', $f['exclude_cats'] ) ) );
if ( ! empty( $ids ) ) {
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $ids,
'operator' => 'NOT IN',
];
}
}
if ( ! empty( $tax_clauses ) ) {
$query_args['tax_query'] = count( $tax_clauses ) > 1
? array_merge( [ 'relation' => 'AND' ], $tax_clauses )
: $tax_clauses;
}
if ( $f['axe'] ) {
$query_args['meta_query'] = [[
'key' => 'axes_thematiques',
'value' => $f['axe'],
'type' => 'NUMERIC',
]];
}
if ( $f['date_from'] || $f['date_to'] ) {
$query_args['thalim_event_date_filter'] = [ 'from' => $f['date_from'], 'to' => $f['date_to'] ];
}
return $query_args;
}
// AJAX handler for infinite scroll on category pages
function thalim_load_more_posts() {
check_ajax_referer('load_more_posts', 'nonce');
$GLOBALS['thalim_lang_override'] = sanitize_key( $_POST['lang'] ?? 'fr' );
$f = thalim_ajax_read_filters();
$query_args = thalim_ajax_build_query_args( $f, false );
// Exclude pinned posts on category pages to avoid duplicates (they already appear at the top,
// pulled out of the main flow by category.php). Must mirror category.php exactly.
if ( $f['category'] ) {
$pinned_ids = thalim_get_active_pinned_ids( $f['category'] );
if ( ! empty( $pinned_ids ) ) {
$query_args['post__not_in'] = $pinned_ids;
}
}
$posts = Timber::get_posts($query_args);
if (empty($posts)) {
wp_send_json_success(['html' => '']);
return;
}
$cards = thalim_get_cards_data($posts);
$html = '';
foreach ($posts as $post) {
$html .= Timber::compile('partials/post-card.twig', [
'post' => $post,
'card' => $cards[$post->ID],
'show_category' => true,
'type_only' => $f['category'] > 0, // category pages: type chip only; everywhere else (taxonomy, search, annonces): type or category name
]);
}
wp_send_json_success(['html' => $html]);
}
add_action('wp_ajax_load_more_posts', 'thalim_load_more_posts');
add_action('wp_ajax_nopriv_load_more_posts', 'thalim_load_more_posts');
/**
* Build structured data for one agenda slider card.
*/
function thalim_get_agenda_card_data( $post_id, $lang = 'fr' ) {
$months_fr = ['jan.','fév.','mars','avr.','mai','juin','juil.','août','sept.','oct.','nov.','déc.'];
$months_en = ['Jan.','Feb.','Mar.','Apr.','May','Jun.','Jul.','Aug.','Sep.','Oct.','Nov.','Dec.'];
$months = $lang === 'en' ? $months_en : $months_fr;
$raw_debut = get_post_meta( $post_id, 'date_de_debut', true );
$raw_datetime = get_post_meta( $post_id, 'datetime', true );
if ( $raw_debut && ! str_starts_with( $raw_debut, '0000' ) ) {
$ts = strtotime( $raw_debut );
} elseif ( $raw_datetime && ! str_starts_with( $raw_datetime, '0000' ) ) {
$ts = strtotime( $raw_datetime );
} else {
$ts = get_post_timestamp( $post_id );
}
$raw_fin = get_post_meta( $post_id, 'date_de_fin', true );
$ts_debut = ( $raw_debut && ! str_starts_with( $raw_debut, '0000' ) ) ? strtotime( $raw_debut ) : 0;
$ts_fin = ( $raw_fin && ! str_starts_with( $raw_fin, '0000' ) ) ? strtotime( $raw_fin ) : 0;
// Build date_label — same rules as single.twig sidebar
$fmt_debut = $ts_debut ? thalim_format_date( $raw_debut, $lang ) : '';
$fmt_fin = $ts_fin ? thalim_format_date( $raw_fin, $lang ) : '';
$fmt_dt = ( $raw_datetime && ! str_starts_with( $raw_datetime, '0000' ) )
? thalim_format_date( $raw_datetime, $lang ) : '';
$h_debut = substr( get_post_meta( $post_id, 'heure_de_debut', true ) ?: '', 0, 5 );
$h_fin = substr( get_post_meta( $post_id, 'heure_de_fin', true ) ?: '', 0, 5 );
if ( $fmt_debut || $fmt_fin ) {
if ( $ts_debut && $ts_fin && date( 'Y-m-d', $ts_debut ) === date( 'Y-m-d', $ts_fin ) ) {
// Same day
if ( $h_debut && $h_fin ) {
$date_label = ( $lang === 'en' ? 'On ' : 'Le ' ) . $fmt_debut
. ' ' . ( $lang === 'en' ? 'from ' : 'de ' ) . $h_debut
. ' ' . ( $lang === 'en' ? 'to ' : 'à ' ) . $h_fin;
} elseif ( $h_debut ) {
$date_label = $fmt_debut . ( $lang === 'en' ? ' at ' : ' à ' ) . $h_debut;
} else {
$date_label = $fmt_debut;
}
} elseif ( $fmt_debut && $fmt_fin ) {
$date_label = ( $lang === 'en' ? 'From ' : 'Du ' ) . $fmt_debut
. ' ' . ( $lang === 'en' ? 'to ' : 'au ' ) . $fmt_fin;
} elseif ( $fmt_debut ) {
$date_label = $h_debut
? $fmt_debut . ( $lang === 'en' ? ' at ' : ' à ' ) . $h_debut
: $fmt_debut;
} else {
$date_label = ( $lang === 'en' ? 'Until ' : "Jusqu'au " ) . $fmt_fin;
}
} elseif ( $fmt_dt ) {
$date_label = $h_debut
? $fmt_dt . ( $lang === 'en' ? ' at ' : ' à ' ) . $h_debut
: $fmt_dt;
} else {
$date_label = '';
}
$type_fields = [
'type_colloque_journee_d_etude', 'type_soutenance', 'type_evenement_culturel',
'type_media', 'type_captation', 'type_revue_collection', 'type_autre',
];
$type_label = '';
foreach ( $type_fields as $f ) {
$v = get_post_meta( $post_id, $f, true );
if ( $v ) { $type_label = thalim_bilingual( $v, $lang ); break; }
}
if ( ! $type_label ) {
foreach ( get_the_category( $post_id ) as $cat ) {
if ( $cat->parent ) { $type_label = thalim_cat_name( $cat, $lang ); break; }
}
}
$end_day = $end_month = $end_year = null;
if ( $ts_fin && date( 'Ymd', $ts_fin ) !== date( 'Ymd', $ts ) ) {
$end_day = (int) date( 'j', $ts_fin );
$end_month = $months[ (int) date( 'n', $ts_fin ) - 1 ];
$end_year = (int) date( 'Y', $ts_fin );
}
// Séance de séminaire: link to parent séminaire at #seance-{ID}
$link = get_permalink( $post_id );
if ( in_array( thalim_cat_id( 'seance' ), wp_list_pluck( get_the_category( $post_id ), 'term_id' ) ) ) {
$link = thalim_get_seance_link( $post_id );
}
return [
'day' => (int) date( 'j', $ts ),
'month' => $months[ (int) date( 'n', $ts ) - 1 ],
'year' => (int) date( 'Y', $ts ),
'end_day' => $end_day,
'end_month' => $end_month,
'end_year' => $end_year,
'type_label' => $type_label,
'date_label' => $date_label,
'lieu' => thalim_bilingual( get_post_meta( $post_id, 'lieu', true ) ?: '', $lang ),
'link' => $link,
];
}
function thalim_load_more_agenda() {
check_ajax_referer( 'load_more_posts', 'nonce' );
$lang = sanitize_key( $_POST['lang'] ?? 'fr' );
$GLOBALS['thalim_lang_override'] = $lang;
$f = thalim_ajax_read_filters();
$query_args = thalim_ajax_build_query_args( $f, ! empty( $_POST['include_children'] ) );
// On first page, count future events to find today's anchor position
$today_offset = 0;
if ( (int) $f['page'] === 1 ) {
$offset_args = $query_args;
$offset_args['posts_per_page'] = 1;
$offset_args['no_found_rows'] = false;
$offset_args['paged'] = 1;
$today_str = current_time( 'Y-m-d' );
$existing_filter = $offset_args['thalim_event_date_filter'] ?? [];
$offset_args['thalim_event_date_filter'] = array_merge(
$existing_filter,
[ 'from' => $today_str ]
);
$future_query = new \WP_Query( $offset_args );
$today_offset = (int) $future_query->found_posts;
}
$posts = Timber::get_posts( $query_args );
if ( empty( $posts ) ) {
$response = [ 'html' => '' ];
if ( (int) $f['page'] === 1 ) $response['today_offset'] = $today_offset;
wp_send_json_success( $response );
return;
}
$html = '';
foreach ( $posts as $post ) {
$data = thalim_get_agenda_card_data( $post->ID, $lang );
$html .= Timber::compile( 'partials/agenda-card.twig', array_merge( $data, [ 'post' => $post ] ) );
}
$response = [ 'html' => $html ];
if ( (int) $f['page'] === 1 ) $response['today_offset'] = $today_offset;
wp_send_json_success( $response );
}
add_action( 'wp_ajax_load_more_agenda', 'thalim_load_more_agenda' );
add_action( 'wp_ajax_nopriv_load_more_agenda', 'thalim_load_more_agenda' );