diff --git a/PROVIDER.md b/PROVIDER.md
new file mode 100644
index 0000000..047e78f
--- /dev/null
+++ b/PROVIDER.md
@@ -0,0 +1,742 @@
+# Guide de développement de providers pour eicaptcha
+
+Ce document explique comment créer et intégrer un nouveau provider de captcha dans le module eicaptcha.
+
+## Table des matières
+
+- [Architecture](#architecture)
+- [Créer un nouveau provider](#créer-un-nouveau-provider)
+- [Implémenter les méthodes](#implémenter-les-méthodes)
+- [Créer le template](#créer-le-template)
+- [Enregistrer le provider](#enregistrer-le-provider)
+- [Tests et validation](#tests-et-validation)
+- [Exemples complets](#exemples-complets)
+- [Best practices](#best-practices)
+
+## Architecture
+
+Le module eicaptcha v3.0 utilise une architecture modulaire basée sur le pattern Strategy :
+
+```
+src/Provider/
+├── CaptchaProviderInterface.php # Interface à implémenter
+├── AbstractCaptchaProvider.php # Classe de base (optionnelle)
+└── [VotreProvider]Provider.php # Votre implémentation
+```
+
+### Composants principaux
+
+- **CaptchaProviderInterface** : Contrat que tous les providers doivent respecter
+- **AbstractCaptchaProvider** : Classe abstraite avec logique commune (recommandée)
+- **CaptchaFactory** : Factory pour instancier les providers dynamiquement
+
+## Créer un nouveau provider
+
+### Étape 1 : Créer la classe du provider
+
+Créez un nouveau fichier dans `src/Provider/` nommé `[NomProvider]Provider.php`.
+
+**Exemple : Cloudflare Turnstile**
+
+```php
+shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ // 2. Vérifier que la réponse n'est pas vide
+ if (empty($response)) {
+ $this->setLastError($this->l('Please validate the captcha field'));
+ return false;
+ }
+
+ // 3. Récupérer les clés de configuration
+ $secretKey = $this->getConfig('CAPTCHA_TURNSTILE_SECRET_KEY');
+
+ if (empty($secretKey)) {
+ $this->setLastError($this->l('Provider not configured'));
+ return false;
+ }
+
+ // 4. Préparer la requête API
+ $data = [
+ 'secret' => $secretKey,
+ 'response' => $response,
+ 'remoteip' => $remoteIp,
+ ];
+
+ // 5. Appeler l'API de vérification
+ $options = [
+ 'http' => [
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => http_build_query($data),
+ ],
+ ];
+
+ $context = stream_context_create($options);
+ $verify = file_get_contents(self::VERIFY_URL, false, $context);
+
+ if ($verify === false) {
+ $this->setLastError($this->l('Unable to verify captcha'));
+ return false;
+ }
+
+ // 6. Traiter la réponse
+ $verifyResponse = json_decode($verify, true);
+
+ if (!isset($verifyResponse['success']) || $verifyResponse['success'] !== true) {
+ $this->setLastError($this->l('Captcha validation failed'));
+
+ // Logger les erreurs si disponibles
+ if (isset($verifyResponse['error-codes'])) {
+ $this->log('Errors: ' . print_r($verifyResponse['error-codes'], true));
+ }
+
+ return false;
+ }
+
+ // 7. Validation réussie
+ $this->log($this->l('Captcha validated successfully'));
+ return true;
+}
+```
+
+#### 4. `getConfigFields()` : array
+
+Retourne les champs de configuration à afficher dans l'admin PrestaShop.
+
+**Format :** Array compatible avec `HelperForm` de PrestaShop
+
+```php
+public function getConfigFields()
+{
+ return [
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Turnstile Site Key'),
+ 'name' => 'CAPTCHA_TURNSTILE_SITE_KEY',
+ 'required' => true,
+ 'desc' => $this->l('Get your site key from Cloudflare dashboard'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Turnstile Secret Key'),
+ 'name' => 'CAPTCHA_TURNSTILE_SECRET_KEY',
+ 'required' => true,
+ 'desc' => $this->l('Get your secret key from Cloudflare dashboard'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'radio',
+ 'label' => $this->l('Widget Theme'),
+ 'name' => 'CAPTCHA_TURNSTILE_THEME',
+ 'values' => [
+ ['id' => 'light', 'value' => 'light', 'label' => $this->l('Light')],
+ ['id' => 'dark', 'value' => 'dark', 'label' => $this->l('Dark')],
+ ['id' => 'auto', 'value' => 'auto', 'label' => $this->l('Auto')],
+ ],
+ 'tab' => 'general',
+ ],
+ ];
+}
+```
+
+**Types de champs disponibles :**
+- `text` : Champ texte simple
+- `textarea` : Zone de texte multi-lignes
+- `radio` : Boutons radio
+- `switch` : Interrupteur ON/OFF
+- `select` : Liste déroulante
+- `html` : Contenu HTML personnalisé
+
+#### 5. `renderHeader(array $context = [])` : string
+
+Retourne le HTML/JavaScript à insérer dans le `
` de la page.
+
+**Paramètre :**
+- `$context` : Array contenant `controller`, `form_type`, etc.
+
+```php
+public function renderHeader(array $context = [])
+{
+ if (!$this->shouldDisplayToCustomer()) {
+ return '';
+ }
+
+ $controller = $this->context->controller;
+ $loadEverywhere = Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1;
+
+ $shouldLoad = (
+ ($controller instanceof \AuthController && Configuration::get('CAPTCHA_ENABLE_ACCOUNT'))
+ || ($controller instanceof \ContactController && Configuration::get('CAPTCHA_ENABLE_CONTACT'))
+ || $loadEverywhere
+ );
+
+ if ($shouldLoad) {
+ $siteKey = $this->getConfig('CAPTCHA_TURNSTILE_SITE_KEY');
+
+ return '';
+ }
+
+ return '';
+}
+```
+
+#### 6. `getTemplateVars()` : array
+
+Retourne les variables Smarty à passer au template.
+
+```php
+public function getTemplateVars()
+{
+ // Récupérer les variables de base (displayCaptcha, provider, lang, theme)
+ $vars = parent::getTemplateVars();
+
+ // Ajouter des variables spécifiques
+ $vars['siteKey'] = $this->getConfig('CAPTCHA_TURNSTILE_SITE_KEY');
+ $vars['turnstileTheme'] = $this->getConfig('CAPTCHA_TURNSTILE_THEME', 'auto');
+
+ return $vars;
+}
+```
+
+#### 7. `isConfigured()` : bool
+
+Vérifie que le provider est correctement configuré.
+
+```php
+public function isConfigured()
+{
+ $siteKey = $this->getConfig('CAPTCHA_TURNSTILE_SITE_KEY');
+ $secretKey = $this->getConfig('CAPTCHA_TURNSTILE_SECRET_KEY');
+
+ return !empty($siteKey) && !empty($secretKey);
+}
+```
+
+#### 8. `getTemplatePath()` : string
+
+Retourne le chemin du template Smarty.
+
+```php
+public function getTemplatePath()
+{
+ return 'module:eicaptcha/views/templates/hook/providers/turnstile.tpl';
+}
+```
+
+### Méthodes optionnelles
+
+#### `getCssFiles()` : array
+
+Retourne un array de fichiers CSS à charger.
+
+```php
+public function getCssFiles()
+{
+ return [
+ 'modules/eicaptcha/views/css/turnstile.css',
+ ];
+}
+```
+
+#### `getJsFiles()` : array
+
+Retourne un array de fichiers JavaScript à charger.
+
+```php
+public function getJsFiles()
+{
+ return [
+ 'modules/eicaptcha/views/js/turnstile.js',
+ ];
+}
+```
+
+## Créer le template
+
+### Étape 2 : Créer le template Smarty
+
+Créez un fichier `views/templates/hook/providers/[nom_provider].tpl`.
+
+**Exemple : turnstile.tpl**
+
+```smarty
+{*
+* Cloudflare Turnstile Template
+*
+* @author Your Name
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+*}
+
+{if $displayCaptcha}
+
+
+
+
+
+
+{/if}
+```
+
+### Variables Smarty disponibles
+
+Variables fournies par `AbstractCaptchaProvider::getTemplateVars()` :
+
+- `$displayCaptcha` : bool - Si le captcha doit être affiché
+- `$provider` : string - Nom du provider (`getName()`)
+- `$captchalang` : string - Code langue (ex: 'fr', 'en')
+- `$captchatheme` : string - Thème ('light' ou 'dark')
+
+Variables personnalisées ajoutées dans `getTemplateVars()` de votre provider.
+
+## Enregistrer le provider
+
+### Étape 3 : Ajouter le provider à la Factory
+
+Éditez `src/Factory/CaptchaFactory.php` et ajoutez votre provider :
+
+```php
+private static $providers = [
+ 'google_recaptcha' => GoogleRecaptchaProvider::class,
+ 'google_enterprise' => GoogleEnterpriseProvider::class,
+ 'hcaptcha' => HCaptchaProvider::class,
+ 'math' => MathCaptchaProvider::class,
+ 'turnstile' => TurnstileProvider::class, // <- Ajouter ici
+];
+```
+
+N'oubliez pas d'ajouter le `use` en haut du fichier :
+
+```php
+use Eicaptcha\Module\Provider\TurnstileProvider;
+```
+
+### Étape 4 : Mettre à jour ConfigForm.php
+
+Ajoutez les nouveaux champs de configuration dans `src/ConfigForm.php` :
+
+**Dans `postProcess()` :**
+
+```php
+// Turnstile
+Configuration::updateValue('CAPTCHA_TURNSTILE_SITE_KEY', Tools::getValue('CAPTCHA_TURNSTILE_SITE_KEY'));
+Configuration::updateValue('CAPTCHA_TURNSTILE_SECRET_KEY', Tools::getValue('CAPTCHA_TURNSTILE_SECRET_KEY'));
+Configuration::updateValue('CAPTCHA_TURNSTILE_THEME', Tools::getValue('CAPTCHA_TURNSTILE_THEME'));
+```
+
+**Dans `getConfigFieldsValues()` :**
+
+```php
+// Turnstile
+'CAPTCHA_TURNSTILE_SITE_KEY' => Tools::getValue('CAPTCHA_TURNSTILE_SITE_KEY', Configuration::get('CAPTCHA_TURNSTILE_SITE_KEY')),
+'CAPTCHA_TURNSTILE_SECRET_KEY' => Tools::getValue('CAPTCHA_TURNSTILE_SECRET_KEY', Configuration::get('CAPTCHA_TURNSTILE_SECRET_KEY')),
+'CAPTCHA_TURNSTILE_THEME' => Tools::getValue('CAPTCHA_TURNSTILE_THEME', Configuration::get('CAPTCHA_TURNSTILE_THEME') ?: 'auto'),
+```
+
+### Étape 5 : Mettre à jour Installer.php (optionnel)
+
+Si vous voulez définir des valeurs par défaut :
+
+```php
+protected function installConfigurations()
+{
+ return Configuration::updateGlobalValue('CAPTCHA_ENABLE_ACCOUNT', 0)
+ // ... autres configs ...
+ && Configuration::updateValue('CAPTCHA_TURNSTILE_THEME', 'auto');
+}
+```
+
+### Étape 6 : Adapter le code pour récupérer la réponse
+
+Si votre provider utilise un nom de champ différent de `g-recaptcha-response`, adaptez `eicaptcha.php::_validateCaptcha()` :
+
+```php
+protected function _validateCaptcha()
+{
+ $provider = $this->getCaptchaProvider();
+
+ if (!$provider->shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ // Adaptez selon votre provider
+ $fieldName = 'g-recaptcha-response'; // Défaut
+
+ if ($provider->getName() === 'turnstile') {
+ $fieldName = 'cf-turnstile-response';
+ } elseif ($provider->getName() === 'math') {
+ $fieldName = 'math-captcha-response';
+ }
+
+ $response = Tools::getValue($fieldName);
+ $remoteIp = Tools::getRemoteAddr();
+
+ $isValid = $provider->validate($response, $remoteIp);
+
+ if (!$isValid) {
+ $this->context->controller->errors[] = $provider->getLastError();
+ }
+
+ return $isValid;
+}
+```
+
+**Ou mieux, créez une méthode dans l'interface :**
+
+```php
+// Dans CaptchaProviderInterface.php
+public function getResponseFieldName();
+
+// Dans votre provider
+public function getResponseFieldName()
+{
+ return 'cf-turnstile-response';
+}
+```
+
+## Tests et validation
+
+### Liste de vérification
+
+- [ ] Le provider apparaît dans la liste déroulante admin
+- [ ] Les champs de configuration s'affichent correctement
+- [ ] Le captcha s'affiche sur le formulaire de contact
+- [ ] Le captcha s'affiche sur le formulaire d'inscription
+- [ ] La validation fonctionne (accepte les bonnes réponses)
+- [ ] La validation rejette les mauvaises réponses
+- [ ] Les messages d'erreur sont affichés
+- [ ] Le debug log fonctionne
+- [ ] Compatible avec les clients connectés (si désactivé)
+- [ ] Le template s'adapte aux thèmes
+
+### Tests manuels
+
+1. **Installation**
+ ```bash
+ # Vider le cache PrestaShop
+ rm -rf var/cache/*
+ ```
+
+2. **Configuration**
+ - Aller dans Modules > Ei Captcha
+ - Sélectionner votre provider
+ - Renseigner les clés
+ - Sauvegarder
+
+3. **Tests formulaires**
+ - Tester formulaire contact (avec/sans captcha valide)
+ - Tester inscription client (avec/sans captcha valide)
+ - Vérifier les erreurs s'affichent
+
+4. **Debug**
+ - Activer le mode debug
+ - Vérifier `logs/debug.log`
+
+## Exemples complets
+
+### Exemple 1 : Provider avec API simple (comme hCaptcha)
+
+Voir `src/Provider/HCaptchaProvider.php` pour un exemple complet.
+
+**Caractéristiques :**
+- Validation via POST à une API REST
+- Deux clés : Site Key + Secret Key
+- Réponse JSON simple
+
+### Exemple 2 : Provider sans dépendances externes (Math Captcha)
+
+Voir `src/Provider/MathCaptchaProvider.php` pour un exemple complet.
+
+**Caractéristiques :**
+- Génération côté serveur
+- Stockage en session
+- Protection CSRF
+- Pas d'API externe
+
+### Exemple 3 : Provider avec librairie externe (Google Enterprise)
+
+Voir `src/Provider/GoogleEnterpriseProvider.php` pour un exemple complet.
+
+**Caractéristiques :**
+- Utilise SDK Google Cloud
+- Vérification de dépendances composer
+- Fallback si librairie absente
+
+## Best practices
+
+### Sécurité
+
+1. **Toujours valider côté serveur**
+ ```php
+ // ✅ BON
+ public function validate($response, $remoteIp)
+ {
+ // Validation API serveur
+ }
+
+ // ❌ MAUVAIS
+ // Faire confiance uniquement au JavaScript
+ ```
+
+2. **Envoyer l'IP utilisateur**
+ ```php
+ $data = [
+ 'secret' => $secretKey,
+ 'response' => $response,
+ 'remoteip' => $remoteIp, // Important pour détecter abus
+ ];
+ ```
+
+3. **Logger les erreurs en mode debug**
+ ```php
+ if (isset($errors)) {
+ $this->log('Validation errors: ' . print_r($errors, true));
+ }
+ ```
+
+4. **Utiliser `setLastError()` pour feedback utilisateur**
+ ```php
+ $this->setLastError($this->l('Captcha validation failed'));
+ ```
+
+### Performance
+
+1. **Lazy loading des scripts**
+ ```php
+ // Ne charger que si nécessaire
+ if ($controller instanceof ContactController && Configuration::get('CAPTCHA_ENABLE_CONTACT')) {
+ return $script;
+ }
+ ```
+
+2. **Mettre en cache si possible**
+ ```php
+ // La factory met déjà en cache les instances
+ // Pas besoin de recréer à chaque fois
+ ```
+
+### Compatibilité
+
+1. **Vérifier les dépendances**
+ ```php
+ public function isConfigured()
+ {
+ if (!class_exists('SomeRequiredClass')) {
+ return false;
+ }
+ return !empty($this->getConfig('KEY'));
+ }
+ ```
+
+2. **Fallback gracieux**
+ ```php
+ try {
+ // Tentative avec nouvelle méthode
+ } catch (\Exception $e) {
+ $this->log('Error: ' . $e->getMessage());
+ // Fallback vers méthode alternative
+ }
+ ```
+
+3. **Support multi-langues**
+ ```php
+ // Toujours utiliser $this->l() pour les traductions
+ $this->l('Error message')
+ ```
+
+### Code quality
+
+1. **Docblocks complets**
+ ```php
+ /**
+ * Validate the captcha response
+ *
+ * @param string $response The captcha response token
+ * @param string $remoteIp The user's IP address
+ *
+ * @return bool True if valid, false otherwise
+ *
+ * @since 3.1.0
+ */
+ public function validate($response, $remoteIp)
+ ```
+
+2. **Constantes pour URLs**
+ ```php
+ const VERIFY_URL = 'https://api.example.com/verify';
+
+ // Utiliser
+ file_get_contents(self::VERIFY_URL, ...);
+ ```
+
+3. **Validation des configs**
+ ```php
+ public function getConfig($key, $default = null)
+ {
+ $value = Configuration::get($key);
+ return $value !== false ? $value : $default;
+ }
+ ```
+
+## Support et contribution
+
+### Soumettre un nouveau provider
+
+1. Créer une branche : `git checkout -b feature/add-[provider]-support`
+2. Implémenter le provider selon ce guide
+3. Ajouter des tests si possible
+4. Mettre à jour `changelog.txt`
+5. Créer une Pull Request
+
+### Documentation
+
+- Ajouter un README spécifique dans `docs/providers/[provider].md`
+- Documenter les prérequis (clés API, comptes nécessaires)
+- Fournir des liens vers la documentation officielle
+
+### Questions ?
+
+- GitHub Issues : https://github.com/nenes25/eicaptcha/issues
+- Wiki : https://github.com/nenes25/eicaptcha/wiki
+
+---
+
+**Version de ce guide :** 3.0.0
+**Dernière mise à jour :** 2025-11-21
diff --git a/eicaptcha.php b/eicaptcha.php
index d227b20..038f524 100644
--- a/eicaptcha.php
+++ b/eicaptcha.php
@@ -23,7 +23,8 @@
use Eicaptcha\Module\ConfigForm;
use Eicaptcha\Module\Debugger;
use Eicaptcha\Module\Installer;
-use ReCaptcha\ReCaptcha;
+use Eicaptcha\Module\Factory\CaptchaFactory;
+use Eicaptcha\Module\Provider\CaptchaProviderInterface;
class EiCaptcha extends Module
{
@@ -48,12 +49,17 @@ class EiCaptcha extends Module
*/
protected $captchaLang = 'en';
+ /**
+ * @var CaptchaProviderInterface
+ */
+ protected $captchaProvider;
+
public function __construct()
{
$this->author = 'hhennes';
$this->name = 'eicaptcha';
$this->tab = 'front_office_features';
- $this->version = '2.6.0';
+ $this->version = '3.0.0';
$this->need_instance = 1;
$this->bootstrap = true;
@@ -141,6 +147,28 @@ public function getContext()
return $this->context;
}
+ /**
+ * Get the current captcha provider
+ *
+ * @return CaptchaProviderInterface
+ *
+ * @since 3.0.0
+ */
+ public function getCaptchaProvider()
+ {
+ if (null === $this->captchaProvider) {
+ try {
+ $this->captchaProvider = CaptchaFactory::create($this);
+ } catch (\Exception $e) {
+ $this->debugger->log('Error loading captcha provider: ' . $e->getMessage());
+ // Fallback to google_recaptcha
+ $this->captchaProvider = CaptchaFactory::create($this, 'google_recaptcha');
+ }
+ }
+
+ return $this->captchaProvider;
+ }
+
/**
* Module Configuration in Back Office
*
@@ -165,108 +193,15 @@ public function getContent()
*/
public function hookHeader(array $params)
{
- if (!$this->shouldDisplayToCustomer()) {
- return;
- }
-
- $captchaVersion = Configuration::get('CAPTCHA_VERSION');
- //Add Content box to contact form page in order to display captcha
- if ($this->context->controller instanceof ContactController
- && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
- ) {
- $this->context->controller->registerJavascript(
- 'modules-eicaptcha-contact-form',
- 'modules/' . $this->name . '/views/js/eicaptcha-contact-form-v' . $captchaVersion . '.js'
- );
- }
+ $provider = $this->getCaptchaProvider();
- if ($captchaVersion == 2) {
- return $this->renderHeaderV2();
- } else {
- return $this->renderHeaderV3();
+ if (!$provider->shouldDisplayToCustomer()) {
+ return;
}
- }
-
- /**
- * Return content for (re)captcha v2
- *
- * @return string|void
- */
- protected function renderHeaderV2()
- {
- if ((
- (
- $this->context->controller instanceof AuthController
- || $this->context->controller instanceof RegistrationController
- )
- && Configuration::get('CAPTCHA_ENABLE_ACCOUNT') == 1
- )
- ||
- ($this->context->controller instanceof ContactController
- && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
- )
- || Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1
- ) {
- $this->context->controller->registerStylesheet(
- 'module-eicaptcha',
- 'modules/' . $this->name . '/views/css/eicaptcha.css'
- );
- //Dynamic insertion of the content
- $js = '';
-
- if (($this->context->controller instanceof ContactController && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1)) {
- $js .= '';
- }
- return $js;
- }
+ return $provider->renderHeader(['controller' => $this->context->controller]);
}
- /**
- * Return content for recaptcha v3
- *
- * @return string|void
- */
- public function renderHeaderV3()
- {
- if (
- ($this->context->controller instanceof ContactController
- && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
- )
- || Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1
- ) {
- $publicKey = Configuration::get('CAPTCHA_PUBLIC_KEY');
- $js = '
-
- ';
-
- return $js;
- }
- }
/**
* Add Captcha on the Customer Registration Form
@@ -280,12 +215,8 @@ public function hookDisplayCustomerAccountForm(array $params)
if ($this->context->controller->php_self != 'identity'
&& Configuration::get('CAPTCHA_ENABLE_ACCOUNT') == 1
) {
- $this->context->smarty->assign([
- 'captchaVersion' => Configuration::get('CAPTCHA_VERSION'),
- 'publicKey' => Configuration::get('CAPTCHA_PUBLIC_KEY'),
- 'captchalang' => $this->captchaLang,
- 'captchatheme' => $this->themes[Configuration::get('CAPTCHA_THEME')],
- ]);
+ $provider = $this->getCaptchaProvider();
+ $this->context->smarty->assign($provider->getTemplateVars());
return $this->display(__FILE__, 'views/templates/hook/hookDisplayCustomerAccountForm.tpl');
}
@@ -398,17 +329,14 @@ public function hookActionAdminControllerSetMedia(array $params)
*/
public function hookDisplayNewsletterRegistration(array $params)
{
+ $provider = $this->getCaptchaProvider();
+
if (
Configuration::get('CAPTCHA_ENABLE_NEWSLETTER') == 1
&& $this->canUseCaptchaOnNewsletter()
- && $this->shouldDisplayToCustomer()
+ && $provider->shouldDisplayToCustomer()
) {
- $this->context->smarty->assign([
- 'captchaVersion' => Configuration::get('CAPTCHA_VERSION'),
- 'publicKey' => Configuration::get('CAPTCHA_PUBLIC_KEY'),
- 'captchalang' => $this->captchaLang,
- 'captchatheme' => $this->themes[Configuration::get('CAPTCHA_THEME')],
- ]);
+ $this->context->smarty->assign($provider->getTemplateVars());
return $this->display(__FILE__, 'views/templates/hook/hookDisplayNewsletterRegistration.tpl');
}
@@ -425,9 +353,11 @@ public function hookDisplayNewsletterRegistration(array $params)
*/
public function hookActionNewsletterRegistrationBefore(array $params)
{
+ $provider = $this->getCaptchaProvider();
+
if (Configuration::get('CAPTCHA_ENABLE_NEWSLETTER') == 1
&& $this->canUseCaptchaOnNewsletter()
- && $this->shouldDisplayToCustomer()
+ && $provider->shouldDisplayToCustomer()
) {
if (!$this->_validateCaptcha()) {
$params['hookError'] = $this->l('Please validate the captcha field before submitting your request');
@@ -442,51 +372,22 @@ public function hookActionNewsletterRegistrationBefore(array $params)
*/
protected function _validateCaptcha()
{
- if (!$this->shouldDisplayToCustomer()) {
+ $provider = $this->getCaptchaProvider();
+
+ if (!$provider->shouldDisplayToCustomer()) {
return true;
}
- $context = Context::getContext();
- $captchaVersion = Configuration::get('CAPTCHA_VERSION');
- $captchaV3MinScore = (float) Configuration::get('CAPTCHA_V3_MINIMAL_SCORE');
- //Fix issue if allow_url_open is set to 0
- if (function_exists('ini_get') && !ini_get('allow_url_fopen')) {
- $recaptchaMethod = new \ReCaptcha\RequestMethod\CurlPost();
- } else {
- $recaptchaMethod = null;
- }
- $captcha = new ReCaptcha(Configuration::get('CAPTCHA_PRIVATE_KEY'), $recaptchaMethod);
- if ($captchaVersion == 3) {
- $captcha->setScoreThreshold($captchaV3MinScore);
- }
- $result = $captcha->verify(
- Tools::getValue('g-recaptcha-response'),
- Tools::getRemoteAddr()
- );
-
- if (!$result->isSuccess()) {
- $errorMessage = $this->l('Please validate the captcha field before submitting your request');
- $this->debugger->log($errorMessage);
- $this->debugger->log(sprintf($this->l('Recaptcha response %s'), print_r($result->getErrorCodes(), true)));
- if ($captchaVersion == 3) {
- if ($result->getScore() < $captchaV3MinScore) {
- $errorMessageV3 =
- sprintf(
- 'Your request has been blocked by the captcha system, due to a low score of %s, required score is %s',
- $result->getScore(),
- $captchaV3MinScore
- );
- $this->debugger->log($errorMessageV3);
- }
- }
- $context->controller->errors[] = $errorMessage;
+ $response = Tools::getValue($provider->getResponseFieldName());
+ $remoteIp = Tools::getRemoteAddr();
- return false;
- }
+ $isValid = $provider->validate($response, $remoteIp);
- $this->debugger->log($this->l('Captcha submited with success'));
+ if (!$isValid) {
+ $this->context->controller->errors[] = $provider->getLastError();
+ }
- return true;
+ return $isValid;
}
/**
@@ -500,13 +401,9 @@ protected function _validateCaptcha()
*/
public function hookActionGetEicaptchaParams(array $params)
{
- return [
- 'displayCaptcha' => $this->shouldDisplayToCustomer(),
- 'captchaVersion' => Configuration::get('CAPTCHA_VERSION'),
- 'publicKey' => Configuration::get('CAPTCHA_PUBLIC_KEY'),
- 'captchaforcelang' => Configuration::get('CAPTCHA_FORCE_LANG'),
- 'captchatheme' => $this->themes[Configuration::get('CAPTCHA_THEME')],
- ];
+ $provider = $this->getCaptchaProvider();
+
+ return $provider->getTemplateVars();
}
/**
@@ -518,13 +415,8 @@ public function hookActionGetEicaptchaParams(array $params)
*/
public function hookDisplayEicaptchaVerification(array $params)
{
- $this->context->smarty->assign([
- 'displayCaptcha' => $this->shouldDisplayToCustomer(),
- 'captchaVersion' => Configuration::get('CAPTCHA_VERSION'),
- 'publicKey' => Configuration::get('CAPTCHA_PUBLIC_KEY'),
- 'captchalang' => $this->captchaLang,
- 'captchatheme' => $this->themes[Configuration::get('CAPTCHA_THEME')],
- ]);
+ $provider = $this->getCaptchaProvider();
+ $this->context->smarty->assign($provider->getTemplateVars());
return $this->display(__FILE__, 'views/templates/hook/hookDisplayEicaptchaVerification.tpl');
}
@@ -558,22 +450,4 @@ public function canUseCaptchaOnNewsletter()
return false;
}
-
- /**
- * Define if the captcha should be displayed to the customer
- *
- * @return bool
- */
- protected function shouldDisplayToCustomer()
- {
- if (
- Configuration::get('CAPTCHA_ENABLE_LOGGED_CUSTOMERS') == 0
- && $this->context->customer->id > 0
- && $this->context->customer->email != null
- ) {
- return false;
- }
-
- return true;
- }
}
diff --git a/src/ConfigForm.php b/src/ConfigForm.php
index be3461d..c0f458c 100644
--- a/src/ConfigForm.php
+++ b/src/ConfigForm.php
@@ -20,6 +20,7 @@
use Configuration;
use Context;
use EiCaptcha;
+use Eicaptcha\Module\Factory\CaptchaFactory;
use HelperForm;
use Language;
use Tools;
@@ -65,234 +66,33 @@ public function renderForm()
],
'tabs' => [
'general' => $this->l('General configuration'),
- 'advanced' => $this->l('Advanded parameters'),
+ 'advanced' => $this->l('Advanced parameters'),
],
- 'description' => $this->l('To get your own public and private keys please click on the folowing link')
- . ' https://www.google.com/recaptcha/intro/index.html',
- 'input' => [
+ 'description' => $this->l('Configure your captcha provider and protect your website from spam and bots'),
+ 'input' => array_merge(
+ // Provider selection
[
- 'type' => 'radio',
- 'label' => $this->l('Recaptcha Version'),
- 'name' => 'CAPTCHA_VERSION',
- 'required' => true,
- 'class' => 't',
- 'values' => [
- [
- 'id' => 'v2',
- 'value' => 2,
- 'label' => $this->l('V2'),
- ],
- [
- 'id' => 'v3',
- 'value' => 3,
- 'label' => $this->l('V3'),
- ],
- ],
- 'tab' => 'general',
- ],
- [
- 'type' => 'text',
- 'label' => $this->l('Captcha V3 mininum score'),
- 'hint' => sprintf(
- $this->l('The minimum score required to validate the captcha is a number between 0 and 1. Default is 0.5 (recommended: 0.5 for normal security, 0.3 for less strict, 0.7 for more strict). %s'),
- 'Learn more'
- ),
- 'name' => 'CAPTCHA_V3_MINIMAL_SCORE',
- 'required' => true,
- 'class' => 'fixed-width-sm',
- 'suffix' => '(0.0 - 1.0)',
- 'empty_message' => $this->l('Please fill the captcha v3 minimal score.'),
- 'tab' => 'general',
- ],
- [
- 'type' => 'text',
- 'label' => $this->l('Captcha public key (Site key)'),
- 'name' => 'CAPTCHA_PUBLIC_KEY',
- 'required' => true,
- 'empty_message' => $this->l('Please fill the captcha public key'),
- 'tab' => 'general',
- ],
- [
- 'type' => 'text',
- 'label' => $this->l('Captcha private key (Secret key)'),
- 'name' => 'CAPTCHA_PRIVATE_KEY',
- 'required' => true,
- 'empty_message' => $this->l('Please fill the captcha private key'),
- 'tab' => 'general',
- ],
- [
- 'type' => 'switch',
- 'label' => $this->l('Enable Captcha for logged customers'),
- 'name' => 'CAPTCHA_ENABLE_LOGGED_CUSTOMERS',
- 'required' => true,
- 'class' => 't',
- 'is_bool' => true,
- 'hint' => $this->l('Define if logged customers need to use captcha or not'),
- 'values' => [
- [
- 'id' => 'active_on',
- 'value' => 1,
- 'label' => $this->l('Enabled'),
- ],
- [
- 'id' => 'active_off',
- 'value' => 0,
- 'label' => $this->l('Disabled'),
- ],
- ],
- 'tab' => 'general',
- ],
- [
- 'type' => 'switch',
- 'label' => $this->l('Enable Captcha for contact form'),
- 'name' => 'CAPTCHA_ENABLE_CONTACT',
- 'required' => true,
- 'class' => 't',
- 'is_bool' => true,
- 'values' => [
- [
- 'id' => 'active_on',
- 'value' => 1,
- 'label' => $this->l('Enabled'),
- ],
- [
- 'id' => 'active_off',
- 'value' => 0,
- 'label' => $this->l('Disabled'),
- ],
- ],
- 'tab' => 'general',
- ],
- [
- 'type' => 'switch',
- 'label' => $this->l('Enable Captcha for account creation'),
- 'name' => 'CAPTCHA_ENABLE_ACCOUNT',
- 'required' => true,
- 'class' => 't',
- 'is_bool' => true,
- 'values' => [
- [
- 'id' => 'active_on',
- 'value' => 1,
- 'label' => $this->l('Enabled'),
- ],
- [
- 'id' => 'active_off',
- 'value' => 0,
- 'label' => $this->l('Disabled'),
- ],
- ],
- 'tab' => 'general',
- ],
- [
- 'type' => 'switch',
- 'label' => $this->l('Enable Captcha for newsletter registration'),
- 'hint' => $this->l('Only availaibles in certain conditions*'),
- 'name' => 'CAPTCHA_ENABLE_NEWSLETTER',
- 'required' => true,
- 'class' => 't',
- 'is_bool' => true,
- 'values' => [
- [
- 'id' => 'active_on',
- 'value' => 1,
- 'label' => $this->l('Enabled'),
- ],
- [
- 'id' => 'active_off',
- 'value' => 0,
- 'label' => $this->l('Disabled'),
- ],
- ],
- 'tab' => 'general',
- ],
- [
- 'type' => 'text',
- 'label' => $this->l('Force Captcha language'),
- 'hint' => $this->l('Language code ( en-GB | fr | de | de-AT | ... ) - Leave empty for autodetect'),
- 'desc' => $this->l('For available language codes see: https://developers.google.com/recaptcha/docs/language'),
- 'name' => 'CAPTCHA_FORCE_LANG',
- 'required' => false,
- 'tab' => 'general',
- ],
- [
- 'type' => 'radio',
- 'label' => $this->l('Theme'),
- 'name' => 'CAPTCHA_THEME',
- 'required' => true,
- 'is_bool' => true,
- 'values' => [
- [
- 'id' => 'cdark',
- 'value' => 1,
- 'label' => $this->l('Dark'),
- ],
- [
- 'id' => 'clight',
- 'value' => 0,
- 'label' => $this->l('Light'),
+ [
+ 'type' => 'select',
+ 'label' => $this->l('Captcha Provider'),
+ 'name' => 'CAPTCHA_PROVIDER',
+ 'required' => true,
+ 'desc' => $this->l('Select the captcha system you want to use'),
+ 'options' => [
+ 'query' => $this->getAvailableProviders(),
+ 'id' => 'id',
+ 'name' => 'name',
],
+ 'tab' => 'general',
],
- 'tab' => 'general',
- ],
- [
- 'type' => 'switch',
- 'name' => 'CAPTCHA_DEBUG',
- 'label' => $this->l('Enable Debug'),
- 'hint' => $this->l('Use only for debug'),
- 'desc' => sprintf(
- $this->l('Enable loging for debuging module, see file %s'),
- _PS_MODULE_DIR_ . 'eicaptcha/logs/debug.log'
- ),
- 'required' => false,
- 'class' => 't',
- 'is_bool' => true,
- 'values' => [
- [
- 'id' => 'active_on',
- 'value' => 1,
- 'label' => $this->l('Enabled'),
- ],
- [
- 'id' => 'active_off',
- 'value' => 0,
- 'label' => $this->l('Disabled'),
- ],
- ],
- 'tab' => 'advanced',
- ],
- [
- 'type' => 'switch',
- 'name' => 'CAPTCHA_LOAD_EVERYWHERE',
- 'label' => $this->l('Load Recaptcha library everywhere'),
- 'hint' => $this->l('Let this option disabled by default if you don\'t use a specific contact form'),
- 'desc' => $this->l('Only If you use a theme with elementor or warehouse and your contact form is not on the default page, enable this option.'),
- 'required' => false,
- 'class' => 't',
- 'is_bool' => true,
- 'values' => [
- [
- 'id' => 'active_on',
- 'value' => 1,
- 'label' => $this->l('Enabled'),
- ],
- [
- 'id' => 'active_off',
- 'value' => 0,
- 'label' => $this->l('Disabled'),
- ],
- ],
- 'tab' => 'advanced',
- ],
- [
- 'type' => 'html',
- 'label' => $this->l('Check module installation'),
- 'name' => 'enable_debug_html',
- 'html_content' => '' . $this->l('Check if module is well installed') . '',
- 'desc' => $this->l('click on this link will reload the page, please go again in tab "advanced parameters" to see the results'),
- 'tab' => 'advanced',
],
- ],
+ // Provider-specific fields (dynamically loaded)
+ $this->getProviderSpecificFields(),
+ // Common fields
+ $this->getCommonFields(),
+ // Advanced fields
+ $this->getAdvancedFields()
+ ),
'submit' => [
'title' => $this->l('Save'),
'class' => 'button btn btn-default pull-right',
@@ -357,6 +157,225 @@ public function renderForm()
return $helper->generateForm([$fields_form]);
}
+ /**
+ * Get provider-specific configuration fields
+ *
+ * @return array
+ *
+ * @since 3.0.0
+ */
+ protected function getProviderSpecificFields()
+ {
+ try {
+ // Get the currently selected provider
+ $currentProvider = Tools::getValue('CAPTCHA_PROVIDER', Configuration::get('CAPTCHA_PROVIDER') ?: 'google_recaptcha');
+ $provider = CaptchaFactory::create($this->module, $currentProvider);
+
+ // Get provider-specific fields
+ return $provider->getConfigFields();
+ } catch (\Exception $e) {
+ return [];
+ }
+ }
+
+ /**
+ * Get common configuration fields (applicable to all providers)
+ *
+ * @return array
+ *
+ * @since 3.0.0
+ */
+ protected function getCommonFields()
+ {
+ return [
+ [
+ 'type' => 'switch',
+ 'label' => $this->l('Enable Captcha for logged customers'),
+ 'name' => 'CAPTCHA_ENABLE_LOGGED_CUSTOMERS',
+ 'required' => true,
+ 'class' => 't',
+ 'is_bool' => true,
+ 'hint' => $this->l('Define if logged customers need to use captcha or not'),
+ 'values' => [
+ [
+ 'id' => 'active_on',
+ 'value' => 1,
+ 'label' => $this->l('Enabled'),
+ ],
+ [
+ 'id' => 'active_off',
+ 'value' => 0,
+ 'label' => $this->l('Disabled'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'switch',
+ 'label' => $this->l('Enable Captcha for contact form'),
+ 'name' => 'CAPTCHA_ENABLE_CONTACT',
+ 'required' => true,
+ 'class' => 't',
+ 'is_bool' => true,
+ 'values' => [
+ [
+ 'id' => 'active_on',
+ 'value' => 1,
+ 'label' => $this->l('Enabled'),
+ ],
+ [
+ 'id' => 'active_off',
+ 'value' => 0,
+ 'label' => $this->l('Disabled'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'switch',
+ 'label' => $this->l('Enable Captcha for account creation'),
+ 'name' => 'CAPTCHA_ENABLE_ACCOUNT',
+ 'required' => true,
+ 'class' => 't',
+ 'is_bool' => true,
+ 'values' => [
+ [
+ 'id' => 'active_on',
+ 'value' => 1,
+ 'label' => $this->l('Enabled'),
+ ],
+ [
+ 'id' => 'active_off',
+ 'value' => 0,
+ 'label' => $this->l('Disabled'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'switch',
+ 'label' => $this->l('Enable Captcha for newsletter registration'),
+ 'hint' => $this->l('Only availaibles in certain conditions*'),
+ 'name' => 'CAPTCHA_ENABLE_NEWSLETTER',
+ 'required' => true,
+ 'class' => 't',
+ 'is_bool' => true,
+ 'values' => [
+ [
+ 'id' => 'active_on',
+ 'value' => 1,
+ 'label' => $this->l('Enabled'),
+ ],
+ [
+ 'id' => 'active_off',
+ 'value' => 0,
+ 'label' => $this->l('Disabled'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Force Captcha language'),
+ 'hint' => $this->l('Language code ( en-GB | fr | de | de-AT | ... ) - Leave empty for autodetect'),
+ 'desc' => $this->l('For available language codes see: https://developers.google.com/recaptcha/docs/language'),
+ 'name' => 'CAPTCHA_FORCE_LANG',
+ 'required' => false,
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'radio',
+ 'label' => $this->l('Theme'),
+ 'name' => 'CAPTCHA_THEME',
+ 'required' => true,
+ 'is_bool' => true,
+ 'values' => [
+ [
+ 'id' => 'cdark',
+ 'value' => 1,
+ 'label' => $this->l('Dark'),
+ ],
+ [
+ 'id' => 'clight',
+ 'value' => 0,
+ 'label' => $this->l('Light'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ ];
+ }
+
+ /**
+ * Get advanced configuration fields
+ *
+ * @return array
+ *
+ * @since 3.0.0
+ */
+ protected function getAdvancedFields()
+ {
+ return [
+ [
+ 'type' => 'switch',
+ 'name' => 'CAPTCHA_DEBUG',
+ 'label' => $this->l('Enable Debug'),
+ 'hint' => $this->l('Use only for debug'),
+ 'desc' => sprintf(
+ $this->l('Enable loging for debuging module, see file %s'),
+ _PS_MODULE_DIR_ . 'eicaptcha/logs/debug.log'
+ ),
+ 'required' => false,
+ 'class' => 't',
+ 'is_bool' => true,
+ 'values' => [
+ [
+ 'id' => 'active_on',
+ 'value' => 1,
+ 'label' => $this->l('Enabled'),
+ ],
+ [
+ 'id' => 'active_off',
+ 'value' => 0,
+ 'label' => $this->l('Disabled'),
+ ],
+ ],
+ 'tab' => 'advanced',
+ ],
+ [
+ 'type' => 'switch',
+ 'name' => 'CAPTCHA_LOAD_EVERYWHERE',
+ 'label' => $this->l('Load Recaptcha library everywhere'),
+ 'hint' => $this->l('Let this option disabled by default if you don\'t use a specific contact form'),
+ 'desc' => $this->l('Only If you use a theme with elementor or warehouse and your contact form is not on the default page, enable this option.'),
+ 'required' => false,
+ 'class' => 't',
+ 'is_bool' => true,
+ 'values' => [
+ [
+ 'id' => 'active_on',
+ 'value' => 1,
+ 'label' => $this->l('Enabled'),
+ ],
+ [
+ 'id' => 'active_off',
+ 'value' => 0,
+ 'label' => $this->l('Disabled'),
+ ],
+ ],
+ 'tab' => 'advanced',
+ ],
+ [
+ 'type' => 'html',
+ 'label' => $this->l('Check module installation'),
+ 'name' => 'enable_debug_html',
+ 'html_content' => '' . $this->l('Check if module is well installed') . '',
+ 'desc' => $this->l('click on this link will reload the page, please go again in tab "advanced parameters" to see the results'),
+ 'tab' => 'advanced',
+ ],
+ ];
+ }
+
/**
* Post Process in back office
*
@@ -365,25 +384,32 @@ public function renderForm()
public function postProcess()
{
if (Tools::isSubmit('SubmitCaptchaConfiguration')) {
+ // Validate specific fields if needed
$minScore = Tools::getValue('CAPTCHA_V3_MINIMAL_SCORE');
-
- if (!is_numeric($minScore) || $minScore < 0 || $minScore > 1) {
+ if (!empty($minScore) && (!is_numeric($minScore) || $minScore < 0 || $minScore > 1)) {
return $this->module->displayError($this->l('The V3 minimal score must be a number between 0 and 1'));
}
- Configuration::updateValue('CAPTCHA_VERSION', Tools::getValue('CAPTCHA_VERSION'));
- Configuration::updateValue('CAPTCHA_V3_MINIMAL_SCORE', (float) $minScore);
- Configuration::updateValue('CAPTCHA_PUBLIC_KEY', Tools::getValue('CAPTCHA_PUBLIC_KEY'));
- Configuration::updateValue('CAPTCHA_PRIVATE_KEY', Tools::getValue('CAPTCHA_PRIVATE_KEY'));
- Configuration::updateValue('CAPTCHA_ENABLE_LOGGED_CUSTOMERS', Tools::getValue('CAPTCHA_ENABLE_LOGGED_CUSTOMERS'));
- Configuration::updateValue('CAPTCHA_ENABLE_ACCOUNT', (int) Tools::getValue('CAPTCHA_ENABLE_ACCOUNT'));
- Configuration::updateValue('CAPTCHA_ENABLE_CONTACT', (int) Tools::getValue('CAPTCHA_ENABLE_CONTACT'));
- Configuration::updateValue('CAPTCHA_ENABLE_NEWSLETTER', (int) Tools::getValue('CAPTCHA_ENABLE_NEWSLETTER'));
- Configuration::updateValue('CAPTCHA_FORCE_LANG', Tools::getValue('CAPTCHA_FORCE_LANG'));
- Configuration::updateValue('CAPTCHA_THEME', (int) Tools::getValue('CAPTCHA_THEME'));
- Configuration::updateValue('CAPTCHA_DEBUG', (int) Tools::getValue('CAPTCHA_DEBUG'));
- Configuration::updateValue('CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE', (int) Tools::getValue('CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE'));
- Configuration::updateValue('CAPTCHA_LOAD_EVERYWHERE', (int) Tools::getValue('CAPTCHA_LOAD_EVERYWHERE'));
+ // Save CAPTCHA_PROVIDER first
+ Configuration::updateValue('CAPTCHA_PROVIDER', Tools::getValue('CAPTCHA_PROVIDER'));
+
+ // Save all configuration values dynamically
+ $allFields = $this->getAllConfigurationFields();
+
+ foreach ($allFields as $field) {
+ if (isset($field['name'])) {
+ $value = Tools::getValue($field['name']);
+
+ // Handle different field types
+ if (isset($field['type']) && $field['type'] === 'switch') {
+ $value = (int) $value;
+ } elseif (isset($field['type']) && $field['type'] === 'text' && $field['name'] === 'CAPTCHA_V3_MINIMAL_SCORE') {
+ $value = (float) $value;
+ }
+
+ Configuration::updateValue($field['name'], $value);
+ }
+ }
return $this->module->displayConfirmation($this->l('Settings updated'));
}
@@ -396,21 +422,113 @@ public function postProcess()
*/
public function getConfigFieldsValues()
{
- return [
- 'CAPTCHA_VERSION' => Tools::getValue('CAPTCHA_VERSION', Configuration::get('CAPTCHA_VERSION')),
- 'CAPTCHA_V3_MINIMAL_SCORE' => Tools::getValue('CAPTCHA_V3_MINIMAL_SCORE', Configuration::get('CAPTCHA_V3_MINIMAL_SCORE')),
- 'CAPTCHA_PRIVATE_KEY' => Tools::getValue('CAPTCHA_PRIVATE_KEY', Configuration::get('CAPTCHA_PRIVATE_KEY')),
- 'CAPTCHA_PUBLIC_KEY' => Tools::getValue('CAPTCHA_PUBLIC_KEY', Configuration::get('CAPTCHA_PUBLIC_KEY')),
- 'CAPTCHA_ENABLE_LOGGED_CUSTOMERS' => Tools::getValue('CAPTCHA_ENABLE_LOGGED_CUSTOMERS', Configuration::get('CAPTCHA_ENABLE_LOGGED_CUSTOMERS')),
- 'CAPTCHA_ENABLE_ACCOUNT' => Tools::getValue('CAPTCHA_ENABLE_ACCOUNT', Configuration::get('CAPTCHA_ENABLE_ACCOUNT')),
- 'CAPTCHA_ENABLE_CONTACT' => Tools::getValue('CAPTCHA_ENABLE_CONTACT', Configuration::get('CAPTCHA_ENABLE_CONTACT')),
- 'CAPTCHA_ENABLE_NEWSLETTER' => Tools::getValue('CAPTCHA_ENABLE_NEWSLETTER', Configuration::get('CAPTCHA_ENABLE_NEWSLETTER')),
- 'CAPTCHA_FORCE_LANG' => Tools::getValue('CAPTCHA_FORCE_LANG', Configuration::get('CAPTCHA_FORCE_LANG')),
- 'CAPTCHA_THEME' => Tools::getValue('CAPTCHA_THEME', Configuration::get('CAPTCHA_THEME')),
- 'CAPTCHA_DEBUG' => Tools::getValue('CAPTCHA_DEBUG', Configuration::get('CAPTCHA_DEBUG')),
- 'CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE' => Tools::getValue('CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE', Configuration::get('CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE')),
- 'CAPTCHA_LOAD_EVERYWHERE' => Tools::getValue('CAPTCHA_LOAD_EVERYWHERE', Configuration::get('CAPTCHA_LOAD_EVERYWHERE')),
+ $values = [];
+
+ // Get all configuration fields
+ $allFields = $this->getAllConfigurationFields();
+
+ foreach ($allFields as $field) {
+ if (isset($field['name'])) {
+ $default = '';
+
+ // Set default values for specific fields
+ if ($field['name'] === 'CAPTCHA_PROVIDER') {
+ $default = 'google_recaptcha';
+ } elseif ($field['name'] === 'CAPTCHA_MATH_DIFFICULTY') {
+ $default = 'easy';
+ } elseif ($field['name'] === 'CAPTCHA_TURNSTILE_THEME') {
+ $default = 'auto';
+ } elseif ($field['name'] === 'CAPTCHA_TURNSTILE_SIZE') {
+ $default = 'normal';
+ }
+
+ $values[$field['name']] = Tools::getValue(
+ $field['name'],
+ Configuration::get($field['name']) ?: $default
+ );
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Get all configuration fields from all sources
+ *
+ * @return array
+ *
+ * @since 3.0.0
+ */
+ protected function getAllConfigurationFields()
+ {
+ $allFields = [];
+
+ // Provider selector
+ $allFields[] = ['name' => 'CAPTCHA_PROVIDER'];
+
+ // Get all providers and their fields
+ try {
+ $providers = CaptchaFactory::getAllProviders($this->module);
+
+ foreach ($providers as $provider) {
+ $providerFields = $provider->getConfigFields();
+ $allFields = array_merge($allFields, $providerFields);
+ }
+ } catch (\Exception $e) {
+ // Continue even if providers fail to load
+ }
+
+ // Common fields
+ $commonFields = [
+ ['name' => 'CAPTCHA_ENABLE_LOGGED_CUSTOMERS'],
+ ['name' => 'CAPTCHA_ENABLE_CONTACT'],
+ ['name' => 'CAPTCHA_ENABLE_ACCOUNT'],
+ ['name' => 'CAPTCHA_ENABLE_NEWSLETTER'],
+ ['name' => 'CAPTCHA_FORCE_LANG'],
+ ['name' => 'CAPTCHA_THEME'],
];
+ $allFields = array_merge($allFields, $commonFields);
+
+ // Advanced fields
+ $advancedFields = [
+ ['name' => 'CAPTCHA_DEBUG', 'type' => 'switch'],
+ ['name' => 'CAPTCHA_LOAD_EVERYWHERE', 'type' => 'switch'],
+ ['name' => 'CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE', 'type' => 'switch'],
+ ];
+ $allFields = array_merge($allFields, $advancedFields);
+
+ return $allFields;
+ }
+
+ /**
+ * Get available captcha providers
+ *
+ * @return array
+ *
+ * @since 3.0.0
+ */
+ protected function getAvailableProviders()
+ {
+ $providers = [];
+
+ try {
+ $allProviders = CaptchaFactory::getAllProviders($this->module);
+
+ foreach ($allProviders as $provider) {
+ $providers[] = [
+ 'id' => $provider->getName(),
+ 'name' => $provider->getDisplayName(),
+ ];
+ }
+ } catch (\Exception $e) {
+ // Fallback to default provider
+ $providers[] = [
+ 'id' => 'google_recaptcha',
+ 'name' => 'Google reCAPTCHA',
+ ];
+ }
+
+ return $providers;
}
/**
diff --git a/src/Debugger.php b/src/Debugger.php
index 90e1e93..cff3424 100644
--- a/src/Debugger.php
+++ b/src/Debugger.php
@@ -19,6 +19,7 @@
use Configuration;
use EiCaptcha;
+use Eicaptcha\Module\Factory\CaptchaFactory;
use Module;
class Debugger
@@ -91,6 +92,7 @@ public function debugModuleInstall()
$overridesChecks = $this->checkOverrides();
$newsletterChecks = $this->checkNewsletter();
$hookHookDisplayCustomerAccountForm = $this->checkHookDisplayCustomerAccountForm();
+ $providerChecks = $this->checkProviders();
$errors = array_merge(
$errors,
@@ -98,7 +100,8 @@ public function debugModuleInstall()
$hookChecks['errors'],
$overridesChecks['errors'],
$hookHookDisplayCustomerAccountForm['errors'],
- $newsletterChecks['errors']
+ $newsletterChecks['errors'],
+ $providerChecks['errors']
);
$success = array_merge(
@@ -107,7 +110,8 @@ public function debugModuleInstall()
$hookChecks['success'],
$overridesChecks['success'],
$hookHookDisplayCustomerAccountForm['success'],
- $newsletterChecks['success']
+ $newsletterChecks['success'],
+ $providerChecks['success']
);
$this->module->getContext()->smarty->assign([
@@ -342,6 +346,71 @@ public function log($message)
}
}
+ /**
+ * Check the current captcha provider configuration
+ *
+ * @return array
+ *
+ * @since 3.0.0
+ */
+ protected function checkProviders()
+ {
+ $errors = $success = [];
+
+ $currentProvider = Configuration::get('CAPTCHA_PROVIDER');
+
+ if (empty($currentProvider)) {
+ $errors[] = $this->l('No captcha provider selected');
+
+ return [
+ 'errors' => $errors,
+ 'success' => $success,
+ ];
+ }
+
+ try {
+ $provider = CaptchaFactory::create($this->module, $currentProvider);
+
+ $success[] = sprintf(
+ $this->l('Current captcha provider: %s'),
+ $provider->getDisplayName()
+ );
+
+ if ($provider->isConfigured()) {
+ $success[] = sprintf(
+ $this->l('Provider %s is properly configured'),
+ $provider->getDisplayName()
+ );
+ } else {
+ $errors[] = sprintf(
+ $this->l('Provider %s is not properly configured. Please check your API keys.'),
+ $provider->getDisplayName()
+ );
+ }
+
+ // Check provider-specific requirements
+ switch ($currentProvider) {
+ case 'google_enterprise':
+ if (!class_exists('\Google\Cloud\RecaptchaEnterprise\V1\RecaptchaEnterpriseServiceClient')) {
+ $errors[] = $this->l('Google Cloud reCAPTCHA Enterprise library is not installed. Run: composer require google/cloud-recaptcha-enterprise');
+ } else {
+ $success[] = $this->l('Google Cloud reCAPTCHA Enterprise library is installed');
+ }
+ break;
+ }
+ } catch (\Exception $e) {
+ $errors[] = sprintf(
+ $this->l('Error loading provider: %s'),
+ $e->getMessage()
+ );
+ }
+
+ return [
+ 'errors' => $errors,
+ 'success' => $success,
+ ];
+ }
+
/**
* Alias of l function with specific context
*
diff --git a/src/Factory/CaptchaFactory.php b/src/Factory/CaptchaFactory.php
new file mode 100644
index 0000000..e536e92
--- /dev/null
+++ b/src/Factory/CaptchaFactory.php
@@ -0,0 +1,167 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Factory;
+
+use Configuration;
+use EiCaptcha;
+use Eicaptcha\Module\Provider\CaptchaProviderInterface;
+use Eicaptcha\Module\Provider\GoogleRecaptchaProvider;
+use Eicaptcha\Module\Provider\GoogleEnterpriseProvider;
+use Eicaptcha\Module\Provider\HCaptchaProvider;
+use Eicaptcha\Module\Provider\MathCaptchaProvider;
+use Eicaptcha\Module\Provider\TurnstileProvider;
+
+/**
+ * Class CaptchaFactory
+ *
+ * Factory for creating captcha provider instances
+ *
+ * @since 3.0.0
+ */
+class CaptchaFactory
+{
+ /**
+ * @var array Map of provider names to their class names
+ */
+ private static $providers = [
+ 'google_recaptcha' => GoogleRecaptchaProvider::class,
+ 'google_enterprise' => GoogleEnterpriseProvider::class,
+ 'hcaptcha' => HCaptchaProvider::class,
+ 'math' => MathCaptchaProvider::class,
+ 'turnstile' => TurnstileProvider::class,
+ ];
+
+ /**
+ * @var array Cached provider instances
+ */
+ private static $instances = [];
+
+ /**
+ * Create a captcha provider instance based on configuration
+ *
+ * @param EiCaptcha $module Module instance
+ * @param string|null $providerName Provider name (null = auto-detect from config)
+ *
+ * @return CaptchaProviderInterface
+ *
+ * @throws \Exception If provider is not found
+ */
+ public static function create(EiCaptcha $module, $providerName = null)
+ {
+ // Auto-detect provider from configuration
+ if ($providerName === null) {
+ $providerName = Configuration::get('CAPTCHA_PROVIDER');
+
+ // Backward compatibility: if no provider is set, use google_recaptcha
+ if (empty($providerName)) {
+ $providerName = 'google_recaptcha';
+ }
+ }
+
+ // Return cached instance if available
+ if (isset(self::$instances[$providerName])) {
+ return self::$instances[$providerName];
+ }
+
+ // Check if provider exists
+ if (!isset(self::$providers[$providerName])) {
+ throw new \Exception(sprintf('Captcha provider "%s" not found', $providerName));
+ }
+
+ $providerClass = self::$providers[$providerName];
+
+ // Check if class exists
+ if (!class_exists($providerClass)) {
+ throw new \Exception(sprintf('Captcha provider class "%s" not found', $providerClass));
+ }
+
+ // Create and cache instance
+ $provider = new $providerClass($module);
+ self::$instances[$providerName] = $provider;
+
+ return $provider;
+ }
+
+ /**
+ * Get all available providers
+ *
+ * @param EiCaptcha $module Module instance
+ *
+ * @return array Array of provider instances indexed by provider name
+ */
+ public static function getAllProviders(EiCaptcha $module)
+ {
+ $providers = [];
+
+ foreach (self::$providers as $name => $class) {
+ try {
+ $providers[$name] = self::create($module, $name);
+ } catch (\Exception $e) {
+ // Skip providers that cannot be instantiated
+ continue;
+ }
+ }
+
+ return $providers;
+ }
+
+ /**
+ * Register a new provider
+ *
+ * @param string $name Provider name
+ * @param string $className Provider class name
+ *
+ * @return void
+ */
+ public static function registerProvider($name, $className)
+ {
+ self::$providers[$name] = $className;
+ }
+
+ /**
+ * Check if a provider is registered
+ *
+ * @param string $name Provider name
+ *
+ * @return bool
+ */
+ public static function hasProvider($name)
+ {
+ return isset(self::$providers[$name]);
+ }
+
+ /**
+ * Get the list of registered provider names
+ *
+ * @return array
+ */
+ public static function getProviderNames()
+ {
+ return array_keys(self::$providers);
+ }
+
+ /**
+ * Clear cached instances
+ *
+ * @return void
+ */
+ public static function clearCache()
+ {
+ self::$instances = [];
+ }
+}
diff --git a/src/Installer.php b/src/Installer.php
index 8600d9d..76ddbcb 100644
--- a/src/Installer.php
+++ b/src/Installer.php
@@ -97,7 +97,13 @@ protected function installConfigurations()
&& Configuration::updateValue('CAPTCHA_ENABLE_LOGGED_CUSTOMERS', 1)
&& Configuration::updateValue('CAPTCHA_USE_AUTHCONTROLLER_OVERRIDE',
version_compare(_PS_VERSION_, '8.0') < 0 ? '1' : '0'
- );
+ )
+ && Configuration::updateValue('CAPTCHA_PROVIDER', 'google_recaptcha')
+ && Configuration::updateValue('CAPTCHA_VERSION', 2)
+ && Configuration::updateValue('CAPTCHA_V3_MINIMAL_SCORE', 0.5)
+ && Configuration::updateValue('CAPTCHA_MATH_DIFFICULTY', 'easy')
+ && Configuration::updateValue('CAPTCHA_TURNSTILE_THEME', 'auto')
+ && Configuration::updateValue('CAPTCHA_TURNSTILE_SIZE', 'normal');
}
/**
diff --git a/src/Provider/AbstractCaptchaProvider.php b/src/Provider/AbstractCaptchaProvider.php
new file mode 100644
index 0000000..d9df98a
--- /dev/null
+++ b/src/Provider/AbstractCaptchaProvider.php
@@ -0,0 +1,209 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+use Configuration;
+use Context;
+use EiCaptcha;
+
+/**
+ * Class AbstractCaptchaProvider
+ *
+ * Base class with common functionality for all captcha providers
+ *
+ * @since 3.0.0
+ */
+abstract class AbstractCaptchaProvider implements CaptchaProviderInterface
+{
+ /**
+ * @var EiCaptcha
+ */
+ protected $module;
+
+ /**
+ * @var Context
+ */
+ protected $context;
+
+ /**
+ * @var string Last error message
+ */
+ protected $lastError = '';
+
+ /**
+ * @var array Themes available for providers that support theming
+ */
+ protected $themes = [0 => 'light', 1 => 'dark'];
+
+ /**
+ * @var string Captcha language
+ */
+ protected $captchaLang = 'en';
+
+ /**
+ * Constructor
+ *
+ * @param EiCaptcha $module
+ */
+ public function __construct(EiCaptcha $module)
+ {
+ $this->module = $module;
+ $this->context = $module->getContext();
+ $this->captchaLang = $this->context->language->iso_code;
+
+ // Allow forcing a specific language
+ $forceLang = Configuration::get('CAPTCHA_FORCE_LANG');
+ if (!empty($forceLang) && \Validate::isLanguageIsoCode($forceLang)) {
+ $this->captchaLang = $forceLang;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLastError()
+ {
+ return $this->lastError;
+ }
+
+ /**
+ * Set the last error message
+ *
+ * @param string $error
+ */
+ protected function setLastError($error)
+ {
+ $this->lastError = $error;
+ $this->log($error);
+ }
+
+ /**
+ * Check if captcha should be displayed to the current customer
+ *
+ * @return bool
+ */
+ public function shouldDisplayToCustomer()
+ {
+ if (
+ Configuration::get('CAPTCHA_ENABLE_LOGGED_CUSTOMERS') == 0
+ && $this->context->customer->id > 0
+ && $this->context->customer->email != null
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Log a message if debug mode is enabled
+ *
+ * @param string $message
+ */
+ protected function log($message)
+ {
+ if (Configuration::get('CAPTCHA_DEBUG')) {
+ $this->module->getDebugger()->log(sprintf('[%s] %s', $this->getName(), $message));
+ }
+ }
+
+ /**
+ * Get the theme selected in configuration
+ *
+ * @return string Theme name (light or dark)
+ */
+ protected function getTheme()
+ {
+ return $this->themes[Configuration::get('CAPTCHA_THEME')];
+ }
+
+ /**
+ * Get the captcha language
+ *
+ * @return string Language ISO code
+ */
+ protected function getCaptchaLang()
+ {
+ return $this->captchaLang;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getCssFiles()
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getJsFiles()
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResponseFieldName()
+ {
+ // Default to Google reCAPTCHA field name for backward compatibility
+ return 'g-recaptcha-response';
+ }
+
+ /**
+ * Translate a string in the module context
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ protected function l($string)
+ {
+ return $this->module->l($string, strtolower(str_replace('Provider', '', basename(get_class($this)))));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplateVars()
+ {
+ return [
+ 'displayCaptcha' => $this->shouldDisplayToCustomer(),
+ 'provider' => $this->getName(),
+ 'captchalang' => $this->captchaLang,
+ 'captchatheme' => $this->getTheme(),
+ ];
+ }
+
+ /**
+ * Get configuration value specific to this provider
+ *
+ * @param string $key Configuration key
+ * @param mixed $default Default value if not set
+ *
+ * @return mixed Configuration value
+ */
+ protected function getConfig($key, $default = null)
+ {
+ $value = Configuration::get($key);
+
+ return $value !== false ? $value : $default;
+ }
+}
diff --git a/src/Provider/CaptchaProviderInterface.php b/src/Provider/CaptchaProviderInterface.php
new file mode 100644
index 0000000..ddb6eb0
--- /dev/null
+++ b/src/Provider/CaptchaProviderInterface.php
@@ -0,0 +1,118 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+/**
+ * Interface CaptchaProviderInterface
+ *
+ * Defines the contract for all captcha providers
+ *
+ * @since 3.0.0
+ */
+interface CaptchaProviderInterface
+{
+ /**
+ * Get the provider name/identifier
+ *
+ * @return string Provider identifier (e.g., 'google_recaptcha', 'hcaptcha', 'math')
+ */
+ public function getName();
+
+ /**
+ * Get the provider display name for UI
+ *
+ * @return string Provider display name (e.g., 'Google reCAPTCHA', 'hCaptcha')
+ */
+ public function getDisplayName();
+
+ /**
+ * Validate the captcha response
+ *
+ * @param string $response The captcha response token from the form
+ * @param string $remoteIp The user's IP address
+ *
+ * @return bool True if validation succeeds, false otherwise
+ */
+ public function validate($response, $remoteIp);
+
+ /**
+ * Get configuration fields specific to this provider
+ * Returns an array of HelperForm field definitions
+ *
+ * @return array Configuration fields array compatible with PrestaShop HelperForm
+ */
+ public function getConfigFields();
+
+ /**
+ * Get JavaScript/HTML to include in the page header
+ *
+ * @param array $context Additional context (controller, form type, etc.)
+ *
+ * @return string HTML/JavaScript code to include in header
+ */
+ public function renderHeader(array $context = []);
+
+ /**
+ * Get template variables for rendering the captcha field
+ *
+ * @return array Smarty template variables
+ */
+ public function getTemplateVars();
+
+ /**
+ * Check if the provider is properly configured
+ *
+ * @return bool True if all required configuration is present
+ */
+ public function isConfigured();
+
+ /**
+ * Get the template file path for this provider
+ *
+ * @return string Template file path relative to module directory
+ */
+ public function getTemplatePath();
+
+ /**
+ * Get the last error message (if validation failed)
+ *
+ * @return string Error message or empty string
+ */
+ public function getLastError();
+
+ /**
+ * Get additional CSS files required by this provider
+ *
+ * @return array Array of CSS file paths
+ */
+ public function getCssFiles();
+
+ /**
+ * Get additional JS files required by this provider
+ *
+ * @return array Array of JS file paths
+ */
+ public function getJsFiles();
+
+ /**
+ * Get the name of the form field containing the captcha response
+ *
+ * @return string Field name (e.g., 'g-recaptcha-response', 'cf-turnstile-response')
+ */
+ public function getResponseFieldName();
+}
diff --git a/src/Provider/GoogleEnterpriseProvider.php b/src/Provider/GoogleEnterpriseProvider.php
new file mode 100644
index 0000000..b7d7703
--- /dev/null
+++ b/src/Provider/GoogleEnterpriseProvider.php
@@ -0,0 +1,193 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+use Configuration;
+use ContactController;
+
+/**
+ * Class GoogleEnterpriseProvider
+ *
+ * Provider for Google reCAPTCHA Enterprise
+ *
+ * Note: This provider extends GoogleRecaptchaProvider as the frontend implementation
+ * is identical. The main difference is in the backend validation which uses the
+ * Google Cloud reCAPTCHA Enterprise API.
+ *
+ * @since 3.0.0
+ */
+class GoogleEnterpriseProvider extends GoogleRecaptchaProvider
+{
+ /**
+ * @inheritDoc
+ */
+ public function getName()
+ {
+ return 'google_enterprise';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDisplayName()
+ {
+ return 'Google reCAPTCHA Enterprise';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($response, $remoteIp)
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ // Check if Google Cloud library is available
+ if (!class_exists('\Google\Cloud\RecaptchaEnterprise\V1\RecaptchaEnterpriseServiceClient')) {
+ $this->setLastError($this->l('Google Cloud reCAPTCHA Enterprise library is not installed. Please run: composer require google/cloud-recaptcha-enterprise'));
+ $this->log('Google Cloud reCAPTCHA Enterprise library not found');
+
+ // Fallback to standard reCAPTCHA validation
+ return parent::validate($response, $remoteIp);
+ }
+
+ try {
+ $projectId = $this->getConfig('CAPTCHA_GOOGLE_ENTERPRISE_PROJECT');
+ $apiKey = $this->getConfig('CAPTCHA_GOOGLE_ENTERPRISE_KEY');
+
+ if (empty($projectId) || empty($apiKey)) {
+ $this->setLastError($this->l('Google reCAPTCHA Enterprise is not properly configured'));
+
+ return false;
+ }
+
+ // Create the reCAPTCHA Enterprise client
+ $client = new \Google\Cloud\RecaptchaEnterprise\V1\RecaptchaEnterpriseServiceClient([
+ 'credentials' => $apiKey,
+ ]);
+
+ // Prepare the assessment request
+ $projectName = $client->projectName($projectId);
+ $event = (new \Google\Cloud\RecaptchaEnterprise\V1\Event())
+ ->setToken($response)
+ ->setSiteKey($this->getConfig('CAPTCHA_PUBLIC_KEY'));
+
+ $assessment = (new \Google\Cloud\RecaptchaEnterprise\V1\Assessment())
+ ->setEvent($event);
+
+ $request = (new \Google\Cloud\RecaptchaEnterprise\V1\CreateAssessmentRequest())
+ ->setParent($projectName)
+ ->setAssessment($assessment);
+
+ // Create the assessment
+ $response = $client->createAssessment($request);
+
+ // Check if the token is valid
+ if (!$response->getTokenProperties()->getValid()) {
+ $this->setLastError($this->l('Please validate the captcha field before submitting your request'));
+ $this->log('Invalid token: ' . $response->getTokenProperties()->getInvalidReason());
+
+ return false;
+ }
+
+ // Check the risk score (for V3)
+ $captchaVersion = $this->getConfig('CAPTCHA_VERSION', 2);
+ if ($captchaVersion == 3) {
+ $score = $response->getRiskAnalysis()->getScore();
+ $minScore = (float) $this->getConfig('CAPTCHA_V3_MINIMAL_SCORE', 0.5);
+
+ if ($score < $minScore) {
+ $errorMessage = sprintf(
+ 'Your request has been blocked by the captcha system, due to a low score of %s, required score is %s',
+ $score,
+ $minScore
+ );
+ $this->setLastError($this->l('Please validate the captcha field before submitting your request'));
+ $this->log($errorMessage);
+
+ return false;
+ }
+ }
+
+ $this->log($this->l('Captcha submitted with success'));
+
+ return true;
+ } catch (\Exception $e) {
+ $this->setLastError($this->l('Error validating captcha with Google Enterprise: ') . $e->getMessage());
+ $this->log('Google Enterprise validation error: ' . $e->getMessage());
+
+ // Fallback to standard validation in case of error
+ return parent::validate($response, $remoteIp);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigFields()
+ {
+ $fields = parent::getConfigFields();
+
+ // Add Enterprise-specific fields
+ $enterpriseFields = [
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Google Cloud Project ID'),
+ 'name' => 'CAPTCHA_GOOGLE_ENTERPRISE_PROJECT',
+ 'required' => true,
+ 'desc' => $this->l('Your Google Cloud project ID where reCAPTCHA Enterprise is enabled'),
+ 'empty_message' => $this->l('Please fill the Google Cloud project ID'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'textarea',
+ 'label' => $this->l('Service Account Key JSON'),
+ 'name' => 'CAPTCHA_GOOGLE_ENTERPRISE_KEY',
+ 'required' => true,
+ 'desc' => $this->l('Paste the content of your service account JSON key file'),
+ 'empty_message' => $this->l('Please fill the service account key'),
+ 'tab' => 'general',
+ 'rows' => 10,
+ ],
+ ];
+
+ return array_merge($fields, $enterpriseFields);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isConfigured()
+ {
+ $publicKey = $this->getConfig('CAPTCHA_PUBLIC_KEY');
+ $projectId = $this->getConfig('CAPTCHA_GOOGLE_ENTERPRISE_PROJECT');
+ $apiKey = $this->getConfig('CAPTCHA_GOOGLE_ENTERPRISE_KEY');
+
+ return !empty($publicKey) && !empty($projectId) && !empty($apiKey);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplatePath()
+ {
+ // Use the same template as Google reCAPTCHA (frontend is identical)
+ return 'module:eicaptcha/views/templates/hook/providers/google_recaptcha.tpl';
+ }
+}
diff --git a/src/Provider/GoogleRecaptchaProvider.php b/src/Provider/GoogleRecaptchaProvider.php
new file mode 100644
index 0000000..cd52bf6
--- /dev/null
+++ b/src/Provider/GoogleRecaptchaProvider.php
@@ -0,0 +1,313 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+use Configuration;
+use ContactController;
+use ReCaptcha\ReCaptcha;
+use Tools;
+
+/**
+ * Class GoogleRecaptchaProvider
+ *
+ * Provider for Google reCAPTCHA v2 and v3
+ *
+ * @since 3.0.0
+ */
+class GoogleRecaptchaProvider extends AbstractCaptchaProvider
+{
+ /**
+ * @inheritDoc
+ */
+ public function getName()
+ {
+ return 'google_recaptcha';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDisplayName()
+ {
+ return 'Google reCAPTCHA v2/v3';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($response, $remoteIp)
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ $captchaVersion = $this->getConfig('CAPTCHA_VERSION', 2);
+ $captchaV3MinScore = (float) $this->getConfig('CAPTCHA_V3_MINIMAL_SCORE', 0.5);
+
+ // Fix issue if allow_url_fopen is set to 0
+ if (function_exists('ini_get') && !ini_get('allow_url_fopen')) {
+ $recaptchaMethod = new \ReCaptcha\RequestMethod\CurlPost();
+ } else {
+ $recaptchaMethod = null;
+ }
+
+ $captcha = new ReCaptcha($this->getConfig('CAPTCHA_PRIVATE_KEY'), $recaptchaMethod);
+
+ if ($captchaVersion == 3) {
+ $captcha->setScoreThreshold($captchaV3MinScore);
+ }
+
+ $result = $captcha->verify($response, $remoteIp);
+
+ if (!$result->isSuccess()) {
+ $errorMessage = $this->l('Please validate the captcha field before submitting your request');
+ $this->setLastError($errorMessage);
+ $this->log(sprintf($this->l('Recaptcha response %s'), print_r($result->getErrorCodes(), true)));
+
+ if ($captchaVersion == 3) {
+ if ($result->getScore() < $captchaV3MinScore) {
+ $errorMessageV3 = sprintf(
+ 'Your request has been blocked by the captcha system, due to a low score of %s, required score is %s',
+ $result->getScore(),
+ $captchaV3MinScore
+ );
+ $this->log($errorMessageV3);
+ }
+ }
+
+ return false;
+ }
+
+ $this->log($this->l('Captcha submitted with success'));
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigFields()
+ {
+ return [
+ [
+ 'type' => 'radio',
+ 'label' => $this->l('Recaptcha Version'),
+ 'name' => 'CAPTCHA_VERSION',
+ 'required' => true,
+ 'class' => 't',
+ 'values' => [
+ [
+ 'id' => 'v2',
+ 'value' => 2,
+ 'label' => $this->l('V2'),
+ ],
+ [
+ 'id' => 'v3',
+ 'value' => 3,
+ 'label' => $this->l('V3'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Captcha V3 minimum score'),
+ 'hint' => sprintf(
+ $this->l('The minimum score required to validate the captcha is a number between 0 and 1. Default is 0.5 (recommended: 0.5 for normal security, 0.3 for less strict, 0.7 for more strict). %s'),
+ 'Learn more'
+ ),
+ 'name' => 'CAPTCHA_V3_MINIMAL_SCORE',
+ 'required' => true,
+ 'class' => 'fixed-width-sm',
+ 'suffix' => '(0.0 - 1.0)',
+ 'empty_message' => $this->l('Please fill the captcha v3 minimal score.'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Captcha public key (Site key)'),
+ 'name' => 'CAPTCHA_PUBLIC_KEY',
+ 'required' => true,
+ 'empty_message' => $this->l('Please fill the captcha public key'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Captcha private key (Secret key)'),
+ 'name' => 'CAPTCHA_PRIVATE_KEY',
+ 'required' => true,
+ 'empty_message' => $this->l('Please fill the captcha private key'),
+ 'tab' => 'general',
+ ],
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function renderHeader(array $context = [])
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return '';
+ }
+
+ $captchaVersion = $this->getConfig('CAPTCHA_VERSION', 2);
+
+ // Add Content box to contact form page in order to display captcha
+ if (isset($context['controller']) && $context['controller'] instanceof ContactController
+ && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
+ ) {
+ $this->context->controller->registerJavascript(
+ 'modules-eicaptcha-contact-form',
+ 'modules/' . $this->module->name . '/views/js/eicaptcha-contact-form-v' . $captchaVersion . '.js'
+ );
+ }
+
+ if ($captchaVersion == 2) {
+ return $this->renderHeaderV2();
+ } else {
+ return $this->renderHeaderV3();
+ }
+ }
+
+ /**
+ * Return content for (re)captcha v2
+ *
+ * @return string|void
+ */
+ protected function renderHeaderV2()
+ {
+ $controller = $this->context->controller;
+ $loadEverywhere = Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1;
+ $isAuthController = $controller instanceof \AuthController || $controller instanceof \RegistrationController;
+ $isContactController = $controller instanceof ContactController;
+
+ if ((
+ $isAuthController
+ && Configuration::get('CAPTCHA_ENABLE_ACCOUNT') == 1
+ )
+ ||
+ ($isContactController
+ && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
+ )
+ || $loadEverywhere
+ ) {
+ $this->context->controller->registerStylesheet(
+ 'module-eicaptcha',
+ 'modules/' . $this->module->name . '/views/css/eicaptcha.css'
+ );
+
+ // Dynamic insertion of the content
+ $publicKey = $this->getConfig('CAPTCHA_PUBLIC_KEY');
+ $theme = $this->getTheme();
+ $lang = $this->getCaptchaLang();
+
+ $js = '';
+
+ if ($isContactController && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1) {
+ $js .= '';
+ }
+
+ return $js;
+ }
+
+ return '';
+ }
+
+ /**
+ * Return content for recaptcha v3
+ *
+ * @return string|void
+ */
+ protected function renderHeaderV3()
+ {
+ $controller = $this->context->controller;
+ $loadEverywhere = Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1;
+
+ if (
+ ($controller instanceof ContactController
+ && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
+ )
+ || $loadEverywhere
+ ) {
+ $publicKey = $this->getConfig('CAPTCHA_PUBLIC_KEY');
+ $js = '
+
+ ';
+
+ return $js;
+ }
+
+ return '';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplateVars()
+ {
+ $vars = parent::getTemplateVars();
+ $vars['captchaVersion'] = $this->getConfig('CAPTCHA_VERSION', 2);
+ $vars['publicKey'] = $this->getConfig('CAPTCHA_PUBLIC_KEY');
+
+ return $vars;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isConfigured()
+ {
+ $publicKey = $this->getConfig('CAPTCHA_PUBLIC_KEY');
+ $privateKey = $this->getConfig('CAPTCHA_PRIVATE_KEY');
+
+ return !empty($publicKey) && !empty($privateKey);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplatePath()
+ {
+ return 'module:eicaptcha/views/templates/hook/providers/google_recaptcha.tpl';
+ }
+}
diff --git a/src/Provider/HCaptchaProvider.php b/src/Provider/HCaptchaProvider.php
new file mode 100644
index 0000000..f13942e
--- /dev/null
+++ b/src/Provider/HCaptchaProvider.php
@@ -0,0 +1,217 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+use Configuration;
+use ContactController;
+use Tools;
+
+/**
+ * Class HCaptchaProvider
+ *
+ * Provider for hCaptcha (privacy-focused alternative to reCAPTCHA)
+ *
+ * @since 3.0.0
+ */
+class HCaptchaProvider extends AbstractCaptchaProvider
+{
+ /**
+ * hCaptcha API verification endpoint
+ */
+ const VERIFY_URL = 'https://hcaptcha.com/siteverify';
+
+ /**
+ * @inheritDoc
+ */
+ public function getName()
+ {
+ return 'hcaptcha';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDisplayName()
+ {
+ return 'hCaptcha';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($response, $remoteIp)
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ if (empty($response)) {
+ $this->setLastError($this->l('Please validate the captcha field before submitting your request'));
+
+ return false;
+ }
+
+ $secretKey = $this->getConfig('CAPTCHA_HCAPTCHA_SECRET_KEY');
+
+ if (empty($secretKey)) {
+ $this->setLastError($this->l('hCaptcha secret key is not configured'));
+
+ return false;
+ }
+
+ // Prepare POST data
+ $data = [
+ 'secret' => $secretKey,
+ 'response' => $response,
+ 'remoteip' => $remoteIp,
+ ];
+
+ // Make API request
+ $options = [
+ 'http' => [
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => http_build_query($data),
+ ],
+ ];
+
+ $context = stream_context_create($options);
+ $verify = file_get_contents(self::VERIFY_URL, false, $context);
+
+ if ($verify === false) {
+ $this->setLastError($this->l('Unable to verify captcha response with hCaptcha API'));
+
+ return false;
+ }
+
+ $verifyResponse = json_decode($verify, true);
+
+ if (!isset($verifyResponse['success']) || $verifyResponse['success'] !== true) {
+ $errorMessage = $this->l('Please validate the captcha field before submitting your request');
+ $this->setLastError($errorMessage);
+
+ if (isset($verifyResponse['error-codes'])) {
+ $this->log(sprintf($this->l('hCaptcha response errors: %s'), print_r($verifyResponse['error-codes'], true)));
+ }
+
+ return false;
+ }
+
+ $this->log($this->l('Captcha submitted with success'));
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigFields()
+ {
+ return [
+ [
+ 'type' => 'text',
+ 'label' => $this->l('hCaptcha Site Key'),
+ 'name' => 'CAPTCHA_HCAPTCHA_SITE_KEY',
+ 'required' => true,
+ 'desc' => $this->l('Get your site key from https://dashboard.hcaptcha.com/'),
+ 'empty_message' => $this->l('Please fill the hCaptcha site key'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('hCaptcha Secret Key'),
+ 'name' => 'CAPTCHA_HCAPTCHA_SECRET_KEY',
+ 'required' => true,
+ 'desc' => $this->l('Get your secret key from https://dashboard.hcaptcha.com/'),
+ 'empty_message' => $this->l('Please fill the hCaptcha secret key'),
+ 'tab' => 'general',
+ ],
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function renderHeader(array $context = [])
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return '';
+ }
+
+ $controller = $this->context->controller;
+ $loadEverywhere = Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1;
+ $isAuthController = $controller instanceof \AuthController || $controller instanceof \RegistrationController;
+ $isContactController = $controller instanceof ContactController;
+
+ if ((
+ $isAuthController
+ && Configuration::get('CAPTCHA_ENABLE_ACCOUNT') == 1
+ )
+ ||
+ ($isContactController
+ && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
+ )
+ || $loadEverywhere
+ ) {
+ $this->context->controller->registerStylesheet(
+ 'module-eicaptcha',
+ 'modules/' . $this->module->name . '/views/css/eicaptcha.css'
+ );
+
+ $siteKey = $this->getConfig('CAPTCHA_HCAPTCHA_SITE_KEY');
+ $lang = $this->getCaptchaLang();
+
+ // hCaptcha uses a different language code format
+ $js = '';
+
+ return $js;
+ }
+
+ return '';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplateVars()
+ {
+ $vars = parent::getTemplateVars();
+ $vars['siteKey'] = $this->getConfig('CAPTCHA_HCAPTCHA_SITE_KEY');
+
+ return $vars;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isConfigured()
+ {
+ $siteKey = $this->getConfig('CAPTCHA_HCAPTCHA_SITE_KEY');
+ $secretKey = $this->getConfig('CAPTCHA_HCAPTCHA_SECRET_KEY');
+
+ return !empty($siteKey) && !empty($secretKey);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplatePath()
+ {
+ return 'module:eicaptcha/views/templates/hook/providers/hcaptcha.tpl';
+ }
+}
diff --git a/src/Provider/MathCaptchaProvider.php b/src/Provider/MathCaptchaProvider.php
new file mode 100644
index 0000000..5a3cf35
--- /dev/null
+++ b/src/Provider/MathCaptchaProvider.php
@@ -0,0 +1,288 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+use Configuration;
+use Tools;
+
+/**
+ * Class MathCaptchaProvider
+ *
+ * Simple math-based captcha provider (no external dependencies)
+ *
+ * @since 3.0.0
+ */
+class MathCaptchaProvider extends AbstractCaptchaProvider
+{
+ /**
+ * Session key for storing the captcha answer
+ */
+ const SESSION_KEY = 'eicaptcha_math_answer';
+
+ /**
+ * Session key for storing the captcha timestamp
+ */
+ const SESSION_TIMESTAMP_KEY = 'eicaptcha_math_timestamp';
+
+ /**
+ * Session key for storing the captcha token (CSRF protection)
+ */
+ const SESSION_TOKEN_KEY = 'eicaptcha_math_token';
+
+ /**
+ * Captcha expiration time (5 minutes)
+ */
+ const EXPIRATION_TIME = 300;
+
+ /**
+ * @inheritDoc
+ */
+ public function getName()
+ {
+ return 'math';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDisplayName()
+ {
+ return 'Math Captcha';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($response, $remoteIp)
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ // Start session if not already started
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+
+ // Get user response
+ $userAnswer = Tools::getValue('math-captcha-response');
+ $userToken = Tools::getValue('math-captcha-token');
+
+ if (empty($userAnswer)) {
+ $this->setLastError($this->l('Please solve the math captcha'));
+
+ return false;
+ }
+
+ // Check if captcha exists in session
+ if (!isset($_SESSION[self::SESSION_KEY]) || !isset($_SESSION[self::SESSION_TIMESTAMP_KEY]) || !isset($_SESSION[self::SESSION_TOKEN_KEY])) {
+ $this->setLastError($this->l('Captcha session expired, please refresh the page'));
+
+ return false;
+ }
+
+ // Check token (CSRF protection)
+ if ($userToken !== $_SESSION[self::SESSION_TOKEN_KEY]) {
+ $this->setLastError($this->l('Invalid captcha token'));
+ $this->log('Invalid captcha token - possible CSRF attack');
+
+ return false;
+ }
+
+ // Check expiration
+ $timestamp = $_SESSION[self::SESSION_TIMESTAMP_KEY];
+ if (time() - $timestamp > self::EXPIRATION_TIME) {
+ unset($_SESSION[self::SESSION_KEY], $_SESSION[self::SESSION_TIMESTAMP_KEY], $_SESSION[self::SESSION_TOKEN_KEY]);
+ $this->setLastError($this->l('Captcha expired, please refresh the page'));
+
+ return false;
+ }
+
+ // Validate answer
+ $correctAnswer = $_SESSION[self::SESSION_KEY];
+ $isValid = ((int) $userAnswer === $correctAnswer);
+
+ // Clear session (one-time use)
+ unset($_SESSION[self::SESSION_KEY], $_SESSION[self::SESSION_TIMESTAMP_KEY], $_SESSION[self::SESSION_TOKEN_KEY]);
+
+ if (!$isValid) {
+ $this->setLastError($this->l('Incorrect answer to the math captcha'));
+ $this->log(sprintf('Math captcha failed: expected %s, got %s', $correctAnswer, $userAnswer));
+
+ return false;
+ }
+
+ $this->log($this->l('Captcha submitted with success'));
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigFields()
+ {
+ return [
+ [
+ 'type' => 'radio',
+ 'label' => $this->l('Math Captcha Difficulty'),
+ 'name' => 'CAPTCHA_MATH_DIFFICULTY',
+ 'required' => true,
+ 'values' => [
+ [
+ 'id' => 'easy',
+ 'value' => 'easy',
+ 'label' => $this->l('Easy (1-10)'),
+ ],
+ [
+ 'id' => 'medium',
+ 'value' => 'medium',
+ 'label' => $this->l('Medium (1-50)'),
+ ],
+ [
+ 'id' => 'hard',
+ 'value' => 'hard',
+ 'label' => $this->l('Hard (1-100)'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function renderHeader(array $context = [])
+ {
+ // No external scripts needed for math captcha
+ return '';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplateVars()
+ {
+ $vars = parent::getTemplateVars();
+
+ // Generate new math question
+ $mathData = $this->generateMathQuestion();
+
+ $vars['mathQuestion'] = $mathData['question'];
+ $vars['mathToken'] = $mathData['token'];
+
+ return $vars;
+ }
+
+ /**
+ * Generate a new math question
+ *
+ * @return array Array with 'question', 'answer', and 'token'
+ */
+ protected function generateMathQuestion()
+ {
+ // Start session if not already started
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+
+ $difficulty = $this->getConfig('CAPTCHA_MATH_DIFFICULTY', 'easy');
+
+ // Determine number range based on difficulty
+ switch ($difficulty) {
+ case 'hard':
+ $max = 100;
+ break;
+ case 'medium':
+ $max = 50;
+ break;
+ case 'easy':
+ default:
+ $max = 10;
+ break;
+ }
+
+ // Generate random numbers
+ $num1 = rand(1, $max);
+ $num2 = rand(1, $max);
+
+ // Random operation (addition or subtraction)
+ $operations = ['+', '-'];
+ $operation = $operations[array_rand($operations)];
+
+ // Ensure no negative results for subtraction
+ if ($operation === '-' && $num1 < $num2) {
+ $temp = $num1;
+ $num1 = $num2;
+ $num2 = $temp;
+ }
+
+ // Calculate answer
+ switch ($operation) {
+ case '+':
+ $answer = $num1 + $num2;
+ break;
+ case '-':
+ $answer = $num1 - $num2;
+ break;
+ default:
+ $answer = $num1 + $num2;
+ }
+
+ // Generate CSRF token
+ $token = bin2hex(random_bytes(16));
+
+ // Store in session
+ $_SESSION[self::SESSION_KEY] = $answer;
+ $_SESSION[self::SESSION_TIMESTAMP_KEY] = time();
+ $_SESSION[self::SESSION_TOKEN_KEY] = $token;
+
+ // Return question
+ return [
+ 'question' => sprintf('%d %s %d', $num1, $operation, $num2),
+ 'answer' => $answer,
+ 'token' => $token,
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isConfigured()
+ {
+ // Math captcha doesn't require external configuration
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplatePath()
+ {
+ return 'module:eicaptcha/views/templates/hook/providers/math.tpl';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResponseFieldName()
+ {
+ return 'math-captcha-response';
+ }
+}
diff --git a/src/Provider/TurnstileProvider.php b/src/Provider/TurnstileProvider.php
new file mode 100644
index 0000000..c53253a
--- /dev/null
+++ b/src/Provider/TurnstileProvider.php
@@ -0,0 +1,274 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+namespace Eicaptcha\Module\Provider;
+
+use Configuration;
+use ContactController;
+use Tools;
+
+/**
+ * Class TurnstileProvider
+ *
+ * Provider for Cloudflare Turnstile
+ * Free, privacy-friendly alternative to reCAPTCHA
+ *
+ * @since 3.0.0
+ */
+class TurnstileProvider extends AbstractCaptchaProvider
+{
+ /**
+ * Cloudflare Turnstile API verification endpoint
+ */
+ const VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
+
+ /**
+ * @inheritDoc
+ */
+ public function getName()
+ {
+ return 'turnstile';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDisplayName()
+ {
+ return 'Cloudflare Turnstile';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($response, $remoteIp)
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return true;
+ }
+
+ if (empty($response)) {
+ $this->setLastError($this->l('Please validate the captcha field before submitting your request'));
+
+ return false;
+ }
+
+ $secretKey = $this->getConfig('CAPTCHA_TURNSTILE_SECRET_KEY');
+
+ if (empty($secretKey)) {
+ $this->setLastError($this->l('Turnstile secret key is not configured'));
+
+ return false;
+ }
+
+ // Prepare POST data
+ $data = [
+ 'secret' => $secretKey,
+ 'response' => $response,
+ 'remoteip' => $remoteIp,
+ ];
+
+ // Make API request
+ $options = [
+ 'http' => [
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => http_build_query($data),
+ ],
+ ];
+
+ $context = stream_context_create($options);
+ $verify = file_get_contents(self::VERIFY_URL, false, $context);
+
+ if ($verify === false) {
+ $this->setLastError($this->l('Unable to verify captcha response with Cloudflare Turnstile API'));
+
+ return false;
+ }
+
+ $verifyResponse = json_decode($verify, true);
+
+ if (!isset($verifyResponse['success']) || $verifyResponse['success'] !== true) {
+ $errorMessage = $this->l('Please validate the captcha field before submitting your request');
+ $this->setLastError($errorMessage);
+
+ if (isset($verifyResponse['error-codes'])) {
+ $this->log(sprintf($this->l('Turnstile response errors: %s'), print_r($verifyResponse['error-codes'], true)));
+ }
+
+ return false;
+ }
+
+ $this->log($this->l('Captcha submitted with success'));
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigFields()
+ {
+ return [
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Turnstile Site Key'),
+ 'name' => 'CAPTCHA_TURNSTILE_SITE_KEY',
+ 'required' => true,
+ 'desc' => sprintf(
+ $this->l('Get your site key from %s'),
+ 'Cloudflare Dashboard'
+ ),
+ 'empty_message' => $this->l('Please fill the Turnstile site key'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'text',
+ 'label' => $this->l('Turnstile Secret Key'),
+ 'name' => 'CAPTCHA_TURNSTILE_SECRET_KEY',
+ 'required' => true,
+ 'desc' => $this->l('Your secret key from Cloudflare Dashboard'),
+ 'empty_message' => $this->l('Please fill the Turnstile secret key'),
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'radio',
+ 'label' => $this->l('Widget Theme'),
+ 'name' => 'CAPTCHA_TURNSTILE_THEME',
+ 'required' => true,
+ 'values' => [
+ [
+ 'id' => 'light',
+ 'value' => 'light',
+ 'label' => $this->l('Light'),
+ ],
+ [
+ 'id' => 'dark',
+ 'value' => 'dark',
+ 'label' => $this->l('Dark'),
+ ],
+ [
+ 'id' => 'auto',
+ 'value' => 'auto',
+ 'label' => $this->l('Auto (matches system theme)'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ [
+ 'type' => 'radio',
+ 'label' => $this->l('Widget Size'),
+ 'name' => 'CAPTCHA_TURNSTILE_SIZE',
+ 'required' => true,
+ 'values' => [
+ [
+ 'id' => 'normal',
+ 'value' => 'normal',
+ 'label' => $this->l('Normal'),
+ ],
+ [
+ 'id' => 'compact',
+ 'value' => 'compact',
+ 'label' => $this->l('Compact'),
+ ],
+ ],
+ 'tab' => 'general',
+ ],
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function renderHeader(array $context = [])
+ {
+ if (!$this->shouldDisplayToCustomer()) {
+ return '';
+ }
+
+ $controller = $this->context->controller;
+ $loadEverywhere = Configuration::get('CAPTCHA_LOAD_EVERYWHERE') == 1;
+ $isAuthController = $controller instanceof \AuthController || $controller instanceof \RegistrationController;
+ $isContactController = $controller instanceof ContactController;
+
+ if ((
+ $isAuthController
+ && Configuration::get('CAPTCHA_ENABLE_ACCOUNT') == 1
+ )
+ ||
+ ($isContactController
+ && Configuration::get('CAPTCHA_ENABLE_CONTACT') == 1
+ )
+ || $loadEverywhere
+ ) {
+ $this->context->controller->registerStylesheet(
+ 'module-eicaptcha',
+ 'modules/' . $this->module->name . '/views/css/eicaptcha.css'
+ );
+
+ $siteKey = $this->getConfig('CAPTCHA_TURNSTILE_SITE_KEY');
+
+ $js = '';
+
+ return $js;
+ }
+
+ return '';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplateVars()
+ {
+ $vars = parent::getTemplateVars();
+ $vars['siteKey'] = $this->getConfig('CAPTCHA_TURNSTILE_SITE_KEY');
+ $vars['turnstileTheme'] = $this->getConfig('CAPTCHA_TURNSTILE_THEME', 'auto');
+ $vars['turnstileSize'] = $this->getConfig('CAPTCHA_TURNSTILE_SIZE', 'normal');
+
+ return $vars;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isConfigured()
+ {
+ $siteKey = $this->getConfig('CAPTCHA_TURNSTILE_SITE_KEY');
+ $secretKey = $this->getConfig('CAPTCHA_TURNSTILE_SECRET_KEY');
+
+ return !empty($siteKey) && !empty($secretKey);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplatePath()
+ {
+ return 'module:eicaptcha/views/templates/hook/providers/turnstile.tpl';
+ }
+
+ /**
+ * Get the response field name for Turnstile
+ *
+ * @return string
+ */
+ public function getResponseFieldName()
+ {
+ return 'cf-turnstile-response';
+ }
+}
diff --git a/upgrade/upgrade-3.0.0.php b/upgrade/upgrade-3.0.0.php
new file mode 100644
index 0000000..41c1f61
--- /dev/null
+++ b/upgrade/upgrade-3.0.0.php
@@ -0,0 +1,62 @@
+ and contributors / https://github.com/nenes25/eicaptcha
+ * @copyright since 2013 Hervé HENNES
+ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License ("AFL") v. 3.0
+ */
+
+if (!defined('_PS_VERSION_')) {
+ exit;
+}
+
+/**
+ * Upgrade to version 3.0.0
+ * Migrates from single captcha system to multi-provider architecture
+ *
+ * @param EiCaptcha $module
+ *
+ * @return bool
+ */
+function upgrade_module_3_0_0($module)
+{
+ $result = true;
+
+ // Set default provider to google_recaptcha for backward compatibility
+ if (!Configuration::get('CAPTCHA_PROVIDER')) {
+ $result = $result && Configuration::updateValue('CAPTCHA_PROVIDER', 'google_recaptcha');
+ }
+
+ // Ensure CAPTCHA_VERSION is set (default to V2 if not set)
+ if (!Configuration::get('CAPTCHA_VERSION')) {
+ $result = $result && Configuration::updateValue('CAPTCHA_VERSION', 2);
+ }
+
+ // Ensure CAPTCHA_V3_MINIMAL_SCORE is set
+ if (!Configuration::get('CAPTCHA_V3_MINIMAL_SCORE')) {
+ $result = $result && Configuration::updateValue('CAPTCHA_V3_MINIMAL_SCORE', 0.5);
+ }
+
+ // Initialize new provider configurations
+ if (!Configuration::get('CAPTCHA_MATH_DIFFICULTY')) {
+ $result = $result && Configuration::updateValue('CAPTCHA_MATH_DIFFICULTY', 'easy');
+ }
+
+ // Log migration if debug is enabled
+ if (Configuration::get('CAPTCHA_DEBUG')) {
+ $debugger = $module->getDebugger();
+ $debugger->log('Module upgraded to version 3.0.0 - Multi-provider architecture enabled');
+ $debugger->log('Current provider: ' . Configuration::get('CAPTCHA_PROVIDER'));
+ }
+
+ return $result;
+}
diff --git a/views/templates/hook/hookDisplayCustomerAccountForm.tpl b/views/templates/hook/hookDisplayCustomerAccountForm.tpl
index 1a4a164..72a5950 100644
--- a/views/templates/hook/hookDisplayCustomerAccountForm.tpl
+++ b/views/templates/hook/hookDisplayCustomerAccountForm.tpl
@@ -23,29 +23,10 @@
* http://www.h-hennes.fr/blog/
*}
-
- {if $captchaVersion == 2}
-
-
- {**
- * Le contenu du captcha est automatiquement ajouté dans le selecteur #captcha-box
- * Captcha content is automaticaly added into the selector #captcha-box
- *}
-
-
-
+{* Load the provider-specific template *}
+{if isset($provider) && $provider}
+ {include file="module:eicaptcha/views/templates/hook/providers/{$provider}.tpl"}
+{else}
+ {* Fallback to google_recaptcha for backward compatibility *}
+ {include file="module:eicaptcha/views/templates/hook/providers/google_recaptcha.tpl"}
+{/if}
diff --git a/views/templates/hook/providers/google_recaptcha.tpl b/views/templates/hook/providers/google_recaptcha.tpl
new file mode 100644
index 0000000..e527a89
--- /dev/null
+++ b/views/templates/hook/providers/google_recaptcha.tpl
@@ -0,0 +1,53 @@
+{*
+* 2007-2016 PrestaShop
+*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* DISCLAIMER
+*
+* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
+* versions in the future. If you wish to customize PrestaShop for your
+* needs please refer to http://www.prestashop.com for more information.
+*
+* @author Hennes Hervé
+* @copyright Hennes Hervé
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* http://www.h-hennes.fr/blog/
+*}
+
+{if $displayCaptcha}
+
+ {if $captchaVersion == 2}
+
+
+ {**
+ * Le contenu du captcha est automatiquement ajouté dans le selecteur #captcha-box
+ * Captcha content is automaticaly added into the selector #captcha-box
+ *}
+
+
+
+ {else}
+
+
+
+ {/if}
+
+{/if}
diff --git a/views/templates/hook/providers/hcaptcha.tpl b/views/templates/hook/providers/hcaptcha.tpl
new file mode 100644
index 0000000..a8e8f81
--- /dev/null
+++ b/views/templates/hook/providers/hcaptcha.tpl
@@ -0,0 +1,25 @@
+{*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* @author Hennes Hervé
+* @copyright Hennes Hervé
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* http://www.h-hennes.fr/blog/
+*}
+
+{if $displayCaptcha}
+
+
+
+
+
+
+{/if}
diff --git a/views/templates/hook/providers/math.tpl b/views/templates/hook/providers/math.tpl
new file mode 100644
index 0000000..9e28275
--- /dev/null
+++ b/views/templates/hook/providers/math.tpl
@@ -0,0 +1,42 @@
+{*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* @author Hennes Hervé
+* @copyright Hennes Hervé
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* http://www.h-hennes.fr/blog/
+*}
+
+{if $displayCaptcha}
+
+
+
+
+
+
+
+
+
+ {l s='Please solve this simple math problem to verify you are human' mod='eicaptcha'}
+
+
+
+{/if}
diff --git a/views/templates/hook/providers/turnstile.tpl b/views/templates/hook/providers/turnstile.tpl
new file mode 100644
index 0000000..615a942
--- /dev/null
+++ b/views/templates/hook/providers/turnstile.tpl
@@ -0,0 +1,29 @@
+{*
+* NOTICE OF LICENSE
+*
+* This source file is subject to the Academic Free License (AFL 3.0)
+* that is bundled with this package in the file LICENSE.txt.
+* It is also available through the world-wide-web at this URL:
+* http://opensource.org/licenses/afl-3.0.php
+* If you did not receive a copy of the license and are unable to
+* obtain it through the world-wide-web, please send an email
+* to license@prestashop.com so we can send you a copy immediately.
+*
+* @author Hennes Hervé
+* @copyright Hennes Hervé
+* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
+* http://www.h-hennes.fr/blog/
+*}
+
+{if $displayCaptcha}
+