#!/usr/bin/env python3
"""Met à jour l'article reCAPTCHA PrestaShop (slug historique) pour PS 8 / 9."""

from __future__ import annotations

import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))

from article_studio.config import load_config  # noqa: E402
from article_studio import wp_client  # noqa: E402

POST_ID = 4158
POST_17_ID = 5003
SLUG = "ajouter-recaptcha-prestashop-tuto-gratuit"
CATEGORY_IDS = [2, 44]  # PrestaShop, E-commerce
TAG_NAMES = [
    "captcha",
    "recaptcha",
    "module prestashop",
    "tuto",
    "tutorial prestashop",
    "tutoriel prestashop",
    "prestashop 9",
    "prestashop 8",
    "spam",
    "sécurité prestashop",
]

TITLE = "Ajouter reCAPTCHA à PrestaShop 8 et 9 : tuto gratuit (sans module payant)"
YOAST_TITLE = "reCAPTCHA PrestaShop 8 et 9 : tuto gratuit 2026"
YOAST_DESC = (
    "Bloque le spam sur PrestaShop 8 et 9 sans module payant : contact, inscription, "
    "newsletter. Clés Google 2026, EiCaptcha, tests, honeypot et FAQ."
)
FOCUS = "recaptcha prestashop"
EXCERPT = (
    "200 faux comptes en 48 h, zéro commande : un bot sur l'inscription. Tuto 2026 "
    "pour ajouter reCAPTCHA à PrestaShop 8 et 9, sans module payant ; plus honeypot "
    "et emails jetables."
)

BASE = "https://www.arnaud-merigeau.fr"
CONTACT = f"{BASE}/contact/"
FREELANCE = f"{BASE}/freelance-prestashop/"
PS9 = f"{BASE}/prestashop-9/"
SKIMMER = f"{BASE}/skimmer-prestashop-2026-guide-detection/"
SECURITE = f"{BASE}/politique-de-securite-themes-modules-prestashop/"
MODULES = f"{BASE}/guide-modules-prestashop-indispensables-2026-ps8-ps9/"
MIGRATION = f"{BASE}/migration-prestashop-1-7-vers-9-sans-perdre-seo/"
ARTICLE_17 = f"{BASE}/prestashop-1-7-ajouter-recaptcha-v2-pour-eviter-le-spam/"
SPAM_ACCOUNTS = f"{BASE}/comment-proteger-prestashop-des-comptes-clients-spams/"
TICKET = f"{BASE}/produit/ticket-intervention-prestashop-wordpress/"

EICAPTCHA = "https://github.com/nenes25/eicaptcha/releases"
GOOGLE_ADMIN = "https://www.google.com/recaptcha/admin"
TEC = "https://github.com/ArteInfoRM/tec_spamguard"
HERO_FREE = "https://prestahero.com/182-free-prestashop-captcha-module.html"

IMG_HERO_LOCAL = ROOT / "assets" / "recaptcha-prestashop-2026-featured.png"
IMG_STEPS_LOCAL = ROOT / "assets" / "recaptcha-prestashop-2026-etapes.png"
IMG_COMPAT_LOCAL = ROOT / "assets" / "recaptcha-prestashop-2026-compatibilite.png"

INDEXNOW_KEY = "5f8e4b8b35b143b988c607967725f4b9"
HOST = "www.arnaud-merigeau.fr"

NOTICE_MARK = "<!-- recaptcha-2026-notice -->"

def strip_emdashes(text: str) -> str:
    """Tirets cadratins = signature IA. Titres d'étape : deux-points ; phrases : point-virgule."""
    for i in (1, 2, 3, 4):
        text = text.replace(f"Étape {i} — ", f"Étape {i} : ")
        text = text.replace(f"Étape {i} - ", f"Étape {i} : ")
        text = text.replace(f"Étape {i} – ", f"Étape {i} : ")
    text = text.replace(" — ", " ; ")
    return text.replace("—", "-")


CSS_CLASSIC = """/* Style reCAPTCHA - thème Classic (PS 1.7 / 8, parfois 9) */
.contact-form form .form-fields #captcha-box > div {
  float: right;
}
.contact-form form .form-footer {
  clear: both;
  padding-top: 1rem;
}
@media screen and (max-width: 767px) {
  #contact #left-column,
  #contact #content-wrapper {
    padding-left: 0 !important;
    padding-right: 0 !important;
    width: 100%;
  }
  .contact-form form .form-fields #captcha-box {
    position: relative;
    min-height: 78px;
  }
  .contact-form form .form-fields #captcha-box > div {
    float: none;
    position: absolute;
    left: 50%;
    top: 0;
    transform: translate(-50%, 0);
  }
}
@media screen and (max-width: 379px) {
  .register-form form .form-group .g-recaptcha {
    position: relative;
    min-height: 78px;
  }
  .register-form form .form-group .g-recaptcha > div {
    float: none;
    position: absolute;
    left: 50%;
    top: 0;
    transform: translate(-50%, 0);
  }
}
"""


