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>
304 lines
9.4 KiB
PHP
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;
|
|
}
|
|
}
|