[ * 'page_css' => '...', * 'block_css' => '...', * 'css' => '...combined...', * 'page_js' => '...', * 'block_js' => '...', * 'js' => '...combined...', * ], * ... * ] * * Toutes les opérations sont enrobées dans des try/catch silencieux afin * d'éviter tout fatal susceptible de casser un site en production. */ if (!defined('ABSPATH')) { exit; } // NOTE : ne jamais utiliser un `return` top-level ici. Le fichier peut être // inclus plusieurs fois dans la même requête via deux chemins canoniques // différents (symlinks Local, chargeurs multiples, etc.), auquel cas le // `return` sauterait l'enregistrement des hooks alors que la classe est // déjà définie. On protège donc uniquement la redéclaration de classe et // on s'assure que l'instanciation du singleton reste toujours exécutée. if (!class_exists('AIBUI_Template_Assets', false)) { class AIBUI_Template_Assets { const OPTION_KEY = 'aibui_template_assets'; const FEATURE_FLAG = 'aibui_template_assets_enabled'; const MAX_BYTES_PER_TEMPLATE = 262144; // 256 Ko par template pour éviter de gonfler wp_options /** * Templates/template parts repérés pendant le rendu d'une requête front. * Clés = identifiants "theme//slug", valeur = true. * * @var array */ private $used_templates = array(); /** * Instance unique pour permettre aux hooks statiques de référencer * la même collection used_templates. * * @var AIBUI_Template_Assets|null */ private static $instance = null; public static function instance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public function __construct() { if (self::$instance === null) { self::$instance = $this; } // Kill switch : si l'option est explicitement désactivée, on ne branche // aucun hook (utile pour débrancher rapidement depuis la base). if (!$this->is_enabled()) { return; } // Capture des templates / template parts utilisés pour la requête. // Ces hooks servent de "filet" supplémentaire. La vraie résolution des // templates pour la page courante est faite au début de wp_head via // resolve_current_templates() (voir inject_css). add_filter('get_block_template', array($this, 'track_block_template'), 10, 3); add_filter('get_block_file_template', array($this, 'track_block_template'), 10, 3); add_filter('pre_render_block', array($this, 'track_template_part_block'), 10, 2); // Injection sur le front (priorité tardive pour passer après les thèmes) add_action('wp_head', array($this, 'inject_css'), 100); add_action('wp_footer', array($this, 'inject_js'), 100); } /** * Résout les templates à utiliser pour la requête courante en lisant les * globals WordPress remplis par locate_block_template() AVANT wp_head : * - $_wp_current_template_id : ex. "twentytwentyfive//404" * - $_wp_current_template_content : contenu HTML sérialisé des blocs * * Puis parse récursivement les blocs core/template-part pour pousser * aussi leur identifiant (header, footer, etc.) dans $used_templates. * * Idempotent (peut être appelée plusieurs fois sans effet de bord). */ private function resolve_current_templates() { try { // Template principal résolu par le cœur WP pour la requête. if (!empty($GLOBALS['_wp_current_template_id']) && is_string($GLOBALS['_wp_current_template_id'])) { $main_id = self::normalize_template_id($GLOBALS['_wp_current_template_id']); if ($main_id !== '') { $this->used_templates[$main_id] = true; } } // Parser le contenu du template principal pour chercher les template parts. if (function_exists('parse_blocks') && !empty($GLOBALS['_wp_current_template_content']) && is_string($GLOBALS['_wp_current_template_content']) ) { $blocks = parse_blocks($GLOBALS['_wp_current_template_content']); $this->collect_template_parts_from_blocks($blocks, 0); } } catch (\Throwable $e) { $this->log_error('resolve_current_templates', $e); } } /** * Parcours récursif (profondeur limitée) des blocs pour collecter les * identifiants de template parts et leurs sous-template-parts. * * @param array $blocks * @param int $depth pour éviter une récursion infinie en cas de * référence circulaire. */ private function collect_template_parts_from_blocks($blocks, $depth = 0) { if (!is_array($blocks) || $depth > 5) { return; } $theme_default = function_exists('get_stylesheet') ? get_stylesheet() : ''; foreach ($blocks as $block) { if (!is_array($block)) { continue; } if (isset($block['blockName']) && $block['blockName'] === 'core/template-part') { $attrs = isset($block['attrs']) && is_array($block['attrs']) ? $block['attrs'] : array(); $slug = isset($attrs['slug']) ? (string) $attrs['slug'] : ''; $theme = isset($attrs['theme']) ? (string) $attrs['theme'] : $theme_default; if ($slug !== '' && $theme !== '') { $part_id = self::normalize_template_id($theme . '//' . $slug); if ($part_id !== '' && !isset($this->used_templates[$part_id])) { $this->used_templates[$part_id] = true; // Descente récursive : on lit le contenu du template // part pour repérer d'éventuels sous-template-parts // (ex. un header qui inclut un autre template part). if (function_exists('get_block_template')) { $tpl = get_block_template($theme . '//' . $slug, 'wp_template_part'); if ($tpl && !empty($tpl->content) && function_exists('parse_blocks')) { $inner = parse_blocks($tpl->content); $this->collect_template_parts_from_blocks($inner, $depth + 1); } } } } } if (!empty($block['innerBlocks'])) { $this->collect_template_parts_from_blocks($block['innerBlocks'], $depth + 1); } } } /** * Active ou non le mécanisme. Option en base, true par défaut. Peut être * désactivé via : * update_option('aibui_template_assets_enabled', 0); */ public function is_enabled() { $value = get_option(self::FEATURE_FLAG, 1); return !empty($value); } /** * Détermine si une valeur ressemble à un identifiant de template * composite "theme//slug" (vs un ID numérique de post). * * @param mixed $id * @return bool */ public static function is_template_id($id) { if (!is_string($id)) { return false; } // "theme//slug" : au moins un caractère avant et après "//" return (bool) preg_match('#^[A-Za-z0-9_\-]+//[A-Za-z0-9_\-]+$#', $id); } /** * Normalise un identifiant de template pour usage en clé. * * @param string $id * @return string */ public static function normalize_template_id($id) { $id = is_string($id) ? trim($id) : ''; // Certaines APIs encodent le slash ; on décode une fois. if (strpos($id, '%2F') !== false || strpos($id, '%2f') !== false) { $id = rawurldecode($id); } return $id; } /** * Récupère la map complète depuis wp_options. * * @return array */ public function get_map() { $map = get_option(self::OPTION_KEY, array()); return is_array($map) ? $map : array(); } /** * Sauvegarde la map complète (autoload = false pour ne pas charger * potentiellement des centaines de Ko à chaque requête). * * @param array $map * @return bool */ private function save_map(array $map) { return update_option(self::OPTION_KEY, $map, false); } /** * Récupère l'entrée d'un template. * * @param string $template_id * @return array */ public function get_entry($template_id) { $template_id = self::normalize_template_id($template_id); if (!self::is_template_id($template_id)) { return array(); } $map = $this->get_map(); return isset($map[$template_id]) && is_array($map[$template_id]) ? $map[$template_id] : array(); } /** * Sauvegarde CSS pour un template donné. * * Règles : * - $type = 'page' : remplace intégralement le CSS "page" * - $type = 'block' + $replace = true : remplace intégralement le CSS "block" * - $type = 'block' + $replace = false : concatène à la suite du CSS "block" * Puis recalcule la clé 'css' combinée = page_css . block_css. * * @param string $template_id * @param string $css_content * @param string $type 'page'|'block' * @param bool $replace * @return bool */ public function save_css($template_id, $css_content, $type = 'page', $replace = false) { return $this->save_asset($template_id, 'css', $css_content, $type, $replace); } /** * Sauvegarde JS pour un template donné (mêmes règles que save_css). * * @param string $template_id * @param string $js_content * @param string $type 'page'|'block' * @param bool $replace * @return bool */ public function save_js($template_id, $js_content, $type = 'page', $replace = false) { return $this->save_asset($template_id, 'js', $js_content, $type, $replace); } /** * Factorisation save_css / save_js. * * @param string $template_id * @param string $asset 'css'|'js' * @param string $content * @param string $type 'page'|'block' * @param bool $replace * @return bool */ private function save_asset($template_id, $asset, $content, $type, $replace) { try { $template_id = self::normalize_template_id($template_id); if (!self::is_template_id($template_id)) { return false; } if (!in_array($asset, array('css', 'js'), true)) { return false; } if (!in_array($type, array('page', 'block'), true)) { $type = 'page'; } $content = is_string($content) ? $content : ''; // Clip dur pour éviter qu'une entrée corrompue ne fasse exploser wp_options. if (strlen($content) > self::MAX_BYTES_PER_TEMPLATE) { $content = substr($content, 0, self::MAX_BYTES_PER_TEMPLATE); } $map = $this->get_map(); $entry = isset($map[$template_id]) && is_array($map[$template_id]) ? $map[$template_id] : array(); $page_key = 'page_' . $asset; $block_key = 'block_' . $asset; $page = isset($entry[$page_key]) ? (string) $entry[$page_key] : ''; $block = isset($entry[$block_key]) ? (string) $entry[$block_key] : ''; if ($type === 'page') { $page = $content; } else { if ($replace) { $block = $content; } else { $separator = "\n/* Block " . strtoupper($asset) . ' - ' . date('Y-m-d H:i:s') . " */\n"; $block = ($block === '' ? '' : $block . "\n") . $separator . $content . "\n"; } } $combined = $page; if ($block !== '') { $combined = ($combined === '' ? '' : $combined . "\n") . $block; } $entry[$page_key] = $page; $entry[$block_key] = $block; $entry[$asset] = $combined; $map[$template_id] = $entry; $this->save_map($map); return true; } catch (\Throwable $e) { $this->log_error('save_asset', $e); return false; } } /** * Hook get_block_template / get_block_file_template. * Enregistre l'id du template chargé (ex: "twentytwentyfive//404"). * * @param WP_Block_Template|null $template * @param string $id * @param string $template_type * @return WP_Block_Template|null */ public function track_block_template($template, $id, $template_type = '') { try { if ($template && isset($template->id) && is_string($template->id) && $template->id !== '') { $this->used_templates[self::normalize_template_id($template->id)] = true; } elseif (is_string($id) && $id !== '') { $this->used_templates[self::normalize_template_id($id)] = true; } } catch (\Throwable $e) { $this->log_error('track_block_template', $e); } return $template; } /** * Hook pre_render_block : intercepte les blocs core/template-part pour * mémoriser "theme//slug" de chaque template part rendu. * * @param string|null $pre_render * @param array $block * @return string|null */ public function track_template_part_block($pre_render, $block) { try { if (is_array($block) && isset($block['blockName']) && $block['blockName'] === 'core/template-part' ) { $attrs = isset($block['attrs']) && is_array($block['attrs']) ? $block['attrs'] : array(); $slug = isset($attrs['slug']) ? (string) $attrs['slug'] : ''; $theme = isset($attrs['theme']) ? (string) $attrs['theme'] : ''; if ($slug !== '') { if ($theme === '' && function_exists('get_stylesheet')) { $theme = get_stylesheet(); } if ($theme !== '') { $this->used_templates[self::normalize_template_id($theme . '//' . $slug)] = true; } } } } catch (\Throwable $e) { $this->log_error('track_template_part_block', $e); } return $pre_render; } /** * Injection CSS dans . */ public function inject_css() { try { // Résolution des templates de la requête courante (template principal // + template parts référencés, récursivement). $this->resolve_current_templates(); // --- DEBUG : commentaire HTML toujours émis pour diagnostiquer // facilement la détection des templates et la présence des entrées // en base sans activer WP_DEBUG. À retirer en production une fois // le mécanisme stabilisé. $this->print_debug_comment(); if (empty($this->used_templates)) { return; } $map = $this->get_map(); if (empty($map)) { return; } $css = ''; foreach (array_keys($this->used_templates) as $tid) { if (!isset($map[$tid]) || !is_array($map[$tid])) { continue; } $value = isset($map[$tid]['css']) ? (string) $map[$tid]['css'] : ''; if ($value !== '') { $css .= "\n/* ai-builder template CSS: " . $tid . " */\n" . $value; } } if ($css !== '') { echo ''; } } catch (\Throwable $e) { $this->log_error('inject_css', $e); } } /** * Injection JS dans le