# ai-builder/2.7.8/includes/class-template-assets.php

AI Builder – Generate pages, blocks, images &amp; translate with AI, version 2.7.8. 579 lines.

- Page: https://pluginprobe.com/plugins/ai-builder/2.7.8/code/includes/class-template-assets.php
- Raw: https://pluginprobe.com/plugins/ai-builder/2.7.8/raw/includes/class-template-assets.php
- Modified: 2026-09-08T10:53:48+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ai-builder/2.7.8/code/includes/class-template-assets.php#L10-L20`.

```php
<?php
/**
 * Service de gestion du CSS / JS personnalisés pour les templates et
 * template parts (Site Editor / Full Site Editing).
 *
 * Pour les pages et articles "classiques" on continue d'utiliser les post meta
 * (ai_builder_css_content, ai_builder_js_content) via AIBUI_CSS_Handler /
 * AIBUI_JS_Handler. Ce service gère uniquement les identifiants composites
 * "theme//slug" (ex : "twentytwentyfive//404", "twentytwentyfive//header").
 *
 * Stockage : un unique wp_option "aibui_template_assets" (autoload = no)
 * contenant une map :
 *
 *   [
 *     'twentytwentyfive//404' => [
 *         '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 <head>.
     */
    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 '<style id="ai-builder-template-css" type="text/css">' . $css . '</style>';
            }
        } catch (\Throwable $e) {
            $this->log_error('inject_css', $e);
        }
    }

    /**
     * Injection JS dans le <footer>.
     */
    public function inject_js()
    {
        try {
            // Résolution / complément (les hooks pre_render_block ont déjà
            // alimenté $used_templates à ce stade, mais on reste idempotent).
            $this->resolve_current_templates();

            if (empty($this->used_templates)) {
                return;
            }
            $map = $this->get_map();
            if (empty($map)) {
                return;
            }
            $js = '';
            foreach (array_keys($this->used_templates) as $tid) {
                if (!isset($map[$tid]) || !is_array($map[$tid])) {
                    continue;
                }
                $value = isset($map[$tid]['js']) ? (string) $map[$tid]['js'] : '';
                if ($value !== '') {
                    // Isolation via IIFE pour limiter les conflits de scope.
                    $js .= "\n;/* ai-builder template JS: " . $tid . " */\n"
                        . "(function(){ try { " . $value . " } catch(e){ if (window.console) console.error('ai-builder template JS error', e); } })();\n";
                }
            }
            if ($js !== '') {
                echo '<script id="ai-builder-template-js" type="text/javascript">' . $js . '</script>';
            }
        } catch (\Throwable $e) {
            $this->log_error('inject_js', $e);
        }
    }

    /**
     * Émet un commentaire HTML de diagnostic dans la sortie pour aider
     * au débogage du mécanisme d'injection sur le front.
     *
     * À retirer une fois le mécanisme stabilisé en production.
     */
    private function print_debug_comment()
    {
        try {
            $main      = isset($GLOBALS['_wp_current_template_id']) ? (string) $GLOBALS['_wp_current_template_id'] : '';
            $has_cont  = !empty($GLOBALS['_wp_current_template_content']);
            $is_block  = function_exists('wp_is_block_theme') ? (int) wp_is_block_theme() : -1;
            $stylesh   = function_exists('get_stylesheet') ? get_stylesheet() : '';
            $detected  = array_keys($this->used_templates);
            $map_keys  = array_keys($this->get_map());
            $enabled   = (int) $this->is_enabled();

            $line = sprintf(
                'ai-builder template-assets :: enabled=%d is_block_theme=%d stylesheet=%s main_template_id=%s has_template_content=%d detected=[%s] stored_keys=[%s]',
                $enabled,
                $is_block,
                $stylesh,
                $main !== '' ? $main : '(empty)',
                $has_cont ? 1 : 0,
                implode(',', $detected),
                implode(',', $map_keys)
            );

            // Un commentaire HTML sûr : on interdit les "--" qui fermeraient
            // prématurément le commentaire.
            $line = str_replace('--', '- -', $line);
            echo "\n<!-- " . $line . " -->\n";
        } catch (\Throwable $e) {
            // Jamais de fatal sur un bloc de debug.
        }
    }

    /**
     * Vérifie qu'un utilisateur a le droit d'éditer les templates / parts.
     * Les capabilities "edit_theme_options" couvre le Site Editor en pratique.
     *
     * @return bool
     */
    public static function current_user_can_edit()
    {
        return current_user_can('edit_theme_options');
    }

    /**
     * Log défensif (ne déclenche jamais de fatal).
     */
    private function log_error($context, $e)
    {
        if (defined('WP_DEBUG') && WP_DEBUG && function_exists('error_log')) {
            error_log('[AIBUI_Template_Assets][' . $context . '] ' . $e->getMessage());
        }
    }
}

} // fin du guard if (!class_exists('AIBUI_Template_Assets', false))

// Initialisation unique (protégée pour ne jamais faire planter le site).
// Grâce au singleton, même si ce fichier est inclus plusieurs fois dans la
// même requête, les hooks ne sont enregistrés qu'à la première exécution.
if (class_exists('AIBUI_Template_Assets')) {
    try {
        AIBUI_Template_Assets::instance();
    } catch (\Throwable $e) {
        if (defined('WP_DEBUG') && WP_DEBUG && function_exists('error_log')) {
            error_log('[AIBUI_Template_Assets] bootstrap error: ' . $e->getMessage());
        }
    }
}

```
