PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.9
AI Builder – Generate pages, blocks, images & translate with AI v2.7.9
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-ajax-handler.php

class-ajax-handler.php in AI Builder – Generate pages, blocks, images & translate with AI 2.7.9, at includes/class-ajax-handler.php

1,029 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class AIBUI_Ajax_Handler
4 {
5 public function __construct()
6 {
7 add_action('wp_ajax_aibui_save_token', array($this, 'save_token'));
8 add_action('wp_ajax_aibui_set_signup_success', array($this, 'set_signup_success'));
9 add_action('wp_ajax_aibui_signout', array($this, 'signout'));
10 add_action('wp_ajax_aibui_get_token', array($this, 'get_token'));
11 add_action('wp_ajax_aibui_save_post_css', array($this, 'save_post_css'));
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'));
15 add_action('wp_ajax_aibui_save_page_prompt', array($this, 'save_page_prompt'));
16 add_action('wp_ajax_aibui_get_page_prompt', array($this, 'get_page_prompt'));
17 add_action('wp_ajax_aibui_save_meta_description', array($this, 'save_meta_description'));
18 add_action('wp_ajax_aibui_create_page', array($this, 'create_page'));
19 add_action('wp_ajax_nopriv_aibui_submit_contact_form', array($this, 'submit_contact_form'));
20 add_action('wp_ajax_aibui_submit_contact_form', array($this, 'submit_contact_form'));
21 add_action('wp_mail_failed', array($this, 'capture_mail_error'));
22
23 // Multi-page generations storage endpoints
24 add_action('wp_ajax_aibui_save_generation', array($this, 'save_generation'));
25 add_action('wp_ajax_aibui_get_generations', array($this, 'get_generations'));
26 add_action('wp_ajax_aibui_get_generation', array($this, 'get_generation'));
27 add_action('wp_ajax_aibui_mark_generation_applied', array($this, 'mark_generation_applied'));
28
29 // Mark pages created via AI
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'));
32 }
33
34 public function save_token()
35 {
36 // Vérifier que les données POST existent
37 if (!isset($_POST['nonce']) || !isset($_POST['token'])) {
38 wp_send_json_error('Missing required data');
39 }
40
41 // Déséchapper et assainir les données
42 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
43 $token = sanitize_text_field(wp_unslash($_POST['token']));
44
45 // Vérifier le nonce
46 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
47 wp_die('Security check failed');
48 }
49
50 if (empty($token)) {
51 wp_send_json_error('Token is required');
52 }
53
54 // Sauvegarder le token JWT
55 update_option('aibui_jwt_token', $token);
56
57 wp_send_json_success('Token saved successfully');
58 }
59
60 public function set_signup_success()
61 {
62 // Vérifier que les données POST existent
63 if (!isset($_POST['nonce'])) {
64 wp_send_json_error('Missing required data');
65 }
66
67 // Déséchapper et assainir les données
68 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
69
70 // Vérifier le nonce
71 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
72 wp_die('Security check failed');
73 }
74
75 // Marquer l'inscription comme réussie
76 update_option('aibui_user_successful_signup', true);
77
78 wp_send_json_success('Signup success flag set');
79 }
80
81 public function signout()
82 {
83 // Vérifier que les données POST existent
84 if (!isset($_POST['nonce'])) {
85 wp_send_json_error('Missing required data');
86 }
87
88 // Déséchapper et assainir les données
89 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
90
91 // Vérifier le nonce
92 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
93 wp_die('Security check failed');
94 }
95
96 // Supprimer le token JWT
97 delete_option('aibui_jwt_token');
98
99 wp_send_json_success('Signed out successfully');
100 }
101
102 public function get_token()
103 {
104 // Vérifier que les données POST existent
105 if (!isset($_POST['nonce'])) {
106 wp_send_json_error('Missing required data', 400);
107 }
108
109 // Déséchapper et assainir les données
110 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
111
112 // Vérifier le nonce
113 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
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 );
125 }
126
127 // Récupérer le token JWT
128 $token = get_option('aibui_jwt_token', '');
129
130 if (empty($token)) {
131 wp_send_json_error('No token found');
132 }
133
134 wp_send_json_success(array('token' => $token));
135 }
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
268 public function save_post_css()
269 {
270 // Vérifier que les données POST existent
271 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['css_content'])) {
272 wp_send_json_error('Missing required data');
273 }
274
275 // Déséchapper et assainir les données
276 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
277 $raw_post_id = wp_unslash($_POST['post_id']);
278 $css_content = wp_unslash($_POST['css_content']);
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;
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
286 // Vérifier le nonce
287 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
288 wp_die('Security check failed');
289 }
290
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)) {
298 wp_send_json_error('Insufficient permissions');
299 }
300
301 if ($css_type === 'page') {
302 // Pour les pages, remplacer complètement le CSS de page
303 update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);
304
305 // Récupérer le CSS de blocs existant
306 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
307
308 // Combiner page CSS + block CSS pour le CSS final
309 $final_css = $css_content;
310 if (!empty($block_css)) {
311 $final_css .= "\n" . $block_css;
312 }
313 update_post_meta($post_id, 'ai_builder_css_content', $final_css);
314
315 } else if ($css_type === 'block') {
316 $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
317 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
318
319 if (empty($page_css)) {
320 $page_css = '';
321 }
322 if (empty($block_css)) {
323 $block_css = '';
324 }
325
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 }
333
334 // Sauvegarder le CSS de bloc
335 update_post_meta($post_id, 'ai_builder_block_css_content', $block_css);
336
337 // Combiner page CSS + block CSS pour le CSS final
338 $final_css = $page_css;
339 if (!empty($block_css)) {
340 $final_css .= "\n" . $block_css;
341 }
342 update_post_meta($post_id, 'ai_builder_css_content', $final_css);
343
344 }
345
346 wp_send_json_success(array(
347 'message' => 'CSS saved successfully',
348 'post_id' => $post_id,
349 ));
350 }
351
352
353 public function get_post_css()
354 {
355 // Vérifier que les données POST existent
356 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
357 wp_send_json_error('Missing required data');
358 }
359
360 // Déséchapper et assainir les données
361 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
362 $raw_post_id = wp_unslash($_POST['post_id']);
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
368 // Vérifier le nonce
369 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
370 wp_die('Security check failed');
371 }
372
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)) {
386 wp_send_json_error('Insufficient permissions');
387 }
388
389 // Récupérer les CSS depuis les meta du post
390 $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
391 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
392 $combined_css = get_post_meta($post_id, 'ai_builder_css_content', true);
393
394 wp_send_json_success(array(
395 'pageCss' => $page_css,
396 'blockCss' => $block_css,
397 'combinedCss' => $combined_css,
398 'post_id' => $post_id,
399 ));
400 }
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
544 public function save_meta_description()
545 {
546 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
547 wp_send_json_error('Missing required data');
548 }
549
550 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
551 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
552 wp_die('Security check failed');
553 }
554
555 $post_id = intval($_POST['post_id']);
556 if (!current_user_can('edit_post', $post_id)) {
557 wp_send_json_error('Insufficient permissions');
558 }
559
560 $raw = isset($_POST['meta_desc']) ? wp_unslash($_POST['meta_desc']) : '';
561 $san = trim(wp_strip_all_tags($raw));
562 if (strlen($san) > 320) {
563 $san = mb_substr($san, 0, 320);
564 }
565
566 if ($san === '') {
567 delete_post_meta($post_id, 'aibui_meta_description');
568 } else {
569 update_post_meta($post_id, 'aibui_meta_description', $san);
570 }
571
572 wp_send_json_success('Meta description saved');
573 }
574
575 // Capture wp_mail() errors and store briefly to surface via AJAX
576 public function capture_mail_error($wp_error)
577 {
578 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
579 $key = 'aibui_cf_mailerr_' . md5($ip);
580 set_transient($key, $wp_error instanceof WP_Error ? $wp_error->get_error_message() : 'Unknown mail error', 120);
581
582 }
583
584 public function submit_contact_form()
585 {
586 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'aibui_contact_form')) {
587 wp_send_json_error('Invalid nonce');
588 }
589
590 // Rate limiting per IP: 1 submission per 30 seconds
591 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
592 $key = 'aibui_cf_rl_' . md5($ip);
593 $last = get_transient($key);
594 if ($last) {
595 wp_send_json_error('Too many requests. Please wait.');
596 }
597 set_transient($key, time(), 30);
598
599 $recipient = isset($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : '';
600 if (empty($recipient) || !is_email($recipient)) {
601 $recipient = sanitize_email(get_option('admin_email'));
602 }
603 if (empty($recipient) || !is_email($recipient)) {
604 wp_send_json_error('No valid recipient configured');
605 }
606
607 $subject = sprintf('[%s] Nouveau message de contact', get_bloginfo('name'));
608
609 $fields = [];
610 $sender_email = '';
611 foreach ($_POST as $key => $value) {
612 if (strpos($key, 'field_') === 0) {
613 $label_key = 'label_' . $key;
614 $type_key = 'type_' . $key;
615 $req_key = 'required_' . $key;
616 $label = isset($_POST[$label_key]) ? sanitize_text_field(wp_unslash($_POST[$label_key])) : 'Champ';
617 $type = isset($_POST[$type_key]) ? sanitize_text_field(wp_unslash($_POST[$type_key])) : 'text';
618 $is_required = isset($_POST[$req_key]) && wp_unslash($_POST[$req_key]) === '1';
619 $raw = wp_unslash($value);
620 switch ($type) {
621 case 'email':
622 $san = sanitize_email($raw);
623 if (!$sender_email && is_email($san)) {
624 $sender_email = $san;
625 }
626 break;
627 case 'number':
628 $san = is_numeric($raw) ? $raw : '';
629 break;
630 case 'date':
631 $san = preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $raw) ? $raw : '';
632 break;
633 case 'textarea':
634 $san = sanitize_textarea_field($raw);
635 break;
636 default:
637 $san = sanitize_text_field($raw);
638 }
639 if ($is_required && $san === '') {
640 wp_send_json_error(sprintf('%s est requis', $label ? $label : 'Ce champ'));
641 }
642 $fields[] = ['label' => $label, 'type' => $type, 'value' => $san];
643 }
644 }
645
646 if (empty($fields)) {
647 wp_send_json_error('No fields provided');
648 }
649
650 // Build HTML email content
651 $rows = '';
652 foreach ($fields as $f) {
653 $val = $f['type'] === 'textarea' ? nl2br(esc_html($f['value'])) : esc_html($f['value']);
654 $rows .= '<tr><td style="padding:8px 12px;border:1px solid #e5e7eb;font-weight:600;">' . esc_html($f['label']) . '</td><td style="padding:8px 12px;border:1px solid #e5e7eb;">' . $val . '</td></tr>';
655 }
656 $message = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;">'
657 . '<h3 style="margin:0 0 12px;">' . esc_html__('Nouveau message de contact', 'ai-builder') . '</h3>'
658 . '<table cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;width:100%;max-width:720px;">'
659 . $rows
660 . '</table>'
661 . '</div>';
662
663 $headers = [];
664 $headers[] = 'Content-Type: text/html; charset=UTF-8';
665 $domain = parse_url(home_url(), PHP_URL_HOST);
666 $default_from = 'no-reply@' . $domain;
667 $user_from = isset($_POST['from_email']) ? sanitize_email(wp_unslash($_POST['from_email'])) : '';
668 $from_email = $default_from;
669 if ($user_from && is_email($user_from)) {
670 // Use as From only if same domain (avoid SPF/DMARC issues)
671 $user_domain = substr(strrchr($user_from, '@'), 1);
672 if ($user_domain && strtolower($user_domain) === strtolower($domain)) {
673 $from_email = $user_from;
674 }
675 }
676 $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from_email . '>';
677 if ($sender_email && is_email($sender_email)) {
678 $headers[] = 'Reply-To: ' . $sender_email;
679 }
680
681 $sent = wp_mail($recipient, $subject, $message, $headers);
682 if (!$sent) {
683 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
684 $key_err = 'aibui_cf_mailerr_' . md5($ip);
685 $last_err = get_transient($key_err);
686 wp_send_json_error($last_err ? $last_err : 'Failed to send');
687 }
688 wp_send_json_success('Sent');
689 }
690
691 public function create_page()
692 {
693 // Vérifier que les données POST existent
694 if (!isset($_POST['nonce']) || !isset($_POST['content_type']) || !isset($_POST['title']) || !isset($_POST['content'])) {
695 wp_send_json_error('Missing required data');
696 }
697
698 // Déséchapper et assainir les données
699 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
700 $content_type = sanitize_text_field(wp_unslash($_POST['content_type']));
701 $title = sanitize_text_field(wp_unslash($_POST['title']));
702 $content = wp_unslash($_POST['content']);
703 $css_content = isset($_POST['css_content']) ? wp_unslash($_POST['css_content']) : '';
704 $meta_description = isset($_POST['meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['meta_description'])) : '';
705
706 // Vérifier le nonce
707 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
708 wp_die('Security check failed');
709 }
710
711 // Vérifier que l'utilisateur peut créer des posts/pages
712 if (!current_user_can('publish_posts')) {
713 wp_send_json_error('Insufficient permissions');
714 }
715
716 // Déséchapper les JSON de commentaires de blocs si l'API a échappé les guillemets
717 // Exemple: <!-- wp:cover {\"align\":\"full\"} --> -> <!-- wp:cover {"align":"full"} -->
718 $original_content = $content;
719 $replacement_count = 0;
720 $debug_log = array(); // Stocker les logs pour debug
721
722 $content = preg_replace_callback(
723 '/<!--\s*wp:([^\s]+)\s+(\{.*?\})\s*-->/',
724 function ($matches) use (&$replacement_count, &$debug_log) {
725 $block_name = $matches[1];
726 $json_str = $matches[2];
727 $fixed_json = stripslashes($json_str);
728
729 // Log pour debug
730 $debug_info = array(
731 'block' => $block_name,
732 'original_json' => substr($json_str, 0, 200),
733 'fixed_json' => substr($fixed_json, 0, 200),
734 'success' => false
735 );
736
737 // Ne remplacer que si le JSON corrigé est valide
738 $decoded = json_decode($fixed_json, true);
739 if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
740 $debug_info['error'] = json_last_error_msg();
741 $debug_info['original_json_full'] = $json_str;
742 $debug_log[] = $debug_info;
743 return $matches[0];
744 }
745
746 $replacement_count++;
747 $debug_info['success'] = true;
748 $debug_log[] = $debug_info;
749 return "<!-- wp:" . $block_name . " " . $fixed_json . " -->";
750 },
751 $content
752 );
753
754 // Vérifier que le contenu est au format HTML sérialisé WordPress
755 // Le contenu doit commencer par un commentaire de bloc WordPress
756 if (empty($content) || strpos(trim($content), '<!-- wp:') !== 0) {
757 wp_send_json_error('Invalid content format: Expected WordPress serialized block HTML');
758 }
759
760 // Valider le format des blocs avec parse_blocks
761 $parsed_blocks = parse_blocks($content);
762 if (empty($parsed_blocks) || (count($parsed_blocks) === 1 && empty($parsed_blocks[0]['blockName']))) {
763 wp_send_json_error('Invalid block format: Could not parse blocks');
764 }
765
766 // Créer le post/page
767 $post_data = array(
768 'post_title' => $title,
769 'post_content' => $content, // Contenu HTML sérialisé directement
770 'post_status' => 'publish',
771 'post_type' => $content_type === 'post' ? 'post' : 'page',
772 'post_author' => get_current_user_id(),
773 );
774
775 $post_id = wp_insert_post($post_data);
776
777 if (is_wp_error($post_id)) {
778 wp_send_json_error('Failed to create ' . $content_type);
779 }
780
781 // Sauvegarder le CSS si présent
782 if (!empty($css_content)) {
783 update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);
784 update_post_meta($post_id, 'ai_builder_css_content', $css_content);
785 }
786
787 // Sauvegarder la meta description si présente
788 if (!empty($meta_description)) {
789 update_post_meta($post_id, 'aibui_meta_description', $meta_description);
790 }
791
792 // Récupérer l'URL de la page créée
793 $page_url = get_permalink($post_id);
794
795
796 wp_send_json_success(array(
797 'page_id' => $post_id,
798 'page_url' => $page_url,
799 'page_title' => $title
800 ));
801 }
802
803 // -----------------------------
804 // Multi-Page: Generations store (using JSON files)
805 // -----------------------------
806 private function get_storage()
807 {
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();
812 }
813 return $storage;
814 }
815
816 // Save a generation item (status: Pending review)
817 public function save_generation()
818 {
819 if (!isset($_POST['nonce'])) {
820 wp_send_json_error('Missing required data');
821 }
822 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
823 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
824 wp_die('Security check failed');
825 }
826 if (!current_user_can('edit_posts')) {
827 wp_send_json_error('Insufficient permissions');
828 }
829
830 $payload_raw = isset($_POST['payload']) ? wp_unslash($_POST['payload']) : '';
831 $payload = json_decode($payload_raw, true);
832 if (!$payload || !is_array($payload)) {
833 wp_send_json_error('Invalid payload');
834 }
835
836 $storage = $this->get_storage();
837 $result = $storage->save($payload);
838
839 if (is_wp_error($result)) {
840 wp_send_json_error($result->get_error_message());
841 }
842
843 wp_send_json_success($result);
844 }
845
846 // List generations
847 public function get_generations()
848 {
849 if (!isset($_POST['nonce'])) {
850 wp_send_json_error('Missing required data');
851 }
852 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
853 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
854 wp_die('Security check failed');
855 }
856 if (!current_user_can('edit_posts')) {
857 wp_send_json_error('Insufficient permissions');
858 }
859
860 $storage = $this->get_storage();
861 $items = $storage->get_all();
862
863 wp_send_json_success($items);
864 }
865
866 // Get one generation by id
867 public function get_generation()
868 {
869 if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
870 wp_send_json_error('Missing required data');
871 }
872 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
873 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
874 wp_die('Security check failed');
875 }
876 if (!current_user_can('edit_posts')) {
877 wp_send_json_error('Insufficient permissions');
878 }
879 $id = sanitize_text_field(wp_unslash($_POST['id']));
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());
886 }
887
888 wp_send_json_success($result);
889 }
890
891 // Mark generation as applied (optionally attach pageId and change status)
892 public function mark_generation_applied()
893 {
894 if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
895 wp_send_json_error('Missing required data');
896 }
897 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
898 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
899 wp_die('Security check failed');
900 }
901 if (!current_user_can('edit_posts')) {
902 wp_send_json_error('Insufficient permissions');
903 }
904 $id = sanitize_text_field(wp_unslash($_POST['id']));
905 $page_id = isset($_POST['page_id']) ? intval($_POST['page_id']) : 0;
906
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());
912 }
913
914 wp_send_json_success($result);
915 }
916
917 public function save_page_prompt()
918 {
919 // Vérifier que les données POST existent
920 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['page_prompt'])) {
921 wp_send_json_error('Missing required data');
922 }
923
924 // Déséchapper et assainir les données
925 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
926 $post_id = intval($_POST['post_id']);
927 $page_prompt = sanitize_textarea_field(wp_unslash($_POST['page_prompt']));
928
929 // Vérifier le nonce
930 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
931 wp_die('Security check failed');
932 }
933
934 // Vérifier que l'utilisateur peut modifier ce post
935 if (!current_user_can('edit_post', $post_id)) {
936 wp_send_json_error('Insufficient permissions');
937 }
938
939 // Sauvegarder le prompt de page
940 update_post_meta($post_id, 'ai_builder_page_prompt', $page_prompt);
941
942 wp_send_json_success('Page prompt saved successfully');
943 }
944
945 public function get_page_prompt()
946 {
947 // Vérifier que les données POST existent
948 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
949 wp_send_json_error('Missing required data');
950 }
951
952 // Déséchapper et assainir les données
953 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
954 $post_id = intval($_POST['post_id']);
955
956 // Vérifier le nonce
957 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
958 wp_die('Security check failed');
959 }
960
961 // Vérifier que l'utilisateur peut lire ce post
962 if (!current_user_can('read_post', $post_id)) {
963 wp_send_json_error('Insufficient permissions');
964 }
965
966 // Récupérer le prompt de page
967 $page_prompt = get_post_meta($post_id, 'ai_builder_page_prompt', true);
968
969 wp_send_json_success(array(
970 'pagePrompt' => $page_prompt ?: ''
971 ));
972 }
973
974 /**
975 * Marquer une page comme créée via IA
976 */
977 public function mark_ai_created()
978 {
979 // Vérifier que les données POST existent
980 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
981 wp_send_json_error('Missing required data');
982 }
983
984 // Déséchapper et assainir les données
985 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
986 $post_id = intval($_POST['post_id']);
987
988 // Vérifier le nonce
989 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
990 wp_die('Security check failed');
991 }
992
993 // Vérifier que l'utilisateur peut éditer ce post
994 if (!current_user_can('edit_post', $post_id)) {
995 wp_send_json_error('Insufficient permissions');
996 }
997
998 // Marquer la page comme créée via IA
999 update_post_meta($post_id, '_aibui_created_by_ai', '1');
1000
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 ));
1028 }
1029 }