def build_content(img_hero: str, img_steps: str, img_compat: str) -> str:
    css = CSS_CLASSIC
    html = f"""
<p>Lundi matin, alerte d'un client : <em>« On a reçu 200 mails de bienvenue en une nuit. »</em></p>
<p>Je me connecte. <strong>200 comptes clients créés en 48 h</strong>. Emails jetables. Zéro commande. Un bot sur le formulaire d'inscription : pas de CAPTCHA, pas de rate limiting.</p>
<p>Nettoyage en 2 h. Puis reCAPTCHA v2 sur inscription + contact, honeypot, validation email renforcée. Depuis : <strong>zéro vague</strong>.</p>
<p>Ce n'est pas spectaculaire comme un <a href="{SKIMMER}">skimmer sur le tunnel de paiement</a>. Mais ça pollue ta base CRM, fait exploser tes quotas mail, et peut dégrader ta réputation d'expéditeur.</p>
<p>Dans ce tuto 2026, tu obtiens :</p>
<ul>
<li>Comment <strong>ajouter reCAPTCHA à PrestaShop 8 et 9</strong> sans module payant</li>
<li>Quelle version choisir : <strong>v2, v3, Enterprise, Turnstile</strong></li>
<li>Le bon module selon ta version (EiCaptcha 2.6 sur PS 8, module déclaré 9.x sur PS 9)</li>
<li>Les tests qui prouvent que ça bloque vraiment (incognito + mobile)</li>
<li>Ce que le captcha <strong>ne suffit plus à faire</strong> : honeypot, emails jetables, nettoyage de vague</li>
</ul>
<p>Spoiler : on ne touche plus à <code>contact-form.tpl</code> comme en PrestaShop 1.6. Sur <a href="{PS9}">PrestaShop 9</a> (Hummingbird, Symfony), un module à jour + les bons hooks, c'est plus propre — et ça survit aux mises à jour. Besoin que je le pose sur ta boutique ? <a href="{CONTACT}">Écris-moi</a>.</p>

<!--more-->

<h2>Sommaire</h2>
<ol>
<li><a href="#pourquoi-spam-prestashop">Pourquoi tes formulaires PrestaShop se font spammer</a></li>
<li><a href="#recaptcha-v2-v3-2026">reCAPTCHA v2, v3, Enterprise : laquelle choisir en 2026</a></li>
<li><a href="#compatibilite-ps9">PrestaShop 1.7, 8 et 9 : ce qui change vraiment</a></li>
<li><a href="#etape-cles-google">Étape 1 — Créer tes clés Google reCAPTCHA</a></li>
<li><a href="#etape-choisir-module">Étape 2 — Choisir le module gratuit selon ta version</a></li>
<li><a href="#etape-installer">Étape 3 — Installer et configurer le module</a></li>
<li><a href="#etape-tester">Étape 4 — Tester (desktop, mobile, cache)</a></li>
<li><a href="#css-classic-hummingbird">CSS : afficher proprement la case (Classic / Hummingbird)</a></li>
<li><a href="#honeypot-emails-jetables">Honeypot, emails jetables, rate limiting : la couche 2</a></li>
<li><a href="#nettoyer-faux-comptes">Tu as déjà 200 faux comptes : comment nettoyer</a></li>
<li><a href="#depannage-recaptcha">Ça ne marche pas : checklist de dépannage</a></li>
<li><a href="#faq-recaptcha-prestashop">FAQ : reCAPTCHA PrestaShop 8 et 9</a></li>
<li><a href="#conclusion-recaptcha">Conclusion</a></li>
</ol>

<figure class="wp-block-image size-large"><img src="{img_hero}" alt="Tuto gratuit reCAPTCHA PrestaShop 8 et 9 : contact, inscription, newsletter sans module payant" width="1200" height="675" loading="lazy" /><figcaption>Mise à jour 2026 : le même objectif qu'en 1.6 / 1.7, une méthode compatible PrestaShop 8, 9 et Hummingbird.</figcaption></figure>

<h2 id="pourquoi-spam-prestashop">1. Pourquoi tes formulaires PrestaShop se font spammer</h2>
<p>Dès qu'un formulaire est public, il est une API ouverte. Les bots n'ont pas besoin de « casser » PrestaShop : ils POST-ent sur <code>/contact</code>, <code>/connexion</code> (création de compte) et parfois la newsletter. Sans challenge, ça passe.</p>
<p>Les cibles les plus fréquentes :</p>
<ul>
<li><strong>Formulaire de contact</strong> — liens malveillants, « partenariats », SEO spam</li>
<li><strong>Création de compte client</strong> — emails jetables, mails de bienvenue en rafale, base CRM pourrie</li>
<li><strong>Newsletter</strong> — opt-in fictifs qui plombent tes taux d'ouverture</li>
<li><strong>Mot de passe oublié</strong> — moins visible, mais utile aux attaques par énumération</li>
</ul>
<p>Le coût n'est pas « 2 minutes à trier ». C'est : quotas transactionnels (Brevo, Mailjet, SMTP), réputation d'expéditeur, clients légitimes qui n'ouvrent plus tes mails de commande, et un back-office clients illisible.</p>
<p>Si tu es encore sur une 1.7 figée, le fond du problème est le même — l'ancien tuto <a href="{ARTICLE_17}">reCAPTCHA v2 pour PrestaShop 1.7</a> reste une archive. La méthode 2026 ci-dessous la remplace pour PS 8 / 9.</p>

<h2 id="recaptcha-v2-v3-2026">2. reCAPTCHA v2, v3, Enterprise : laquelle choisir en 2026</h2>
<p>Un captcha, c'est un test « humain vs robot ». <strong>reCAPTCHA</strong>, c'est la version Google. En 2026, tu croises quatre familles — et beaucoup de tutos mélangent encore les clés.</p>
<table>
<thead>
<tr><th>Type</th><th>Ce que voit le client</th><th>Idéal pour</th><th>Piège</th></tr>
</thead>
<tbody>
<tr><td><strong>v2 « case à cocher »</strong></td><td>« Je ne suis pas un robot », parfois des images</td><td>Contact, inscription</td><td>Friction légère, mais debug simple</td></tr>
<tr><td><strong>v2 invisible</strong></td><td>Rien, challenge seulement si doute</td><td>Parcours où tu veux peu de friction</td><td>Plus dur à diagnostiquer</td></tr>
<tr><td><strong>v3 (score)</strong></td><td>Rien</td><td>Monitoring + seuil</td><td>Seuil mal réglé = faux positifs ou spam qui passe</td></tr>
<tr><td><strong>Turnstile (Cloudflare)</strong></td><td>Widget discret</td><td>Alternative plus « RGPD-friendly »</td><td>Le module doit le supporter</td></tr>
</tbody>
</table>
<p><strong>Mon avis terrain :</strong> pour une boutique PME, pars sur <strong>reCAPTCHA v2 case à cocher</strong> sur contact + inscription. C'est visible, ça rassure, et tu vois tout de suite si ça casse. La v3, je la réserve quand le module gère un seuil (EiCaptcha 2.6 le fait) et que tu as le temps de l'ajuster.</p>
<p>Côté Google : les clés se gèrent de plus en plus dans <strong>Google Cloud</strong> (Security → reCAPTCHA). La console historique <a href="{GOOGLE_ADMIN}" rel="noopener noreferrer">google.com/recaptcha/admin</a> reste le chemin le plus simple pour créer une clé « Challenge / checkbox » ou « Score-based ». La plupart des modules PrestaShop attendent encore une <strong>clé du site + clé secrète</strong> (mode classique). Après migration Cloud, ces clés continuent en général de fonctionner avec le même module — à condition de ne pas mélanger une clé v3 avec un module réglé en v2.</p>
<p>Quota : Google documente un <strong>socle gratuit d'assessments</strong> (ordre de grandeur : 10 000 / mois sur l'offre Cloud). Pour un contact + des inscriptions, une PME y reste. Un gros trafic catalogue + login partout, c'est un autre sujet (Turnstile, ou facturation Cloud).</p>

<h2 id="compatibilite-ps9">3. PrestaShop 1.7, 8 et 9 : ce qui change vraiment</h2>
<p>Le spam n'a pas changé. L'<strong>architecture</strong> si :</p>
<ul>
<li><strong>PS 1.6</strong> : <code>contact-form.tpl</code> à la racine du thème. Les copier-coller de 2016 cassent dès que tu changes de thème.</li>
<li><strong>PS 1.7 / 8 + thème Classic</strong> : module natif <code>contactform</code>, templates dans le thème (<code>templates/customer/</code>), hooks. Un module comme <strong>EiCaptcha</strong> s'appuie là-dessus.</li>
<li><strong>PS 9 + Hummingbird</strong> : nouveau thème par défaut, formulaires plus Symfony. Un module qui ne déclare que 1.7/8 peut installer… et ne rien afficher, ou casser l'inscription au checkout.</li>
</ul>
<figure class="wp-block-image size-large"><img src="{img_compat}" alt="Tableau de compatibilité reCAPTCHA PrestaShop 1.7, 8 et 9 : EiCaptcha 2.6 versus module déclaré 9.x" width="1200" height="680" loading="lazy" /><figcaption>Ne force pas un ZIP 1.7 sur une PrestaShop 9 « pour voir ». Vérifie la ligne de compatibilité avant d'uploader.</figcaption></figure>
<p>EiCaptcha (<a href="{EICAPTCHA}" rel="noopener noreferrer">nenes25/eicaptcha</a>) est le module open source que je recommande depuis des années sur 1.7 / 8 — j'avais même poussé un correctif CSS dès la 2.0.1. La branche <strong>2.6.0</strong> (fin 2025) cible 1.7 et 8, avec score v3 configurable. La ligne <strong>3.x</strong> (PrestaShop 9 only, PHP 8.1+) était en cours côté GitHub en 2026 : à vérifier sur la page Releases au moment où tu installes.</p>
<p>Sur une <strong>PrestaShop 9 en production</strong> aujourd'hui, je prends un module qui <em>déclare</em> 9.x : par exemple <a href="{TEC}" rel="noopener noreferrer">Tec Spam Guard</a> (MIT, reCAPTCHA + Turnstile + emails jetables) ou le <a href="{HERO_FREE}" rel="noopener noreferrer">reCAPTCHA gratuit PrestaHero</a> (contact / inscription / login). Pas de religion : la fiche « compatible 9.x » + un test staging battent n'importe quel nom connu.</p>

<h2 id="etape-cles-google">4. Étape 1 — Créer tes clés Google reCAPTCHA</h2>
<ol>
<li>Ouvre <a href="{GOOGLE_ADMIN}" rel="noopener noreferrer">la console reCAPTCHA</a> (compte Google de la boîte, pas ton Gmail perso si tu peux).</li>
<li>Crée un site / une clé. <strong>Libellé</strong> : le nom de la boutique (ex. « Bohomane prod »).</li>
<li><strong>Type</strong> : Challenge / case à cocher (v2) si tu suis ce tuto « simple ». Score-based (v3) seulement si ton module le demande clairement.</li>
<li><strong>Domaines</strong> : <code>votreboutique.fr</code> <em>et</em> <code>www.votreboutique.fr</code>. Ajoute le domaine de préprod si tu testes là-bas.</li>
<li>Valide les conditions. Tu obtiens une <strong>clé du site</strong> (publique, front) et une <strong>clé secrète</strong> (serveur). La secrète = mot de passe : pas dans un ticket Trello public.</li>
</ol>
<p>Si Google te pousse vers Cloud Console : Security → reCAPTCHA → Create key. Même logique. Si ton module PrestaShop a un mode « Enterprise » (Key ID + Project ID + API key), c'est un autre formulaire — ne colle pas une clé Enterprise dans un champ « secret key » classique.</p>

<h2 id="etape-choisir-module">5. Étape 2 — Choisir le module gratuit selon ta version</h2>
<table>
<thead>
<tr><th>Version PrestaShop</th><th>Module gratuit</th><th>Formulaires</th><th>À retenir</th></tr>
</thead>
<tbody>
<tr><td>1.7 / 8</td><td><a href="{EICAPTCHA}" rel="noopener noreferrer">EiCaptcha 2.6.x</a></td><td>Contact, compte, newsletter</td><td>Télécharge le ZIP des <em>Releases</em>, pas « Code → Download ZIP » (vendor Composer manquant)</td></tr>
<tr><td>9</td><td>Module déclaré 9.x (Tec Spam Guard, PrestaHero free, EiCaptcha 3.x s'il est sorti)</td><td>Contact, compte, login, parfois checkout</td><td>Teste sur staging. Hummingbird n'est pas Classic.</td></tr>
<tr><td>1.6</td><td>EiCaptcha 0.4.x (legacy)</td><td>Contact</td><td>Le vrai sujet, c'est la <a href="{MIGRATION}">migration 1.7 → 8/9</a></td></tr>
</tbody>
</table>
<p>Prérequis fréquents : module natif <strong>contactform</strong> activé. Pour la newsletter EiCaptcha : <strong>ps_emailsubscription</strong> ≥ 2.6.0.</p>
<figure class="wp-block-image size-large"><img src="{img_steps}" alt="Trois étapes pour ajouter reCAPTCHA à PrestaShop 8 et 9 : clés Google, module gratuit, tests réels" width="1200" height="680" loading="lazy" /><figcaption>L'ordre compte : clés d'abord, module ensuite, tests en navigation privée en dernier — pas l'inverse.</figcaption></figure>

<h2 id="etape-installer">6. Étape 3 — Installer et configurer le module</h2>
<p>Sauvegarde (fichiers + BDD) avant d'uploader un ZIP. Cinq minutes, pas un optionnel.</p>
<ol>
<li>Back-office → <strong>Modules</strong> → <strong>Module Manager</strong> → <em>Upload a module</em> / « Ajouter un module ».</li>
<li>Envoie le ZIP. Installe. Si PrestaShop refuse la version : tu n'as pas le bon ZIP (PS 9 vs 2.6, ou l'inverse).</li>
<li>Configure : colle <strong>clé du site</strong> + <strong>clé secrète</strong>.</li>
<li>Active les formulaires : <strong>contact</strong> et <strong>création de compte</strong> a minima. Login et newsletter selon ta surface d'attaque.</li>
<li>Si le module propose v2 / v3 : aligne-le sur le type de clé Google. Un mismatch = widget invisible ou validation qui échoue toujours.</li>
<li>Enregistre. Vide le cache PrestaShop (Performances). Sur PS 9, vide aussi le cache Symfony si tu as la main SSH : <code>php bin/console cache:clear</code>.</li>
</ol>
<p>Tu n'as <strong>pas</strong> à coller <code>&lt;div class="g-recaptcha"&gt;</code> dans le thème si le module est compatible. C'était le bricolage 1.6. Sur 8 / 9, c'est un anti-pattern : tu perds la validation serveur au prochain update de thème.</p>

<h2 id="etape-tester">7. Étape 4 — Tester (desktop, mobile, cache)</h2>
<p>Un widget qui s'affiche ne prouve rien. La preuve, c'est le refus serveur.</p>
<ol>
<li>Déconnecte-toi du back-office. Fenêtre privée. Idéalement le téléphone en 4G (comme pour un <a href="{SKIMMER}">test anti-skimmer</a> : l'admin n'est pas le client).</li>
<li><strong>Contact</strong> : envoie un message <em>sans</em> cocher. Tu dois être bloqué. Puis avec la case : le mail part.</li>
<li><strong>Création de compte</strong> : même protocole. Vérifie qu'un compte n'est pas créé en base si le captcha est vide.</li>
<li><strong>Mobile</strong> : la case v2 fait 304×78 px. Sur Classic, elle passe parfois sous le bouton — d'où le CSS plus bas.</li>
<li>Newsletter / checkout (si activés) : un bot moderne s'inscrit souvent au tunnel, pas seulement sur <code>/connexion</code>.</li>
</ol>
<p>Si « ça marche chez moi en admin » mais pas en privé : cache, CDN (Cloudflare Rocket Loader qui casse le JS Google), ou module de perf qui concatène les scripts.</p>

<h2 id="css-classic-hummingbird">8. CSS : afficher proprement la case (Classic / Hummingbird)</h2>
<p>Sur <strong>Classic</strong>, le widget se cale mal à droite / trop large en mobile. Colle ceci à la fin de <code>themes/ton-theme/assets/css/custom.css</code> (enfant, pas le parent que tu écrases à chaque update) :</p>
<pre><code>{css}</code></pre>
<p>Sur <strong>Hummingbird</strong>, ne copie pas ce CSS les yeux fermés : les classes ne sont pas les mêmes. Inspecte le DOM (<code>#captcha-box</code>, <code>.g-recaptcha</code>) et adapte. Un padding / <code>min-height: 78px</code> suffit souvent pour que le bouton d'envoi ne recouvre pas la case.</p>

<h2 id="honeypot-emails-jetables">9. Honeypot, emails jetables, rate limiting : la couche 2</h2>
<p>En 2026, les fermes à captcha existent. reCAPTCHA <strong>réduit</strong> le bruit, il ne le supprime pas. Sur le client des 200 comptes, le combo qui a tenu :</p>
<ul>
<li><strong>reCAPTCHA v2</strong> sur inscription + contact</li>
<li><strong>Honeypot</strong> : champ caché (CSS <code>left: -9999px</code> ou <code>display:none</code>), nom type <code>url</code> / <code>website</code>. Un humain le laisse vide. Un bot le remplit. Rejet côté serveur (hook <code>actionSubmitAccountBefore</code> / contact), pas seulement en JS.</li>
<li><strong>Emails jetables</strong> : yopmail, guerrillamail, tempmail, etc. Un module comme Tec Spam Guard le fait. Sinon une liste de domaines + rejet à l'inscription.</li>
<li><strong>Rate limiting</strong> : Cloudflare, Fail2ban, ou au minimum un WAF. 200 POST / heure sur <code>création de compte</code> depuis la même classe d'IP, ce n'est pas un client indécis.</li>
</ul>
<p>Pour une première barrière « nom + URL dans le prénom », j'avais documenté des overrides <code>Validate.php</code> / <code>Customer.php</code> dans <a href="{SPAM_ACCOUNTS}">Comment protéger PrestaShop des comptes clients spams</a>. Sur PS 8 / 9, je ne recopie plus ces overrides tels quels : le <code>$definition</code> a bougé. Captcha + emails jetables + honeypot, c'est plus maintenable qu'un override core figé en 2019.</p>

<h2 id="nettoyer-faux-comptes">10. Tu as déjà 200 faux comptes : comment nettoyer</h2>
<ol>
<li>Back-office → Clients. Filtre par <strong>date d'inscription</strong> (les 48 h de la vague).</li>
<li>Croise : <strong>0 commande</strong>, domaines jetables, prénoms identiques, même IP.</li>
<li>Exporte le filtre (CSV) avant de supprimer — pour le support si un vrai client était dans le lot.</li>
<li>Supprime. Ne vide pas toute la table clients.</li>
<li>Vérifie la file mail (module mail, logs SMTP) : les 200 « bienvenue » ont peut-être déjà brûlé un quota.</li>
<li><strong>Ensuite seulement</strong> pose le captcha. Nettoyer sans fermer la porte, c'est recommencer mardi.</li>
</ol>

<h2 id="depannage-recaptcha">11. Ça ne marche pas : checklist de dépannage</h2>
<table>
<thead>
<tr><th>Symptôme</th><th>Cause fréquente</th><th>Quoi faire</th></tr>
</thead>
<tbody>
<tr><td>Pas de case</td><td>Cache, mauvais thème, clé v3 dans un module v2, domaine absent</td><td>Vider cache + CDN. Vérifier le type de clé et www / non-www</td></tr>
<tr><td>Toujours refusé</td><td>Clé secrète inversée, firewall sortant vers Google</td><td>Recoller les clés. Tester <code>siteverify</code> depuis le serveur</td></tr>
<tr><td>OK en admin, KO en privé</td><td>Cache page, Cloudflare, minification JS</td><td>Désactiver Rocket Loader / combine JS sur les pages compte et contact</td></tr>
<tr><td>PS 9 : installé, rien</td><td>Module 1.7/8 sur Hummingbird</td><td>Module déclaré 9.x, test staging</td></tr>
<tr><td>Newsletter passe encore</td><td>Hook newsletter non géré</td><td>Activer l'option, vérifier ps_emailsubscription</td></tr>
<tr><td>Checkout guest sans captcha</td><td>L'inscription n'est pas <code>/connexion</code></td><td>Module qui couvre l'identité checkout (surtout en OPC PS 9.2)</td></tr>
</tbody>
</table>
<p>Thème lourd, checkout one-page, formulaire d'un module tiers : le captcha « contactform » ne s'y accroche pas tout seul. C'est du sur-mesure — <a href="{TICKET}">ticket d'intervention</a> ou <a href="{FREELANCE}">accompagnement freelance</a>.</p>

<h2 id="faq-recaptcha-prestashop">12. FAQ : reCAPTCHA PrestaShop 8 et 9</h2>

<h3>reCAPTCHA fonctionne-t-il sur PrestaShop 9 ?</h3>
<p>Oui, à condition d'utiliser un module qui déclare PrestaShop 9 (et de tester sur Hummingbird si c'est ton thème). Ce n'est plus le copier-coller <code>contact-form.tpl</code> de PrestaShop 1.6. EiCaptcha 2.6 est le standard 1.7/8 ; la 3.x vise PS 9 — vérifie les Releases GitHub le jour J.</p>

<h3>Faut-il un module payant pour ajouter reCAPTCHA à PrestaShop ?</h3>
<p>Non. EiCaptcha (GitHub) et plusieurs modules gratuits suffisent pour contact + inscription. Un module payant se justifie si tu veux support éditeur, Turnstile, blacklist, checkout et emails jetables dans la même interface.</p>

<h3>reCAPTCHA v2 ou v3 : lequel choisir sur une boutique ?</h3>
<p>v2 case à cocher pour démarrer : visible, débogable, efficace contre le brute-force d'inscription. v3 si tu veux zéro friction et que tu acceptes de régler un seuil (et de surveiller les faux rejets clients).</p>

<h3>Pourquoi la case reCAPTCHA ne s'affiche pas sur PrestaShop ?</h3>
<p>Dans l'ordre : cache (Smarty + Symfony + CDN), domaine Google incomplet (www), type de clé (v2 vs v3), JS Google bloqué par un module de perf, thème enfant qui n'appelle pas le hook. Teste en navigation privée après avoir vidé tous les caches.</p>

<h3>reCAPTCHA suffit-il contre les bots en 2026 ?</h3>
<p>Non, plus comme seule barrière. Les fermes à captcha existent. Combine captcha + honeypot + rejet des emails jetables + rate limiting. C'est ce combo qui a arrêté la vague des 200 comptes, pas la case toute seule.</p>

<h3>Cloudflare Turnstile est-il mieux que Google reCAPTCHA ?</h3>
<p>Souvent plus agréable (vie privée, UX). Il faut un module PrestaShop qui le parle (Tec Spam Guard le fait). Si ton module ne gère que Google, reste sur reCAPTCHA v2 — un captcha qui marche bat une « meilleure » techno non branchée.</p>

<h3>Comment supprimer 200 faux comptes clients PrestaShop ?</h3>
<p>Filtre par date + 0 commande + domaines jetables, export CSV, puis suppression. Pose le captcha tout de suite après, sinon la vague revient. Ne vide pas toute la table clients.</p>

<h3>reCAPTCHA ralentit-il le site ou le SEO ?</h3>
<p>Le script Google s'ajoute sur les pages où le module le charge (contact, compte), pas sur tout le catalogue si tu configures correctement. Ce n'est pas un sujet Core Web Vitals de homepage. Évite « charger le script partout » sauf besoin réel.</p>

<h3>Faut-il un captcha sur le tunnel de commande PrestaShop 9 ?</h3>
<p>Si tu autorises la création de compte / l'invité au checkout, oui : les bots ne passent pas que par <code>/connexion</code>. Surtout avec un One Page Checkout. Vérifie que ton module couvre cette étape, pas seulement la page compte isolée.</p>

<h3>Les clés Google reCAPTCHA sont-elles encore gratuites en 2026 ?</h3>
<p>Oui pour un usage boutique classique, via le socle gratuit Cloud documenté par Google. Au-delà, facturation possible. Si tu exploses le quota, Turnstile ou un WAF devient plus intéressant qu'une case à 50 000 assessments.</p>

<h3>Hummingbird change-t-il l'installation du captcha ?</h3>
<p>Oui : autres templates, autres classes CSS. Un module pensé Classic peut s'installer sans rien afficher. Choisis un module testé PS 9 / Hummingbird, et n'applique le CSS Classic de ce tuto que si tes classes DOM matchent.</p>

<h3>Où trouver un tuto reCAPTCHA PrestaShop 1.7 ?</h3>
<p>L'ancienne version est ici : <a href="{ARTICLE_17}">Ajouter reCAPTCHA v2 pour PrestaShop 1.7</a>. Pour une boutique encore en 8 ou déjà en 9, reste sur <strong>cette page</strong> — c'est le guide à jour.</p>

<h2 id="conclusion-recaptcha">13. Conclusion</h2>
<p>Ajouter reCAPTCHA à PrestaShop, ce n'est plus un exploit FTP de 2016. En 2026, sur <strong>PS 8 et 9</strong>, la recette est : <strong>bonnes clés Google</strong> (v2 pour démarrer), <strong>module gratuit compatible avec ta version</strong>, <strong>tests en navigation privée</strong>, puis <strong>honeypot + emails jetables</strong> parce que la case seule ne tient plus face à une ferme de bots.</p>
<p>Si tes formulaires d'inscription et de contact ne sont pas protégés, tu n'as pas un « petit spam » : tu as une porte ouverte sur ta CRM et tes quotas mail. Ferme-la ce week-end.</p>
<p>Tu veux que je pose reCAPTCHA, honeypot et le nettoyage de vague sur ta boutique — sans module payant inutile, avec un test staging ? <a href="{CONTACT}"><strong>Discutons-en →</strong></a></p>
<p>Pour aller plus loin : <a href="{SECURITE}">politique de sécurité thèmes et modules</a>, <a href="{MODULES}">guide modules PrestaShop 2026</a>, et le <a href="{SKIMMER}">protocole skimmer 2026</a> si tu n'as pas audité ton tunnel de paiement ce mois-ci.</p>
""".strip()
    return strip_emdashes(html)


