| @@ -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>'; |