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
← All changes | includes/class-ajax-handler.php +390 -97 2.2.3trunk View file →
@@ -9,8 +9,10 @@
9 9 add_action('wp_ajax_aibui_signout', array($this, 'signout'));
10 10 add_action('wp_ajax_aibui_get_token', array($this, 'get_token'));
11 11 add_action('wp_ajax_aibui_save_post_css', array($this, 'save_post_css'));
12 12 add_action('wp_ajax_aibui_get_post_css', array($this, 'get_post_css'));
13 + add_action('wp_ajax_aibui_save_post_js', array($this, 'save_post_js'));
14 + add_action('wp_ajax_aibui_get_post_js', array($this, 'get_post_js'));
13 15 add_action('wp_ajax_aibui_save_page_prompt', array($this, 'save_page_prompt'));
14 16 add_action('wp_ajax_aibui_get_page_prompt', array($this, 'get_page_prompt'));
15 17 add_action('wp_ajax_aibui_save_meta_description', array($this, 'save_meta_description'));
16 18 add_action('wp_ajax_aibui_create_page', array($this, 'create_page'));
@@ -25,8 +27,9 @@
25 27 add_action('wp_ajax_aibui_mark_generation_applied', array($this, 'mark_generation_applied'));
26 28
27 29 // Mark pages created via AI
28 30 add_action('wp_ajax_aibui_mark_ai_created', array($this, 'mark_ai_created'));
31 + add_action('wp_ajax_aibui_get_ai_created_status', array($this, 'get_ai_created_status'));
29 32 }
30 33
31 34 public function save_token()
32 35 {
@@ -99,9 +102,9 @@
99 102 public function get_token()
100 103 {
101 104 // Vérifier que les données POST existent
102 105 if (!isset($_POST['nonce'])) {
103 - wp_send_json_error('Missing required data');
106 + wp_send_json_error('Missing required data', 400);
104 107 }
105 108
106 109 // Déséchapper et assainir les données
107 110 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
@@ -107,9 +110,19 @@
107 110 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
108 111
109 112 // Vérifier le nonce
110 113 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
111 - wp_die('Security check failed');
114 + // wp_die() répondrait une page HTML avec un statut 200 : le client JS
115 + // ne peut ni la parser ni la distinguer d'un vrai échec. On renvoie du
116 + // JSON + 403 pour qu'il affiche « session expirée, rechargez la page »
117 + // plutôt qu'une erreur générique.
118 + wp_send_json_error(
119 + array(
120 + 'code' => 'nonce_expired',
121 + 'message' => 'Security check failed: the WordPress nonce has expired.',
122 + ),
123 + 403
124 + );
112 125 }
113 126
114 127 // Récupérer le token JWT
115 128 $token = get_option('aibui_jwt_token', '');
@@ -120,8 +133,139 @@
120 133
121 134 wp_send_json_success(array('token' => $token));
122 135 }
123 136
137 + /**
138 + * Résout l'ID numérique de post cible pour les endpoints CSS/JS.
139 + *
140 + * Accepte soit :
141 + * - un post_id numérique classique (pages/articles, et templates déjà
142 + * matérialisés en base), soit
143 + * - un template_id composite "theme//slug" + template_type
144 + * ("wp_template" ou "wp_template_part"), utilisé par le Site Editor.
145 + *
146 + * Pour les templates/parts file-based non encore en base, le post
147 + * correspondant est créé à la volée (même stratégie que le Site Editor
148 + * quand l'utilisateur clique sur Save).
149 + *
150 + * @return int Post ID positif, ou 0 si non résoluble.
151 + */
152 + private function resolve_target_post_id($raw_post_id, $template_id, $template_type)
153 + {
154 + // Chemin rapide : un post_id numérique valide est accepté tel quel.
155 + $post_id = 0;
156 + if ($raw_post_id !== '' && is_numeric($raw_post_id)) {
157 + $post_id = intval($raw_post_id);
158 + if ($post_id > 0 && get_post($post_id)) {
159 + return $post_id;
160 + }
161 + $post_id = 0;
162 + }
163 +
164 + // Sinon, on tente la résolution via template_id "theme//slug".
165 + if (!is_string($template_id) || $template_id === '') {
166 + return 0;
167 + }
168 + if (!in_array($template_type, array('wp_template', 'wp_template_part'), true)) {
169 + return 0;
170 + }
171 + if (strpos($template_id, '//') === false) {
172 + return 0;
173 + }
174 + if (!function_exists('get_block_template')) {
175 + return 0;
176 + }
177 +
178 + try {
179 + $tpl = get_block_template($template_id, $template_type);
180 + } catch (\Throwable $e) {
181 + return 0;
182 + }
183 + if (!$tpl) {
184 + return 0;
185 + }
186 +
187 + // Déjà en base → on réutilise.
188 + if (!empty($tpl->wp_id) && (int) $tpl->wp_id > 0) {
189 + return (int) $tpl->wp_id;
190 + }
191 +
192 + // Pas encore en base : on matérialise le template file-based en post
193 + // de la même manière que le Site Editor. Cela nécessite la capability
194 + // edit_theme_options (vérifiée ici, en plus de la vérif au niveau
195 + // endpoint) pour ne jamais créer d'entrée theme à cause d'un save JS.
196 + if (!current_user_can('edit_theme_options')) {
197 + return 0;
198 + }
199 +
200 + list($theme_slug, $slug) = array_pad(explode('//', $template_id, 2), 2, '');
201 + if ($theme_slug === '' || $slug === '') {
202 + return 0;
203 + }
204 +
205 + $title = isset($tpl->title) && $tpl->title !== '' ? (string) $tpl->title : $slug;
206 + $content = isset($tpl->content) ? (string) $tpl->content : '';
207 +
208 + try {
209 + $new_post_id = wp_insert_post(array(
210 + 'post_type' => $template_type,
211 + 'post_status' => 'publish',
212 + 'post_title' => $title,
213 + 'post_name' => $slug,
214 + 'post_content' => $content,
215 + ), true);
216 + } catch (\Throwable $e) {
217 + return 0;
218 + }
219 + if (is_wp_error($new_post_id) || !$new_post_id) {
220 + return 0;
221 + }
222 +
223 + // Rattacher au theme courant (taxonomy wp_theme).
224 + try {
225 + wp_set_object_terms((int) $new_post_id, $theme_slug, 'wp_theme');
226 + } catch (\Throwable $e) {
227 + // non bloquant
228 + }
229 +
230 + // Pour les template parts, rattacher la zone (header/footer/uncategorized...).
231 + if ($template_type === 'wp_template_part') {
232 + $area = isset($tpl->area) && is_string($tpl->area) && $tpl->area !== ''
233 + ? $tpl->area
234 + : 'uncategorized';
235 + try {
236 + wp_set_object_terms((int) $new_post_id, $area, 'wp_template_part_area');
237 + } catch (\Throwable $e) {
238 + // non bloquant
239 + }
240 + }
241 +
242 + return (int) $new_post_id;
243 + }
244 +
245 + /**
246 + * Vérif de capability adaptée au type de cible.
247 + * - wp_template / wp_template_part : edit_theme_options (Site Editor)
248 + * - autres posts : edit_post sur l'ID
249 + */
250 + private function current_user_can_edit_target($post_id)
251 + {
252 + $post = get_post($post_id);
253 + if ($post && in_array($post->post_type, array('wp_template', 'wp_template_part'), true)) {
254 + return current_user_can('edit_theme_options');
255 + }
256 + return current_user_can('edit_post', $post_id);
257 + }
258 +
259 + private function current_user_can_read_target($post_id)
260 + {
261 + $post = get_post($post_id);
262 + if ($post && in_array($post->post_type, array('wp_template', 'wp_template_part'), true)) {
263 + return current_user_can('edit_theme_options');
264 + }
265 + return current_user_can('read_post', $post_id);
266 + }
267 +
124 268 public function save_post_css()
125 269 {
126 270 // Vérifier que les données POST existent
127 271 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['css_content'])) {
@@ -129,19 +273,29 @@
129 273 }
130 274
131 275 // Déséchapper et assainir les données
132 276 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
133 - $post_id = intval($_POST['post_id']);
277 + $raw_post_id = wp_unslash($_POST['post_id']);
134 278 $css_content = wp_unslash($_POST['css_content']);
135 279 $css_type = isset($_POST['css_type']) ? sanitize_text_field(wp_unslash($_POST['css_type'])) : 'page';
280 + $replace = isset($_POST['replace']) ? filter_var(wp_unslash($_POST['replace']), FILTER_VALIDATE_BOOLEAN) : false;
136 281
282 + // Paramètres optionnels pour le Site Editor (templates / template parts)
283 + $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
284 + $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
285 +
137 286 // Vérifier le nonce
138 287 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
139 288 wp_die('Security check failed');
140 289 }
141 290
142 - // Vérifier que l'utilisateur peut éditer ce post
143 - if (!current_user_can('edit_post', $post_id)) {
291 + $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
292 + if ($post_id <= 0) {
293 + wp_send_json_error('Invalid target (save the template once in the Site Editor first)');
294 + }
295 +
296 + // Vérifier que l'utilisateur peut éditer cette cible
297 + if (!$this->current_user_can_edit_target($post_id)) {
144 298 wp_send_json_error('Insufficient permissions');
145 299 }
146 300
147 301 if ($css_type === 'page') {
@@ -157,11 +311,9 @@
157 311 $final_css .= "\n" . $block_css;
158 312 }
159 313 update_post_meta($post_id, 'ai_builder_css_content', $final_css);
160 314
161 - error_log("CSS Debug - Saving PAGE CSS. Page length: " . strlen($css_content) . ", Block length: " . strlen($block_css) . ", Final length: " . strlen($final_css));
162 315 } else if ($css_type === 'block') {
163 - // Pour les blocs, ajouter au CSS existant
164 316 $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
165 317 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
166 318
167 319 if (empty($page_css)) {
@@ -170,10 +322,15 @@
170 322 if (empty($block_css)) {
171 323 $block_css = '';
172 324 }
173 325
174 - // Ajouter le nouveau CSS de bloc
175 - $block_css .= "\n/* Block CSS - " . date('Y-m-d H:i:s') . " */\n" . $css_content . "\n";
326 + if ($replace) {
327 + // Si c'est une édition manuelle, remplacer complètement le CSS de blocs
328 + $block_css = $css_content;
329 + } else {
330 + // Si c'est une génération IA, ajouter au CSS existant
331 + $block_css .= "\n/* Block CSS - " . date('Y-m-d H:i:s') . " */\n" . $css_content . "\n";
332 + }
176 333
177 334 // Sauvegarder le CSS de bloc
178 335 update_post_meta($post_id, 'ai_builder_block_css_content', $block_css);
179 336
@@ -183,12 +340,14 @@
183 340 $final_css .= "\n" . $block_css;
184 341 }
185 342 update_post_meta($post_id, 'ai_builder_css_content', $final_css);
186 343
187 - error_log("CSS Debug - Saving BLOCK CSS. Page length: " . strlen($page_css) . ", Block length: " . strlen($block_css) . ", Final length: " . strlen($final_css));
188 344 }
189 345
190 - wp_send_json_success('CSS saved successfully');
346 + wp_send_json_success(array(
347 + 'message' => 'CSS saved successfully',
348 + 'post_id' => $post_id,
349 + ));
191 350 }
192 351
193 352
194 353 public function get_post_css()
@@ -199,17 +358,32 @@
199 358 }
200 359
201 360 // Déséchapper et assainir les données
202 361 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
203 - $post_id = intval($_POST['post_id']);
362 + $raw_post_id = wp_unslash($_POST['post_id']);
204 363
364 + // Paramètres optionnels pour le Site Editor (templates / template parts)
365 + $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
366 + $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
367 +
205 368 // Vérifier le nonce
206 369 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
207 370 wp_die('Security check failed');
208 371 }
209 372
210 - // Vérifier que l'utilisateur peut lire ce post
211 - if (!current_user_can('read_post', $post_id)) {
373 + $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
374 + if ($post_id <= 0) {
375 + // Rien à retourner mais on ne bloque pas l'UI : réponse vide neutre.
376 + wp_send_json_success(array(
377 + 'pageCss' => '',
378 + 'blockCss' => '',
379 + 'combinedCss' => '',
380 + 'post_id' => 0,
381 + ));
382 + }
383 +
384 + // Vérifier que l'utilisateur peut lire cette cible
385 + if (!$this->current_user_can_read_target($post_id)) {
212 386 wp_send_json_error('Insufficient permissions');
213 387 }
214 388
215 389 // Récupérer les CSS depuis les meta du post
@@ -216,20 +390,158 @@
216 390 $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
217 391 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
218 392 $combined_css = get_post_meta($post_id, 'ai_builder_css_content', true);
219 393
220 - // Log pour debug
221 - error_log("CSS Debug - Page CSS length: " . strlen($page_css));
222 - error_log("CSS Debug - Block CSS length: " . strlen($block_css));
223 - error_log("CSS Debug - Combined CSS length: " . strlen($combined_css));
224 -
225 394 wp_send_json_success(array(
226 395 'pageCss' => $page_css,
227 396 'blockCss' => $block_css,
228 - 'combinedCss' => $combined_css
397 + 'combinedCss' => $combined_css,
398 + 'post_id' => $post_id,
229 399 ));
230 400 }
231 401
402 + public function save_post_js()
403 + {
404 + // Vérifier que les données POST existent
405 + if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['js_content'])) {
406 + wp_send_json_error('Missing required data');
407 + }
408 +
409 + // Déséchapper et assainir les données
410 + $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
411 + $raw_post_id = wp_unslash($_POST['post_id']);
412 + $js_content = wp_unslash($_POST['js_content']);
413 + $js_type = isset($_POST['js_type']) ? sanitize_text_field(wp_unslash($_POST['js_type'])) : 'page';
414 + $replace = isset($_POST['replace']) ? filter_var(wp_unslash($_POST['replace']), FILTER_VALIDATE_BOOLEAN) : false;
415 +
416 + // Paramètres optionnels pour le Site Editor (templates / template parts)
417 + $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
418 + $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
419 +
420 + // Vérifier le nonce
421 + if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
422 + wp_die('Security check failed');
423 + }
424 +
425 + $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
426 + if ($post_id <= 0) {
427 + wp_send_json_error('Invalid target (save the template once in the Site Editor first)');
428 + }
429 +
430 + // Vérifier que l'utilisateur peut éditer cette cible
431 + if (!$this->current_user_can_edit_target($post_id)) {
432 + wp_send_json_error('Insufficient permissions');
433 + }
434 +
435 + // Le JS enregistré ici est ré-émis tel quel dans un <script> sur le front
436 + // (pages, blocs, templates du Site Editor). Seuls les utilisateurs disposant
437 + // de la capacité unfiltered_html peuvent stocker du script exécuté chez
438 + // d'autres personnes : même barrière que le bloc HTML personnalisé de WordPress.
439 + // Placé avant toutes les branches (page / block / site editor) pour qu'elles
440 + // en héritent.
441 + if (!current_user_can('unfiltered_html')) {
442 + wp_send_json_error('Insufficient permissions: saving custom JavaScript requires the unfiltered_html capability');
443 + }
444 +
445 + if ($js_type === 'page') {
446 + // Pour les pages, remplacer complètement le JS de page
447 + update_post_meta($post_id, 'ai_builder_page_js_content', $js_content);
448 +
449 + // Récupérer le JS de blocs existant
450 + $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
451 +
452 + // Combiner page JS + block JS pour le JS final
453 + $final_js = $js_content;
454 + if (!empty($block_js)) {
455 + $final_js .= "\n" . $block_js;
456 + }
457 + update_post_meta($post_id, 'ai_builder_js_content', $final_js);
458 +
459 + } else if ($js_type === 'block') {
460 + $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
461 + $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
462 +
463 + if (empty($page_js)) {
464 + $page_js = '';
465 + }
466 + if (empty($block_js)) {
467 + $block_js = '';
468 + }
469 +
470 + if ($replace) {
471 + // Si c'est une édition manuelle, remplacer complètement le JS de blocs
472 + $block_js = $js_content;
473 + } else {
474 + // Si c'est une génération IA, ajouter au JS existant
475 + $block_js .= "\n/* Block JS - " . date('Y-m-d H:i:s') . " */\n" . $js_content . "\n";
476 + }
477 +
478 + // Sauvegarder le JS de bloc
479 + update_post_meta($post_id, 'ai_builder_block_js_content', $block_js);
480 +
481 + // Combiner page JS + block JS pour le JS final
482 + $final_js = $page_js;
483 + if (!empty($block_js)) {
484 + $final_js .= "\n" . $block_js;
485 + }
486 + update_post_meta($post_id, 'ai_builder_js_content', $final_js);
487 +
488 + }
489 +
490 + wp_send_json_success(array(
491 + 'message' => 'JS saved successfully',
492 + 'post_id' => $post_id,
493 + ));
494 + }
495 +
496 + public function get_post_js()
497 + {
498 + // Vérifier que les données POST existent
499 + if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
500 + wp_send_json_error('Missing required data');
501 + }
502 +
503 + // Déséchapper et assainir les données
504 + $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
505 + $raw_post_id = wp_unslash($_POST['post_id']);
506 +
507 + // Paramètres optionnels pour le Site Editor (templates / template parts)
508 + $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
509 + $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
510 +
511 + // Vérifier le nonce
512 + if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
513 + wp_die('Security check failed');
514 + }
515 +
516 + $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
517 + if ($post_id <= 0) {
518 + wp_send_json_success(array(
519 + 'pageJS' => '',
520 + 'blockJS' => '',
521 + 'combinedJS' => '',
522 + 'post_id' => 0,
523 + ));
524 + }
525 +
526 + // Vérifier que l'utilisateur peut lire cette cible
527 + if (!$this->current_user_can_read_target($post_id)) {
528 + wp_send_json_error('Insufficient permissions');
529 + }
530 +
531 + // Récupérer les JS depuis les meta du post
532 + $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
533 + $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
534 + $combined_js = get_post_meta($post_id, 'ai_builder_js_content', true);
535 +
536 + wp_send_json_success(array(
537 + 'pageJS' => $page_js ?: '',
538 + 'blockJS' => $block_js ?: '',
539 + 'combinedJS' => $combined_js ?: '',
540 + 'post_id' => $post_id
541 + ));
542 + }
543 +
232 544 public function save_meta_description()
233 545 {
234 546 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
235 547 wp_send_json_error('Missing required data');
@@ -265,11 +577,9 @@
265 577 {
266 578 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
267 579 $key = 'aibui_cf_mailerr_' . md5($ip);
268 580 set_transient($key, $wp_error instanceof WP_Error ? $wp_error->get_error_message() : 'Unknown mail error', 120);
269 - if (defined('WP_DEBUG') && WP_DEBUG) {
270 - error_log('[AIBUI] wp_mail_failed: ' . (is_object($wp_error) && method_exists($wp_error, 'get_error_message') ? $wp_error->get_error_message() : print_r($wp_error, true)));
271 - }
581 +
272 582 }
273 583
274 584 public function submit_contact_form()
275 585 {
@@ -440,32 +750,8 @@
440 750 },
441 751 $content
442 752 );
443 753
444 - // Créer un fichier de log dans le plugin
445 - $log_file = plugin_dir_path(__FILE__) . '../debug-unescape.log';
446 - $log_content = "=== DEBUG UNESCAPE - " . date('Y-m-d H:i:s') . " ===\n";
447 - $log_content .= "Total corrections appliquées: " . $replacement_count . "\n\n";
448 - $log_content .= "Contenu original (premiers 500 chars):\n" . substr($original_content, 0, 500) . "\n\n";
449 - $log_content .= "Contenu corrigé (premiers 500 chars):\n" . substr($content, 0, 500) . "\n\n";
450 - if (strlen($content) > 500) {
451 - $log_content .= "Contenu corrigé (derniers 500 chars):\n" . substr($content, -500) . "\n\n";
452 - }
453 - $log_content .= "\n=== Détails par bloc ===\n";
454 - foreach ($debug_log as $log) {
455 - $log_content .= "\nBloc: " . $log['block'] . "\n";
456 - $log_content .= "Original JSON: " . $log['original_json'] . "\n";
457 - $log_content .= "Fixed JSON: " . $log['fixed_json'] . "\n";
458 - if (isset($log['error'])) {
459 - $log_content .= "ERREUR: " . $log['error'] . "\n";
460 - $log_content .= "JSON complet: " . $log['original_json_full'] . "\n";
461 - } else {
462 - $log_content .= "SUCCÈS\n";
463 - }
464 - }
465 - $log_content .= "\n=== FIN DEBUG ===\n\n";
466 - file_put_contents($log_file, $log_content, FILE_APPEND);
467 -
468 754 // Vérifier que le contenu est au format HTML sérialisé WordPress
469 755 // Le contenu doit commencer par un commentaire de bloc WordPress
470 756 if (empty($content) || strpos(trim($content), '<!-- wp:') !== 0) {
471 757 wp_send_json_error('Invalid content format: Expected WordPress serialized block HTML');
@@ -514,25 +800,20 @@
514 800 ));
515 801 }
516 802
517 803 // -----------------------------
518 - // Multi-Page: Generations store
804 + // Multi-Page: Generations store (using JSON files)
519 805 // -----------------------------
520 - private function get_generations_option()
806 + private function get_storage()
521 807 {
522 - $stored = get_option('aibui_multi_page_generations', array());
523 - if (!is_array($stored)) {
524 - $stored = array();
808 + static $storage = null;
809 + if ($storage === null) {
810 + require_once plugin_dir_path(__FILE__) . 'class-generations-storage.php';
811 + $storage = new AIBUI_Generations_Storage();
525 812 }
526 - return $stored;
813 + return $storage;
527 814 }
528 815
529 - private function persist_generations_option($generations)
530 - {
531 - if (!is_array($generations)) $generations = array();
532 - update_option('aibui_multi_page_generations', $generations, false);
533 - }
534 -
535 816 // Save a generation item (status: Pending review)
536 817 public function save_generation()
537 818 {
538 819 if (!isset($_POST['nonce'])) {
@@ -551,28 +832,16 @@
551 832 if (!$payload || !is_array($payload)) {
552 833 wp_send_json_error('Invalid payload');
553 834 }
554 835
555 - $id = isset($payload['id']) && is_string($payload['id']) ? $payload['id'] : wp_generate_uuid4();
556 - $now = current_time('mysql');
836 + $storage = $this->get_storage();
837 + $result = $storage->save($payload);
557 838
558 - $item = array(
559 - 'id' => $id,
560 - 'title' => sanitize_text_field($payload['title'] ?? ''),
561 - 'metaDesc' => sanitize_textarea_field($payload['metaDesc'] ?? ''),
562 - 'cssContent' => wp_kses_post($payload['cssContent'] ?? ''),
563 - 'blocksJson' => isset($payload['blocksJson']) ? $payload['blocksJson'] : array(),
564 - 'status' => 'Pending review',
565 - 'createdAt' => $now,
566 - 'applied' => false,
567 - 'pageId' => 0,
568 - );
839 + if (is_wp_error($result)) {
840 + wp_send_json_error($result->get_error_message());
841 + }
569 842
570 - $generations = $this->get_generations_option();
571 - $generations[$id] = $item;
572 - $this->persist_generations_option($generations);
573 -
574 - wp_send_json_success($item);
843 + wp_send_json_success($result);
575 844 }
576 845
577 846 // List generations
578 847 public function get_generations()
@@ -586,14 +855,12 @@
586 855 }
587 856 if (!current_user_can('edit_posts')) {
588 857 wp_send_json_error('Insufficient permissions');
589 858 }
590 - $generations = $this->get_generations_option();
591 - // Return newest first
592 - $items = array_values($generations);
593 - usort($items, function($a, $b) {
594 - return strcmp($b['createdAt'] ?? '', $a['createdAt'] ?? '');
595 - });
859 +
860 + $storage = $this->get_storage();
861 + $items = $storage->get_all();
862 +
596 863 wp_send_json_success($items);
597 864 }
598 865
599 866 // Get one generation by id
@@ -609,13 +876,17 @@
609 876 if (!current_user_can('edit_posts')) {
610 877 wp_send_json_error('Insufficient permissions');
611 878 }
612 879 $id = sanitize_text_field(wp_unslash($_POST['id']));
613 - $generations = $this->get_generations_option();
614 - if (!isset($generations[$id])) {
615 - wp_send_json_error('Not found');
880 +
881 + $storage = $this->get_storage();
882 + $result = $storage->get($id);
883 +
884 + if (is_wp_error($result)) {
885 + wp_send_json_error($result->get_error_message());
616 886 }
617 - wp_send_json_success($generations[$id]);
887 +
888 + wp_send_json_success($result);
618 889 }
619 890
620 891 // Mark generation as applied (optionally attach pageId and change status)
621 892 public function mark_generation_applied()
@@ -632,20 +903,16 @@
632 903 }
633 904 $id = sanitize_text_field(wp_unslash($_POST['id']));
634 905 $page_id = isset($_POST['page_id']) ? intval($_POST['page_id']) : 0;
635 906
636 - $generations = $this->get_generations_option();
637 - if (!isset($generations[$id])) {
638 - wp_send_json_error('Not found');
907 + $storage = $this->get_storage();
908 + $result = $storage->mark_applied($id, $page_id);
909 +
910 + if (is_wp_error($result)) {
911 + wp_send_json_error($result->get_error_message());
639 912 }
640 - $generations[$id]['applied'] = true;
641 - if ($page_id > 0) {
642 - $generations[$id]['pageId'] = $page_id;
643 - }
644 - $generations[$id]['status'] = 'Applied';
645 - $this->persist_generations_option($generations);
646 913
647 - wp_send_json_success($generations[$id]);
914 + wp_send_json_success($result);
648 915 }
649 916
650 917 public function save_page_prompt()
651 918 {
@@ -731,6 +998,32 @@
731 998 // Marquer la page comme créée via IA
732 999 update_post_meta($post_id, '_aibui_created_by_ai', '1');
733 1000
734 1001 wp_send_json_success('Page marked as AI-created');
1002 + }
1003 +
1004 + /**
1005 + * Get whether a post was created via AI Builder.
1006 + */
1007 + public function get_ai_created_status()
1008 + {
1009 + if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
1010 + wp_send_json_error('Missing required data');
1011 + }
1012 +
1013 + $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
1014 + $post_id = intval($_POST['post_id']);
1015 +
1016 + if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
1017 + wp_die('Security check failed');
1018 + }
1019 +
1020 + if (!current_user_can('read_post', $post_id)) {
1021 + wp_send_json_error('Insufficient permissions');
1022 + }
1023 +
1024 + $flag = get_post_meta($post_id, '_aibui_created_by_ai', true);
1025 + wp_send_json_success(array(
1026 + 'isAICreated' => ($flag === '1' || $flag === 1 || $flag === true),
1027 + ));
735 1028 }
736 1029 }