SOCIAL_LINKEDIN = """Lundi matin, alerte d'un client :

« On a reçu 200 mails de bienvenue en une nuit. »

Je me connecte : 200 comptes clients créés en 48 h, emails jetables, zéro commande. Bot sur le formulaire d'inscription ; pas de CAPTCHA, pas de rate limiting.

Nettoyage en 2 h.

Puis reCAPTCHA v2 sur inscription + contact, honeypot, validation email renforcée. Depuis : zéro vague.

Ce n'est pas spectaculaire comme un skimmer. Mais ça pollue votre base CRM, fait exploser vos quotas mail, et peut dégrader votre réputation d'expéditeur.

J'ai mis à jour le tuto pas-à-pas (gratuit) : PrestaShop 8 et 9, Hummingbird, clés Google 2026, sans module payant.

{url}

Votre formulaire d'inscription est protégé ?

👋 Arnaud Mérigeau · freelance PrestaShop certifié Experts (+15 ans)
→ Me contacter : https://www.arnaud-merigeau.fr/contact
#PrestaShop #Sécurité #Spam"""

SOCIAL_X = """200 comptes clients bots en 48 h. Zéro CAPTCHA sur l'inscription.

Nettoyage + reCAPTCHA v2 + honeypot → silence radio depuis.

Tuto gratuit PrestaShop 8 et 9 👇
{url}

👋 Freelance certifié Experts · arnaud-merigeau.fr/contact"""

