Prueba de edición de código

<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");

// Si es una solicitud preflight (OPTIONS), responder inmediatamente
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204); // No Content
    exit;
}
date_default_timezone_set('Europe/Madrid');

defined('DS') or define('DS', str_replace('\\', '/', DIRECTORY_SEPARATOR));
defined('ROOTPATH') or define('ROOTPATH', str_replace('\\', '/', __DIR__));

// CUIDADO: display_errors y error_reporting(E_ALL) deben ser 0 en producción.
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Definir constante de la ruta raíz
defined('PHPVERSION') or define('PHPVERSION', (float) PHP_VERSION);

defined('RELPATH') or define('RELPATH', substr(dirname($_SERVER['SCRIPT_NAME']), 1));
defined('BASEDIR') or define('BASEDIR', basename(RELPATH));
defined('BASEURL') or define('BASEURL', 'https://' . $_SERVER['SERVER_NAME'] . '/' . RELPATH);
defined('WP_ROOT') or define('WP_ROOT', $_SERVER['DOCUMENT_ROOT'] . '/');
defined('USE_OUTPUT_BUFFERING') or define('USE_OUTPUT_BUFFERING', false);

// Configuración de sesión segura
if (session_status() == PHP_SESSION_NONE) {
    // Seguridad de las cookies de sesión
    ini_set('session.use_only_cookies', 1);
    ini_set('session.cookie_httponly', 1);
    ini_set('session.cookie_samesite', 'Lax'); // 'Strict' es más seguro, 'Lax' es más compatible con enlaces externos

    // Detectar si la solicitud es HTTPS
    $isSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
                (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
                (!empty($_SERVER['HTTP_CF_VISITOR']) && strpos($_SERVER['HTTP_CF_VISITOR'], '"scheme":"https"') !== false);

    if ($isSecure) {
        ini_set('session.cookie_secure', 1);
    }

    // Iniciar sesión con un nombre seguro
    session_name('COMPEXDB_V2');
    session_start();

    // Generar token CSRF si no existe en la sesión
    if (!isset($_SESSION['csrf_token'])) {
        try {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        } catch (Exception $e) {
            error_log("Error generando token CSRF: " . $e->getMessage());
            // En caso de fallo crítico, no permitimos la ejecución para evitar riesgos
            http_response_code(500);
            echo json_encode(['success' => false, 'message' => 'Error interno de seguridad.']);
            exit;
        }
    }
}

final class WPCrudManager {

    // --- CONFIGURACIÓN PRINCIPAL ---
    // ATENCIÓN: Las credenciales hardcodeadas son un RIESGO DE SEGURIDAD CRÍTICO.
    // Deberás implementar un sistema de autenticación seguro aquí.
    private const WP_USER = 'JuanPerezEdX';        // Usuario con permisos para publicar
    private const WP_PASSWORD = '@JuanPerez2025'; // Contraseña del usuario
    private const TARGET_CATEGORY_NAME = 'Noticias'; // Categoría por defecto. ¡Debe existir!
    // Define una acción para el nonce de WordPress que usarás en el frontend y el backend
    private const NONCE_ACTION = 'wp_crud_manager_ajax';

    // --- CONFIGURACIÓN DE RUTAS ---
    private const WP_ROOT_PATH = WP_ROOT;
    private const SCRIPT_URL_PATH = ''; // No es necesario asignar un valor aquí, se calcula en renderInterface.

    // --- CONFIGURACIÓN DE LOGS ---
    private static bool $enableLogging = true;
    private static string $logDirectory;
    private static ?string $logFile = null;
    private static array $logBuffer = [];

    /**
     * Punto de entrada. Decide si mostrar la interfaz o manejar una petición AJAX.
     */
    public static function init(): void {
        self::initLogger();
        self::log("Script iniciado.");
        register_shutdown_function([self::class, 'writeLog']);

        // Validación básica de la petición para evitar errores si 'action' no está definido
        $action = $_POST['action'] ?? null;

        if ($action !== null && !empty($action)) {
            self::handleAjaxRequest();
        } else {
            self::renderInterface();
        }
    }

    /**
     * Enrutador para las peticiones AJAX.
     */
    private static function handleAjaxRequest(): void {
        header('Content-Type: application/json; charset=utf-8'); // Añadido charset
        ob_start(); // Inicia el buffer de salida

        $requestedAction = $_POST['action'] ?? '';
        self::log("Petición AJAX recibida. Acción: " . self::escapeLog($requestedAction ?: 'ninguna')); // Escapado para logs

        $response = [];
        try {
            // --- Carga de WordPress si no está cargado ---
            if (!defined('WP_LOADED')) {
                $wp_load_path = self::WP_ROOT_PATH . '/wp-load.php';
                if (!file_exists($wp_load_path)) {
                    throw new Exception("ERROR FATAL: wp-load.php no encontrado en la ruta esperada: " . $wp_load_path);
                }
                require_once $wp_load_path;
                define('WP_LOADED', true);
                self::log("WordPress cargado para la petición AJAX.");
            }

            // --- 1. Verificación de Token CSRF basado en sesión ---
            $clientCsrfToken = $_POST['csrf_token'] ?? '';
            if (!isset($_SESSION['csrf_token']) || $clientCsrfToken !== $_SESSION['csrf_token']) {
                self::log("Fallo de seguridad: Token CSRF inválido o ausente. Cliente: '" . self::escapeLog($clientCsrfToken) . "', Sesión: '" . self::escapeLog($_SESSION['csrf_token'] ?? 'N/A') . "'", 'SECURITY_ALERT');
                throw new Exception("Fallo de seguridad: Token CSRF no válido.");
            }
            // Opcional: Regenerar el token después de un uso exitoso para mayor seguridad (anti-replay)
            // $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
            self::log("Token CSRF de sesión verificado correctamente.");

            // --- 2. Verificación de Nonce de WordPress (específico de WP) ---
            // Un nonce es un token de un solo uso para proteger contra CSRF,
            // complementa el token de sesión si usas funcionalidades de WP.
            $nonce = $_POST['_wpnonce'] ?? $_REQUEST['_wpnonce'] ?? ''; // Convención de WordPress para nonces
            if (!function_exists('wp_verify_nonce') || !wp_verify_nonce($nonce, self::NONCE_ACTION)) {
                self::log("Fallo de seguridad: Nonce de WordPress inválido para acción '" . self::NONCE_ACTION . "'. Nonce recibido: '" . self::escapeLog($nonce) . "'", 'SECURITY_ALERT');
                throw new Exception("Fallo de seguridad: Nonce de WordPress no válido.");
            }
            self::log("Nonce de WordPress '" . self::NONCE_ACTION . "' verificado correctamente.");


            // --- 3. Autenticación de Usuario (¡Punto a mejorar más adelante!) ---
            // Por ahora, se mantiene el login hardcodeado para que funcione el resto del script.
            // La mejora futura debería basarse en el usuario actualmente logueado en WordPress,
            // o un sistema de autenticación de API externo.
            $user = self::login();
            wp_set_current_user($user->ID);
            self::log("Autenticación AJAX exitosa para usuario ID: {$user->ID}");

            // --- 4. Verificación de Permisos Generales ---
            if (!current_user_can('publish_posts')) {
                self::log("Usuario ID: {$user->ID} no tiene permisos para 'publish_posts'.", 'SECURITY_ALERT');
                throw new Exception("El usuario no tiene permisos suficientes para realizar esta acción.");
            }

            // --- 5. Enrutamiento de Acciones ---
            switch ($requestedAction) {
                case 'create':
                    // Permiso adicional para 'edit_posts' si se desea ser más granular
                    if (!current_user_can('edit_posts')) {
                        throw new Exception("El usuario no tiene permisos para crear posts.");
                    }
                    $response = self::ajaxCreatePost();
                    break;
                case 'read':
                    // La lectura suele no requerir permisos elevados si los posts son públicos
                    $response = self::ajaxReadPosts();
                    self::log("Respuesta de lectura: " . json_encode($response));
                    break;
                case 'update':
                    // Permiso para editar otros posts o sus propios posts
                    if (!current_user_can('edit_others_posts')) {
                        throw new Exception("El usuario no tiene permisos para actualizar posts.");
                    }
                    $response = self::ajaxUpdatePost();
                    break;
                case 'delete':
                    // Permiso MUY IMPORTANTE para eliminar posts
                    if (!current_user_can('delete_posts')) {
                        throw new Exception("El usuario no tiene permisos para eliminar posts.");
                    }
                    $response = self::ajaxDeletePost();
                    break;
                default:
                    throw new Exception('Acción AJAX no válida.');
            }
        } catch (Exception $e) {
            self::log("ERROR: " . self::escapeLog($e->getMessage()), 'ERROR');
            $response = ['success' => false, 'message' => $e->getMessage()];
            http_response_code(500); // Internal Server Error
        } catch (Error $e) {
            self::log("ERROR FATAL PHP: " . self::escapeLog($e->getMessage()) . " en " . self::escapeLog($e->getFile()) . " línea " . $e->getLine(), 'CRITICAL');
            $response = ['success' => false, 'message' => "Error interno del servidor: " . $e->getMessage()];
            http_response_code(500); // Internal Server Error
        } finally {
            // Asegura que la respuesta se imprima incluso si hay errores
            echo json_encode($response);
            ob_end_flush(); // Envía el contenido del buffer

            // Terminar la ejecución de PHP de forma segura en WordPress
            if (function_exists('wp_die')) {
                wp_die();
            } else {
                exit;
            }
        }
    }

    // --- LÓGICA DE OPERACIONES CRUD ---

    private static function ajaxCreatePost(): array {
        $title = $_POST['title'] ?? '';
        $content = $_POST['content'] ?? ''; // El contenido puede tener HTML, así que se sanitiza más tarde

        // Saneamiento de los datos de entrada
        $cleanTitle = self::filterTextField($title);
        // Para el contenido, si se permite HTML, usar wp_kses_post()
        $cleanContent = function_exists('wp_kses_post') ? wp_kses_post($content) : sanitize_text_field($content);

        if (empty($cleanTitle)) {
            throw new Exception('El título es obligatorio.');
        }

        self::log("Creando post con título saneado: '" . self::escapeLog($cleanTitle) . "'");

        try {
            $category_id = get_cat_ID(self::TARGET_CATEGORY_NAME);
            if ($category_id === 0) {
                throw new Exception("La categoría '" . self::TARGET_CATEGORY_NAME . "' no existe.");
            }

            $post_data = [
                'post_title'    => $cleanTitle,
                'post_content'  => $cleanContent, // Usar el contenido saneado
                'post_status'   => 'publish',
                'post_author'   => get_current_user_id(),
                'post_category' => [$category_id],
            ];

            $new_post_id = wp_insert_post($post_data, true);
            if (is_wp_error($new_post_id)) {
                throw new Exception($new_post_id->get_error_message());
            }
        } catch (Exception $e) {
            self::log("Error al crear post: " . self::escapeLog($e->getMessage()), 'ERROR');
            throw $e; // Re-lanzar la excepción para que sea capturada por handleAjaxRequest
        }

        self::log("Post creado con éxito. ID: {$new_post_id}");
        // Escapar el título para la salida en el mensaje de éxito
        return ['success' => true, 'message' => "Post '" . self::escapeHtml($cleanTitle) . "' creado con éxito (ID: {$new_post_id})."];
    }

    private static function ajaxReadPosts(): array {
        self::log("Leyendo posts de la categoría: '" . self::TARGET_CATEGORY_NAME . "'.");
        try {
            $args = [
                'category_name' => self::TARGET_CATEGORY_NAME,
                'posts_per_page' => 20,
                'post_status' => 'publish',
            ];
            $posts = get_posts($args);

            $posts_data = [];
            foreach ($posts as $post) {
                $posts_data[] = [
                    'id' => $post->ID,
                    'title' => self::escapeHtml($post->post_title), // Escapar título para HTML
                    'content' => self::escapeHtml($post->post_content), // Escapar contenido para HTML
                    'link' => esc_url(get_permalink($post->ID)) // Escapar URL
                ];
            }
        } catch (Exception $e) {
            self::log("Error al leer posts: " . self::escapeLog($e->getMessage()), 'ERROR');
            throw $e;
        }

        self::log("Se encontraron " . count($posts_data) . " posts.");
        return ['success' => true, 'posts' => $posts_data];
    }

    private static function ajaxUpdatePost(): array {
        $id = !empty($_POST['id']) ? absint($_POST['id']) : 0;
        $title = $_POST['title'] ?? '';
        $content = $_POST['content'] ?? '';

        if ($id === 0) throw new Exception('ID de post no válido.');
        if (empty($title)) throw new Exception('El título es obligatorio.');

        $cleanTitle = self::filterTextField($title);
        $cleanContent = function_exists('wp_kses_post') ? wp_kses_post($content) : sanitize_text_field($content);

        self::log("Actualizando post ID: {$id} con título: '" . self::escapeLog($cleanTitle) . "'");

        try {
            $post_data = [
                'ID' => $id,
                'post_title' => $cleanTitle,
                'post_content' => $cleanContent,
            ];

            $updated_post_id = wp_update_post($post_data, true);
            if (is_wp_error($updated_post_id)) {
                throw new Exception($updated_post_id->get_error_message());
            }
        } catch (Exception $e) {
            self::log("Error al actualizar post ID: {$id}: " . self::escapeLog($e->getMessage()), 'ERROR');
            throw $e;
        }

        self::log("Post ID: {$id} actualizado con éxito.");
        return ['success' => true, 'message' => "Post '" . self::escapeHtml($cleanTitle) . "' actualizado correctamente."];
    }

    private static function ajaxDeletePost(): array {
        $id = !empty($_POST['id']) ? absint($_POST['id']) : 0;
        if ($id === 0) throw new Exception('ID de post no válido para eliminar.');

        self::log("Intentando eliminar post ID: {$id}.");
        try {
            $result = wp_delete_post($id, true);

            if ($result === false || $result === null) {
                throw new Exception("No se pudo eliminar el post ID: {$id}. Es posible que ya no exista o no tengas permisos.");
            }
        } catch (Exception $e) {
            self::log("Error al eliminar post ID: {$id}: " . self::escapeLog($e->getMessage()), 'ERROR');
            throw $e;
        }

        self::log("Post ID: {$id} eliminado con éxito.");
        return ['success' => true, 'message' => "El post (ID: {$id}) ha sido eliminado permanentemente."];
    }

    // --- FUNCIONES AUXILIARES Y DE SEGURIDAD ---

    // ¡ADVERTENCIA CRÍTICA: Esta función de login debe ser revisada!
    // No uses credenciales hardcodeadas en producción.
    private static function login(): WP_User {
        self::log("Intentando autenticar al usuario: '" . self::WP_USER . "'");
        try {
            $creds = ['user_login' => self::WP_USER, 'user_password' => self::WP_PASSWORD, 'remember' => true];
            $user = wp_signon($creds, false);
            if (is_wp_error($user)) {
                throw new Exception("Error de autenticación: " . $user->get_error_message());
            }
        } catch (Exception $e) {
            self::log("Fallo en login de usuario: " . self::escapeLog($e->getMessage()), 'CRITICAL');
            throw $e; // Re-lanzar para que handleAjaxRequest lo capture
        }
        return $user;
    }

    /**
     * Función de saneamiento personalizada para campos de texto.
     * Elimina etiquetas HTML y normaliza espacios.
     */
    public static function filterTextField(string $text): string {
        $text = strip_tags($text);
        $text = trim($text);
        $text = preg_replace('/\s+/', ' ', $text);
        self::log("Filtro aplicado (filterTextField): '" . self::escapeLog($text) . "'");
        return $text;
    }

    /**
     * Función de escapado personalizada para salida HTML.
     * Usa ENT_QUOTES para escapar comillas simples y dobles.
     */
    public static function escapeHtml(string $text): string {
        return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    /**
     * Función para escapar cadenas antes de escribirlas en el log,
     * para evitar inyección de logs o problemas de formato.
     */
    private static function escapeLog(string $message): string {
        return str_replace(["\n", "\r"], ['\\n', '\\r'], filter_var($message, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH));
    }


    // --- SISTEMA DE LOGS ---

    private static function initLogger(): void {
        if (!self::$enableLogging) return;

        try {
            if (defined('WP_CONTENT_DIR')) {
                self::$logDirectory = WP_CONTENT_DIR . '/wp-crud-logs';
                self::log("WP_CONTENT_DIR definido. Usando directorio de logs: " . self::$logDirectory);
            } else {
                self::$logDirectory = __DIR__ . '/wp-crud-logs';
                self::log("WP_CONTENT_DIR no está definido. Usando directorio de logs relativo al script: " . self::$logDirectory, 'WARNING');
            }

            if (!is_dir(self::$logDirectory)) {
                if (!mkdir(self::$logDirectory, 0755, true)) {
                    throw new Exception("No se pudo crear el directorio de logs en: " . self::$logDirectory);
                }
                self::log("Directorio de logs creado en: " . self::$logDirectory);
            }

            // Protección del directorio de logs
            $htaccess_path = self::$logDirectory . '/.htaccess';
            if (!file_exists($htaccess_path)) {
                file_put_contents($htaccess_path, 'Deny from all');
                self::log("Archivo .htaccess de seguridad creado en directorio de logs.");
            }

            $index_path = self::$logDirectory . '/index.html';
            if (!file_exists($index_path)) {
                file_put_contents($index_path, '');
                self::log("Archivo index.html de seguridad creado en directorio de logs.");
            }

            self::$logFile = self::$logDirectory . '/log_' . date('Y-m-d') . '.log';
        } catch (Exception $e) {
            self::$enableLogging = false; // Deshabilitar logging si falla la inicialización
            error_log("WPCrudManager FATAL ERROR DE LOGGER: " . $e->getMessage());
        }
    }

    public static function log(string $message, string $level = 'INFO'): void {
        self::$logBuffer[] = "[" . date('Y-m-d H:i:s') . "] [{$level}] " . self::escapeLog($message); // Escapar mensajes de log
    }

    public static function writeLog(): void {
        if (!self::$enableLogging || empty(self::$logBuffer) || self::$logFile === null) {
            return;
        }
        $log_content = implode("\n", self::$logBuffer) . "\n";
        file_put_contents(self::$logFile, $log_content, FILE_APPEND);
        self::$logBuffer = [];
    }

    // --- RENDERIZADO DE LA INTERFAZ ---

    public static function renderInterface(): void {
        // Generar el nonce de WordPress que se usará en el frontend
        // wp_create_nonce() requiere que WordPress esté cargado.
        // Asegúrate de que WP_LOADED esté definido antes de llamar a esta función,
        // o muévela al `init()` antes de la bifurcación a handleAjaxRequest
        // para que siempre esté disponible si se renderiza la interfaz.
        if (!defined('WP_LOADED')) {
             $wp_load_path = self::WP_ROOT_PATH . '/wp-load.php';
             if (file_exists($wp_load_path)) {
                 require_once $wp_load_path;
                 define('WP_LOADED', true);
             } else {
                 self::log("Advertencia: wp-load.php no encontrado para generar nonce en la interfaz.", 'WARNING');
                 // Si wp-load.php no se carga, no se podrá generar el nonce de WP.
                 // Asegúrate de manejar esto en tu frontend.
             }
        }

        $wpNonce = '';
        if (function_exists('wp_create_nonce')) {
            $wpNonce = wp_create_nonce(self::NONCE_ACTION);
        } else {
            self::log("Advertencia: wp_create_nonce no disponible. Asegúrate de que WordPress esté cargado.", 'WARNING');
        }

        // Obtener el token CSRF de sesión para pasarlo al frontend
        $sessionCsrfToken = $_SESSION['csrf_token'] ?? '';

        $scriptPath = dirname($_SERVER['PHP_SELF']);
        $scriptUrl = 'https://' . $_SERVER['SERVER_NAME'] . rtrim($scriptPath, '/') . '/' . basename(__FILE__);

        $escapedCategoryName = self::escapeHtml(self::TARGET_CATEGORY_NAME);

        // Aquí se incluye tu archivo de vista (frontend.php)
        require_once(ROOTPATH . '/views/frontend.php');

        // Ejemplo de cómo frontend.php podría usar los tokens:
        // En frontend.php, podrías tener algo como:
        /*
        <script>
            const WPCrudConfig = {
                ajax_url: '<?= self::escapeHtml($scriptUrl) ?>', // URL de este script
                wp_nonce: '<?= self::escapeHtml($wpNonce) ?>', // Nonce de WordPress
                csrf_token: '<?= self::escapeHtml($sessionCsrfToken) ?>', // Token CSRF de sesión
                target_category_name: '<?= self::escapeHtml(self::TARGET_CATEGORY_NAME) ?>'
            };
            // Luego, en tu JS, en tus peticiones AJAX:
            // data: {
            //     action: 'create',
            //     _wpnonce: WPCrudConfig.wp_nonce,
            //     csrf_token: WPCrudConfig.csrf_token,
            //     title: '...',
            //     content: '...'
            // }
        </script>
        */
    }
}

// Iniciar el gestor
WPCrudManager::init();

 

0