# ai-builder/2.7.8/includes/class-ajax-handler.php

AI Builder – Generate pages, blocks, images &amp; translate with AI, version 2.7.8. 1,019 lines.

- Page: https://pluginprobe.com/plugins/ai-builder/2.7.8/code/includes/class-ajax-handler.php
- Raw: https://pluginprobe.com/plugins/ai-builder/2.7.8/raw/includes/class-ajax-handler.php
- Modified: 2026-09-08T10:53:48+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ai-builder/2.7.8/code/includes/class-ajax-handler.php#L10-L20`.

```php
<?php

class AIBUI_Ajax_Handler
{
    public function __construct()
    {
        add_action('wp_ajax_aibui_save_token', array($this, 'save_token'));
        add_action('wp_ajax_aibui_set_signup_success', array($this, 'set_signup_success'));
        add_action('wp_ajax_aibui_signout', array($this, 'signout'));
        add_action('wp_ajax_aibui_get_token', array($this, 'get_token'));
        add_action('wp_ajax_aibui_save_post_css', array($this, 'save_post_css'));
        add_action('wp_ajax_aibui_get_post_css', array($this, 'get_post_css'));
        add_action('wp_ajax_aibui_save_post_js', array($this, 'save_post_js'));
        add_action('wp_ajax_aibui_get_post_js', array($this, 'get_post_js'));
        add_action('wp_ajax_aibui_save_page_prompt', array($this, 'save_page_prompt'));
        add_action('wp_ajax_aibui_get_page_prompt', array($this, 'get_page_prompt'));
        add_action('wp_ajax_aibui_save_meta_description', array($this, 'save_meta_description'));
        add_action('wp_ajax_aibui_create_page', array($this, 'create_page'));
        add_action('wp_ajax_nopriv_aibui_submit_contact_form', array($this, 'submit_contact_form'));
        add_action('wp_ajax_aibui_submit_contact_form', array($this, 'submit_contact_form'));
        add_action('wp_mail_failed', array($this, 'capture_mail_error'));

        // Multi-page generations storage endpoints
        add_action('wp_ajax_aibui_save_generation', array($this, 'save_generation'));
        add_action('wp_ajax_aibui_get_generations', array($this, 'get_generations'));
        add_action('wp_ajax_aibui_get_generation', array($this, 'get_generation'));
        add_action('wp_ajax_aibui_mark_generation_applied', array($this, 'mark_generation_applied'));
        
        // Mark pages created via AI
        add_action('wp_ajax_aibui_mark_ai_created', array($this, 'mark_ai_created'));
        add_action('wp_ajax_aibui_get_ai_created_status', array($this, 'get_ai_created_status'));
    }

    public function save_token()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['token'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $token = sanitize_text_field(wp_unslash($_POST['token']));

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        if (empty($token)) {
            wp_send_json_error('Token is required');
        }

        // Sauvegarder le token JWT
        update_option('aibui_jwt_token', $token);

        wp_send_json_success('Token saved successfully');
    }

    public function set_signup_success()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Marquer l'inscription comme réussie
        update_option('aibui_user_successful_signup', true);

        wp_send_json_success('Signup success flag set');
    }

    public function signout()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Supprimer le token JWT
        delete_option('aibui_jwt_token');

        wp_send_json_success('Signed out successfully');
    }

    public function get_token()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Récupérer le token JWT
        $token = get_option('aibui_jwt_token', '');

        if (empty($token)) {
            wp_send_json_error('No token found');
        }

        wp_send_json_success(array('token' => $token));
    }

    /**
     * Résout l'ID numérique de post cible pour les endpoints CSS/JS.
     *
     * Accepte soit :
     *   - un post_id numérique classique (pages/articles, et templates déjà
     *     matérialisés en base), soit
     *   - un template_id composite "theme//slug" + template_type
     *     ("wp_template" ou "wp_template_part"), utilisé par le Site Editor.
     *
     * Pour les templates/parts file-based non encore en base, le post
     * correspondant est créé à la volée (même stratégie que le Site Editor
     * quand l'utilisateur clique sur Save).
     *
     * @return int  Post ID positif, ou 0 si non résoluble.
     */
    private function resolve_target_post_id($raw_post_id, $template_id, $template_type)
    {
        // Chemin rapide : un post_id numérique valide est accepté tel quel.
        $post_id = 0;
        if ($raw_post_id !== '' && is_numeric($raw_post_id)) {
            $post_id = intval($raw_post_id);
            if ($post_id > 0 && get_post($post_id)) {
                return $post_id;
            }
            $post_id = 0;
        }

        // Sinon, on tente la résolution via template_id "theme//slug".
        if (!is_string($template_id) || $template_id === '') {
            return 0;
        }
        if (!in_array($template_type, array('wp_template', 'wp_template_part'), true)) {
            return 0;
        }
        if (strpos($template_id, '//') === false) {
            return 0;
        }
        if (!function_exists('get_block_template')) {
            return 0;
        }

        try {
            $tpl = get_block_template($template_id, $template_type);
        } catch (\Throwable $e) {
            return 0;
        }
        if (!$tpl) {
            return 0;
        }

        // Déjà en base → on réutilise.
        if (!empty($tpl->wp_id) && (int) $tpl->wp_id > 0) {
            return (int) $tpl->wp_id;
        }

        // Pas encore en base : on matérialise le template file-based en post
        // de la même manière que le Site Editor. Cela nécessite la capability
        // edit_theme_options (vérifiée ici, en plus de la vérif au niveau
        // endpoint) pour ne jamais créer d'entrée theme à cause d'un save JS.
        if (!current_user_can('edit_theme_options')) {
            return 0;
        }

        list($theme_slug, $slug) = array_pad(explode('//', $template_id, 2), 2, '');
        if ($theme_slug === '' || $slug === '') {
            return 0;
        }

        $title   = isset($tpl->title) && $tpl->title !== '' ? (string) $tpl->title : $slug;
        $content = isset($tpl->content) ? (string) $tpl->content : '';

        try {
            $new_post_id = wp_insert_post(array(
                'post_type'    => $template_type,
                'post_status'  => 'publish',
                'post_title'   => $title,
                'post_name'    => $slug,
                'post_content' => $content,
            ), true);
        } catch (\Throwable $e) {
            return 0;
        }
        if (is_wp_error($new_post_id) || !$new_post_id) {
            return 0;
        }

        // Rattacher au theme courant (taxonomy wp_theme).
        try {
            wp_set_object_terms((int) $new_post_id, $theme_slug, 'wp_theme');
        } catch (\Throwable $e) {
            // non bloquant
        }

        // Pour les template parts, rattacher la zone (header/footer/uncategorized...).
        if ($template_type === 'wp_template_part') {
            $area = isset($tpl->area) && is_string($tpl->area) && $tpl->area !== ''
                ? $tpl->area
                : 'uncategorized';
            try {
                wp_set_object_terms((int) $new_post_id, $area, 'wp_template_part_area');
            } catch (\Throwable $e) {
                // non bloquant
            }
        }

        return (int) $new_post_id;
    }

    /**
     * Vérif de capability adaptée au type de cible.
     * - wp_template / wp_template_part : edit_theme_options (Site Editor)
     * - autres posts                   : edit_post sur l'ID
     */
    private function current_user_can_edit_target($post_id)
    {
        $post = get_post($post_id);
        if ($post && in_array($post->post_type, array('wp_template', 'wp_template_part'), true)) {
            return current_user_can('edit_theme_options');
        }
        return current_user_can('edit_post', $post_id);
    }

    private function current_user_can_read_target($post_id)
    {
        $post = get_post($post_id);
        if ($post && in_array($post->post_type, array('wp_template', 'wp_template_part'), true)) {
            return current_user_can('edit_theme_options');
        }
        return current_user_can('read_post', $post_id);
    }

    public function save_post_css()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['css_content'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $raw_post_id = wp_unslash($_POST['post_id']);
        $css_content = wp_unslash($_POST['css_content']);
        $css_type = isset($_POST['css_type']) ? sanitize_text_field(wp_unslash($_POST['css_type'])) : 'page';
        $replace = isset($_POST['replace']) ? filter_var(wp_unslash($_POST['replace']), FILTER_VALIDATE_BOOLEAN) : false;

        // Paramètres optionnels pour le Site Editor (templates / template parts)
        $template_id   = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
        $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
        if ($post_id <= 0) {
            wp_send_json_error('Invalid target (save the template once in the Site Editor first)');
        }

        // Vérifier que l'utilisateur peut éditer cette cible
        if (!$this->current_user_can_edit_target($post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        if ($css_type === 'page') {
            // Pour les pages, remplacer complètement le CSS de page
            update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);

            // Récupérer le CSS de blocs existant
            $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);

            // Combiner page CSS + block CSS pour le CSS final
            $final_css = $css_content;
            if (!empty($block_css)) {
                $final_css .= "\n" . $block_css;
            }
            update_post_meta($post_id, 'ai_builder_css_content', $final_css);

        } else if ($css_type === 'block') {
            $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
            $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);

            if (empty($page_css)) {
                $page_css = '';
            }
            if (empty($block_css)) {
                $block_css = '';
            }

            if ($replace) {
                // Si c'est une édition manuelle, remplacer complètement le CSS de blocs
                $block_css = $css_content;
            } else {
                // Si c'est une génération IA, ajouter au CSS existant
                $block_css .= "\n/* Block CSS - " . date('Y-m-d H:i:s') . " */\n" . $css_content . "\n";
            }

            // Sauvegarder le CSS de bloc
            update_post_meta($post_id, 'ai_builder_block_css_content', $block_css);

            // Combiner page CSS + block CSS pour le CSS final
            $final_css = $page_css;
            if (!empty($block_css)) {
                $final_css .= "\n" . $block_css;
            }
            update_post_meta($post_id, 'ai_builder_css_content', $final_css);

        }

        wp_send_json_success(array(
            'message' => 'CSS saved successfully',
            'post_id' => $post_id,
        ));
    }


    public function get_post_css()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $raw_post_id = wp_unslash($_POST['post_id']);

        // Paramètres optionnels pour le Site Editor (templates / template parts)
        $template_id   = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
        $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
        if ($post_id <= 0) {
            // Rien à retourner mais on ne bloque pas l'UI : réponse vide neutre.
            wp_send_json_success(array(
                'pageCss'     => '',
                'blockCss'    => '',
                'combinedCss' => '',
                'post_id'     => 0,
            ));
        }

        // Vérifier que l'utilisateur peut lire cette cible
        if (!$this->current_user_can_read_target($post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        // Récupérer les CSS depuis les meta du post
        $page_css = get_post_meta($post_id, 'ai_builder_page_css_content', true);
        $block_css = get_post_meta($post_id, 'ai_builder_block_css_content', true);
        $combined_css = get_post_meta($post_id, 'ai_builder_css_content', true);

        wp_send_json_success(array(
            'pageCss' => $page_css,
            'blockCss' => $block_css,
            'combinedCss' => $combined_css,
            'post_id' => $post_id,
        ));
    }

    public function save_post_js()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['js_content'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $raw_post_id = wp_unslash($_POST['post_id']);
        $js_content = wp_unslash($_POST['js_content']);
        $js_type = isset($_POST['js_type']) ? sanitize_text_field(wp_unslash($_POST['js_type'])) : 'page';
        $replace = isset($_POST['replace']) ? filter_var(wp_unslash($_POST['replace']), FILTER_VALIDATE_BOOLEAN) : false;

        // Paramètres optionnels pour le Site Editor (templates / template parts)
        $template_id   = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
        $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
        if ($post_id <= 0) {
            wp_send_json_error('Invalid target (save the template once in the Site Editor first)');
        }

        // Vérifier que l'utilisateur peut éditer cette cible
        if (!$this->current_user_can_edit_target($post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        // Le JS enregistré ici est ré-émis tel quel dans un <script> sur le front
        // (pages, blocs, templates du Site Editor). Seuls les utilisateurs disposant
        // de la capacité unfiltered_html peuvent stocker du script exécuté chez
        // d'autres personnes : même barrière que le bloc HTML personnalisé de WordPress.
        // Placé avant toutes les branches (page / block / site editor) pour qu'elles
        // en héritent.
        if (!current_user_can('unfiltered_html')) {
            wp_send_json_error('Insufficient permissions: saving custom JavaScript requires the unfiltered_html capability');
        }

        if ($js_type === 'page') {
            // Pour les pages, remplacer complètement le JS de page
            update_post_meta($post_id, 'ai_builder_page_js_content', $js_content);

            // Récupérer le JS de blocs existant
            $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);

            // Combiner page JS + block JS pour le JS final
            $final_js = $js_content;
            if (!empty($block_js)) {
                $final_js .= "\n" . $block_js;
            }
            update_post_meta($post_id, 'ai_builder_js_content', $final_js);

        } else if ($js_type === 'block') {
            $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
            $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);

            if (empty($page_js)) {
                $page_js = '';
            }
            if (empty($block_js)) {
                $block_js = '';
            }

            if ($replace) {
                // Si c'est une édition manuelle, remplacer complètement le JS de blocs
                $block_js = $js_content;
            } else {
                // Si c'est une génération IA, ajouter au JS existant
                $block_js .= "\n/* Block JS - " . date('Y-m-d H:i:s') . " */\n" . $js_content . "\n";
            }

            // Sauvegarder le JS de bloc
            update_post_meta($post_id, 'ai_builder_block_js_content', $block_js);

            // Combiner page JS + block JS pour le JS final
            $final_js = $page_js;
            if (!empty($block_js)) {
                $final_js .= "\n" . $block_js;
            }
            update_post_meta($post_id, 'ai_builder_js_content', $final_js);

        }

        wp_send_json_success(array(
            'message' => 'JS saved successfully',
            'post_id' => $post_id,
        ));
    }

    public function get_post_js()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $raw_post_id = wp_unslash($_POST['post_id']);

        // Paramètres optionnels pour le Site Editor (templates / template parts)
        $template_id   = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : '';
        $template_type = isset($_POST['template_type']) ? sanitize_text_field(wp_unslash($_POST['template_type'])) : '';

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        $post_id = $this->resolve_target_post_id($raw_post_id, $template_id, $template_type);
        if ($post_id <= 0) {
            wp_send_json_success(array(
                'pageJS'     => '',
                'blockJS'    => '',
                'combinedJS' => '',
                'post_id'    => 0,
            ));
        }

        // Vérifier que l'utilisateur peut lire cette cible
        if (!$this->current_user_can_read_target($post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        // Récupérer les JS depuis les meta du post
        $page_js = get_post_meta($post_id, 'ai_builder_page_js_content', true);
        $block_js = get_post_meta($post_id, 'ai_builder_block_js_content', true);
        $combined_js = get_post_meta($post_id, 'ai_builder_js_content', true);

        wp_send_json_success(array(
            'pageJS' => $page_js ?: '',
            'blockJS' => $block_js ?: '',
            'combinedJS' => $combined_js ?: '',
            'post_id' => $post_id
        ));
    }

    public function save_meta_description()
    {
        if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
            wp_send_json_error('Missing required data');
        }

        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        $post_id = intval($_POST['post_id']);
        if (!current_user_can('edit_post', $post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        $raw = isset($_POST['meta_desc']) ? wp_unslash($_POST['meta_desc']) : '';
        $san = trim(wp_strip_all_tags($raw));
        if (strlen($san) > 320) {
            $san = mb_substr($san, 0, 320);
        }

        if ($san === '') {
            delete_post_meta($post_id, 'aibui_meta_description');
        } else {
            update_post_meta($post_id, 'aibui_meta_description', $san);
        }

        wp_send_json_success('Meta description saved');
    }

    // Capture wp_mail() errors and store briefly to surface via AJAX
    public function capture_mail_error($wp_error)
    {
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = 'aibui_cf_mailerr_' . md5($ip);
        set_transient($key, $wp_error instanceof WP_Error ? $wp_error->get_error_message() : 'Unknown mail error', 120);

    }

    public function submit_contact_form()
    {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'aibui_contact_form')) {
            wp_send_json_error('Invalid nonce');
        }

        // Rate limiting per IP: 1 submission per 30 seconds
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = 'aibui_cf_rl_' . md5($ip);
        $last = get_transient($key);
        if ($last) {
            wp_send_json_error('Too many requests. Please wait.');
        }
        set_transient($key, time(), 30);

        $recipient = isset($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : '';
        if (empty($recipient) || !is_email($recipient)) {
            $recipient = sanitize_email(get_option('admin_email'));
        }
        if (empty($recipient) || !is_email($recipient)) {
            wp_send_json_error('No valid recipient configured');
        }

        $subject = sprintf('[%s] Nouveau message de contact', get_bloginfo('name'));

        $fields = [];
        $sender_email = '';
        foreach ($_POST as $key => $value) {
            if (strpos($key, 'field_') === 0) {
                $label_key = 'label_' . $key;
                $type_key = 'type_' . $key;
                $req_key = 'required_' . $key;
                $label = isset($_POST[$label_key]) ? sanitize_text_field(wp_unslash($_POST[$label_key])) : 'Champ';
                $type = isset($_POST[$type_key]) ? sanitize_text_field(wp_unslash($_POST[$type_key])) : 'text';
                $is_required = isset($_POST[$req_key]) && wp_unslash($_POST[$req_key]) === '1';
                $raw = wp_unslash($value);
                switch ($type) {
                    case 'email':
                        $san = sanitize_email($raw);
                        if (!$sender_email && is_email($san)) {
                            $sender_email = $san;
                        }
                        break;
                    case 'number':
                        $san = is_numeric($raw) ? $raw : '';
                        break;
                    case 'date':
                        $san = preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $raw) ? $raw : '';
                        break;
                    case 'textarea':
                        $san = sanitize_textarea_field($raw);
                        break;
                    default:
                        $san = sanitize_text_field($raw);
                }
                if ($is_required && $san === '') {
                    wp_send_json_error(sprintf('%s est requis', $label ? $label : 'Ce champ'));
                }
                $fields[] = ['label' => $label, 'type' => $type, 'value' => $san];
            }
        }

        if (empty($fields)) {
            wp_send_json_error('No fields provided');
        }

        // Build HTML email content
        $rows = '';
        foreach ($fields as $f) {
            $val = $f['type'] === 'textarea' ? nl2br(esc_html($f['value'])) : esc_html($f['value']);
            $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>';
        }
        $message = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;">'
            . '<h3 style="margin:0 0 12px;">' . esc_html__('Nouveau message de contact', 'ai-builder') . '</h3>'
            . '<table cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;width:100%;max-width:720px;">'
            . $rows
            . '</table>'
            . '</div>';

        $headers = [];
        $headers[] = 'Content-Type: text/html; charset=UTF-8';
        $domain = parse_url(home_url(), PHP_URL_HOST);
        $default_from = 'no-reply@' . $domain;
        $user_from = isset($_POST['from_email']) ? sanitize_email(wp_unslash($_POST['from_email'])) : '';
        $from_email = $default_from;
        if ($user_from && is_email($user_from)) {
            // Use as From only if same domain (avoid SPF/DMARC issues)
            $user_domain = substr(strrchr($user_from, '@'), 1);
            if ($user_domain && strtolower($user_domain) === strtolower($domain)) {
                $from_email = $user_from;
            }
        }
        $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from_email . '>';
        if ($sender_email && is_email($sender_email)) {
            $headers[] = 'Reply-To: ' . $sender_email;
        }

        $sent = wp_mail($recipient, $subject, $message, $headers);
        if (!$sent) {
            $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
            $key_err = 'aibui_cf_mailerr_' . md5($ip);
            $last_err = get_transient($key_err);
            wp_send_json_error($last_err ? $last_err : 'Failed to send');
        }
        wp_send_json_success('Sent');
    }

    public function create_page()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['content_type']) || !isset($_POST['title']) || !isset($_POST['content'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $content_type = sanitize_text_field(wp_unslash($_POST['content_type']));
        $title = sanitize_text_field(wp_unslash($_POST['title']));
        $content = wp_unslash($_POST['content']);
        $css_content = isset($_POST['css_content']) ? wp_unslash($_POST['css_content']) : '';
        $meta_description = isset($_POST['meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['meta_description'])) : '';

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Vérifier que l'utilisateur peut créer des posts/pages
        if (!current_user_can('publish_posts')) {
            wp_send_json_error('Insufficient permissions');
        }

        // Déséchapper les JSON de commentaires de blocs si l'API a échappé les guillemets
        // Exemple: <!-- wp:cover {\"align\":\"full\"} --> -> <!-- wp:cover {"align":"full"} -->
        $original_content = $content;
        $replacement_count = 0;
        $debug_log = array(); // Stocker les logs pour debug
        
        $content = preg_replace_callback(
            '/<!--\s*wp:([^\s]+)\s+(\{.*?\})\s*-->/',
            function ($matches) use (&$replacement_count, &$debug_log) {
                $block_name = $matches[1];
                $json_str = $matches[2];
                $fixed_json = stripslashes($json_str);
                
                // Log pour debug
                $debug_info = array(
                    'block' => $block_name,
                    'original_json' => substr($json_str, 0, 200),
                    'fixed_json' => substr($fixed_json, 0, 200),
                    'success' => false
                );
                
                // Ne remplacer que si le JSON corrigé est valide
                $decoded = json_decode($fixed_json, true);
                if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
                    $debug_info['error'] = json_last_error_msg();
                    $debug_info['original_json_full'] = $json_str;
                    $debug_log[] = $debug_info;
                    return $matches[0];
                }
                
                $replacement_count++;
                $debug_info['success'] = true;
                $debug_log[] = $debug_info;
                return "<!-- wp:" . $block_name . " " . $fixed_json . " -->";
            },
            $content
        );
        
        // Vérifier que le contenu est au format HTML sérialisé WordPress
        // Le contenu doit commencer par un commentaire de bloc WordPress
        if (empty($content) || strpos(trim($content), '<!-- wp:') !== 0) {
            wp_send_json_error('Invalid content format: Expected WordPress serialized block HTML');
        }

        // Valider le format des blocs avec parse_blocks
        $parsed_blocks = parse_blocks($content);
        if (empty($parsed_blocks) || (count($parsed_blocks) === 1 && empty($parsed_blocks[0]['blockName']))) {
            wp_send_json_error('Invalid block format: Could not parse blocks');
        }

        // Créer le post/page
        $post_data = array(
            'post_title' => $title,
            'post_content' => $content, // Contenu HTML sérialisé directement
            'post_status' => 'publish',
            'post_type' => $content_type === 'post' ? 'post' : 'page',
            'post_author' => get_current_user_id(),
        );

        $post_id = wp_insert_post($post_data);

        if (is_wp_error($post_id)) {
            wp_send_json_error('Failed to create ' . $content_type);
        }

        // Sauvegarder le CSS si présent
        if (!empty($css_content)) {
            update_post_meta($post_id, 'ai_builder_page_css_content', $css_content);
            update_post_meta($post_id, 'ai_builder_css_content', $css_content);
        }

        // Sauvegarder la meta description si présente
        if (!empty($meta_description)) {
            update_post_meta($post_id, 'aibui_meta_description', $meta_description);
        }

        // Récupérer l'URL de la page créée
        $page_url = get_permalink($post_id);
        

        wp_send_json_success(array(
            'page_id' => $post_id,
            'page_url' => $page_url,
            'page_title' => $title
        ));
    }

    // -----------------------------
    // Multi-Page: Generations store (using JSON files)
    // -----------------------------
    private function get_storage()
    {
        static $storage = null;
        if ($storage === null) {
            require_once plugin_dir_path(__FILE__) . 'class-generations-storage.php';
            $storage = new AIBUI_Generations_Storage();
        }
        return $storage;
    }

    // Save a generation item (status: Pending review)
    public function save_generation()
    {
        if (!isset($_POST['nonce'])) {
            wp_send_json_error('Missing required data');
        }
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
        }

        $payload_raw = isset($_POST['payload']) ? wp_unslash($_POST['payload']) : '';
        $payload = json_decode($payload_raw, true);
        if (!$payload || !is_array($payload)) {
            wp_send_json_error('Invalid payload');
        }

        $storage = $this->get_storage();
        $result = $storage->save($payload);

        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
        }

        wp_send_json_success($result);
    }

    // List generations
    public function get_generations()
    {
        if (!isset($_POST['nonce'])) {
            wp_send_json_error('Missing required data');
        }
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
        }

        $storage = $this->get_storage();
        $items = $storage->get_all();

        wp_send_json_success($items);
    }

    // Get one generation by id
    public function get_generation()
    {
        if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
            wp_send_json_error('Missing required data');
        }
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
        }
        $id = sanitize_text_field(wp_unslash($_POST['id']));

        $storage = $this->get_storage();
        $result = $storage->get($id);

        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
        }

        wp_send_json_success($result);
    }

    // Mark generation as applied (optionally attach pageId and change status)
    public function mark_generation_applied()
    {
        if (!isset($_POST['nonce']) || !isset($_POST['id'])) {
            wp_send_json_error('Missing required data');
        }
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
        }
        $id = sanitize_text_field(wp_unslash($_POST['id']));
        $page_id = isset($_POST['page_id']) ? intval($_POST['page_id']) : 0;

        $storage = $this->get_storage();
        $result = $storage->mark_applied($id, $page_id);

        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
        }

        wp_send_json_success($result);
    }

    public function save_page_prompt()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id']) || !isset($_POST['page_prompt'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $post_id = intval($_POST['post_id']);
        $page_prompt = sanitize_textarea_field(wp_unslash($_POST['page_prompt']));

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Vérifier que l'utilisateur peut modifier ce post
        if (!current_user_can('edit_post', $post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        // Sauvegarder le prompt de page
        update_post_meta($post_id, 'ai_builder_page_prompt', $page_prompt);

        wp_send_json_success('Page prompt saved successfully');
    }

    public function get_page_prompt()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $post_id = intval($_POST['post_id']);

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Vérifier que l'utilisateur peut lire ce post
        if (!current_user_can('read_post', $post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        // Récupérer le prompt de page
        $page_prompt = get_post_meta($post_id, 'ai_builder_page_prompt', true);

        wp_send_json_success(array(
            'pagePrompt' => $page_prompt ?: ''
        ));
    }

    /**
     * Marquer une page comme créée via IA
     */
    public function mark_ai_created()
    {
        // Vérifier que les données POST existent
        if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
            wp_send_json_error('Missing required data');
        }

        // Déséchapper et assainir les données
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $post_id = intval($_POST['post_id']);

        // Vérifier le nonce
        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        // Vérifier que l'utilisateur peut éditer ce post
        if (!current_user_can('edit_post', $post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        // Marquer la page comme créée via IA
        update_post_meta($post_id, '_aibui_created_by_ai', '1');

        wp_send_json_success('Page marked as AI-created');
    }

    /**
     * Get whether a post was created via AI Builder.
     */
    public function get_ai_created_status()
    {
        if (!isset($_POST['nonce']) || !isset($_POST['post_id'])) {
            wp_send_json_error('Missing required data');
        }

        $nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
        $post_id = intval($_POST['post_id']);

        if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
            wp_die('Security check failed');
        }

        if (!current_user_can('read_post', $post_id)) {
            wp_send_json_error('Insufficient permissions');
        }

        $flag = get_post_meta($post_id, '_aibui_created_by_ai', true);
        wp_send_json_success(array(
            'isAICreated' => ($flag === '1' || $flag === 1 || $flag === true),
        ));
    }
}
```