SOCIAL_FACEBOOK = """200 comptes clients créés par des bots en un week-end.

Pas de commande. Emails jetables. Quotas mail explosés.

Fix : reCAPTCHA + honeypot. Tuto gratuit PrestaShop 8 et 9 (sans module payant) :

👉 {url}

👋 Arnaud Mérigeau · freelance PrestaShop certifié Experts (+15 ans)
→ Me contacter : https://www.arnaud-merigeau.fr/contact"""

SOCIAL_INSTAGRAM = """200 comptes bots en 48 h. Zéro CAPTCHA.

reCAPTCHA + honeypot sur PrestaShop 8 et 9.
Tuto gratuit, sans module payant ; lien en bio.

#prestashop #ecommerce #securite #spam #recaptcha #arnaudmerigeau"""

NOTICE_17 = f"""{NOTICE_MARK}
<div style="background:#fff4eb;border-left:4px solid #e98030;padding:1rem 1.25rem;margin:0 0 1.5rem;">
<p style="margin:0;"><strong>Mise à jour 2026 :</strong> ce tuto PrestaShop 1.7 reste une archive utile. Pour PrestaShop 8 et 9 (Hummingbird, clés Google Cloud, modules gratuits à jour), suis le guide complet : <a href="{BASE}/{SLUG}/">Ajouter reCAPTCHA à PrestaShop 8 et 9 - tuto gratuit</a>.</p>
</div>
"""


