PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.6
AI Builder – Generate pages, blocks, images & translate with AI v2.7.6
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 2.3.11 All 121 releases
ai-builder / includes / class-ajax-handler.php

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

1,009 lines 38.0 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 if ($js_type === 'page') {
426 // Pour les pages, remplacer complètement le JS de page
427 update_post_meta($post_id, 'ai_builder_page_js_content', $js_content);
428
429 // Récupérer le JS de blocs existant
430 $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
431
432 // Combiner page JS + block JS pour le JS final
433 $final_js = $js_content;
434 if (!empty($block_js)) {
435 $final_js .= "\n" . $block_js;
436 }
437 update_post_meta($post_id, 'ai_builder_js_content', $final_js);
438
439 } else if ($js_type === 'block') {
440 $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
441 $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
442
443 if (empty($page_js)) {
444 $page_js = '';
445 }
446 if (empty($block_js)) {
447 $block_js = '';
448 }
449
450 if ($replace) {
451 // Si c'est une édition manuelle, remplacer complètement le JS de blocs
452 $block_js = $js_content;
453 } else {
454 // Si c'est une génération IA, ajouter au JS existant
455 $block_js .= "\n/* Block JS - " . date('Y-m-d H:i:s') . " */\n" . $js_content . "\n";
456 }
457
458 // Sauvegarder le JS de bloc
459 update_post_meta($post_id, 'ai_builder_block_js_content', $block_js);
460
461 // Combiner page JS + block JS pour le JS final
462 $final_js = $page_js;
463 if (!empty($block_js)) {
464 $final_js .= "\n" . $block_js;
465 }
466 update_post_meta($post_id, 'ai_builder_js_content', $final_js);
467
468 }
469
470 wp_send_json_success(array(
471 'message' => 'JS saved successfully',
472 'post_id' => $post_id,
473 ));
474 }
475
476 public function get_post_js()
477 {
478 // Vérifier que les données POST existent
479 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
480 wp_send_json_error('Missing required data');
481 }
482
483 // Déséchapper et assainir les données
484 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
485 $raw_post_id = wp_unslash($_POST['post_id']);
486
487 // Paramètres optionnels pour le Site Editor (templates / template parts)
488 $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
489 $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';
490
491 // Vérifier le nonce
492 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
493 wp_die('Security check failed');
494 }
495
496 $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
497 if ($post_id <= 0) {
498 wp_send_json_success(array(
499 'pageJS' => '',
500 'blockJS' => '',
501 'combinedJS' => '',
502 'post_id' => 0,
503 ));
504 }
505
506 // Vérifier que l'utilisateur peut lire cette cible
507 if (!$this->current_user_can_read_target($post_id)) {
508 wp_send_json_error('Insufficient permissions');
509 }
510
511 // Récupérer les JS depuis les meta du post
512 $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
513 $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
514 $combined_js = get_post_meta($post_id, 'ai_builder_js_content', true);
515
516 wp_send_json_success(array(
517 'pageJS' => $page_js ?: '',
518 'blockJS' => $block_js ?: '',
519 'combinedJS' => $combined_js ?: '',
520 'post_id' => $post_id
521 ));
522 }
523
524 public function save_meta_description()
525 {
526 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
527 wp_send_json_error('Missing required data');
528 }
529
530 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
531 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
532 wp_die('Security check failed');
533 }
534
535 $post_id = intval($_POST['post_id']);
536 if (!current_user_can('edit_post', $post_id)) {
537 wp_send_json_error('Insufficient permissions');
538 }
539
540 $raw = isset($_POST['meta_desc']) ? wp_unslash($_POST['meta_desc']) : '';
541 $san = trim(wp_strip_all_tags($raw));
542 if (strlen($san) > 320) {
543 $san = mb_substr($san, 0, 320);
544 }
545
546 if ($san === '') {
547 delete_post_meta($post_id, 'aibui_meta_description');
548 } else {
549 update_post_meta($post_id, 'aibui_meta_description', $san);
550 }
551
552 wp_send_json_success('Meta description saved');
553 }
554
555 // Capture wp_mail() errors and store briefly to surface via AJAX
556 public function capture_mail_error($wp_error)
557 {
558 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
559 $key = 'aibui_cf_mailerr_' . md5($ip);
560 set_transient($key, $wp_error instanceof WP_Error ? $wp_error->get_error_message() : 'Unknown mail error', 120);
561
562 }
563
564 public function submit_contact_form()
565 {
566 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'aibui_contact_form')) {
567 wp_send_json_error('Invalid nonce');
568 }
569
570 // Rate limiting per IP: 1 submission per 30 seconds
571 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
572 $key = 'aibui_cf_rl_' . md5($ip);
573 $last = get_transient($key);
574 if ($last) {
575 wp_send_json_error('Too many requests. Please wait.');
576 }
577 set_transient($key, time(), 30);
578
579 $recipient = isset($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : '';
580 if (empty($recipient) || !is_email($recipient)) {
581 $recipient = sanitize_email(get_option('admin_email'));
582 }
583 if (empty($recipient) || !is_email($recipient)) {
584 wp_send_json_error('No valid recipient configured');
585 }
586
587 $subject = sprintf('[%s] Nouveau message de contact', get_bloginfo('name'));
588
589 $fields = [];
590 $sender_email = '';
591 foreach ($_POST as $key => $value) {
592 if (strpos($key, 'field_') === 0) {
593 $label_key = 'label_' . $key;
594 $type_key = 'type_' . $key;
595 $req_key = 'required_' . $key;
596 $label = isset($_POST[$label_key]) ? sanitize_text_field(wp_unslash($_POST[$label_key])) : 'Champ';
597 $type = isset($_POST[$type_key]) ? sanitize_text_field(wp_unslash($_POST[$type_key])) : 'text';
598 $is_required = isset($_POST[$req_key]) && wp_unslash($_POST[$req_key]) === '1';
599 $raw = wp_unslash($value);
600 switch ($type) {
601 case 'email':
602 $san = sanitize_email($raw);
603 if (!$sender_email && is_email($san)) {
604 $sender_email = $san;
605 }
606 break;
607 case 'number':
608 $san = is_numeric($raw) ? $raw : '';
609 break;
610 case 'date':
611 $san = preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $raw) ? $raw : '';
612 break;
613 case 'textarea':
614 $san = sanitize_textarea_field($raw);
615 break;
616 default:
617 $san = sanitize_text_field($raw);
618 }
619 if ($is_required && $san === '') {
620 wp_send_json_error(sprintf('%s est requis', $label ? $label : 'Ce champ'));
621 }
622 $fields[] = ['label' => $label, 'type' => $type, 'value' => $san];
623 }
624 }
625
626 if (empty($fields)) {
627 wp_send_json_error('No fields provided');
628 }
629
630 // Build HTML email content
631 $rows = '';
632 foreach ($fields as $f) {
633 $val = $f['type'] === 'textarea' ? nl2br(esc_html($f['value'])) : esc_html($f['value']);
634 $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>';
635 }
636 $message = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;">'
637 . '<h3 style="margin:0 0 12px;">' . esc_html__('Nouveau message de contact', 'ai-builder') . '</h3>'
638 . '<table cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;width:100%;max-width:720px;">'
639 . $rows
640 . '</table>'
641 . '</div>';
642
643 $headers = [];
644 $headers[] = 'Content-Type: text/html; charset=UTF-8';
645 $domain = parse_url(home_url(), PHP_URL_HOST);
646 $default_from = 'no-reply@' . $domain;
647 $user_from = isset($_POST['from_email']) ? sanitize_email(wp_unslash($_POST['from_email'])) : '';
648 $from_email = $default_from;
649 if ($user_from && is_email($user_from)) {
650 // Use as From only if same domain (avoid SPF/DMARC issues)
651 $user_domain = substr(strrchr($user_from, '@'), 1);
652 if ($user_domain && strtolower($user_domain) === strtolower($domain)) {
653 $from_email = $user_from;
654 }
655 }
656 $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from_email . '>';
657 if ($sender_email && is_email($sender_email)) {
658 $headers[] = 'Reply-To: ' . $sender_email;
659 }
660
661 $sent = wp_mail($recipient, $subject, $message, $headers);
662 if (!$sent) {
663 $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
664 $key_err = 'aibui_cf_mailerr_' . md5($ip);
665 $last_err = get_transient($key_err);
666 wp_send_json_error($last_err ? $last_err : 'Failed to send');
667 }
668 wp_send_json_success('Sent');
669 }
670
671 public function create_page()
672 {
673 // Vérifier que les données POST existent
674 if (!isset($_POST['nonce']) || !isset($_POST['content_type']) || !isset($_POST['title']) || !isset($_POST['content'])) {
675 wp_send_json_error('Missing required data');
676 }
677
678 // Déséchapper et assainir les données
679 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
680 $content_type = sanitize_text_field(wp_unslash($_POST['content_type']));
681 $title = sanitize_text_field(wp_unslash($_POST['title']));
682 $content = wp_unslash($_POST['content']);
683 $css_content = isset($_POST['css_content']) ? wp_unslash($_POST['css_content']) : '';
684 $meta_description = isset($_POST['meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['meta_description'])) : '';
685
686 // Vérifier le nonce
687 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
688 wp_die('Security check failed');
689 }
690
691 // Vérifier que l'utilisateur peut créer des posts/pages
692 if (!current_user_can('publish_posts')) {
693 wp_send_json_error('Insufficient permissions');
694 }
695
696 // Déséchapper les JSON de commentaires de blocs si l'API a échappé les guillemets
697 // Exemple: <!-- wp:cover {\"align\":\"full\"} --> -> <!-- wp:cover {"align":"full"} -->
698 $original_content = $content;
699 $replacement_count = 0;
700 $debug_log = array(); // Stocker les logs pour debug
701
702 $content = preg_replace_callback(
703 '/<!--\s*wp:([^\s]+)\s+(\{.*?\})\s*-->/',
704 function ($matches) use (&$replacement_count, &$debug_log) {
705 $block_name = $matches[1];
706 $json_str = $matches[2];
707 $fixed_json = stripslashes($json_str);
708
709 // Log pour debug
710 $debug_info = array(
711 'block' => $block_name,
712 'original_json' => substr($json_str, 0, 200),
713 'fixed_json' => substr($fixed_json, 0, 200),
714 'success' => false
715 );
716
717 // Ne remplacer que si le JSON corrigé est valide
718 $decoded = json_decode($fixed_json, true);
719 if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
720 $debug_info['error'] = json_last_error_msg();
721 $debug_info['original_json_full'] = $json_str;
722 $debug_log[] = $debug_info;
723 return $matches[0];
724 }
725
726 $replacement_count++;
727 $debug_info['success'] = true;
728 $debug_log[] = $debug_info;
729 return "<!-- wp:" . $block_name . " " . $fixed_json . " -->";
730 },
731 $content
732 );
733
734 // Vérifier que le contenu est au format HTML sérialisé WordPress
735 // Le contenu doit commencer par un commentaire de bloc WordPress
736 if (empty($content) || strpos(trim($content), '<!-- wp:') !== 0) {
737 wp_send_json_error('Invalid content format: Expected WordPress serialized block HTML');
738 }
739
740 // Valider le format des blocs avec parse_blocks
741 $parsed_blocks = parse_blocks($content);
742 if (empty($parsed_blocks) || (count($parsed_blocks) === 1 && empty($parsed_blocks[0]['blockName']))) {
743 wp_send_json_error('Invalid block format: Could not parse blocks');
744 }
745
746 // Créer le post/page
747 $post_data = array(
748 'post_title' => $title,
749 'post_content' => $content, // Contenu HTML sérialisé directement
750 'post_status' => 'publish',
751 'post_type' => $content_type === 'post' ? 'post' : 'page',
752 'post_author' => get_current_user_id(),
753 );
754
755 $post_id = wp_insert_post($post_data);
756
757 if (is_wp_error($post_id)) {
758 wp_send_json_error('Failed to create ' . $content_type);
759 }
760
761 // Sauvegarder le CSS si présent
762 if (!empty($css_content)) {
763 update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);
764 update_post_meta($post_id, 'ai_builder_css_content', $css_content);
765 }
766
767 // Sauvegarder la meta description si présente
768 if (!empty($meta_description)) {
769 update_post_meta($post_id, 'aibui_meta_description', $meta_description);
770 }
771
772 // Récupérer l'URL de la page créée
773 $page_url = get_permalink($post_id);
774
775
776 wp_send_json_success(array(
777 'page_id' => $post_id,
778 'page_url' => $page_url,
779 'page_title' => $title
780 ));
781 }
782
783 // -----------------------------
784 // Multi-Page: Generations store (using JSON files)
785 // -----------------------------
786 private function get_storage()
787 {
788 static $storage = null;
789 if ($storage === null) {
790 require_once plugin_dir_path(__FILE__) . 'class-generations-storage.php';
791 $storage = new AIBUI_Generations_Storage();
792 }
793 return $storage;
794 }
795
796 // Save a generation item (status: Pending review)
797 public function save_generation()
798 {
799 if (!isset($_POST['nonce'])) {
800 wp_send_json_error('Missing required data');
801 }
802 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
803 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
804 wp_die('Security check failed');
805 }
806 if (!current_user_can('edit_posts')) {
807 wp_send_json_error('Insufficient permissions');
808 }
809
810 $payload_raw = isset($_POST['payload']) ? wp_unslash($_POST['payload']) : '';
811 $payload = json_decode($payload_raw, true);
812 if (!$payload || !is_array($payload)) {
813 wp_send_json_error('Invalid payload');
814 }
815
816 $storage = $this->get_storage();
817 $result = $storage->save($payload);
818
819 if (is_wp_error($result)) {
820 wp_send_json_error($result->get_error_message());
821 }
822
823 wp_send_json_success($result);
824 }
825
826 // List generations
827 public function get_generations()
828 {
829 if (!isset($_POST['nonce'])) {
830 wp_send_json_error('Missing required data');
831 }
832 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
833 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
834 wp_die('Security check failed');
835 }
836 if (!current_user_can('edit_posts')) {
837 wp_send_json_error('Insufficient permissions');
838 }
839
840 $storage = $this->get_storage();
841 $items = $storage->get_all();
842
843 wp_send_json_success($items);
844 }
845
846 // Get one generation by id
847 public function get_generation()
848 {
849 if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
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 $id = sanitize_text_field(wp_unslash($_POST['id']));
860
861 $storage = $this->get_storage();
862 $result = $storage->get($id);
863
864 if (is_wp_error($result)) {
865 wp_send_json_error($result->get_error_message());
866 }
867
868 wp_send_json_success($result);
869 }
870
871 // Mark generation as applied (optionally attach pageId and change status)
872 public function mark_generation_applied()
873 {
874 if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
875 wp_send_json_error('Missing required data');
876 }
877 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
878 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
879 wp_die('Security check failed');
880 }
881 if (!current_user_can('edit_posts')) {
882 wp_send_json_error('Insufficient permissions');
883 }
884 $id = sanitize_text_field(wp_unslash($_POST['id']));
885 $page_id = isset($_POST['page_id']) ? intval($_POST['page_id']) : 0;
886
887 $storage = $this->get_storage();
888 $result = $storage->mark_applied($id, $page_id);
889
890 if (is_wp_error($result)) {
891 wp_send_json_error($result->get_error_message());
892 }
893
894 wp_send_json_success($result);
895 }
896
897 public function save_page_prompt()
898 {
899 // Vérifier que les données POST existent
900 if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['page_prompt'])) {
901 wp_send_json_error('Missing required data');
902 }
903
904 // Déséchapper et assainir les données
905 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
906 $post_id = intval($_POST['post_id']);
907 $page_prompt = sanitize_textarea_field(wp_unslash($_POST['page_prompt']));
908
909 // Vérifier le nonce
910 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
911 wp_die('Security check failed');
912 }
913
914 // Vérifier que l'utilisateur peut modifier ce post
915 if (!current_user_can('edit_post', $post_id)) {
916 wp_send_json_error('Insufficient permissions');
917 }
918
919 // Sauvegarder le prompt de page
920 update_post_meta($post_id, 'ai_builder_page_prompt', $page_prompt);
921
922 wp_send_json_success('Page prompt saved successfully');
923 }
924
925 public function get_page_prompt()
926 {
927 // Vérifier que les données POST existent
928 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
929 wp_send_json_error('Missing required data');
930 }
931
932 // Déséchapper et assainir les données
933 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
934 $post_id = intval($_POST['post_id']);
935
936 // Vérifier le nonce
937 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
938 wp_die('Security check failed');
939 }
940
941 // Vérifier que l'utilisateur peut lire ce post
942 if (!current_user_can('read_post', $post_id)) {
943 wp_send_json_error('Insufficient permissions');
944 }
945
946 // Récupérer le prompt de page
947 $page_prompt = get_post_meta($post_id, 'ai_builder_page_prompt', true);
948
949 wp_send_json_success(array(
950 'pagePrompt' => $page_prompt ?: ''
951 ));
952 }
953
954 /**
955 * Marquer une page comme créée via IA
956 */
957 public function mark_ai_created()
958 {
959 // Vérifier que les données POST existent
960 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
961 wp_send_json_error('Missing required data');
962 }
963
964 // Déséchapper et assainir les données
965 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
966 $post_id = intval($_POST['post_id']);
967
968 // Vérifier le nonce
969 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
970 wp_die('Security check failed');
971 }
972
973 // Vérifier que l'utilisateur peut éditer ce post
974 if (!current_user_can('edit_post', $post_id)) {
975 wp_send_json_error('Insufficient permissions');
976 }
977
978 // Marquer la page comme créée via IA
979 update_post_meta($post_id, '_aibui_created_by_ai', '1');
980
981 wp_send_json_success('Page marked as AI-created');
982 }
983
984 /**
985 * Get whether a post was created via AI Builder.
986 */
987 public function get_ai_created_status()
988 {
989 if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
990 wp_send_json_error('Missing required data');
991 }
992
993 $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
994 $post_id = intval($_POST['post_id']);
995
996 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
997 wp_die('Security check failed');
998 }
999
1000 if (!current_user_can('read_post', $post_id)) {
1001 wp_send_json_error('Insufficient permissions');
1002 }
1003
1004 $flag = get_post_meta($post_id, '_aibui_created_by_ai', true);
1005 wp_send_json_success(array(
1006 'isAICreated' => ($flag === '1' || $flag === 1 || $flag === true),
1007 ));
1008 }
1009 }