$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