PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / trunk
AI Builder – Generate pages, blocks, images & translate with AI vtrunk
2.7.10 2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.3.10 All 122 releases
ai-builder / includes / class-template-assets-renderer.php

class-template-assets-renderer.php in AI Builder – Generate pages, blocks, images & translate with AI trunk, at includes/class-template-assets-renderer.php

507 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Injection du CSS / JS personnalisés stockés dans les post meta
4 * des templates (wp_template) et template parts (wp_template_part).
5 *
6 * Le stockage existant (ai_builder_css_content / ai_builder_js_content
7 * en post meta) est réutilisé tel quel. Aucune nouvelle option, aucune
8 * nouvelle table, aucune migration.
9 *
10 * Principes pour éviter les fatals en production :
11 * - Garde class_exists() + flag statique d'instanciation
12 * - Chaque callback wrappé dans try/catch (\Throwable)
13 * - Aucune récursion maison : on laisse WP rappeler render_block
14 * pour les sous-template-parts
15 * - Bail immédiat en admin / AJAX / cron / REST
16 * - Lookups mis en cache par requête
17 * - Ne jamais échouer : en cas de souci on renvoie la valeur d'origine
18 */
19
20 if (!defined('ABSPATH')) {
21 exit;
22 }
23
24 if (!class_exists('AIBUI_Template_Assets_Renderer', false)) {
25
26 class AIBUI_Template_Assets_Renderer
27 {
28 const META_CSS = 'ai_builder_css_content';
29 const META_JS = 'ai_builder_js_content';
30 const MAX_BYTES = 262144; // 256 Ko max par bloc, limite défensive
31
32 /**
33 * Empêche toute double-initialisation (double require, symlinks, etc.).
34 *
35 * @var bool
36 */
37 private static $booted = false;
38
39 /**
40 * Cache des assets par identifiant "theme//slug".
41 *
42 * @var array<string, array{css:string, js:string}|null>
43 */
44 private $cache = array();
45
46 /**
47 * Cache spécifique pour le template principal de la requête.
48 *
49 * @var array{id:string, tpl_exists:bool, wp_id:int, css:string, js:string}|null
50 */
51 private $main_cache = null;
52
53 /**
54 * Cache des assets de tous les template parts customisés (global).
55 *
56 * @var array<int, array{id:string, slug:string, theme:string, css:string, js:string}>|null
57 */
58 private $all_parts_cache = null;
59
60 /**
61 * Amorçage unique.
62 */
63 public static function boot()
64 {
65 if (self::$booted) {
66 return;
67 }
68 self::$booted = true;
69
70 try {
71 new self();
72 } catch (\Throwable $e) {
73 // Silencieux : mieux vaut ne rien injecter que casser le site.
74 }
75 }
76
77 public function __construct()
78 {
79 // Ne tourner que sur un rendu front classique.
80 if (is_admin()) {
81 return;
82 }
83 if (defined('DOING_AJAX') && DOING_AJAX) {
84 return;
85 }
86 if (defined('DOING_CRON') && DOING_CRON) {
87 return;
88 }
89 if (defined('REST_REQUEST') && REST_REQUEST) {
90 return;
91 }
92
93 // On injecte tout via wp_head / wp_footer plutôt que render_block
94 // pour rester robuste quand le template parent inline directement
95 // le contenu du header/footer et ne référence plus le template-part
96 // en tant que bloc.
97 add_action('wp_head', array($this, 'inject_main_template_css'), 100);
98 add_action('wp_footer', array($this, 'inject_main_template_js'), 100);
99 }
100
101 /**
102 * Pour chaque bloc core/template-part rendu, on préfixe le contenu
103 * par un <style> et on suffixe par un <script> (si présents en meta).
104 * Aucun parcours récursif : si le template part contient d'autres
105 * core/template-part, WP rappellera ce même filtre pour chacun.
106 *
107 * @param string $block_content
108 * @param array<string,mixed> $block
109 * @return string
110 */
111 public function inject_template_part_assets($block_content, $block)
112 {
113 try {
114 if (!is_array($block)) {
115 return $block_content;
116 }
117 if (empty($block['blockName']) || $block['blockName'] !== 'core/template-part') {
118 return $block_content;
119 }
120
121 $attrs = isset($block['attrs']) && is_array($block['attrs']) ? $block['attrs'] : array();
122 $slug = isset($attrs['slug']) ? (string) $attrs['slug'] : '';
123
124 $theme_attr = isset($attrs['theme']) && is_string($attrs['theme']) ? (string) $attrs['theme'] : '';
125 $theme = $theme_attr !== ''
126 ? $theme_attr
127 : (function_exists('get_stylesheet') ? (string) get_stylesheet() : '');
128
129 // Commentaire de diagnostic systématique pour pouvoir tracer
130 // sans activer WP_DEBUG ce qu'on voit au niveau du filtre
131 // render_block (slug/theme/détection du post + taille assets).
132 $debug = 'ai-builder render_block core/template-part: '
133 . 'slug=' . ($slug !== '' ? $slug : '(empty)')
134 . ' attr_theme=' . ($theme_attr !== '' ? $theme_attr : '(empty)')
135 . ' resolved_theme=' . ($theme !== '' ? $theme : '(empty)');
136
137 if ($slug === '' || $theme === '') {
138 return $this->debug_comment($debug . ' => SKIP (missing slug or theme)')
139 . $block_content;
140 }
141
142 $template_id = $theme . '//' . $slug;
143 $lookup = $this->get_template_assets_verbose($template_id, 'wp_template_part');
144 $debug .= ' id=' . $template_id
145 . ' tpl_exists=' . ($lookup['tpl_exists'] ? 1 : 0)
146 . ' wp_id=' . (int) $lookup['wp_id']
147 . ' css_len=' . strlen($lookup['css'])
148 . ' js_len=' . strlen($lookup['js']);
149
150 $prefix = $this->debug_comment($debug);
151
152 $id_suffix = $this->sanitize_html_id($template_id);
153 if ($lookup['css'] !== '') {
154 $prefix .= '<style id="ai-builder-tpl-css-' . $id_suffix . '" type="text/css">'
155 . $lookup['css']
156 . '</style>';
157 }
158 $suffix = '';
159 if ($lookup['js'] !== '') {
160 $suffix .= '<script id="ai-builder-tpl-js-' . $id_suffix . '" type="text/javascript">'
161 . '(function(){ try{ ' . $lookup['js']
162 . ' } catch(e){ if (window.console) console.error("ai-builder template part JS", e); } })();'
163 . '</script>';
164 }
165
166 return $prefix . $block_content . $suffix;
167 } catch (\Throwable $e) {
168 return $block_content;
169 }
170 }
171
172 /**
173 * Émet un commentaire HTML sûr (pas de "--" qui pourrait fermer
174 * prématurément un commentaire, pas de retour chariot).
175 */
176 private function debug_comment($line)
177 {
178 $line = (string) $line;
179 $line = str_replace(array("\r", "\n"), ' ', $line);
180 $line = str_replace('--', '- -', $line);
181 return "\n<!-- " . $line . " -->\n";
182 }
183
184 /**
185 * CSS du template principal + CSS de tous les template parts customisés
186 * par l'utilisateur, émis dans <head>.
187 *
188 * On injecte tous les template parts sans dépendre du filtre
189 * render_block parce que certains thèmes / customisations inlineent
190 * directement le contenu du header/footer et ne conservent plus de
191 * bloc core/template-part dans le template parent.
192 */
193 public function inject_main_template_css()
194 {
195 try {
196 $info = $this->get_main_template_info();
197 echo $this->debug_comment(
198 'ai-builder main template (head): id=' . ($info['id'] !== '' ? $info['id'] : '(empty)')
199 . ' tpl_exists=' . ($info['tpl_exists'] ? 1 : 0)
200 . ' wp_id=' . (int) $info['wp_id']
201 . ' css_len=' . strlen($info['css'])
202 . ' js_len=' . strlen($info['js'])
203 );
204 if ($info['css'] !== '') {
205 echo '<style id="ai-builder-template-css" type="text/css">'
206 . $info['css']
207 . '</style>';
208 }
209
210 $parts = $this->get_all_template_parts_assets();
211 echo $this->debug_comment(
212 'ai-builder template parts (head): count=' . count($parts)
213 . ' ids=' . $this->summarize_part_ids($parts)
214 );
215 foreach ($parts as $part) {
216 if ($part['css'] === '') {
217 continue;
218 }
219 $id_suffix = $this->sanitize_html_id($part['id']);
220 echo '<style id="ai-builder-tpl-css-' . $id_suffix . '" type="text/css">'
221 . $part['css']
222 . '</style>';
223 }
224 } catch (\Throwable $e) {
225 // silencieux
226 }
227 }
228
229 /**
230 * JS du template principal + JS de tous les template parts customisés,
231 * émis dans <footer>.
232 */
233 public function inject_main_template_js()
234 {
235 try {
236 $info = $this->get_main_template_info();
237 echo $this->debug_comment(
238 'ai-builder main template (footer): id=' . ($info['id'] !== '' ? $info['id'] : '(empty)')
239 . ' wp_id=' . (int) $info['wp_id']
240 . ' js_len=' . strlen($info['js'])
241 );
242 if ($info['js'] !== '') {
243 echo '<script id="ai-builder-template-js" type="text/javascript">'
244 . '(function(){ try{ ' . $info['js']
245 . ' } catch(e){ if (window.console) console.error("ai-builder template JS", e); } })();'
246 . '</script>';
247 }
248
249 $parts = $this->get_all_template_parts_assets();
250 echo $this->debug_comment(
251 'ai-builder template parts (footer): count=' . count($parts)
252 );
253 foreach ($parts as $part) {
254 if ($part['js'] === '') {
255 continue;
256 }
257 $id_suffix = $this->sanitize_html_id($part['id']);
258 echo '<script id="ai-builder-tpl-js-' . $id_suffix . '" type="text/javascript">'
259 . '(function(){ try{ ' . $part['js']
260 . ' } catch(e){ if (window.console) console.error("ai-builder template part JS", e); } })();'
261 . '</script>';
262 }
263 } catch (\Throwable $e) {
264 // silencieux
265 }
266 }
267
268 /**
269 * Résumé compact des ids de template parts pour les commentaires.
270 *
271 * @param array<int, array{id:string}> $parts
272 * @return string
273 */
274 private function summarize_part_ids($parts)
275 {
276 $ids = array();
277 foreach ($parts as $part) {
278 $ids[] = $part['id'];
279 }
280 if (empty($ids)) {
281 return '(none)';
282 }
283 return implode(',', array_slice($ids, 0, 20));
284 }
285
286 /**
287 * Retourne (en cache) les assets de tous les posts wp_template_part
288 * qui ont du contenu CSS ou JS dans leur post meta.
289 *
290 * Limite défensive à 50 entrées pour éviter une boulette utilisateur.
291 *
292 * @return array<int, array{id:string, slug:string, theme:string, css:string, js:string}>
293 */
294 private function get_all_template_parts_assets()
295 {
296 if (is_array($this->all_parts_cache)) {
297 return $this->all_parts_cache;
298 }
299 $this->all_parts_cache = array();
300
301 try {
302 $query = new \WP_Query(array(
303 'post_type' => 'wp_template_part',
304 'post_status' => array('publish', 'auto-draft'),
305 'posts_per_page' => 50,
306 'no_found_rows' => true,
307 'fields' => 'ids',
308 'meta_query' => array(
309 'relation' => 'OR',
310 array(
311 'key' => self::META_CSS,
312 'compare' => 'EXISTS',
313 ),
314 array(
315 'key' => self::META_JS,
316 'compare' => 'EXISTS',
317 ),
318 ),
319 ));
320
321 $post_ids = isset($query->posts) && is_array($query->posts) ? $query->posts : array();
322 foreach ($post_ids as $post_id) {
323 $post_id = (int) $post_id;
324 if ($post_id <= 0) {
325 continue;
326 }
327 $css = (string) get_post_meta($post_id, self::META_CSS, true);
328 $js = (string) get_post_meta($post_id, self::META_JS, true);
329 if ($css === '' && $js === '') {
330 continue;
331 }
332 if (strlen($css) > self::MAX_BYTES) {
333 $css = substr($css, 0, self::MAX_BYTES);
334 }
335 if (strlen($js) > self::MAX_BYTES) {
336 $js = substr($js, 0, self::MAX_BYTES);
337 }
338
339 $slug = (string) get_post_field('post_name', $post_id);
340
341 $theme = '';
342 $terms = get_the_terms($post_id, 'wp_theme');
343 if (is_array($terms)) {
344 foreach ($terms as $term) {
345 if (isset($term->name) && is_string($term->name) && $term->name !== '') {
346 $theme = (string) $term->name;
347 break;
348 }
349 }
350 }
351 if ($theme === '' && function_exists('get_stylesheet')) {
352 $theme = (string) get_stylesheet();
353 }
354
355 $composite_id = ($theme !== '' ? $theme : 'theme') . '//' . ($slug !== '' ? $slug : ('post-' . $post_id));
356
357 $this->all_parts_cache[] = array(
358 'id' => $composite_id,
359 'slug' => $slug,
360 'theme' => $theme,
361 'css' => $css,
362 'js' => $js,
363 );
364 }
365 } catch (\Throwable $e) {
366 // silencieux
367 }
368
369 return $this->all_parts_cache;
370 }
371
372 /**
373 * Récupère (+ met en cache) les infos détaillées pour le template
374 * principal, utilisées à la fois pour l'injection et pour les
375 * commentaires de diagnostic HTML.
376 *
377 * @return array{id:string, tpl_exists:bool, wp_id:int, css:string, js:string}
378 */
379 private function get_main_template_info()
380 {
381 if ($this->main_cache !== null) {
382 return $this->main_cache;
383 }
384
385 $info = array(
386 'id' => '',
387 'tpl_exists' => false,
388 'wp_id' => 0,
389 'css' => '',
390 'js' => '',
391 );
392
393 try {
394 $id = isset($GLOBALS['_wp_current_template_id']) && is_string($GLOBALS['_wp_current_template_id'])
395 ? $GLOBALS['_wp_current_template_id']
396 : '';
397 $info['id'] = $id;
398 if ($id !== '') {
399 $lookup = $this->get_template_assets_verbose($id, 'wp_template');
400 $info['tpl_exists'] = $lookup['tpl_exists'];
401 $info['wp_id'] = $lookup['wp_id'];
402 $info['css'] = $lookup['css'];
403 $info['js'] = $lookup['js'];
404 }
405 } catch (\Throwable $e) {
406 // silencieux
407 }
408
409 $this->main_cache = $info;
410 return $info;
411 }
412
413 /**
414 * Résout un "theme//slug" en post_id via get_block_template()
415 * puis lit les post meta. Version "verbose" qui renvoie aussi des
416 * infos de diagnostic (tpl_exists, wp_id) — utile pour les
417 * commentaires HTML de trace.
418 *
419 * @param string $template_id ex. "twentytwentyfive//404"
420 * @param string $type "wp_template" ou "wp_template_part"
421 * @return array{tpl_exists:bool, wp_id:int, css:string, js:string}
422 */
423 private function get_template_assets_verbose($template_id, $type)
424 {
425 $cache_key = $type . ':' . $template_id;
426 if (array_key_exists($cache_key, $this->cache)) {
427 return $this->cache[$cache_key];
428 }
429
430 $out = array(
431 'tpl_exists' => false,
432 'wp_id' => 0,
433 'css' => '',
434 'js' => '',
435 );
436
437 try {
438 if (!function_exists('get_block_template')) {
439 $this->cache[$cache_key] = $out;
440 return $out;
441 }
442 if ($type !== 'wp_template' && $type !== 'wp_template_part') {
443 $this->cache[$cache_key] = $out;
444 return $out;
445 }
446
447 $tpl = get_block_template($template_id, $type);
448 if (!$tpl) {
449 $this->cache[$cache_key] = $out;
450 return $out;
451 }
452 $out['tpl_exists'] = true;
453 $out['wp_id'] = isset($tpl->wp_id) ? (int) $tpl->wp_id : 0;
454
455 if ($out['wp_id'] <= 0) {
456 $this->cache[$cache_key] = $out;
457 return $out;
458 }
459
460 $css = (string) get_post_meta($out['wp_id'], self::META_CSS, true);
461 $js = (string) get_post_meta($out['wp_id'], self::META_JS, true);
462
463 if (strlen($css) > self::MAX_BYTES) {
464 $css = substr($css, 0, self::MAX_BYTES);
465 }
466 if (strlen($js) > self::MAX_BYTES) {
467 $js = substr($js, 0, self::MAX_BYTES);
468 }
469
470 $out['css'] = $css;
471 $out['js'] = $js;
472 } catch (\Throwable $e) {
473 // silencieux
474 }
475
476 $this->cache[$cache_key] = $out;
477 return $out;
478 }
479
480 /**
481 * Rend un id HTML sûr à partir d'un "theme//slug".
482 *
483 * @param string $raw
484 * @return string
485 */
486 private function sanitize_html_id($raw)
487 {
488 $raw = (string) $raw;
489 $san = preg_replace('/[^A-Za-z0-9_\-]/', '-', $raw);
490 if (!is_string($san) || $san === '') {
491 $san = 'x';
492 }
493 return $san;
494 }
495 }
496
497 } // fin class_exists guard
498
499 // Amorçage unique et défensif.
500 if (class_exists('AIBUI_Template_Assets_Renderer', false)) {
501 try {
502 AIBUI_Template_Assets_Renderer::boot();
503 } catch (\Throwable $e) {
504 // Ne jamais faire tomber le plugin pour un souci d'injection.
505 }
506 }
507