Files
roi-theme/Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php
FrankZamora d135ec8a41 feat(api): implement recaptcha v3 anti-spam protection
- Add RecaptchaValidatorInterface and RecaptchaResult entity in Domain
- Create RecaptchaValidationService in Application layer
- Implement GoogleRecaptchaValidator for API integration
- Add recaptcha-settings schema and admin FormBuilder
- Integrate reCAPTCHA validation in NewsletterAjaxHandler
- Integrate reCAPTCHA validation in ContactFormAjaxHandler
- Update FooterRenderer and ContactFormRenderer with reCAPTCHA scripts
- Configure DIContainer with RecaptchaValidationService injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:01:46 -06:00

315 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Admin\RecaptchaSettings\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para reCAPTCHA Settings
*
* RESPONSABILIDAD: Generar formulario de configuracion de Google reCAPTCHA v3
* para proteccion anti-spam de formularios.
*
* @package ROITheme\Admin\RecaptchaSettings\Infrastructure\Ui
*/
final class RecaptchaSettingsFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildCredentialsGroup($componentId);
$html .= $this->buildBehaviorGroup($componentId);
$html .= $this->buildHelpSection();
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-shield-check me-2" style="color: #FF8600;"></i>';
$html .= ' Google reCAPTCHA v3';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Proteccion anti-spam invisible para formularios';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="recaptcha-settings">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Estado';
$html .= ' </h5>';
$isEnabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', false);
$html .= $this->buildToggle(
'recaptchaIsEnabled',
'Habilitar reCAPTCHA v3',
$isEnabled,
'Activa la proteccion reCAPTCHA en Newsletter y Formulario de Contacto'
);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildCredentialsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-key me-2" style="color: #FF8600;"></i>';
$html .= ' Credenciales';
$html .= ' </h5>';
$html .= ' <div class="row g-3">';
// Site Key
$html .= ' <div class="col-md-6">';
$siteKey = $this->renderer->getFieldValue($componentId, 'credentials', 'site_key', '');
$html .= $this->buildTextInput(
'recaptchaSiteKey',
'Site Key (Clave del sitio)',
$siteKey,
'Clave publica visible en frontend'
);
$html .= ' </div>';
// Secret Key
$html .= ' <div class="col-md-6">';
$secretKey = $this->renderer->getFieldValue($componentId, 'credentials', 'secret_key', '');
$html .= $this->buildPasswordInput(
'recaptchaSecretKey',
'Secret Key (Clave secreta)',
$secretKey,
'Clave privada - NUNCA se expone en frontend'
);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="alert alert-warning small mb-0 mt-3">';
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
$html .= ' Obtiene tus claves en <a href="https://www.google.com/recaptcha/admin" target="_blank" class="alert-link">Google reCAPTCHA Admin</a>. ';
$html .= ' Asegurate de seleccionar <strong>reCAPTCHA v3</strong>.';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildBehaviorGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-sliders me-2" style="color: #FF8600;"></i>';
$html .= ' Comportamiento';
$html .= ' </h5>';
$html .= ' <div class="row g-3">';
// Score Threshold
$html .= ' <div class="col-md-6">';
$threshold = $this->renderer->getFieldValue($componentId, 'behavior', 'score_threshold', '0.5');
$html .= $this->buildSelect(
'recaptchaScoreThreshold',
'Umbral de Score',
$threshold,
[
'0.3' => '0.3 - Permisivo (menos bloqueos)',
'0.5' => '0.5 - Balanceado (recomendado)',
'0.7' => '0.7 - Estricto',
'0.9' => '0.9 - Muy estricto (mas bloqueos)'
],
'Score minimo para considerar humano (0=bot, 1=humano)'
);
$html .= ' </div>';
// Fail Open
$html .= ' <div class="col-md-6">';
$failOpen = $this->renderer->getFieldValue($componentId, 'behavior', 'fail_open', true);
$html .= $this->buildToggle(
'recaptchaFailOpen',
'Permitir si API falla',
$failOpen,
'Si Google no responde, permite el envio (recomendado)'
);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <hr class="my-3">';
$html .= ' <div class="row g-3">';
// Action Newsletter
$html .= ' <div class="col-md-4">';
$actionNewsletter = $this->renderer->getFieldValue($componentId, 'behavior', 'action_newsletter', 'newsletter_submit');
$html .= $this->buildTextInput(
'recaptchaActionNewsletter',
'Accion Newsletter',
$actionNewsletter,
'Identificador para formulario newsletter'
);
$html .= ' </div>';
// Action Contact
$html .= ' <div class="col-md-4">';
$actionContact = $this->renderer->getFieldValue($componentId, 'behavior', 'action_contact', 'contact_submit');
$html .= $this->buildTextInput(
'recaptchaActionContact',
'Accion Contacto',
$actionContact,
'Identificador para formulario contacto'
);
$html .= ' </div>';
// Log Blocked
$html .= ' <div class="col-md-4">';
$logBlocked = $this->renderer->getFieldValue($componentId, 'behavior', 'log_blocked', true);
$html .= $this->buildToggle(
'recaptchaLogBlocked',
'Registrar bloqueados',
$logBlocked,
'Guarda log de intentos bloqueados'
);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildHelpSection(): string
{
$html = '<div class="card shadow-sm mb-3 bg-light">';
$html .= ' <div class="card-body">';
$html .= ' <h6 class="fw-bold mb-2" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-info-circle me-1" style="color: #FF8600;"></i>';
$html .= ' Como funciona reCAPTCHA v3';
$html .= ' </h6>';
$html .= ' <ul class="small mb-0">';
$html .= ' <li><strong>Invisible:</strong> No requiere interaccion del usuario (sin checkboxes ni imagenes)</li>';
$html .= ' <li><strong>Score:</strong> Google asigna un score de 0.0 (bot) a 1.0 (humano)</li>';
$html .= ' <li><strong>Protege:</strong> Newsletter Footer y Formulario de Contacto</li>';
$html .= ' <li><strong>Logs:</strong> Los intentos bloqueados se registran en <code>wp-content/debug.log</code></li>';
$html .= ' </ul>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildToggle(string $id, string $label, mixed $value, string $helpText = ''): string
{
$checked = ($value === true || $value === '1' || $value === 1) ? ' checked' : '';
$html = ' <div class="mb-3">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '"' . $checked . '>';
$html .= ' <label class="form-check-label fw-semibold" for="' . esc_attr($id) . '">';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' </div>';
if (!empty($helpText)) {
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
}
$html .= ' </div>';
return $html;
}
private function buildTextInput(string $id, string $label, mixed $value, string $helpText = ''): string
{
$value = is_string($value) ? $value : '';
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
if (!empty($helpText)) {
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
}
$html .= ' </div>';
return $html;
}
private function buildPasswordInput(string $id, string $label, mixed $value, string $helpText = ''): string
{
$value = is_string($value) ? $value : '';
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <div class="input-group input-group-sm">';
$html .= ' <input type="password" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
$html .= ' <button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility(\'' . esc_attr($id) . '\')">';
$html .= ' <i class="bi bi-eye"></i>';
$html .= ' </button>';
$html .= ' </div>';
if (!empty($helpText)) {
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
}
$html .= ' </div>';
return $html;
}
private function buildSelect(string $id, string $label, mixed $value, array $options, string $helpText = ''): string
{
$value = is_string($value) ? $value : '';
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($id) . '">';
foreach ($options as $optionValue => $optionLabel) {
$selected = ($value === (string) $optionValue) ? ' selected' : '';
$html .= ' <option value="' . esc_attr($optionValue) . '"' . $selected . '>' . esc_html($optionLabel) . '</option>';
}
$html .= ' </select>';
if (!empty($helpText)) {
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
}
$html .= ' </div>';
return $html;
}
}