def get_or_create_tag(base: str, user: str, password: str, name: str) -> int:
    slug = name.lower().replace(" ", "-")
    lookup = f"{base}/wp-json/wp/v2/tags?slug={urllib.parse.quote(slug)}"
    _, tags = wp_client._request("GET", lookup, user, password)
    if tags:
        return int(tags[0]["id"])
    _, tag = wp_client._request(
        "POST",
        f"{base}/wp-json/wp/v2/tags",
        user,
        password,
        data=json.dumps({"name": name, "slug": slug}).encode("utf-8"),
        headers={"Content-Type": "application/json"},
    )
    return int(tag["id"])


def upload_image(base: str, user: str, password: str, path: Path, *, alt: str, title: str) -> tuple[str, int | None]:
    if not path.is_file():
        raise FileNotFoundError(f"Image introuvable : {path}")
    print(f"Upload : {path.name}")
    media = wp_client.upload_media(base, user, password, path, alt_text=alt, title=title)
    url = media.get("source_url") or media.get("guid", {}).get("rendered", "")
    media_id = media.get("id")
    print(f"  media id={media_id} — {url}")
    return url, media_id


def ping_indexnow(urls: list[str]) -> None:
    payload = {
        "host": HOST,
        "key": INDEXNOW_KEY,
        "keyLocation": f"https://{HOST}/{INDEXNOW_KEY}.txt",
        "urlList": urls,
    }
    req = urllib.request.Request(
        "https://api.indexnow.org/indexnow",
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json; charset=utf-8"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            print(f"IndexNow: HTTP {resp.status}")
    except urllib.error.HTTPError as e:
        print(f"IndexNow: HTTP {e.code}")


def save_social_exports(article_url: str) -> Path:
    out = ROOT / "exports" / "recaptcha-prestashop-2026"
    social = out / "social"
    social.mkdir(parents=True, exist_ok=True)
    kit = ROOT / "www/propositions/kit-social-recaptcha-prestashop-2026"
    kit.mkdir(parents=True, exist_ok=True)
    posts = {
        "linkedin.txt": SOCIAL_LINKEDIN,
        "x.txt": SOCIAL_X,
        "facebook.txt": SOCIAL_FACEBOOK,
        "instagram.txt": SOCIAL_INSTAGRAM,
    }
    for name, text in posts.items():
        rendered = text.format(url=article_url)
        (social / name).write_text(rendered + "\n", encoding="utf-8")
        (kit / name).write_text(rendered + "\n", encoding="utf-8")
    (out / "README.txt").write_text(
        f"Article mis à jour : {article_url}\n\n"
        "Posts prêts à coller :\n"
        "- social/linkedin.txt\n"
        "- social/x.txt\n"
        "- social/facebook.txt\n"
        "- social/instagram.txt\n"
        f"Visuel 1920×1080 : {kit / 'visuels' / 'visuel-recaptcha-prestashop-2026.jpg'}\n",
        encoding="utf-8",
    )
    return out


def update_article_17(base: str, user: str, password: str) -> None:
    _, post = wp_client._request(
        "GET",
        f"{base}/wp-json/wp/v2/posts/{POST_17_ID}?context=edit",
        user,
        password,
    )
    raw = (post.get("content") or {}).get("raw") or ""
    if NOTICE_MARK in raw:
        new_content = re.sub(
            re.escape(NOTICE_MARK) + r".*?</div>\s*",
            NOTICE_17,
            raw,
            count=1,
            flags=re.S,
        )
    else:
        new_content = NOTICE_17 + raw.lstrip()
    if new_content == raw:
        print("Article 1.7 : notice déjà à jour")
        return
    wp_client._request(
        "POST",
        f"{base}/wp-json/wp/v2/posts/{POST_17_ID}",
        user,
        password,
        data=json.dumps({"content": new_content}).encode("utf-8"),
        headers={"Content-Type": "application/json"},
    )
    print(f"Article 1.7 mis à jour (ID {POST_17_ID})")


EXISTING_IMAGES = {
    "hero": "https://www.arnaud-merigeau.fr/wp-content/uploads/2026/09/recaptcha-prestashop-2026-featured.png",
    "steps": "https://www.arnaud-merigeau.fr/wp-content/uploads/2026/09/recaptcha-prestashop-2026-etapes.png",
    "compat": "https://www.arnaud-merigeau.fr/wp-content/uploads/2026/09/recaptcha-prestashop-2026-compatibilite.png",
}
FEATURED_MEDIA_ID = 16128


def main() -> int:
    cfg = load_config()
    base = cfg.wp_base_url.rstrip("/")
    content_only = "--content-only" in sys.argv

    if content_only:
        img_hero = EXISTING_IMAGES["hero"]
        img_steps = EXISTING_IMAGES["steps"]
        img_compat = EXISTING_IMAGES["compat"]
        featured_id = FEATURED_MEDIA_ID
        print("Mise à jour contenu uniquement (images existantes)")
    else:
        img_hero, featured_id = upload_image(
            base,
            cfg.wp_user,
            cfg.wp_application_password,
            IMG_HERO_LOCAL,
            alt="Tuto gratuit reCAPTCHA PrestaShop 8 et 9 : contact, inscription, newsletter sans module payant",
            title="reCAPTCHA PrestaShop 8 et 9 - tuto gratuit 2026",
        )
        img_steps, _ = upload_image(
            base,
            cfg.wp_user,
            cfg.wp_application_password,
            IMG_STEPS_LOCAL,
            alt="Trois étapes pour ajouter reCAPTCHA à PrestaShop 8 et 9 : clés Google, module gratuit, tests réels",
            title="reCAPTCHA PrestaShop - 3 étapes",
        )
        img_compat, _ = upload_image(
            base,
            cfg.wp_user,
            cfg.wp_application_password,
            IMG_COMPAT_LOCAL,
            alt="Tableau de compatibilité reCAPTCHA PrestaShop 1.7, 8 et 9 : EiCaptcha 2.6 versus module déclaré 9.x",
            title="reCAPTCHA PrestaShop - compatibilité versions",
        )

    content = build_content(img_hero, img_steps, img_compat)
    tag_ids = [
        get_or_create_tag(base, cfg.wp_user, cfg.wp_application_password, t)
        for t in TAG_NAMES
    ]

    payload = {
        "title": TITLE,
        "content": content,
        "excerpt": strip_emdashes(EXCERPT),
        "status": "publish",
        "slug": SLUG,
        "author": cfg.wp_author_id,
        "categories": CATEGORY_IDS,
        "tags": tag_ids,
        "meta": {
            "_yoast_wpseo_title": YOAST_TITLE,
            "_yoast_wpseo_metadesc": YOAST_DESC,
            "_yoast_wpseo_focuskw": FOCUS,
            "_yoast_wpseo_linkdex": "78",
        },
    }
    if featured_id:
        payload["featured_media"] = featured_id

    _, post = wp_client._request(
        "POST",
        f"{base}/wp-json/wp/v2/posts/{POST_ID}",
        cfg.wp_user,
        cfg.wp_application_password,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"},
    )

    article_url = post.get("link", f"{base}/{SLUG}/")
    print(f"Publié — ID {post.get('id')} — {article_url}")

    update_article_17(base, cfg.wp_user, cfg.wp_application_password)

    out_dir = save_social_exports(article_url)
    print(f"Posts social : {out_dir / 'social'}/")

    ping_indexnow(
        [
            article_url,
            f"{base}/prestashop-1-7-ajouter-recaptcha-v2-pour-eviter-le-spam/",
        ]
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
