- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/ - 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero, FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost, RelatedPost, ContactForm, Footer - Panel de administración con tabs Bootstrap 5 funcionales - Schemas JSON para configuración de componentes - Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado) - FormBuilders para UI admin con Design System consistente - Fix: Bootstrap JS cargado en header para tabs funcionales - Fix: buildTextInput maneja valores mixed (bool/string) - Eliminación de estructura legacy (src/, admin/, assets/css/componente-*) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
186 lines
5.5 KiB
PHP
186 lines
5.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
|
|
|
|
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
|
|
|
/**
|
|
* NewsletterAjaxHandler - Procesa suscripciones al newsletter
|
|
*
|
|
* RESPONSABILIDAD: Recibir email y enviarlo al webhook configurado
|
|
*
|
|
* SEGURIDAD:
|
|
* - Verifica nonce
|
|
* - Webhook URL nunca se expone al cliente
|
|
* - Rate limiting basico
|
|
* - Sanitizacion de inputs
|
|
*
|
|
* @package ROITheme\Public\Footer\Infrastructure\Api\WordPress
|
|
*/
|
|
final class NewsletterAjaxHandler
|
|
{
|
|
private const NONCE_ACTION = 'roi_newsletter_nonce';
|
|
private const COMPONENT_NAME = 'footer';
|
|
|
|
public function __construct(
|
|
private ComponentSettingsRepositoryInterface $settingsRepository
|
|
) {}
|
|
|
|
/**
|
|
* Registrar hooks AJAX
|
|
*/
|
|
public function register(): void
|
|
{
|
|
add_action('wp_ajax_roi_newsletter_subscribe', [$this, 'handleSubscribe']);
|
|
add_action('wp_ajax_nopriv_roi_newsletter_subscribe', [$this, 'handleSubscribe']);
|
|
}
|
|
|
|
/**
|
|
* Procesar suscripcion
|
|
*/
|
|
public function handleSubscribe(): 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 (1 suscripcion por IP cada 60 segundos)
|
|
if (!$this->checkRateLimit()) {
|
|
wp_send_json_error([
|
|
'message' => __('Por favor espera un momento antes de intentar de nuevo.', 'roi-theme')
|
|
], 429);
|
|
return;
|
|
}
|
|
|
|
// 3. Validar email
|
|
$email = sanitize_email($_POST['email'] ?? '');
|
|
if (empty($email) || !is_email($email)) {
|
|
wp_send_json_error([
|
|
'message' => __('Por favor ingresa un email valido.', 'roi-theme')
|
|
], 422);
|
|
return;
|
|
}
|
|
|
|
// 4. Obtener configuracion del componente
|
|
$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;
|
|
}
|
|
|
|
$newsletter = $settings['newsletter'] ?? [];
|
|
$webhookUrl = $newsletter['newsletter_webhook_url'] ?? '';
|
|
$successMsg = $newsletter['newsletter_success_message'] ?? __('Gracias por suscribirte!', 'roi-theme');
|
|
$errorMsg = $newsletter['newsletter_error_message'] ?? __('Error al suscribirse. Intenta de nuevo.', 'roi-theme');
|
|
|
|
if (empty($webhookUrl)) {
|
|
// Si no hay webhook, simular exito para UX pero loguear warning
|
|
error_log('ROI Theme Newsletter: No webhook URL configured');
|
|
wp_send_json_success([
|
|
'message' => $successMsg
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// 5. Preparar payload
|
|
$payload = [
|
|
'email' => $email,
|
|
'source' => 'newsletter-footer',
|
|
'timestamp' => current_time('c'),
|
|
'siteName' => get_bloginfo('name'),
|
|
'siteUrl' => home_url(),
|
|
];
|
|
|
|
// 6. Enviar a webhook
|
|
$result = $this->sendToWebhook($webhookUrl, $payload);
|
|
|
|
if ($result['success']) {
|
|
wp_send_json_success([
|
|
'message' => $successMsg
|
|
]);
|
|
} else {
|
|
error_log('ROI Theme Newsletter webhook error: ' . $result['error']);
|
|
wp_send_json_error([
|
|
'message' => $errorMsg
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enviar datos al webhook
|
|
*/
|
|
private function sendToWebhook(string $url, array $payload): array
|
|
{
|
|
$response = wp_remote_post($url, [
|
|
'timeout' => 30,
|
|
'headers' => [
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
],
|
|
'body' => wp_json_encode($payload),
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $response->get_error_message()
|
|
];
|
|
}
|
|
|
|
$statusCode = wp_remote_retrieve_response_code($response);
|
|
|
|
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 por IP
|
|
*/
|
|
private function checkRateLimit(): bool
|
|
{
|
|
$ip = $this->getClientIP();
|
|
$transientKey = 'roi_newsletter_' . md5($ip);
|
|
$lastSubmit = get_transient($transientKey);
|
|
|
|
if ($lastSubmit !== false) {
|
|
return false;
|
|
}
|
|
|
|
set_transient($transientKey, time(), 60);
|
|
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;
|
|
}
|
|
}
|