Files
roi-theme/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php
FrankZamora a062529e82 fix: Case-sensitivity en namespaces Wordpress -> WordPress
PROBLEMA:
- El modal de contacto no se mostraba en producción (Linux)
- Funcionaba en local (Windows) porque filesystem es case-insensitive
- Carpeta: `WordPress` (con P mayúscula)
- Namespaces: `Wordpress` (con p minúscula)

SOLUCION:
- Corregir todos los namespaces de `Wordpress` a `WordPress`
- También corregir paths incorrectos `ROITheme\Component\...` a `ROITheme\Shared\...`

ARCHIVOS CORREGIDOS (14):
- functions.php
- Admin/Infrastructure/Api/WordPress/AdminMenuRegistrar.php
- Admin/Shared/Infrastructure/Api/WordPress/AdminAjaxHandler.php
- Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php
- Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php
- Shared/Infrastructure/Api/WordPress/AjaxController.php
- Shared/Infrastructure/Api/WordPress/MigrationCommand.php
- Shared/Infrastructure/Di/DIContainer.php
- Shared/Infrastructure/Persistence/WordPress/WordPressComponentRepository.php
- Shared/Infrastructure/Persistence/WordPress/WordPressComponentSettingsRepository.php
- Shared/Infrastructure/Persistence/WordPress/WordPressDefaultsRepository.php
- Shared/Infrastructure/Services/CleanupService.php
- Shared/Infrastructure/Services/SchemaSyncService.php
- Shared/Infrastructure/Services/WordPressValidationService.php

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 11:11:13 -06:00

