El post anterior comparó reCAPTCHA con las alternativas privacy-first y terminó recomendando Cloudflare Turnstile para tiendas PrestaShop 8 con tráfico UE. Este es el tutorial técnico: cómo integrar Turnstile en el formulario de contacto de PrestaShop 8, validar el token server-side, manejar errores, y los casos edge que se descubren en producción. Al final hay un atajo si prefieres no escribir el módulo desde cero.
Lo que necesitas antes de empezar
- Cuenta Cloudflare gratuita. No necesitas mover tu dominio a Cloudflare. La cuenta gratuita da acceso a Turnstile sin condiciones adicionales.
- Widget creado. Login →
dash.cloudflare.com → Turnstile → Add widget. Configura: hostname (tu dominio principal), modomanaged(default, recomendado), nombre identificativo. Cloudflare te genera Site Key y Secret Key. Cópialas — la Secret Key solo se muestra una vez. - PrestaShop 8.0+. Probado en 8.2.x. Las versiones 1.7 tienen un sistema de hooks distinto y no se cubre aquí.
- PHP 7.4+ con cURL habilitado. Lo necesitas para validar el token contra el endpoint de Cloudflare.
Estructura mínima del módulo
zeyvroturnstile/
├── zeyvroturnstile.php
├── config.xml
├── logo.png
├── controllers/
│ └── admin/
│ └── AdminZeyvroTurnstileController.php
├── views/
│ └── templates/
│ ├── admin/settings.tpl
│ └── front/turnstile_widget.tpl
├── sql/
│ ├── install.sql
│ └── uninstall.sql
└── upgrade/
Hooks que vas a usar
displayBeforeBodyClosingTag: para inyectar el script de Turnstile y el código que renderiza el widget en cualquier form de contacto que aparezca en la página. Se ejecuta justo antes de</body>, de modo que el DOM ya está listo.actionFrontControllerSetMedia: el momento limpio para encolar el script de Cloudflare con el atributodefery el data-attributes necesarios.actionContactFormSubmitBefore(o equivalente según versión PS): hook donde validar el token recibido contra Cloudflare antes de procesar el mensaje. Si el token falla, devolver false para abortar el envío.
Inyección del widget en el formulario
El widget de Turnstile se renderiza con un <div class="cf-turnstile" data-sitekey="...">. El script de Cloudflare lo detecta por el selector .cf-turnstile y lo monta automáticamente. Pero el form de contacto de PrestaShop ya está renderizado por el theme cuando llegamos al hook, así que tenemos dos opciones:
- Override del template del form (más limpio pero menos portable):
views/templates/contact/_partials/contact-form.tplcon el div del widget añadido antes del botón submit. - Inyección DOM via JavaScript en displayBeforeBodyClosingTag (menos limpio pero portable a cualquier theme): script que busca el form de contacto y le añade el div del widget. Es el que usaremos aquí porque es el que NO requiere overrides.
(function () {
function injectTurnstile() {
var forms = document.querySelectorAll('form[action*="contact-us"], form#contact-form');
forms.forEach(function (form) {
if (form.querySelector('.cf-turnstile')) return;
var div = document.createElement('div');
div.className = 'cf-turnstile';
div.setAttribute('data-sitekey', window.ZEYVRO_TURNSTILE_KEY);
div.setAttribute('data-theme', 'auto');
// Insertarlo antes del botón submit
var submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitBtn) {
submitBtn.parentNode.insertBefore(div, submitBtn);
} else {
form.appendChild(div);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectTurnstile);
} else {
injectTurnstile();
}
})();
Detalle: window.ZEYVRO_TURNSTILE_KEY se setea desde PHP en el hook con un echo <script>window.ZEYVRO_TURNSTILE_KEY = "..."</script> antes del JS. Esto evita hardcodear la Site Key.
Validación server-side del token
Cuando el usuario envía el formulario, el widget incluye un campo oculto cf-turnstile-response con un token. Hay que validar ese token contra el endpoint de Cloudflare antes de procesar el mensaje. Sin esta validación, un bot que envíe POST directo (saltándose el JS) pasa sin oposición.
public function hookActionContactFormSubmitBefore($params) {
$token = Tools::getValue('cf-turnstile-response');
if (empty($token)) {
return $this->rejectSubmit($params, 'missing_token');
}
$secret = Configuration::get('ZEYVROTURNSTILE_SECRET_KEY');
$remoteIp = Tools::getRemoteAddr();
$ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'secret' => $secret,
'response' => $token,
'remoteip' => $remoteIp,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if (empty($data['success'])) {
return $this->rejectSubmit($params, 'invalid_token', $data['error-codes'] ?? []);
}
return true;
}
Trampa importante: CURLOPT_TIMEOUT 5. Si Cloudflare tarda más de 5 segundos en responder (raro pero ocurre en mantenimientos), tu form se queda colgado. Configurar timeout corto + manejar el caso de timeout como «permitir el envío con flag de revisión manual» o «rechazar con mensaje». Decisión de diseño que debes tomar antes.
Logging para auditoría
Crear una tabla simple en BD para guardar las últimas 50 verificaciones (token, resultado, IP, timestamp). Útil para auditar qué bots están intentando pasar y para debug de falsos positivos.
CREATE TABLE IF NOT EXISTS `PREFIX_zeyvroturnstile_log` (
`id_log` INT AUTO_INCREMENT PRIMARY KEY,
`date_add` DATETIME NOT NULL,
`result` ENUM('ok', 'fail', 'missing', 'timeout') NOT NULL,
`error_codes` VARCHAR(255) NULL,
`remote_ip` VARCHAR(45) NULL,
`user_agent` VARCHAR(255) NULL,
INDEX idx_date (`date_add`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Mantener solo las últimas 50 entradas con un trigger DELETE o un cron que ejecute periódicamente. No hace falta retención larga; este log es para diagnóstico, no para compliance.
Casos edge que descubrirás en producción
- Form en página AJAX: si tu theme reemplaza el form de contacto vía AJAX (algunos themes hacen esto en popups), el script de Turnstile no detecta el nuevo div. Necesitas un MutationObserver o un trigger manual al re-render.
- Múltiples formularios en una página: si tu sitio tiene form de contacto en footer Y form de contacto en página dedicada, el widget de Turnstile renderiza una vez por div. Validación: verificar que cada submit tiene su propio token.
- Caché de página: si tu hosting cachea el HTML de la página entera, el Site Key embebido se cachea también. Eso está bien (es público). Pero si el caché incluye el token generado por Turnstile, problema. Asegurar que Turnstile inyecta tras el render del cache.
- Modo «non-interactive» vs «managed»: managed (default) hace pop-up de challenge solo cuando duda. Non-interactive nunca hace pop-up — falla silenciosamente si duda. Para form de contacto B2C, managed es correcto.
- Bots que renderizan JS: Puppeteer y similares pueden renderizar el widget. Turnstile los detecta por behavioral signals + IP reputation, no solo por presencia de JS. La tasa de éxito de bots sofisticados es <1% según métricas Cloudflare.
Atajo: módulo Zeyvro Turnstile para PS8
Si todo lo anterior te suena a 1-2 días de desarrollo más una semana de pruebas, hay un atajo: Zeyvro Turnstile v1.0.3. Módulo PS8 free MIT que cubre todos los puntos descritos:
- Inyección automática del widget en form de contacto detectado.
- Validación server-side con timeout configurable.
- 3 modos seleccionables (managed / non-interactive / invisible).
- Log de los últimos 50 intentos en BO → Servicio al Cliente → Anti SPAM.
- Configuración mínima: pegar Site Key + Secret Key + activar.
- Sin overrides del theme. Desinstalación limpia (drop tabla + clear config keys).
Disponible gratis, MIT, instalación en 30 segundos.
Esta serie continúa con un caso real: una tienda PS8 con bot signup masivo que redujo de forma notable sus intentos de spam tras integrar Turnstile. Si tu integración con Turnstile contradice algo de este tutorial o has descubierto un caso edge no listado, escríbenos a hola@zeyvro.com.
El módulo ya hace esto por ti: instalación en 2 minutos, sin tocar código.
Zeyvro Turnstile para PrestaShop 8 →Zeyvro desarrolla módulos para PrestaShop 8 desde una tienda real en producción. Código sin ofuscar.