PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.8
AI Builder – Generate pages, blocks, images & translate with AI v2.7.8
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.8, at includes/class-ajax-handler.php

1,019 lines 38.7 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');
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('Security check failed');
115 }
116
117 // Récupérer le token JWT
118 $token = get_option('aibui_jwt_token', '');
119
120 if (empty($token)) {
121 wp_send_json_error('No token found');
122 }
123
124 wp_send_json_success(array('token' => $token));
125 }
126
127 /**
128 * Résout l'ID numérique de post cible pour les endpoints CSS/JS.
129 *
130 * Accepte soit :
131 * - un post_id numérique classique (pages/articles, et templates déjà
132 * matérialisés en base), soit
133 * - un template_id composite "theme//slug" + template_type
134 * ("wp_template" ou "wp_template_part"), utilisé par le Site Editor.
135 *
136 * Pour les templates/parts file-based non encore en base, le post
137 * correspondant est créé à la volée (même stratégie que le Site Editor
138 * quand l'utilisateur clique sur Save).
139 *
140 * @return int Post ID positif, ou 0 si non résoluble.
141 */
142 private function resolve_target_post_id($raw_post_id, $template_id, $template_type)
143 {
144 // Chemin rapide : un post_id numérique valide est accepté tel quel.
145 $post_id = 0;
146 if ($raw_post_id !== '' && is_numeric($raw_post_id)) {
147 $post_id = intval($raw_post_id);
148 if ($post_id > 0 && get_post($post_id)) {
149 return $post_id;
150 }
151 $post_id = 0;
152 }
153
154 // Sinon, on tente la résolution via template_id "theme//slug".
155 if (!is_string($template_id) || $template_id === '') {
156 return 0;
157 }
158 if (!in_array($template_type, array('wp_template', 'wp_template_part'), true)) {
159 return 0;
160 }
161 if (strpos($template_id, '//') === false) {
162 return 0;
163 }
164 if (!function_exists('get_block_template')) {
165 return 0;
166 }
167
168 try {
169 $tpl = get_block_template($template_id, $template_type);
170 } catch (\Throwable $e) {
171 return 0;
172 }
173 if (!$tpl) {
174 return 0;
175 }
176
177 // Déjà en base → on réutilise.
178 if (!empty($tpl->wp_id) && (int) $tpl->wp_id > 0) {
179 return (int) $tpl->wp_id;
180 }
181
182 // Pas encore en base : on matérialise le template file-based en post
183 // de la même manière que le Site Editor. Cela nécessite la capability
184 // edit_theme_options (vérifiée ici, en plus de la vérif au niveau
185 // endpoint) pour ne jamais créer d'entrée theme à cause d'un save JS.
186 if (!current_user_can('edit_theme_options')) {
187 return 0;
188 }
189
190 list($theme_slug, $slug) = array_pad(explode('//', $template_id, 2), 2, '');
191 if ($theme_slug === '' || $slug === '') {
192 return 0;
193 }
194
195 $title = isset($tpl->title) && $tpl->title !== '' ? (string) $tpl->title : $slug;
196 $content = isset($tpl->content) ? (string) $tpl->content : '';
197
198 try {
199 $new_post_id = wp_insert_post(array(
200 'post_type' => $template_type,
201 'post_status' => 'publish',
202 'post_title' => $title,
203 'post_name' => $slug,
204 'post_content' => $content,
205 ), true);
206 } catch (\Throwable $e) {
207 return 0;
208 }
209 if (is_wp_error($new_post_id) || !$new_post_id) {
210 return 0;
211 }
212
213 // Rattacher au theme courant (taxonomy wp_theme).
214 try {
215 wp_set_object_terms((int) $new_post_id, $theme_slug, 'wp_theme');
216 } catch (\Throwable $e) {
217 // non bloquant
218 }
219
220 // Pour les template parts, rattacher la zone (header/footer/uncategorized...).
221 if ($template_type === 'wp_template_part') {
222 $area = isset($tpl->area) && is_string($tpl->area) && $tpl->area !== ''
223 ? $tpl->area
224 : 'uncategorized';
225 try {
226 wp_set_object_terms((int) $new_post_id, $area, 'wp_template_part_area');
227 } catch (\Throwable $e) {
228 // non bloquant
229 }
230 }
231
232 return (int) $new_post_id;
233 }
234
235 /**
236 * Vérif de capability adaptée au type de cible.
237 * - wp_template / wp_template_part : edit_theme_options (Site Editor)
238 * - autres posts : edit_post sur l'ID
239 */
240 private function current_user_can_edit_target($post_id)
241 {
242 $post = get_post($post_id);
243 if ($post && in_array($post->post_type, array('wp_template', 'wp_template_part'), true)) {
244 return current_user_can('edit_theme_options');
245 }
246 return current_user_can('edit_post', $post_id);
247 }
248
249 private function current_user_can_read_target($post_id)
250 {
251 $post = get_post($post_id);
252 if ($post && in_array($post->post_type, array('wp_template', 'wp_template_part'), true)) {
253 return current_user_can('edit_theme_options');
254 }
255 return current_user_can('read_post', $post_id);
256 }
257
258 public function save_post_css()
259 {
260 // Vérifier que les données POST existent
261 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['css_content'])) {
262 wp_send_json_error('Missing required data');
263 }
264
265 // Déséchapper et assainir les données
266 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
267 $raw_post_id = wp_unslash($_POST['post_id']);
268 $css_content = wp_unslash($_POST['css_content']);
269 $css_type = isset($_POST['css_type']) ? sanitize_text_field(wp_unslash($_POST['css_type'])) : 'page';
270 $replace = isset($_POST['replace']) ? filter_var(wp_unslash($_POST['replace']), FILTER_VALIDATE_BOOLEAN) : false;
271
272 // Paramètres optionnels pour le Site Editor (templates / template parts)
273 $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
274 $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
275
276 // Vérifier le nonce
277 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
278 wp_die('Security check failed');
279 }
280
281 $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
282 if ($post_id <= 0) {
283 wp_send_json_error('Invalid target (save the template once in the Site Editor first)');
284 }
285
286 // Vérifier que l'utilisateur peut éditer cette cible
287 if (!$this->current_user_can_edit_target($post_id)) {
288 wp_send_json_error('Insufficient permissions');
289 }
290
291 if ($css_type === 'page') {
292 // Pour les pages, remplacer complètement le CSS de page
293 update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);
294
295 // Récupérer le CSS de blocs existant
296 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
297
298 // Combiner page CSS + block CSS pour le CSS final
299 $final_css = $css_content;
300 if (!empty($block_css)) {
301 $final_css .= "\n" . $block_css;
302 }
303 update_post_meta($post_id, 'ai_builder_css_content', $final_css);
304
305 } else if ($css_type === 'block') {
306 $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
307 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
308
309 if (empty($page_css)) {
310 $page_css = '';
311 }
312 if (empty($block_css)) {
313 $block_css = '';
314 }
315
316 if ($replace) {
317 // Si c'est une édition manuelle, remplacer complètement le CSS de blocs
318 $block_css = $css_content;
319 } else {
320 // Si c'est une génération IA, ajouter au CSS existant
321 $block_css .= "\n/* Block CSS - " . date('Y-m-d H:i:s') . " */\n" . $css_content . "\n";
322 }
323
324 // Sauvegarder le CSS de bloc
325 update_post_meta($post_id, 'ai_builder_block_css_content', $block_css);
326
327 // Combiner page CSS + block CSS pour le CSS final
328 $final_css = $page_css;
329 if (!empty($block_css)) {
330 $final_css .= "\n" . $block_css;
331 }
332 update_post_meta($post_id, 'ai_builder_css_content', $final_css);
333
334 }
335
336 wp_send_json_success(array(
337 'message' => 'CSS saved successfully',
338 'post_id' => $post_id,
339 ));
340 }
341
342
343 public function get_post_css()
344 {
345 // Vérifier que les données POST existent
346 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
347 wp_send_json_error('Missing required data');
348 }
349
350 // Déséchapper et assainir les données
351 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
352 $raw_post_id = wp_unslash($_POST['post_id']);
353
354 // Paramètres optionnels pour le Site Editor (templates / template parts)
355 $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
356 $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
357
358 // Vérifier le nonce
359 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
360 wp_die('Security check failed');
361 }
362
363 $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
364 if ($post_id <= 0) {
365 // Rien à retourner mais on ne bloque pas l'UI : réponse vide neutre.
366 wp_send_json_success(array(
367 'pageCss' => '',
368 'blockCss' => '',
369 'combinedCss' => '',
370 'post_id' => 0,
371 ));
372 }
373
374 // Vérifier que l'utilisateur peut lire cette cible
375 if (!$this->current_user_can_read_target($post_id)) {
376 wp_send_json_error('Insufficient permissions');
377 }
378
379 // Récupérer les CSS depuis les meta du post
380 $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
381 $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
382 $combined_css = get_post_meta($post_id, 'ai_builder_css_content', true);
383
384 wp_send_json_success(array(
385 'pageCss' => $page_css,
386 'blockCss' => $block_css,
387 'combinedCss' => $combined_css,
388 'post_id' => $post_id,
389 ));
390 }
391
392 public function save_post_js()
393 {
394 // Vérifier que les données POST existent
395 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['js_content'])) {
396 wp_send_json_error('Missing required data');
397 }
398
399 // Déséchapper et assainir les données
400 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
401 $raw_post_id = wp_unslash($_POST['post_id']);
402 $js_content = wp_unslash($_POST['js_content']);
403 $js_type = isset($_POST['js_type']) ? sanitize_text_field(wp_unslash($_POST['js_type'])) : 'page';
404 $replace = isset($_POST['replace']) ? filter_var(wp_unslash($_POST['replace']), FILTER_VALIDATE_BOOLEAN) : false;
405
406 // Paramètres optionnels pour le Site Editor (templates / template parts)
407 $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
408 $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
409
410 // Vérifier le nonce
411 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
412 wp_die('Security check failed');
413 }
414
415 $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
416 if ($post_id <= 0) {
417 wp_send_json_error('Invalid target (save the template once in the Site Editor first)');
418 }
419
420 // Vérifier que l'utilisateur peut éditer cette cible
421 if (!$this->current_user_can_edit_target($post_id)) {
422 wp_send_json_error('Insufficient permissions');
423 }
424
425 // Le JS enregistré ici est ré-émis tel quel dans un <script> sur le front
426 // (pages, blocs, templates du Site Editor). Seuls les utilisateurs disposant
427 // de la capacité unfiltered_html peuvent stocker du script exécuté chez
428 // d'autres personnes : même barrière que le bloc HTML personnalisé de WordPress.
429 // Placé avant toutes les branches (page / block / site editor) pour qu'elles
430 // en héritent.
431 if (!current_user_can('unfiltered_html')) {
432 wp_send_json_error('Insufficient permissions: saving custom JavaScript requires the unfiltered_html capability');
433 }
434
435 if ($js_type === 'page') {
436 // Pour les pages, remplacer complètement le JS de page
437 update_post_meta($post_id, 'ai_builder_page_js_content', $js_content);
438
439 // Récupérer le JS de blocs existant
440 $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
441
442 // Combiner page JS + block JS pour le JS final
443 $final_js = $js_content;
444 if (!empty($block_js)) {
445 $final_js .= "\n" . $block_js;
446 }
447 update_post_meta($post_id, 'ai_builder_js_content', $final_js);
448
449 } else if ($js_type === 'block') {
450 $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
451 $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
452
453 if (empty($page_js)) {
454 $page_js = '';
455 }
456 if (empty($block_js)) {
457 $block_js = '';
458 }
459
460 if ($replace) {
461 // Si c'est une édition manuelle, remplacer complètement le JS de blocs
462 $block_js = $js_content;
463 } else {
464 // Si c'est une génération IA, ajouter au JS existant
465 $block_js .= "\n/* Block JS - " . date('Y-m-d H:i:s') . " */\n" . $js_content . "\n";
466 }
467
468 // Sauvegarder le JS de bloc
469 update_post_meta($post_id, 'ai_builder_block_js_content', $block_js);
470
471 // Combiner page JS + block JS pour le JS final
472 $final_js = $page_js;
473 if (!empty($block_js)) {
474 $final_js .= "\n" . $block_js;
475 }
476 update_post_meta($post_id, 'ai_builder_js_content', $final_js);
477
478 }
479
480 wp_send_json_success(array(
481 'message' => 'JS saved successfully',
482 'post_id' => $post_id,
483 ));
484 }
485
486 public function get_post_js()
487 {
488 // Vérifier que les données POST existent
489 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
490 wp_send_json_error('Missing required data');
491 }
492
493 // Déséchapper et assainir les données
494 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
495 $raw_post_id = wp_unslash($_POST['post_id']);
496
497 // Paramètres optionnels pour le Site Editor (templates / template parts)
498 $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
499 $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
500
501 // Vérifier le nonce
502 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
503 wp_die('Security check failed');
504 }
505
506 $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
507 if ($post_id <= 0) {
508 wp_send_json_success(array(
509 'pageJS' => '',
510 'blockJS' => '',
511 'combinedJS' => '',
512 'post_id' => 0,
513 ));
514 }
515
516 // Vérifier que l'utilisateur peut lire cette cible
517 if (!$this->current_user_can_read_target($post_id)) {
518 wp_send_json_error('Insufficient permissions');
519 }
520
521 // Récupérer les JS depuis les meta du post
522 $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
523 $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
524 $combined_js = get_post_meta($post_id, 'ai_builder_js_content', true);
525
526 wp_send_json_success(array(
527 'pageJS' => $page_js ?: '',
528 'blockJS' => $block_js ?: '',
529 'combinedJS' => $combined_js ?: '',
530 'post_id' => $post_id
531 ));
532 }
533
534 public function save_meta_description()
535 {
536 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
537 wp_send_json_error('Missing required data');
538 }
539
540 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
541 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
542 wp_die('Security check failed');
543 }
544
545 $post_id = intval($_POST['post_id']);
546 if (!current_user_can('edit_post', $post_id)) {
547 wp_send_json_error('Insufficient permissions');
548 }
549
550 $raw = isset($_POST['meta_desc']) ? wp_unslash($_POST['meta_desc']) : '';
551 $san = trim(wp_strip_all_tags($raw));
552 if (strlen($san) > 320) {
553 $san = mb_substr($san, 0, 320);
554 }
555
556 if ($san === '') {
557 delete_post_meta($post_id, 'aibui_meta_description');
558 } else {
559 update_post_meta($post_id, 'aibui_meta_description', $san);
560 }
561
562 wp_send_json_success('Meta description saved');
563 }
564
565 // Capture wp_mail() errors and store briefly to surface via AJAX
566 public function capture_mail_error($wp_error)
567 {
568 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
569 $key = 'aibui_cf_mailerr_' . md5($ip);
570 set_transient($key, $wp_error instanceof WP_Error ? $wp_error->get_error_message() : 'Unknown mail error', 120);
571
572 }
573
574 public function submit_contact_form()
575 {
576 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'aibui_contact_form')) {
577 wp_send_json_error('Invalid nonce');
578 }
579
580 // Rate limiting per IP: 1 submission per 30 seconds
581 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
582 $key = 'aibui_cf_rl_' . md5($ip);
583 $last = get_transient($key);
584 if ($last) {
585 wp_send_json_error('Too many requests. Please wait.');
586 }
587 set_transient($key, time(), 30);
588
589 $recipient = isset($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : '';
590 if (empty($recipient) || !is_email($recipient)) {
591 $recipient = sanitize_email(get_option('admin_email'));
592 }
593 if (empty($recipient) || !is_email($recipient)) {
594 wp_send_json_error('No valid recipient configured');
595 }
596
597 $subject = sprintf('[%s] Nouveau message de contact', get_bloginfo('name'));
598
599 $fields = [];
600 $sender_email = '';
601 foreach ($_POST as $key => $value) {
602 if (strpos($key, 'field_') === 0) {
603 $label_key = 'label_' . $key;
604 $type_key = 'type_' . $key;
605 $req_key = 'required_' . $key;
606 $label = isset($_POST[$label_key]) ? sanitize_text_field(wp_unslash($_POST[$label_key])) : 'Champ';
607 $type = isset($_POST[$type_key]) ? sanitize_text_field(wp_unslash($_POST[$type_key])) : 'text';
608 $is_required = isset($_POST[$req_key]) && wp_unslash($_POST[$req_key]) === '1';
609 $raw = wp_unslash($value);
610 switch ($type) {
611 case 'email':
612 $san = sanitize_email($raw);
613 if (!$sender_email && is_email($san)) {
614 $sender_email = $san;
615 }
616 break;
617 case 'number':
618 $san = is_numeric($raw) ? $raw : '';
619 break;
620 case 'date':
621 $san = preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $raw) ? $raw : '';
622 break;
623 case 'textarea':
624 $san = sanitize_textarea_field($raw);
625 break;
626 default:
627 $san = sanitize_text_field($raw);
628 }
629 if ($is_required && $san === '') {
630 wp_send_json_error(sprintf('%s est requis', $label ? $label : 'Ce champ'));
631 }
632 $fields[] = ['label' => $label, 'type' => $type, 'value' => $san];
633 }
634 }
635
636 if (empty($fields)) {
637 wp_send_json_error('No fields provided');
638 }
639
640 // Build HTML email content
641 $rows = '';
642 foreach ($fields as $f) {
643 $val = $f['type'] === 'textarea' ? nl2br(esc_html($f['value'])) : esc_html($f['value']);
644 $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>';
645 }
646 $message = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;">'
647 . '<h3 style="margin:0 0 12px;">' . esc_html__('Nouveau message de contact', 'ai-builder') . '</h3>'
648 . '<table cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;width:100%;max-width:720px;">'
649 . $rows
650 . '</table>'
651 . '</div>';
652
653 $headers = [];
654 $headers[] = 'Content-Type: text/html; charset=UTF-8';
655 $domain = parse_url(home_url(), PHP_URL_HOST);
656 $default_from = 'no-reply@' . $domain;
657 $user_from = isset($_POST['from_email']) ? sanitize_email(wp_unslash($_POST['from_email'])) : '';
658 $from_email = $default_from;
659 if ($user_from && is_email($user_from)) {
660 // Use as From only if same domain (avoid SPF/DMARC issues)
661 $user_domain = substr(strrchr($user_from, '@'), 1);
662 if ($user_domain && strtolower($user_domain) === strtolower($domain)) {
663 $from_email = $user_from;
664 }
665 }
666 $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from_email . '>';
667 if ($sender_email && is_email($sender_email)) {
668 $headers[] = 'Reply-To: ' . $sender_email;
669 }
670
671 $sent = wp_mail($recipient, $subject, $message, $headers);
672 if (!$sent) {
673 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
674 $key_err = 'aibui_cf_mailerr_' . md5($ip);
675 $last_err = get_transient($key_err);
676 wp_send_json_error($last_err ? $last_err : 'Failed to send');
677 }
678 wp_send_json_success('Sent');
679 }
680
681 public function create_page()
682 {
683 // Vérifier que les données POST existent
684 if (!isset($_POST['nonce']) || !isset($_POST['content_type']) || !isset($_POST['title']) || !isset($_POST['content'])) {
685 wp_send_json_error('Missing required data');
686 }
687
688 // Déséchapper et assainir les données
689 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
690 $content_type = sanitize_text_field(wp_unslash($_POST['content_type']));
691 $title = sanitize_text_field(wp_unslash($_POST['title']));
692 $content = wp_unslash($_POST['content']);
693 $css_content = isset($_POST['css_content']) ? wp_unslash($_POST['css_content']) : '';
694 $meta_description = isset($_POST['meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['meta_description'])) : '';
695
696 // Vérifier le nonce
697 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
698 wp_die('Security check failed');
699 }
700
701 // Vérifier que l'utilisateur peut créer des posts/pages
702 if (!current_user_can('publish_posts')) {
703 wp_send_json_error('Insufficient permissions');
704 }
705
706 // Déséchapper les JSON de commentaires de blocs si l'API a échappé les guillemets
707 // Exemple: <!-- wp:cover {\"align\":\"full\"} --> -> <!-- wp:cover {"align":"full"} -->
708 $original_content = $content;
709 $replacement_count = 0;
710 $debug_log = array(); // Stocker les logs pour debug
711
712 $content = preg_replace_callback(
713 '/<!--\s*wp:([^\s]+)\s+(\{.*?\})\s*-->/',
714 function ($matches) use (&$replacement_count, &$debug_log) {
715 $block_name = $matches[1];
716 $json_str = $matches[2];
717 $fixed_json = stripslashes($json_str);
718
719 // Log pour debug
720 $debug_info = array(
721 'block' => $block_name,
722 'original_json' => substr($json_str, 0, 200),
723 'fixed_json' => substr($fixed_json, 0, 200),
724 'success' => false
725 );
726
727 // Ne remplacer que si le JSON corrigé est valide
728 $decoded = json_decode($fixed_json, true);
729 if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
730 $debug_info['error'] = json_last_error_msg();
731 $debug_info['original_json_full'] = $json_str;
732 $debug_log[] = $debug_info;
733 return $matches[0];
734 }
735
736 $replacement_count++;
737 $debug_info['success'] = true;
738 $debug_log[] = $debug_info;
739 return "<!-- wp:" . $block_name . " " . $fixed_json . " -->";
740 },
741 $content
742 );
743
744 // Vérifier que le contenu est au format HTML sérialisé WordPress
745 // Le contenu doit commencer par un commentaire de bloc WordPress
746 if (empty($content) || strpos(trim($content), '<!-- wp:') !== 0) {
747 wp_send_json_error('Invalid content format: Expected WordPress serialized block HTML');
748 }
749
750 // Valider le format des blocs avec parse_blocks
751 $parsed_blocks = parse_blocks($content);
752 if (empty($parsed_blocks) || (count($parsed_blocks) === 1 && empty($parsed_blocks[0]['blockName']))) {
753 wp_send_json_error('Invalid block format: Could not parse blocks');
754 }
755
756 // Créer le post/page
757 $post_data = array(
758 'post_title' => $title,
759 'post_content' => $content, // Contenu HTML sérialisé directement
760 'post_status' => 'publish',
761 'post_type' => $content_type === 'post' ? 'post' : 'page',
762 'post_author' => get_current_user_id(),
763 );
764
765 $post_id = wp_insert_post($post_data);
766
767 if (is_wp_error($post_id)) {
768 wp_send_json_error('Failed to create ' . $content_type);
769 }
770
771 // Sauvegarder le CSS si présent
772 if (!empty($css_content)) {
773 update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);
774 update_post_meta($post_id, 'ai_builder_css_content', $css_content);
775 }
776
777 // Sauvegarder la meta description si présente
778 if (!empty($meta_description)) {
779 update_post_meta($post_id, 'aibui_meta_description', $meta_description);
780 }
781
782 // Récupérer l'URL de la page créée
783 $page_url = get_permalink($post_id);
784
785
786 wp_send_json_success(array(
787 'page_id' => $post_id,
788 'page_url' => $page_url,
789 'page_title' => $title
790 ));
791 }
792
793 // -----------------------------
794 // Multi-Page: Generations store (using JSON files)
795 // -----------------------------
796 private function get_storage()
797 {
798 static $storage = null;
799 if ($storage === null) {
800 require_once plugin_dir_path(__FILE__) . 'class-generations-storage.php';
801 $storage = new AIBUI_Generations_Storage();
802 }
803 return $storage;
804 }
805
806 // Save a generation item (status: Pending review)
807 public function save_generation()
808 {
809 if (!isset($_POST['nonce'])) {
810 wp_send_json_error('Missing required data');
811 }
812 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
813 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
814 wp_die('Security check failed');
815 }
816 if (!current_user_can('edit_posts')) {
817 wp_send_json_error('Insufficient permissions');
818 }
819
820 $payload_raw = isset($_POST['payload']) ? wp_unslash($_POST['payload']) : '';
821 $payload = json_decode($payload_raw, true);
822 if (!$payload || !is_array($payload)) {
823 wp_send_json_error('Invalid payload');
824 }
825
826 $storage = $this->get_storage();
827 $result = $storage->save($payload);
828
829 if (is_wp_error($result)) {
830 wp_send_json_error($result->get_error_message());
831 }
832
833 wp_send_json_success($result);
834 }
835
836 // List generations
837 public function get_generations()
838 {
839 if (!isset($_POST['nonce'])) {
840 wp_send_json_error('Missing required data');
841 }
842 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
843 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
844 wp_die('Security check failed');
845 }
846 if (!current_user_can('edit_posts')) {
847 wp_send_json_error('Insufficient permissions');
848 }
849
850 $storage = $this->get_storage();
851 $items = $storage->get_all();
852
853 wp_send_json_success($items);
854 }
855
856 // Get one generation by id
857 public function get_generation()
858 {
859 if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
860 wp_send_json_error('Missing required data');
861 }
862 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
863 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
864 wp_die('Security check failed');
865 }
866 if (!current_user_can('edit_posts')) {
867 wp_send_json_error('Insufficient permissions');
868 }
869 $id = sanitize_text_field(wp_unslash($_POST['id']));
870
871 $storage = $this->get_storage();
872 $result = $storage->get($id);
873
874 if (is_wp_error($result)) {
875 wp_send_json_error($result->get_error_message());
876 }
877
878 wp_send_json_success($result);
879 }
880
881 // Mark generation as applied (optionally attach pageId and change status)
882 public function mark_generation_applied()
883 {
884 if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
885 wp_send_json_error('Missing required data');
886 }
887 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
888 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
889 wp_die('Security check failed');
890 }
891 if (!current_user_can('edit_posts')) {
892 wp_send_json_error('Insufficient permissions');
893 }
894 $id = sanitize_text_field(wp_unslash($_POST['id']));
895 $page_id = isset($_POST['page_id']) ? intval($_POST['page_id']) : 0;
896
897 $storage = $this->get_storage();
898 $result = $storage->mark_applied($id, $page_id);
899
900 if (is_wp_error($result)) {
901 wp_send_json_error($result->get_error_message());
902 }
903
904 wp_send_json_success($result);
905 }
906
907 public function save_page_prompt()
908 {
909 // Vérifier que les données POST existent
910 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['page_prompt'])) {
911 wp_send_json_error('Missing required data');
912 }
913
914 // Déséchapper et assainir les données
915 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
916 $post_id = intval($_POST['post_id']);
917 $page_prompt = sanitize_textarea_field(wp_unslash($_POST['page_prompt']));
918
919 // Vérifier le nonce
920 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
921 wp_die('Security check failed');
922 }
923
924 // Vérifier que l'utilisateur peut modifier ce post
925 if (!current_user_can('edit_post', $post_id)) {
926 wp_send_json_error('Insufficient permissions');
927 }
928
929 // Sauvegarder le prompt de page
930 update_post_meta($post_id, 'ai_builder_page_prompt', $page_prompt);
931
932 wp_send_json_success('Page prompt saved successfully');
933 }
934
935 public function get_page_prompt()
936 {
937 // Vérifier que les données POST existent
938 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
939 wp_send_json_error('Missing required data');
940 }
941
942 // Déséchapper et assainir les données
943 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
944 $post_id = intval($_POST['post_id']);
945
946 // Vérifier le nonce
947 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
948 wp_die('Security check failed');
949 }
950
951 // Vérifier que l'utilisateur peut lire ce post
952 if (!current_user_can('read_post', $post_id)) {
953 wp_send_json_error('Insufficient permissions');
954 }
955
956 // Récupérer le prompt de page
957 $page_prompt = get_post_meta($post_id, 'ai_builder_page_prompt', true);
958
959 wp_send_json_success(array(
960 'pagePrompt' => $page_prompt ?: ''
961 ));
962 }
963
964 /**
965 * Marquer une page comme créée via IA
966 */
967 public function mark_ai_created()
968 {
969 // Vérifier que les données POST existent
970 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
971 wp_send_json_error('Missing required data');
972 }
973
974 // Déséchapper et assainir les données
975 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
976 $post_id = intval($_POST['post_id']);
977
978 // Vérifier le nonce
979 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
980 wp_die('Security check failed');
981 }
982
983 // Vérifier que l'utilisateur peut éditer ce post
984 if (!current_user_can('edit_post', $post_id)) {
985 wp_send_json_error('Insufficient permissions');
986 }
987
988 // Marquer la page comme créée via IA
989 update_post_meta($post_id, '_aibui_created_by_ai', '1');
990
991 wp_send_json_success('Page marked as AI-created');
992 }
993
994 /**
995 * Get whether a post was created via AI Builder.
996 */
997 public function get_ai_created_status()
998 {
999 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
1000 wp_send_json_error('Missing required data');
1001 }
1002
1003 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
1004 $post_id = intval($_POST['post_id']);
1005
1006 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
1007 wp_die('Security check failed');
1008 }
1009
1010 if (!current_user_can('read_post', $post_id)) {
1011 wp_send_json_error('Insufficient permissions');
1012 }
1013
1014 $flag = get_post_meta($post_id, '_aibui_created_by_ai', true);
1015 wp_send_json_success(array(
1016 'isAICreated' => ($flag === '1' || $flag === 1 || $flag === true),
1017 ));
1018 }
1019 }