304 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Infrastructure\Api\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
/**
* ContactFormAjaxHandler - Procesa envios del formulario de contacto
*
* RESPONSABILIDAD: Recibir datos del formulario y enviarlos al webhook configurado
*
* SEGURIDAD:
* - Verifica nonce
* - Webhook URL NUNCA se expone al cliente
* - Webhook URL se obtiene de BD server-side
* - Rate limiting basico
* - Sanitizacion de inputs
*
* @package ROITheme\Public\ContactForm\Infrastructure\Api\WordPress
*/
final class ContactFormAjaxHandler
{
private const NONCE_ACTION = 'roi_contact_form_nonce';
private const COMPONENT_NAME = 'contact-form';
public function __construct(
private ComponentSettingsRepositoryInterface $settingsRepository
) {}
/**
* Registrar hooks AJAX
* Usa wp_ajax_nopriv para usuarios no logueados
*/
public function register(): void
{
add_action('wp_ajax_roi_contact_form_submit', [$this, 'handleSubmit']);
add_action('wp_ajax_nopriv_roi_contact_form_submit', [$this, 'handleSubmit']);
}
/**
* Procesar envio del formulario
*/
public function handleSubmit(): void
{
// 1. Verificar nonce
$nonce = sanitize_text_field($_POST['nonce'] ?? '');
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
wp_send_json_error([
'message' => __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
], 403);
return;
}
// 2. Rate limiting basico (1 envio por IP cada 30 segundos)
if (!$this->checkRateLimit()) {
wp_send_json_error([
'message' => __('Por favor espera un momento antes de enviar otro mensaje.', 'roi-theme')
], 429);
return;
}
// 3. Sanitizar y validar inputs
$formData = $this->sanitizeFormData($_POST);
$validation = $this->validateFormData($formData);
if (!$validation['valid']) {
wp_send_json_error([
'message' => $validation['message'],
'errors' => $validation['errors']
], 422);
return;
}
// 4. Obtener configuracion del componente (incluye webhook URL)
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
if (empty($settings)) {
wp_send_json_error([
'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
], 500);
return;
}
$integration = $settings['integration'] ?? [];
$webhookUrl = $integration['webhook_url'] ?? '';
$webhookMethod = $integration['webhook_method'] ?? 'POST';
$includePageUrl = $this->toBool($integration['include_page_url'] ?? true);
$includeTimestamp = $this->toBool($integration['include_timestamp'] ?? true);
if (empty($webhookUrl)) {
// Si no hay webhook configurado, simular exito para UX
// pero loguear warning para admin
error_log('ROI Theme Contact Form: No webhook URL configured');
wp_send_json_success([
'message' => $this->getSuccessMessage($settings)
]);
return;
}
// 5. Preparar payload para webhook
$payload = $this->preparePayload($formData, $includePageUrl, $includeTimestamp);
// 6. Enviar a webhook
$result = $this->sendToWebhook($webhookUrl, $webhookMethod, $payload);
if ($result['success']) {
wp_send_json_success([
'message' => $this->getSuccessMessage($settings)
]);
} else {
error_log('ROI Theme Contact Form webhook error: ' . $result['error']);
wp_send_json_error([
'message' => $this->getErrorMessage($settings)
], 500);
}
}
/**
* Sanitizar datos del formulario
*/
private function sanitizeFormData(array $post): array
{
return [
'fullName' => sanitize_text_field($post['fullName'] ?? ''),
'company' => sanitize_text_field($post['company'] ?? ''),
'whatsapp' => sanitize_text_field($post['whatsapp'] ?? ''),
'email' => sanitize_email($post['email'] ?? ''),
'message' => sanitize_textarea_field($post['message'] ?? ''),
];
}
/**
* Validar datos del formulario
*/
private function validateFormData(array $data): array
{
$errors = [];
// Nombre requerido
if (empty($data['fullName'])) {
$errors['fullName'] = __('El nombre es obligatorio', 'roi-theme');
}
// WhatsApp requerido
if (empty($data['whatsapp'])) {
$errors['whatsapp'] = __('El WhatsApp es obligatorio', 'roi-theme');
}
// Email requerido y valido
if (empty($data['email'])) {
$errors['email'] = __('El email es obligatorio', 'roi-theme');
} elseif (!is_email($data['email'])) {
$errors['email'] = __('Por favor ingresa un email valido', 'roi-theme');
}
if (!empty($errors)) {
return [
'valid' => false,
'message' => __('Por favor corrige los errores del formulario', 'roi-theme'),
'errors' => $errors
];
}
return ['valid' => true, 'message' => '', 'errors' => []];
}
/**
* Preparar payload para webhook
*/
private function preparePayload(array $formData, bool $includePageUrl, bool $includeTimestamp): array
{
$payload = [
'fullName' => $formData['fullName'],
'company' => $formData['company'],
'whatsapp' => $formData['whatsapp'],
'email' => $formData['email'],
'message' => $formData['message'],
];
if ($includePageUrl) {
$payload['pageUrl'] = sanitize_url($_POST['pageUrl'] ?? '');
$payload['pageTitle'] = sanitize_text_field($_POST['pageTitle'] ?? '');
}
if ($includeTimestamp) {
$payload['timestamp'] = current_time('c');
$payload['timezone'] = wp_timezone_string();
}
// Metadata adicional util para el webhook
$payload['source'] = 'contact-form';
$payload['siteName'] = get_bloginfo('name');
$payload['siteUrl'] = home_url();
return $payload;
}
/**
* Enviar datos al webhook
*/
private function sendToWebhook(string $url, string $method, array $payload): array
{
$args = [
'method' => strtoupper($method),
'timeout' => 30,
'redirection' => 5,
'httpversion' => '1.1',
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
];
if ($method === 'POST') {
$args['body'] = wp_json_encode($payload);
} else {
$url = add_query_arg($payload, $url);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return [
'success' => false,
'error' => $response->get_error_message()
];
}
$statusCode = wp_remote_retrieve_response_code($response);
// Considerar 2xx como exito
if ($statusCode >= 200 && $statusCode < 300) {
return ['success' => true, 'error' => ''];
}
return [
'success' => false,
'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
];
}
/**
* Rate limiting basico por IP
*/
private function checkRateLimit(): bool
{
$ip = $this->getClientIP();
$transientKey = 'roi_contact_form_' . md5($ip);
$lastSubmit = get_transient($transientKey);
if ($lastSubmit !== false) {
return false;
}
set_transient($transientKey, time(), 30);
return true;
}
/**
* Obtener IP del cliente
*/
private function getClientIP(): string
{
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
}
return $ip;
}
/**
* Obtener mensaje de exito desde configuracion
*/
private function getSuccessMessage(array $data): string
{
$messages = $data['messages'] ?? [];
return $messages['success_message'] ?? __('¡Gracias por contactarnos! Te responderemos pronto.', 'roi-theme');
}
/**
* Obtener mensaje de error desde configuracion
*/
private function getErrorMessage(array $data): string
{
$messages = $data['messages'] ?? [];
return $messages['error_message'] ?? __('Hubo un error al enviar el mensaje. Por favor intenta de nuevo.', 'roi-theme');
}
/**
* Convertir valor a boolean
*/
private function toBool($value): bool
{
return $value === true || $value === '1' || $value === 1;
}
}