PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / trunk
AI Builder – Generate pages, blocks, images & translate with AI vtrunk
2.8.0 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 All 123 releases
← All changes | includes/class-ajax-handler.php +111 -6 2.7.10 → trunk View file →
@@ -46,8 +46,17 @@
46 46 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
47 47 wp_die('Security check failed');
48 48 }
49 49
50 + // Le nonce prouve l'origine de la requête, pas le droit de l'émettre.
51 + // Ce endpoint n'est appelé que depuis la page Account (capacité
52 + // manage_options) : sans ce contrôle, tout utilisateur connecté capable
53 + // d'obtenir un nonce aibui_nonce — un Contributeur ouvrant l'éditeur,
54 + // par exemple — pourrait substituer le jeton du compte du site.
55 + if (!current_user_can('manage_options')) {
56 + wp_send_json_error('Insufficient permissions', 403);
57 + }
58 +
50 59 if (empty($token)) {
51 60 wp_send_json_error('Token is required');
52 61 }
53 62
@@ -71,8 +80,13 @@
71 80 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
72 81 wp_die('Security check failed');
73 82 }
74 83
84 + // Appelé uniquement depuis la page Account (manage_options).
85 + if (!current_user_can('manage_options')) {
86 + wp_send_json_error('Insufficient permissions', 403);
87 + }
88 +
75 89 // Marquer l'inscription comme réussie
76 90 update_option('aibui_user_successful_signup', true);
77 91
78 92 wp_send_json_success('Signup success flag set');
@@ -92,8 +106,15 @@
92 106 if (!wp_verify_nonce($nonce, 'aibui_nonce')) {
93 107 wp_die('Security check failed');
94 108 }
95 109
110 + // Appelé uniquement depuis la page Account (manage_options) : sans ce
111 + // contrôle, n'importe quel utilisateur connecté pouvait déconnecter le
112 + // compte AI Builder du site.
113 + if (!current_user_can('manage_options')) {
114 + wp_send_json_error('Insufficient permissions', 403);
115 + }
116 +
96 117 // Supprimer le token JWT
97 118 delete_option('aibui_jwt_token');
98 119
99 120 wp_send_json_success('Signed out successfully');
@@ -123,8 +144,16 @@
123 144 403
124 145 );
125 146 }
126 147
148 + // Ce endpoint renvoie le jeton du compte AI Builder au navigateur : il
149 + // doit rester accessible depuis l'éditeur (chat widget, blocs IA), donc
150 + // à partir de edit_posts, mais pas au-delà. Sans ce contrôle, un simple
151 + // abonné pouvait l'exfiltrer et consommer les crédits du site.
152 + if (!current_user_can('edit_posts')) {
153 + wp_send_json_error('Insufficient permissions', 403);
154 + }
155 +
127 156 // Récupérer le token JWT
128 157 $token = get_option('aibui_jwt_token', '');
129 158
130 159 if (empty($token)) {
@@ -580,8 +609,60 @@
580 609 set_transient($key, $wp_error instanceof WP_Error ? $wp_error->get_error_message() : 'Unknown mail error', 120);
581 610
582 611 }
583 612
613 + /**
614 + * Adresses de destination légitimement déclarées par les blocs formulaire
615 + * de contact d'un post donné, plus celle de l'administrateur.
616 + *
617 + * Sert de liste blanche : le formulaire public peut choisir parmi ces
618 + * adresses, jamais en imposer une autre.
619 + *
620 + * @param int $post_id
621 + * @return string[] Adresses en minuscules.
622 + */
623 + private function get_declared_contact_recipients($post_id)
624 + {
625 + $allowed = array();
626 +
627 + $admin_email = sanitize_email(get_option('admin_email'));
628 + if ($admin_email && is_email($admin_email)) {
629 + $allowed[] = strtolower($admin_email);
630 + }
631 +
632 + if ($post_id <= 0) {
633 + return $allowed;
634 + }
635 +
636 + $post = get_post($post_id);
637 + if (!$post || !function_exists('parse_blocks')) {
638 + return $allowed;
639 + }
640 +
641 + // Le bloc peut être imbriqué (colonnes, groupes) : on descend récursivement.
642 + $stack = parse_blocks($post->post_content);
643 + while ($stack) {
644 + $block = array_pop($stack);
645 +
646 + if (isset($block['blockName']) && $block['blockName'] === 'ai-builder/aibui-contact-form') {
647 + $declared = isset($block['attrs']['recipientEmail'])
648 + ? sanitize_email($block['attrs']['recipientEmail'])
649 + : '';
650 + if ($declared && is_email($declared)) {
651 + $allowed[] = strtolower($declared);
652 + }
653 + }
654 +
655 + if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
656 + foreach ($block['innerBlocks'] as $inner) {
657 + $stack[] = $inner;
658 + }
659 + }
660 + }
661 +
662 + return array_values(array_unique($allowed));
663 + }
664 +
584 665 public function submit_contact_form()
585 666 {
586 667 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'aibui_contact_form')) {
587 668 wp_send_json_error('Invalid nonce');
@@ -595,10 +676,26 @@
595 676 wp_send_json_error('Too many requests. Please wait.');
596 677 }
597 678 set_transient($key, time(), 30);
598 679
599 - $recipient = isset($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : '';
600 - if (empty($recipient) || !is_email($recipient)) {
680 + // Le destinataire arrivait d'un champ caché du formulaire et était utilisé
681 + // tel quel : n'importe qui pouvait donc faire expédier un message à
682 + // l'adresse de son choix depuis ce domaine, puisque le nonce est public
683 + // (il est rendu dans le HTML du formulaire). On ne retient désormais
684 + // l'adresse postée que si elle figure parmi celles réellement déclarées
685 + // sur la page qui héberge le formulaire ; sinon on retombe sur
686 + // l'administrateur du site.
687 + $posted_recipient = isset($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : '';
688 + $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
689 +
690 + $recipient = '';
691 + if ($posted_recipient && is_email($posted_recipient)) {
692 + $allowed = $this->get_declared_contact_recipients($post_id);
693 + if (in_array(strtolower($posted_recipient), $allowed, true)) {
694 + $recipient = $posted_recipient;
695 + }
696 + }
697 + if (empty($recipient)) {
601 698 $recipient = sanitize_email(get_option('admin_email'));
602 699 }
603 700 if (empty($recipient) || !is_email($recipient)) {
604 701 wp_send_json_error('No valid recipient configured');
@@ -603,9 +700,13 @@
603 700 if (empty($recipient) || !is_email($recipient)) {
604 701 wp_send_json_error('No valid recipient configured');
605 702 }
606 703
607 - $subject = sprintf('[%s] Nouveau message de contact', get_bloginfo('name'));
704 + $subject = sprintf(
705 + /* translators: %s: site name. */
706 + __('[%s] New contact message', 'ai-builder'),
707 + get_bloginfo('name')
708 + );
608 709
609 710 $fields = [];
610 711 $sender_email = '';
611 712 foreach ($_POST as $key => $value) {
@@ -612,9 +713,9 @@
612 713 if (strpos($key, 'field_') === 0) {
613 714 $label_key = 'label_' . $key;
614 715 $type_key = 'type_' . $key;
615 716 $req_key = 'required_' . $key;
616 - $label = isset($_POST[$label_key]) ? sanitize_text_field(wp_unslash($_POST[$label_key])) : 'Champ';
717 + $label = isset($_POST[$label_key]) ? sanitize_text_field(wp_unslash($_POST[$label_key])) : __('Field', 'ai-builder');
617 718 $type = isset($_POST[$type_key]) ? sanitize_text_field(wp_unslash($_POST[$type_key])) : 'text';
618 719 $is_required = isset($_POST[$req_key]) && wp_unslash($_POST[$req_key]) === '1';
619 720 $raw = wp_unslash($value);
620 721 switch ($type) {
@@ -636,9 +737,13 @@
636 737 default:
637 738 $san = sanitize_text_field($raw);
638 739 }
639 740 if ($is_required && $san === '') {
640 - wp_send_json_error(sprintf('%s est requis', $label ? $label : 'Ce champ'));
741 + wp_send_json_error(sprintf(
742 + /* translators: %s: form field label. */
743 + __('%s is required', 'ai-builder'),
744 + $label ? $label : __('This field', 'ai-builder')
745 + ));
641 746 }
642 747 $fields[] = ['label' => $label, 'type' => $type, 'value' => $san];
643 748 }
644 749 }
@@ -653,9 +758,9 @@
653 758 $val = $f['type'] === 'textarea' ? nl2br(esc_html($f['value'])) : esc_html($f['value']);
654 759 $rows .= '<tr><td style="padding:8px 12px;border:1px solid #e5e7eb;font-weight:600;">' . esc_html($f['label']) . '</td><td style="padding:8px 12px;border:1px solid #e5e7eb;">' . $val . '</td></tr>';
655 760 }
656 761 $message = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;">'
657 - . '<h3 style="margin:0 0 12px;">' . esc_html__('Nouveau message de contact', 'ai-builder') . '</h3>'
762 + . '<h3 style="margin:0 0 12px;">' . esc_html__('New contact message', 'ai-builder') . '</h3>'
658 763 . '<table cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #e5e7eb;width:100%;max-width:720px;">'
659 764 . $rows
660 765 . '</table>'
661 766 . '</div>';