Migración completa a Clean Architecture con componentes funcionales
- 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>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user