Plugin «Noticias KSLK» genera entradas en una web a partir de la URL de una noticia

¿Qué hace este «plugin»?

El «plugin» Noticias KSK es una potente herramienta que te permite capturar, resumir con inteligencia artificial y gestionar noticias de cualquier URL externa, integrándolas directamente en tu panel de administración de WordPress.

Las funcionalidades clave incluyen:

  • Generación de Noticias desde URL: Introduce la URL de una noticia y el sistema extraerá automáticamente el título, el contenido y la imagen principal.

  • Resumen Automático con IA: Utiliza la API de OpenAI (ChatGPT) para generar un resumen conciso y relevante del contenido de la noticia.

  • Gestión Centralizada: Almacena todas las noticias procesadas en un listado dentro de tu panel de WordPress, lo que te permite visualizarlas cómodamente.

  • Publicación como Borrador: Convierte una noticia guardada en un post de borrador en tu sitio de WordPress con un solo clic, incluyendo el título, el resumen como contenido y la imagen destacada. Esto facilita la revisión y edición antes de su publicación final.

  • Interfaz Intuitiva: Se integra en el menú de administración de WordPress con una interfaz sencilla para su uso y configuración.

  • Estilo Visual Distintivo: El menú del plugin en el panel de administración tiene un color personalizado (amarillo con letras negras) para diferenciarlo rápidamente de otras secciones de WordPress.


Requisitos

Para que este «plugin» funcione correctamente, necesitas:

  • WordPress: Una instalación funcional de WordPress (versión 5.x o superior).

  • PHP: Versión de PHP compatible con tu instalación de WordPress (generalmente PHP 7.4 o superior).

  • Conexión a Internet: Para acceder a las URLs de las noticias y a las APIs externas.

  • Clave API de OpenAI: Imprescindible para la funcionalidad de resumen con inteligencia artificial. Puedes obtenerla en platform.openai.com/account/api-keys.

  • Clave API de ScrapingBee (Opcional): Muy recomendable para una extracción de contenido más robusta y fiable, especialmente de sitios web complejos o con protecciones anti-scraping. Puedes obtenerla en www.scrapingbee.com. Si no la usas, el sistema intentará el scraping directo, que puede ser menos eficaz.


Instalación

Dado que este código se proporciona como un bloque de funciones para tu tema, la instalación es sencilla:

  1. Accede a los archivos de tu sitio WordPress:

    • Puedes hacerlo a través de un cliente FTP/SFTP (como FileZilla).

    • O bien, desde el Administrador de Archivos de tu panel de control de hosting (cPanel, Plesk, etc.).

  2. Localiza el archivo functions.php de tu tema activo:

    • Ve a la ruta wp-content/themes/tu-tema-activo/.

    • Dentro de esa carpeta, encontrarás el archivo functions.php.

    • Advertencia: Es altamente recomendable hacer una copia de seguridad de este archivo antes de modificarlo. Los errores en functions.php pueden dejar tu sitio inaccesible.

  3. Edita el archivo functions.php:

    • Abre functions.php con un editor de texto plano (no uses un procesador de texto como Word).

    • Copia todo el código proporcionado (desde la primera <?php hasta el final) y pégalo al final del archivo functions.php. Si el archivo ya tiene un ?> al final, asegúrate de pegar el código antes de esa etiqueta de cierre, o simplemente omite esa etiqueta de cierre si no hay nada más después de tu código.

    • Importante: Asegúrate de que no haya caracteres extraños o espacios en blanco antes de la primera <?php o después de la última ?> (si decides mantenerla).

  4. Guarda el archivo y súbelo de nuevo a tu servidor (si lo editaste localmente).


Configuración y Uso

Una vez instalado el código, sigue estos pasos para configurar y empezar a usar el «plugin»:

1. Configuración Inicial (¡Obligatorio!)

  • Inicia sesión en tu panel de administración de WordPress.

  • En el menú lateral izquierdo, verás una nueva entrada llamada «Noticias KSK«. Haz clic en ella para desplegar sus submenús.

  • Selecciona el submenú «Ajustes«.

  • En esta página, encontrarás los siguientes campos:

    • Clave API de OpenAI: Pega aquí tu clave API de OpenAI (empieza por sk-...). Esto es fundamental para que la función de resumen por IA funcione.

    • Prompt base para resumir noticias: Escribe las instrucciones que quieres darle a la IA. Por ejemplo: Resume el siguiente artículo en 3 párrafos y en español:. Puedes experimentar con diferentes prompts para obtener los resultados deseados.

    • Clave API de ScrapingBee (opcional): Si tienes una, pégala aquí. Mejorará la fiabilidad de la extracción de contenido e imágenes de los sitios web.

  • Haz clic en el botón «Guardar Cambios» para almacenar tus configuraciones.

2. Generar Noticias desde una URL

  • En el menú «Noticias KSK«, haz clic en el submenú «Generar desde URL«.

  • Verás un campo de texto donde puedes pegar la URL completa de la noticia que deseas procesar (ej. https://www.ejemplo.com/noticia-interesante).

  • Haz clic en el botón «✨ Procesar URL«.

  • El sistema procesará la URL en segundo plano. Verás mensajes de estado en la página, indicando si la noticia se obtuvo correctamente, si se generó el resumen con IA y si se guardó en tu base de datos interna.

3. Ver y Gestionar Noticias Guardadas

  • En el menú «Noticias KSK«, haz clic en el submenú «Ver BBDD«.

  • Aquí se mostrará una tabla con todas las noticias que has guardado. Cada fila incluye:

    • Número de fila.

    • Una vista previa de la Imagen (si se extrajo).

    • El Título de la noticia (con un enlace para ver el artículo original).

    • El Resumen generado por la inteligencia artificial.

    • La Fuente (dominio del sitio web).

    • Las Fechas de publicación original y de guardado en tu sistema.

    • Una columna de Acciones.

4. Publicar una Noticia como Borrador

  • En la tabla de «Ver BBDD«, en la columna «Acciones», encontrarás dos iconos:

    • 🗑️ (Papelera): Haz clic aquí para borrar la noticia de tu base de datos interna de «Noticias KSK». Esto no borrará ningún post que ya hayas publicado en WordPress.

    • 🚀 (Cohete): Haz clic aquí para publicar la noticia como un borrador en tu sitio de WordPress.

      • Se creará un nuevo post en la sección «Entradas» con el título de la noticia.

      • El contenido del post será el resumen generado por IA.

      • La imagen destacada del post será la imagen que se haya logrado extraer de la URL original.

      • La noticia permanecerá en la tabla de «Ver BBDD», pero el icono del cohete cambiará a un checkmark verde () para indicar que ya ha sido publicada.

  • Después de publicar como borrador, puedes ir a Entradas > Todas las entradas en tu panel de WordPress para revisar y editar el post antes de publicarlo definitivamente.

/* BLOQUE-030 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 Generar Noticias desde URL (PÁGINA SEPARADA) 🟩 */
// ==============================
// BLOQUE: Generar Noticias desde una URL específica con resumen OpenAI
// Guardado en la opción ‘ksk_noticias_guardadas’ de WordPress
// ==============================
// Este bloque gestiona la adición de un menú al panel de administración de WordPress,
// permitiendo al usuario introducir una URL para generar una noticia (título, imagen, resumen IA)
// y guardarla en la base de datos de WordPress. También permite visualizar y gestionar
// las noticias guardadas. Requiere la configuración de claves API (OpenAI, ScrapingBee opcional).
// Incluye CSS personalizado para destacar el menú en el panel de administración.
// **MEJORA EN LA EXTRACCIÓN DE IMAGEN DESTACADA**
// **FUNCIONALIDAD DE PUBLICACIÓN DE NOTICIAS COMO BORRADOR**
// **CORREGIDO: No borrar de la tabla al publicar, asegurar imagen destacada.**
// **NUEVO: Selector de Idioma para el resumen generado por IA (Castellano, Catalán, Inglés).**
// ==============================

// — 1. REGISTRO DE MENÚS Y PÁGINAS DE ADMINISTRACIÓN —
// Esta función registra los menús y submenús necesarios en el panel de administración.
// Se engancha al ‘admin_menu’ hook de WordPress.
add_action(‘admin_menu’, ‘ksk_registrar_menu_y_estilos’);

if (!function_exists(‘ksk_registrar_menu_y_estilos’)) {
function ksk_registrar_menu_y_estilos() {
// Añadir el menú principal de nivel superior para las Noticias KSK
add_menu_page(
‘Gestor de Noticias KSK’, // Título que aparece en la etiqueta <title> de la página
‘Noticias KSK’, // Texto que aparece en el menú de navegación
‘manage_options’, // Capacidad de usuario requerida para ver este menú
‘ksk_noticias’, // Slug único para este menú (usado en la URL)
null, // Función de callback; ‘null’ porque tendrá submenús
‘dashicons-rss’, // Icono de Dashicons (o una URL a una imagen)
6 // Posición en el menú (6 es justo debajo de «Entradas»)
);

// Submenú para la página de «Generar Noticias desde URL»
add_submenu_page(
‘ksk_noticias’, // Slug del menú padre (el menú principal que acabamos de crear)
‘Generar Noticia desde URL’, // Título de la página
‘Generar desde URL’, // Texto del submenú
‘manage_options’, // Capacidad de usuario requerida
‘ksk_generar_noticia_url’, // Slug único para este submenú
‘ksk_generar_noticia_url_page’ // Función de callback que renderiza el contenido de la página
);

// Submenú para la página de «Ver Base de Datos de Noticias»
add_submenu_page(
‘ksk_noticias’, // Slug del menú padre
‘Ver Noticias Guardadas’, // Título de la página
‘Ver BBDD’, // Texto del submenú
‘manage_options’, // Capacidad de usuario requerida
‘ksk_ver_bbdd’, // Slug único para este submenú
‘ksk_ver_base_datos_page’ // Función de callback que renderiza el contenido de la página
);

// Submenú para la página de «Ajustes» (donde se configuran las claves API)
add_submenu_page(
‘ksk_noticias’, // Slug del menú padre
‘Ajustes de Noticias KSK’, // Título de la página
‘Ajustes’, // Texto del submenú
‘manage_options’, // Capacidad de usuario requerida
‘ksk_noticias_settings’, // Slug único para este submenú
‘ksk_noticias_settings_page_content’ // Función de callback que renderiza el contenido
);
}
}

// — 1.1. CSS PERSONALIZADO PARA EL MENÚ DE ADMINISTRACIÓN —
// Esta función encola el CSS personalizado para el menú «Noticias KSK» y sus submenús.
// Se engancha al ‘admin_head’ hook para imprimir el CSS directamente en la cabecera.
// Es una forma de asegurar que el estilo se carga y se aplica correctamente.
add_action(‘admin_head’, ‘ksk_admin_menu_styles’);

if (!function_exists(‘ksk_admin_menu_styles’)) {
function ksk_admin_menu_styles() {
?>
<style type=»text/css»>
/* Estilos para el menú principal «Noticias KSK» */
#adminmenu #toplevel_page_ksk_noticias .wp-menu-image {
/* Color del icono (opcional) */
filter: invert(0%) sepia(100%) saturate(7465%) hue-rotate(360deg) brightness(97%) contrast(105%); /* Icono negro */
}

#adminmenu #toplevel_page_ksk_noticias .wp-menu-image:before {
color: #000 !important; /* Asegura el color del icono si es un Dashicon */
}

#adminmenu #toplevel_page_ksk_noticias .wp-menu-image {
background-color: #FFD700 !important; /* Fondo amarillo para el icono */
border-radius: 3px; /* Bordes ligeramente redondeados */
}


#adminmenu #toplevel_page_ksk_noticias > a {
background-color: #FFD700 !important; /* Fondo amarillo para el enlace principal del menú */
color: #000 !important; /* Letras negras para el enlace principal */
font-weight: bold; /* Opcional: negrita para el texto del menú principal */
border-left: 4px solid #FFA500 !important; /* Borde naranja para destacar aún más */
}

#adminmenu #toplevel_page_ksk_noticias > a:hover,
#adminmenu #toplevel_page_ksk_noticias.wp-has-current-submenu > a,
#adminmenu #toplevel_page_ksk_noticias.current > a {
background-color: #FFB700 !important; /* Un amarillo ligeramente más oscuro al pasar el ratón o si está activo */
color: #000 !important; /* Letras negras */
}

/* Estilos para los submenús de «Noticias KSK» */
#adminmenu #toplevel_page_ksk_noticias ul.wp-submenu a {
background-color: #FFD700 !important; /* Fondo amarillo para los submenús */
color: #000 !important; /* Letras negras para los submenús */
border-left: 4px solid #FFA500 !important; /* Borde naranja para los submenús */
}

#adminmenu #toplevel_page_ksk_noticias ul.wp-submenu a:hover,
#adminmenu #toplevel_page_ksk_noticias ul.wp-submenu li.current > a,
#adminmenu #toplevel_page_ksk_noticias ul.wp-submenu li.current > a.current {
background-color: #FFB700 !important; /* Un amarillo ligeramente más oscuro al pasar el ratón o si está activo */
color: #000 !important; /* Letras negras */
}

/* Asegurar que el icono Dashicon dentro del submenú también sea negro */
#adminmenu #toplevel_page_ksk_noticias ul.wp-submenu .wp-menu-image:before {
color: #000 !important;
}

/* Estilo para el icono del menú principal cuando el menú padre está abierto o activo */
#adminmenu #toplevel_page_ksk_noticias.opensub li.wp-has-submenu a.wp-has-current-submenu .wp-menu-image:before,
#adminmenu #toplevel_page_ksk_noticias.current .wp-menu-image:before,
#adminmenu #toplevel_page_ksk_noticias.wp-menu-open .wp-menu-image:before {
color: #000 !important;
}

/* Estilos para los radio buttons de idioma */
.ksk-language-selector label {
margin-right: 15px;
font-weight: normal;
display: inline-block;
padding: 5px 0;
}
.ksk-language-selector input[type=»radio»] {
margin-right: 5px;
vertical-align: middle;
}
</style>
<?php
}
}


// — 2. REGISTRO DE AJUSTES (CLAVES API Y PROMPT) —
// Estas funciones gestionan el registro de las opciones de WordPress para almacenar
// las claves API y el prompt base, y renderizan la página de ajustes.
// Se engancha al ‘admin_init’ hook para registrar los ajustes.
add_action(‘admin_init’, ‘ksk_register_settings’);

if (!function_exists(‘ksk_register_settings’)) {
function ksk_register_settings() {
// Registrar el grupo de ajustes y las opciones individuales
register_setting(‘ksk_noticias_settings_group’, ‘ksk_openai_api_key’, array(
‘type’ => ‘string’,
‘sanitize_callback’ => ‘sanitize_text_field’,
‘default’ => »
));
register_setting(‘ksk_noticias_settings_group’, ‘ksk_prompt_resumir_noticias’, array(
‘type’ => ‘string’,
‘sanitize_callback’ => ‘sanitize_textarea_field’, // Mejor para texto largo como prompts
‘default’ => ‘Resume el siguiente artículo en 3 párrafos y en español:’
));
register_setting(‘ksk_noticias_settings_group’, ‘ksk_scrapingbee_api_key’, array(
‘type’ => ‘string’,
‘sanitize_callback’ => ‘sanitize_text_field’,
‘default’ => »
));

// Añadir una sección de ajustes
add_settings_section(
‘ksk_noticias_api_section’, // ID único para la sección
‘Configuración de API y Prompts’, // Título de la sección
‘ksk_noticias_api_section_callback’, // Función de callback para el contenido de la sección
‘ksk_noticias_settings’ // Slug de la página donde se mostrará esta sección
);

// Añadir los campos individuales a la sección
add_settings_field(
‘ksk_openai_api_key_field’, // ID único del campo
‘Clave API de OpenAI’, // Título del campo
‘ksk_openai_api_key_callback’, // Función de callback para renderizar el campo
‘ksk_noticias_settings’, // Slug de la página
‘ksk_noticias_api_section’ // ID de la sección a la que pertenece
);

add_settings_field(
‘ksk_prompt_resumir_noticias_field’, // ID único del campo
‘Prompt base para resumir noticias’, // Título del campo
‘ksk_prompt_resumir_noticias_callback’, // Función de callback para renderizar el campo
‘ksk_noticias_settings’, // Slug de la página
‘ksk_noticias_api_section’ // ID de la sección a la que pertenece
);

add_settings_field(
‘ksk_scrapingbee_api_key_field’, // ID único del campo
‘Clave API de ScrapingBee (opcional)’, // Título del campo
‘ksk_scrapingbee_api_key_callback’, // Función de callback para renderizar el campo
‘ksk_noticias_settings’, // Slug de la página
‘ksk_noticias_api_section’ // ID de la sección a la que pertenece
);
}
}

// Callbacks para renderizar los campos de la página de ajustes
if (!function_exists(‘ksk_noticias_api_section_callback’)) {
function ksk_noticias_api_section_callback() {
echo ‘<p>Introduce tus claves API para los servicios externos y el prompt base para la IA. Necesitas al menos la clave de OpenAI y el prompt.</p>’;
}
}

if (!function_exists(‘ksk_openai_api_key_callback’)) {
function ksk_openai_api_key_callback() {
$value = get_option(‘ksk_openai_api_key’);
echo ‘<input type=»text» name=»ksk_openai_api_key» value=»‘ . esc_attr($value) . ‘» class=»regular-text» placeholder=»sk-…» />’;
echo ‘<p class=»description»>Obtén tu clave de <a href=»https://platform.openai.com/account/api-keys» target=»_blank»>OpenAI</a>. Es esencial para generar resúmenes.</p>’;
}
}

if (!function_exists(‘ksk_prompt_resumir_noticias_callback’)) {
function ksk_prompt_resumir_noticias_callback() {
$value = get_option(‘ksk_prompt_resumir_noticias’);
echo ‘<textarea name=»ksk_prompt_resumir_noticias» rows=»5″ cols=»50″ class=»large-text code»>’ . esc_textarea($value) . ‘</textarea>’;
echo ‘<p class=»description»>Define el prompt que se enviará a OpenAI para generar el resumen. Ejemplo: «Resume el siguiente artículo en 3 párrafos y en español:».</p>’;
}
}

if (!function_exists(‘ksk_scrapingbee_api_key_callback’)) {
function ksk_scrapingbee_api_key_callback() {
$value = get_option(‘ksk_scrapingbee_api_key’);
echo ‘<input type=»text» name=»ksk_scrapingbee_api_key» value=»‘ . esc_attr($value) . ‘» class=»regular-text» placeholder=»Tu clave de ScrapingBee» />’;
echo ‘<p class=»description»>Opcional. Obtén tu clave de <a href=»https://www.scrapingbee.com/» target=»_blank»>ScrapingBee</a> para una extracción de contenido más robusta, especialmente en sitios complejos.</p>’;
}
}

// Función que renderiza el contenido de la página de ajustes
if (!function_exists(‘ksk_noticias_settings_page_content’)) {
function ksk_noticias_settings_page_content() {
?>
<div class=»wrap»>
<h1>Ajustes de Noticias KSK</h1>
<form method=»post» action=»options.php»>
<?php
// Agrupa los campos de ajustes
settings_fields(‘ksk_noticias_settings_group’);
// Muestra todas las secciones y campos registrados para esta página
do_settings_sections(‘ksk_noticias_settings’);
// Botón para guardar los cambios
submit_button(‘Guardar Cambios’);
?>
</form>
</div>
<?php
}
}


// ==============================
// 2.2. FUNCIÓN AJAX PARA GENERAR NOTICIAS DESDE URL (SERVIDOR)
// Esta función maneja la solicitud AJAX enviada desde el frontend para procesar una URL.
// Se engancha al ‘wp_ajax_ksk_generar_noticia_url_ajax’ hook.
add_action(‘wp_ajax_ksk_generar_noticia_url_ajax’, function() {
// Verificar el nonce de seguridad para prevenir ataques CSRF
check_ajax_referer(‘ksk_generar_noticia_url_nonce’, ‘nonce’);

// Obtener las claves API y el prompt base de las opciones de WordPress
$openai_key = get_option(‘ksk_openai_api_key’);
$prompt_base = get_option(‘ksk_prompt_resumir_noticias’);
$scrapingbee_key = get_option(‘ksk_scrapingbee_api_key’);
// Sanitizar y validar la URL de entrada
$url_to_scrape = isset($_POST[‘url’]) ? esc_url_raw($_POST[‘url’]) : »;
// Obtener el idioma seleccionado por el usuario
$selected_language = isset($_POST[‘lang’]) ? sanitize_text_field($_POST[‘lang’]) : ‘castellano’; // Default a castellano

// Validaciones iniciales
if (empty($url_to_scrape)) {
wp_send_json_error([‘message’ => ‘La URL no puede estar vacía.’]);
return;
}
if (empty($openai_key) || empty($prompt_base)) {
wp_send_json_error([‘message’ => ‘Clave API de OpenAI o Prompt base no configurados. Por favor, ve a «Noticias KSK -> Ajustes» para configurarlos.’]);
return;
}

// Obtener noticias existentes para comprobar duplicados
$existentes = get_option(‘ksk_noticias_guardadas’, []);
if (!is_array($existentes)) {
$existentes = []; // Asegurarse de que sea un array
}
$urls_existentes = array_column($existentes, ‘url’);
if (in_array($url_to_scrape, $urls_existentes)) {
wp_send_json_success([
‘status’ => ‘skipped’,
‘message’ => ‘La URL ya existe en la base de datos.’,
‘log’ => [[‘title’ => $url_to_scrape, ‘status’ => ‘URL ya existente (saltada)’]] // Título para el log
]);
return;
}

// — LÓGICA DE OBTENCIÓN DE CONTENIDO, TÍTULO E IMAGEN DE LA URL —
$article_content_for_prompt = »;
$article_title = ‘Título de URL Desconocido’;
$article_image = »; // Inicializado como vacío
$article_source_name = parse_url($url_to_scrape, PHP_URL_HOST); // Obtener el dominio como nombre de la fuente

// Intentar obtener contenido con ScrapingBee primero si la clave está disponible
if (!empty($scrapingbee_key)) {
libxml_use_internal_errors(true); // Suprimir errores de análisis HTML para DOMDocument
$scrapingbee_url = ‘https://app.scrapingbee.com/api/v1/?api_key=’ . $scrapingbee_key . ‘&url=’ . urlencode($url_to_scrape) . ‘&premium_proxy=false&block_ads=true’;
$response = wp_remote_get($scrapingbee_url, [‘timeout’ => 45]);

if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
$html_content = wp_remote_retrieve_body($response);
$dom = new DOMDocument();
@$dom->loadHTML(‘<!DOCTYPE html><html><head><meta charset=»UTF-8″></head><body>’ . $html_content . ‘</body></html>’);
libxml_clear_errors();
$xpath = new DOMXPath($dom);

// Extraer título
$title_nodes = $xpath->query(‘//meta[@property=»og:title»]/@content | //title | //h1’);
if ($title_nodes->length > 0 && !empty(trim($title_nodes->item(0)->nodeValue))) {
$article_title = sanitize_text_field(trim($title_nodes->item(0)->nodeValue));
}

// Extraer imagen: PRIORIZAR OG:IMAGE, LUEGO OTRAS IMÁGENES RELEVANTES
$image_nodes = $xpath->query(‘//meta[@property=»og:image»]/@content’);
if ($image_nodes->length > 0 && !empty(trim($image_nodes->item(0)->nodeValue))) {
$article_image = esc_url_raw(trim($image_nodes->item(0)->nodeValue));
} else {
// Si no hay og:image, buscar la primera imagen dentro del cuerpo principal
// Filtra algunas palabras clave comunes en URLs de iconos/favicons o clases comunes de elementos no deseados
$body_img_nodes = $xpath->query(‘//body//img[not(contains(@src, «favicon»)) and not(contains(@src, «icon»)) and not(contains(@src, «logo»)) and not(contains(@class, «emoji»)) and not(contains(@class, «avatar»)) and not(contains(@class, «wp-smiley»))]’);
if ($body_img_nodes->length > 0) {
foreach ($body_img_nodes as $img_node) {
$src = $img_node->getAttribute(‘src’);
// Heurística básica: evitar miniaturas explícitas en la URL o nombres de archivo comunes para elementos pequeños
if (!empty($src) && !strpos($src, ‘thumb’) && !strpos($src, ‘mini’) && !strpos($src, ‘small’) && !strpos($src, ‘avatar’) && !strpos($src, ‘sprite’)) {
$article_image = esc_url_raw($src);
break; // Tomar la primera imagen que parezca relevante
}
}
}
}

// Extraer contenido para el prompt (similar a antes)
$content_nodes = $xpath->query(‘//p | //h1 | //h2 | //h3 | //li | //meta[@name=»description»]/@content’);
$temp_content = »;
foreach ($content_nodes as $node) {
if ($node->nodeName === ‘meta’ && $node->hasAttribute(‘content’)) {
if (empty($temp_content) || strlen($temp_content) < 100) {
$temp_content .= $node->getAttribute(‘content’) . «\n»;
}
} else {
$temp_content .= $node->textContent . «\n»;
}
}
$article_content_for_prompt = sanitize_text_field(trim($temp_content));
error_log(‘DEBUG KSK URL (ScrapingBee): Contenido y imagen obtenidos para ‘ . $url_to_scrape);
} else {
error_log(‘DEBUG KSK URL (ScrapingBee): Error o fallo para ‘ . $url_to_scrape . ‘: ‘ . (is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_response_code($response)));
}
}

// Intento 2: Fallback con wp_remote_get directo y DOMDocument/XPath mejorado para imagen y contenido
// Se ejecuta si ScrapingBee falló, no se usó, o el contenido/imagen obtenidos fueron insuficientes.
if (empty($article_content_for_prompt) || strlen($article_content_for_prompt) < 50 || empty($article_image)) {
$response = wp_remote_get($url_to_scrape, [‘timeout’ => 30]);
if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
$html = wp_remote_retrieve_body($response);
libxml_use_internal_errors(true); // Suprimir errores para DOMDocument
$dom = new DOMDocument();
@$dom->loadHTML(‘<!DOCTYPE html><html><head><meta charset=»UTF-8″></head><body>’ . $html . ‘</body></html>’);
libxml_clear_errors();
$xpath = new DOMXPath($dom);

// Extraer título (priorizando og:title, luego title, luego h1)
$title_nodes = $xpath->query(‘//meta[@property=»og:title»]/@content | //title | //h1’);
if ($title_nodes->length > 0 && !empty(trim($title_nodes->item(0)->nodeValue))) {
$article_title = sanitize_text_field(trim($title_nodes->item(0)->nodeValue));
}

// Extraer imagen: PRIORIZAR OG:IMAGE, LUEGO OTRAS IMÁGENES RELEVANTES EN EL BODY
$image_nodes = $xpath->query(‘//meta[@property=»og:image»]/@content’);
if ($image_nodes->length > 0 && !empty(trim($image_nodes->item(0)->nodeValue))) {
$article_image = esc_url_raw(trim($image_nodes->item(0)->nodeValue));
} else {
// Si no hay og:image, buscar la primera imagen dentro del cuerpo principal
$body_img_nodes = $xpath->query(‘//body//img[not(contains(@src, «favicon»)) and not(contains(@src, «icon»)) and not(contains(@src, «logo»)) and not(contains(@class, «emoji»)) and not(contains(@class, «avatar»)) and not(contains(@class, «wp-smiley»))]’);
if ($body_img_nodes->length > 0) {
foreach ($body_img_nodes as $img_node) {
$src = $img_node->getAttribute(‘src’);
if (!empty($src) && !strpos($src, ‘thumb’) && !strpos($src, ‘mini’) && !strpos($src, ‘small’) && !strpos($src, ‘avatar’) && !strpos($src, ‘sprite’)) {
$article_image = esc_url_raw($src);
break;
}
}
}
}

// Extraer contenido (similar a la lógica de ScrapingBee, pero usando el HTML directo)
$content_nodes = $xpath->query(‘//p | //h1 | //h2 | //h3 | //li | //meta[@name=»description»]/@content’);
$temp_content = »;
foreach ($content_nodes as $node) {
if ($node->nodeName === ‘meta’ && $node->hasAttribute(‘content’)) {
if (empty($temp_content) || strlen($temp_content) < 100) {
$temp_content .= $node->getAttribute(‘content’) . «\n»;
}
} else {
$temp_content .= $node->textContent . «\n»;
}
}
$article_content_for_prompt = sanitize_text_field(trim($temp_content));
error_log(‘DEBUG KSK URL (wp_remote_get fallback): Contenido y imagen obtenidos para ‘ . $url_to_scrape);

} else {
error_log(‘DEBUG KSK URL (wp_remote_get fallback): Error o fallo para ‘ . $url_to_scrape . ‘: ‘ . (is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_response_code($response)));
}
}

// Si no se pudo obtener contenido suficiente, se detiene el proceso
if (empty($article_content_for_prompt) || strlen(trim($article_content_for_prompt)) < 50) {
wp_send_json_error([‘message’ => ‘No se pudo obtener contenido suficiente de la URL para generar un resumen.’]);
return;
}

// — GENERAR RESUMEN CON OPENAI —
$resumen = »;
$resumen_generado_ok = false;
$log_status = »;
if (!empty($openai_key) && !empty($prompt_base)) {
// Adaptar el prompt base con la instrucción de idioma
$language_instruction = »;
switch ($selected_language) {
case ‘castellano’:
$language_instruction = ‘y en castellano’;
break;
case ‘catalan’:
$language_instruction = ‘y en catalán’;
break;
case ‘ingles’:
$language_instruction = ‘and in English’;
break;
}

// Construir el prompt para la IA, añadiendo la instrucción de idioma al prompt base
$final_prompt = rtrim($prompt_base, ‘:’) . ‘ ‘ . $language_instruction . ‘:\n\n’ . $article_content_for_prompt;

// Realizar la solicitud a la API de OpenAI
$res = wp_remote_post(‘https://api.openai.com/v1/chat/completions’, [
‘headers’ => [
‘Content-Type’ => ‘application/json’,
‘Authorization’ => ‘Bearer ‘ . $openai_key,
],
‘body’ => json_encode([
‘model’ => ‘gpt-3.5-turbo’, // Modelo a utilizar
‘messages’ => [[‘role’ => ‘user’, ‘content’ => $final_prompt]], // Mensaje del usuario
‘temperature’ => 0.7, // Creatividad del modelo
]),
‘timeout’ => 60 // Aumentar timeout para la API de OpenAI
]);

if (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) {
$body = json_decode(wp_remote_retrieve_body($res), true);
$ai_response_content = trim($body[‘choices’][0][‘message’][‘content’] ?? »);

// Patrones para identificar respuestas rechazadas o genéricas de la IA
$rejected_patterns = [
‘Lo siento, pero no puedo completar tu solicitud’, ‘no puedo generar un resumen’, ‘información no directamente relacionada’, ‘no puedo resumir el texto proporcionado’, ‘como modelo de lenguaje’
];
$is_rejected_or_empty = empty($ai_response_content);
foreach ($rejected_patterns as $pattern) {
if (stripos($ai_response_content, $pattern) !== false) {
$is_rejected_or_empty = true;
break;
}
}

if ($is_rejected_or_empty) {
$resumen = ‘Resumen rechazado/vacío por OpenAI: ‘ . (empty($ai_response_content) ? ‘Respuesta vacía.’ : ‘Contenido no apto o demasiado largo.’);
error_log(‘DEBUG KSK URL: OpenAI rechazó/vacío resumen para «‘ . $article_title . ‘»: ‘ . $resumen);
$log_status = ‘Resumen IA: Rechazado/Vacío (no guardada)’;
$resumen_generado_ok = false;
} else {
$resumen = $ai_response_content;
$resumen_generado_ok = true;
$log_status = ‘Resumen IA: OK’;
}
} else {
// Manejo de errores de la API de OpenAI
$error_message = is_wp_error($res) ? $res->get_error_message() : ‘HTTP Code: ‘ . wp_remote_retrieve_response_code($res) . ‘ – Body: ‘ . wp_remote_retrieve_body($res);
error_log(‘DEBUG KSK URL: OpenAI API Error for «‘ . $article_title . ‘»: ‘ . $error_message);
$resumen = ‘Error al generar resumen (OpenAI API falló).’;
$log_status = ‘Resumen IA: Error API (‘ . $error_message . ‘) (no guardada)’;
$resumen_generado_ok = false;
}
} else {
// Si las claves de OpenAI no están configuradas, se guarda sin resumen IA
$resumen = ‘Resumen IA no disponible (API Key o Prompt base de OpenAI no configurados).’;
$log_status = ‘Resumen IA: Claves no configuradas (guardada sin resumen IA)’;
$resumen_generado_ok = true; // Permite guardar la noticia aunque no haya resumen IA
}

// — GUARDAR NOTICIA —
if ($resumen_generado_ok) {
$new_article_data = [
‘title’ => $article_title,
‘url’ => $url_to_scrape,
‘image’ => $article_image,
‘description’ => $article_content_for_prompt, // Contenido original extraído
‘resumen’ => $resumen,
‘source’ => [‘name’ => $article_source_name],
‘publishedAt’ => current_time(‘mysql’), // Fecha de la operación actual
‘fecha_base’ => current_time(‘mysql’), // Otra referencia a la fecha de guardado
];

$existentes[] = $new_article_data;
update_option(‘ksk_noticias_guardadas’, $existentes); // Guardar el array actualizado en la base de datos
$log_status .= ‘ (Guardada)’;
wp_send_json_success([
‘status’ => ‘success’,
‘message’ => ‘Noticia procesada y guardada correctamente.’,
‘log’ => [[‘title’ => $article_title, ‘status’ => $log_status, ‘image’ => $article_image]] // Incluir imagen para el log de verificación
]);
} else {
// Si el resumen no se generó correctamente y no se permitió guardar sin él
wp_send_json_error([
‘message’ => ‘No se pudo generar ni guardar la noticia: ‘ . $log_status,
‘log’ => [[‘title’ => $article_title, ‘status’ => $log_status]]
]);
}
});

// ==============================
// 2.3. FUNCIÓN AJAX PARA PUBLICAR NOTICIA EN WORDPRESS (SERVIDOR)
// Esta función maneja la solicitud AJAX para publicar una noticia guardada como borrador.
add_action(‘wp_ajax_ksk_publicar_noticia_ajax’, function() {
// Verificar el nonce de seguridad
check_ajax_referer(‘ksk_publicar_noticia_nonce’, ‘nonce’);

$url_to_publish = isset($_POST[‘url’]) ? esc_url_raw($_POST[‘url’]) : »;

if (empty($url_to_publish)) {
wp_send_json_error([‘message’ => ‘URL de noticia no proporcionada para publicar.’]);
return;
}

$noticias = get_option(‘ksk_noticias_guardadas’, []);
if (!is_array($noticias)) {
$noticias = [];
}

$found_key = -1;
$article_data = null;
foreach ($noticias as $key => $n) {
if (($n[‘url’] ?? ») === $url_to_publish) {
$found_key = $key;
$article_data = $n;
break;
}
}

if ($found_key === -1 || $article_data === null) {
wp_send_json_error([‘message’ => ‘Noticia no encontrada en la base de datos para publicar.’]);
return;
}

// Preparar los datos del post
$new_post = array(
‘post_title’ => wp_strip_all_tags($article_data[‘title’]),
‘post_content’ => $article_data[‘resumen’], // Usar el resumen de IA como contenido
‘post_status’ => ‘draft’, // Publicar como borrador
‘post_type’ => ‘post’, // Tipo de post estándar
‘post_author’ => get_current_user_id(), // Asignar al usuario actual
);

// Insertar el post
$post_id = wp_insert_post($new_post, true);

if (is_wp_error($post_id)) {
wp_send_json_error([‘message’ => ‘Error al crear el borrador del post: ‘ . $post_id->get_error_message()]);
return;
}

$image_attached = false;
// Gestionar la imagen destacada si existe
if (!empty($article_data[‘image’])) {
// Necesitamos incluir los archivos de carga de medios de WordPress
// Si ya están incluidos por otro plugin/tema, no pasa nada.
if ( ! function_exists( ‘media_handle_sideload’ ) ) {
require_once( ABSPATH . ‘wp-admin/includes/image.php’ );
require_once( ABSPATH . ‘wp-admin/includes/file.php’ );
require_once( ABSPATH . ‘wp-admin/includes/media.php’ );
}

// Descargar y asignar la imagen destacada
$image_url = $article_data[‘image’];

// media_sideload_image descarga la imagen, la añade a la biblioteca de medios
// y opcionalmente la adjunta a un post.
// El cuarto parámetro ‘id’ hace que devuelva el ID del adjunto.
$attachment_id = media_sideload_image( $image_url, $post_id, wp_strip_all_tags($article_data[‘title’]), ‘id’ );

if (!is_wp_error($attachment_id)) {
// Asignar la imagen cargada como imagen destacada del post
set_post_thumbnail($post_id, $attachment_id);
$image_attached = true;
} else {
error_log(‘KSK Publicar: Error al cargar la imagen destacada con media_sideload_image para URL: ‘ . $image_url . ‘. Error: ‘ . $attachment_id->get_error_message());
}
}

// *** La noticia se mantiene en ‘ksk_noticias_guardadas’ como se solicitó. ***

wp_send_json_success([
‘message’ => ‘Noticia publicada como borrador correctamente.’,
‘post_id’ => $post_id,
‘edit_link’ => get_edit_post_link($post_id),
‘image_attached’ => $image_attached,
]);
});

// 3. PÁGINA DE GENERACIÓN DE NOTICIAS DESDE URL (FRONTEND HTML Y JAVASCRIPT)
// Esta función renderiza la interfaz de usuario para introducir URLs y procesarlas.
// Ya está enganchada a un submenú en ksk_registrar_menu_y_estilos().
if (!function_exists(‘ksk_generar_noticia_url_page’)) {
function ksk_generar_noticia_url_page() {
// Definir los idiomas disponibles
$languages = [
‘castellano’ => ‘Castellano’,
‘catalan’ => ‘Catalán’,
‘ingles’ => ‘Inglés’,
];
// Idioma por defecto
$default_language = ‘castellano’;

echo ‘<div class=»wrap»><h1>🔗 Generar Noticias desde URL</h1>’;
echo ‘<p>Pega la URL de una noticia para obtener su contenido, resumirlo con IA y guardarlo en tu base de datos.</p>’;
echo ‘<p>Asegúrate de haber configurado tu clave API de OpenAI y el prompt en <a href=»‘ . esc_url(admin_url(‘admin.php?page=ksk_noticias_settings’)) . ‘»>Ajustes</a>.</p>’;

// Selector de Idioma (Radio Buttons)
echo ‘<h3>Idioma del Resumen:</h3>’;
echo ‘<div class=»ksk-language-selector»>’;
foreach ($languages as $value => $label) {
$checked = ($value === $default_language) ? ‘checked’ : »;
echo ‘<label><input type=»radio» name=»ksk_lang_select» value=»‘ . esc_attr($value) . ‘» ‘ . $checked . ‘> ‘ . esc_html($label) . ‘</label>’;
}
echo ‘</div>’; // Fin ksk-language-selector

echo ‘<p><input type=»url» id=»ksk-url-input» placeholder=»Pega la URL aquí (ej: https://ejemplo.com/noticia)» style=»width: 100%; margin-right: 10px;» /></p>’;
echo ‘<p><button id=»ksk-process-url-btn» class=»button button-primary»>✨ Procesar URL</button></p>’;
echo ‘<div id=»ksk-url-status» style=»margin-top:1em; font-weight: bold;»></div>’;
echo ‘<div id=»ksk-url-log» style=»margin-top:1em; padding:1em; background:#f9f9f9; border:1px solid #ddd; max-height: 300px; overflow-y: auto;»></div>’;
echo ‘</div>’; // Fin del wrap

// JAVASCRIPT INCRUSTADO PARA AJAX ASÍNCRONO
// Generar un nonce de seguridad para la solicitud AJAX
$nonce_url_val = wp_create_nonce(‘ksk_generar_noticia_url_nonce’);
$ajax_url = admin_url(‘admin-ajax.php’); // URL del endpoint AJAX de WordPress

// El JavaScript se imprime directamente en la página.
// Se recomienda encolar scripts en archivos separados para proyectos más grandes.
echo <<<JS
<script type=»text/javascript»>
jQuery(document).ready(function($) {
var \$processUrlBtn = $(«#ksk-process-url-btn»);
var \$urlInput = $(«#ksk-url-input»);
var \$urlStatusDiv = $(«#ksk-url-status»);
var \$urlLogDiv = $(«#ksk-url-log»);

\$processUrlBtn.on(«click», function() {
var url = \$urlInput.val().trim(); // Obtener la URL y limpiar espacios
var selectedLang = $(‘input[name=»ksk_lang_select»]:checked’).val(); // Obtener el idioma seleccionado

if (url === «») {
\$urlStatusDiv.removeClass(«notice-success»).addClass(«notice notice-error»).html(«❌ <strong>Error:</strong> Debes introducir una URL.»);
return;
}

// Deshabilitar botón y actualizar estado
\$processUrlBtn.prop(«disabled», true).text(«Procesando URL…»);
\$urlStatusDiv.removeClass(«notice notice-error notice-success»).text(«Obteniendo y resumiendo noticia de la URL en » + selectedLang + «…»);
\$urlLogDiv.empty(); // Limpiar log para cada nueva URL procesada
\$urlLogDiv.append(«<p style=’font-weight:bold;’>— Iniciando procesamiento de URL: » + url + » (» + new Date().toLocaleString() + «) —</p>»);
\$urlLogDiv.scrollTop(\$urlLogDiv[0].scrollHeight); // Desplazar al final del log

// Realizar la solicitud AJAX
$.ajax({
url: «{$ajax_url}», // Usar la URL AJAX de WordPress
type: «POST»,
data: {
action: «ksk_generar_noticia_url_ajax», // La acción AJAX a llamar en el servidor
nonce: «{$nonce_url_val}», // El nonce de seguridad
url: url, // La URL a procesar
lang: selectedLang // El idioma seleccionado
},
success: function(response) {
console.log(«URL Process Response:», response);
if (response.success) {
\$urlStatusDiv.removeClass(«notice-error»).addClass(«notice notice-success»).html(«✅ Noticia de URL procesada correctamente.»);
// Iterar sobre los elementos del log recibidos y añadirlos al div de log
$.each(response.data.log, function(index, item) {
var statusColor = «green»;
if (item.status.includes(«saltada») || item.status.includes(«no guardada»)) { statusColor = «orange»; }
else if (item.status.includes(«Error»)) { statusColor = «red»; }
var imageHtml = item.image ? ‘<img src=»‘ + item.image + ‘» style=»max-width:100px; max-height:100px; margin-right:10px; vertical-align:middle; border-radius:3px;»>’ : »;
\$urlLogDiv.append(«<p style=\\»color:» + statusColor + «;\\»>» + imageHtml + (item.title ? item.title + «: » : «») + item.status + «</p>»);
});
\$urlLogDiv.scrollTop(\$urlLogDiv[0].scrollHeight); // Desplazar al final
} else {
\$urlStatusDiv.removeClass(«notice-success»).addClass(«notice notice-error»).html(«❌ <strong>Error al procesar URL:</strong> » + (response.data.message || «Error desconocido del servidor.»));
\$urlLogDiv.append(«<p style=\\»color: red;\\»>Error: » + (response.data.message || «Respuesta de servidor inválida.») + «</p>»);
\$urlLogDiv.scrollTop(\$urlLogDiv[0].scrollHeight); // Desplazar al final
}
\$processUrlBtn.prop(«disabled», false).text(«✨ Procesar URL»); // Re-habilitar botón
},
error: function(xhr, status, error) {
console.error(«AJAX URL Error:», status, error, xhr.responseText);
\$urlStatusDiv.removeClass(«notice-success»).addClass(«notice notice-error»).html(«❌ <strong>Fallo la solicitud AJAX para la URL.</strong> Revisa la consola del navegador para más detalles.»);
\$urlLogDiv.append(«<p style=\\»color: red;\\»>Error AJAX URL: » + error + » (HTTP Status: » + xhr.status + «). Posiblemente timeout del servidor. Consulta la consola (F12) para más información.</p>»);
if (xhr.responseText) {
\$urlLogDiv.append(«<p style=\\»color: red;\\»>Respuesta del servidor (parcial): » + xhr.responseText.substring(0, 200) + «…</p>»);
}
\$urlLogDiv.scrollTop(\$urlLogDiv[0].scrollHeight); // Desplazar al final
\$processUrlBtn.prop(«disabled», false).text(«✨ Procesar URL»); // Re-habilitar botón
}
});
});
});
</script>
JS;
}
} // Fin del if (!function_exists…)

// ==============================
// 5. PÁGINA «VER BBDD» (Muestra las noticias guardadas y permite publicar)
// Esta función renderiza la tabla de noticias guardadas en el panel de administración.
// Ya está enganchada a un submenú en ksk_registrar_menu_y_estilos().
// ==============================
if (!function_exists(‘ksk_ver_base_datos_page’)) { // Se añade if para prevenir redeclaración
function ksk_ver_base_datos_page() {
echo ‘<div class=»wrap»><h1>📊 Noticias Guardadas en la Base de Datos</h1>’;
echo ‘<div id=»ksk-bbdd-status» style=»margin-top:1em; font-weight: bold;»></div>’; // Para mensajes de publicación/borrado AJAX
$noticias = get_option(‘ksk_noticias_guardadas’, []); // Obtener todas las noticias guardadas

// Asegurarse de que $noticias es un array antes de procesar
if (!is_array($noticias)) {
$noticias = []; // Si no es un array, lo inicializamos como vacío para evitar errores
}

// Lógica para borrar una noticia individual (Manejo directo para mantener compatibilidad, aunque el JS manejará el AJAX)
// Este bloque es un fallback para recargas de página o si el JS falla.
if (isset($_GET[‘borrar_url’]) && !empty($_GET[‘borrar_url’])) {
$url_a_borrar = urldecode(sanitize_url($_GET[‘borrar_url’]));
$noticias_filtradas = array_filter($noticias, function ($n) use ($url_a_borrar) {
return ($n[‘url’] ?? ») !== $url_a_borrar;
});
update_option(‘ksk_noticias_guardadas’, array_values($noticias_filtradas));
echo ‘<div class=»notice notice-warning is-dismissible»><p>🗑️ Noticia eliminada correctamente.</p></div>’;
echo ‘<script>window.location.href = «‘ . admin_url(‘admin.php?page=ksk_ver_bbdd’) . ‘»;</script>’;
exit;
}

if (empty($noticias)) {
echo ‘<p id=»no-noticias-message»>No hay noticias guardadas en la base de datos.</p>’;
} else {
// Ordenar las noticias para mostrar las PUBLICADAS más recientemente primero.
// Se usa ‘publishedAt’ o ‘fecha_base’ para ordenar.
usort($noticias, function($a, $b) {
$timeA = isset($a[‘publishedAt’]) ? strtotime($a[‘publishedAt’]) : (isset($a[‘fecha_base’]) ? strtotime($a[‘fecha_base’]) : 0);
$timeB = isset($b[‘publishedAt’]) ? strtotime($b[‘publishedAt’]) : (isset($b[‘fecha_base’]) ? strtotime($b[‘fecha_base’]) : 0);

// La lógica es la misma: B – A para que las más recientes aparezcan arriba.
return $timeB – $timeA;
});

// Iniciar la tabla HTML para mostrar las noticias
echo ‘<table id=»ksk-ver-bbdd-table» class=»wp-list-table widefat fixed striped»><thead><tr>’
. ‘<th style=»width:5%;»>#</th>’
. ‘<th style=»width:10%;»>Imagen</th>’
. ‘<th style=»width:15%;»>Título</th>’
. ‘<th style=»width:35%;»>Resumen</th>’ // Reducir un poco el ancho del resumen
. ‘<th style=»width:10%;»>Fuente</th>’
. ‘<th style=»width:10%;»>Fecha Pub.</th>’
. ‘<th style=»width:5%;»>Fecha Guardado</th>’
. ‘<th style=»width:10%;»>Acciones</th>’ // Aumentar un poco el ancho de acciones
. ‘</tr></thead><tbody>’;

$nonce_borrar_val = wp_create_nonce(‘ksk_generar_noticia_url_nonce’); // Usamos el mismo nonce para consistencia
$nonce_publicar_val = wp_create_nonce(‘ksk_publicar_noticia_nonce’); // Nonce específico para publicar

$row_count = 1; // La numeración de filas empieza desde 1
foreach ($noticias as $n) {
// Asegurar que las variables existen y están sanitizadas antes de intentar usarlas
$titulo_display = esc_html($n[‘title’] ?? ‘Sin título’);
$url_display = esc_url($n[‘url’] ?? ‘#’);
$resumen_display = esc_html($n[‘resumen’] ?? $n[‘description’] ?? ‘No disponible’);
$fuente_display = esc_html($n[‘source’][‘name’] ?? ‘Desconocido’);
$fecha_original_display = isset($n[‘publishedAt’]) ? wp_date(‘d/m/Y H:i’, strtotime($n[‘publishedAt’])) : ‘N/A’;
$fecha_base_display = isset($n[‘fecha_base’]) ? wp_date(‘d/m/Y H:i’, strtotime($n[‘fecha_base’])) : ‘N/A’;
$image_display = !empty($n[‘image’]) ? esc_url($n[‘image’]) : »;

echo ‘<tr id=»ksk-noticia-row-‘ . md5($url_display) . ‘»>’; // ID de fila para fácil manipulación con JS
echo ‘<td>’ . $row_count++ . ‘</td>’; // Numera la fila
echo ‘<td>’;
if (!empty($image_display)) {
echo ‘<img src=»‘ . $image_display . ‘» style=»width:100%; height:auto; border-radius:3px; display:block;» alt=»Imagen Noticia»>’;
} else {
echo ‘No img’;
}
echo ‘</td>’;
echo ‘<td><strong>’ . $titulo_display . ‘</strong><br><small><a href=»‘ . $url_display . ‘» target=»_blank»>Ver original</a></small></td>’;
echo ‘<td>’ . $resumen_display . ‘</td>’;
echo ‘<td>’ . $fuente_display . ‘</td>’;
echo ‘<td>’ . $fecha_original_display . ‘</td>’;
echo ‘<td>’ . $fecha_base_display . ‘</td>’;
echo ‘<td>’;
// Enlace para borrar con confirmación JavaScript
echo ‘<a href=»#» class=»ksk-borrar-noticia» data-url=»‘ . esc_attr($url_display) . ‘» data-nonce=»‘ . esc_attr($nonce_borrar_val) . ‘» title=»Borrar Noticia» style=»margin-right: 5px;»>🗑️</a>’;
// Nuevo enlace para publicar
echo ‘<a href=»#» class=»ksk-publicar-noticia» data-url=»‘ . esc_attr($url_display) . ‘» data-nonce=»‘ . esc_attr($nonce_publicar_val) . ‘» title=»Publicar como Borrador»>🚀</a>’;
echo ‘</td>’;
echo ‘</tr>’;
}
echo ‘</tbody></table>’;
}
echo ‘</div>’; // Cerrar div wrap para la página «Ver BBDD»

// JavaScript para manejar las acciones AJAX de borrar y publicar
$ajax_url = admin_url(‘admin-ajax.php’);
echo <<<JS
<script type=»text/javascript»>
jQuery(document).ready(function($) {
var \$bbddStatusDiv = $(«#ksk-bbdd-status»);

// Manejador para borrar noticia
// Nota: Este manejador AJAX ahora elimina la noticia de la opción ‘ksk_noticias_guardadas’
// para mantener un listado limpio de «noticias pendientes».
$(«.ksk-borrar-noticia»).on(«click», function(e) {
e.preventDefault();
if (!confirm(‘¿Estás seguro de que quieres borrar esta noticia individualmente de la base de datos guardada? (No afectará a posts ya publicados).’)) {
return;
}

var \$this = $(this);
var urlToDelete = \$this.data(‘url’);
var nonce = \$this.data(‘nonce’);
var \$row = \$this.closest(‘tr’);

\$bbddStatusDiv.removeClass(«notice-success notice-error notice-warning»).html(«Borrando noticia…»);

$.ajax({
url: «{$ajax_url}»,
type: «POST»,
data: {
action: «ksk_borrar_noticia_ajax», // Nueva acción AJAX para borrar
nonce: nonce,
url: urlToDelete
},
success: function(response) {
if (response.success) {
\$bbddStatusDiv.addClass(«notice notice-success»).html(«🗑️ Noticia borrada correctamente.»);
\$row.fadeOut(300, function() {
\$row.remove();
// Opcional: Re-numerar las filas si es importante
var remainingRows = $(«#ksk-ver-bbdd-table tbody tr»);
if (remainingRows.length === 0) {
$(«#ksk-ver-bbdd-table»).after(«<p id=\\»no-noticias-message\\»>No hay noticias guardadas en la base de datos.</p>»).remove();
} else {
remainingRows.each(function(index) {
$(this).find(«td:first»).text(index + 1);
});
}
});
} else {
\$bbddStatusDiv.addClass(«notice notice-error»).html(«❌ <strong>Error al borrar noticia:</strong> » + (response.data.message || «Error desconocido.»));
}
},
error: function(xhr, status, error) {
\$bbddStatusDiv.addClass(«notice notice-error»).html(«❌ <strong>Fallo la solicitud AJAX para borrar.</strong> » + error);
}
});
});

// Manejador para publicar noticia
$(«.ksk-publicar-noticia»).on(«click», function(e) {
e.preventDefault();
if (!confirm(‘¿Estás seguro de que quieres publicar esta noticia como borrador en WordPress? (La noticia permanecerá en esta lista).’)) {
return;
}

var \$this = $(this);
var urlToPublish = \$this.data(‘url’);
var nonce = \$this.data(‘nonce’);
// La fila no se elimina, por lo que no la necesitamos directamente para `remove()`

var originalButtonHtml = \$this.html(); // Guardar el HTML original del botón (icono)
\$this.html(‘<span class=»dashicons dashicons-update-alt spin» style=»vertical-align:text-top;»></span>’); // Icono de carga con animación
\$this.prop(‘disabled’, true); // Deshabilitar el botón

\$bbddStatusDiv.removeClass(«notice-success notice-error notice-warning»).html(«Publicando noticia como borrador…»);

$.ajax({
url: «{$ajax_url}»,
type: «POST»,
data: {
action: «ksk_publicar_noticia_ajax», // Acción AJAX para publicar
nonce: nonce,
url: urlToPublish
},
success: function(response) {
if (response.success) {
var message = ‘✅ Noticia publicada como borrador correctamente.’;
if (response.data.edit_link) {
message += ‘ <a href=»‘ + response.data.edit_link + ‘» target=»_blank»>Editar post</a>’;
}
if (response.data.image_attached) {
message += ‘ (Imagen destacada incluida)’;
} else {
message += ‘ (Sin imagen destacada)’;
}
\$bbddStatusDiv.addClass(«notice notice-success»).html(message);
// Cambiar a un checkmark verde para indicar que ya ha sido publicada
\$this.html(‘<span class=»dashicons dashicons-yes» style=»color: green; vertical-align:text-top;» title=»Publicado»></span>’);
// Opcional: Desactivar futuros clics en el botón de publicar si ya se publicó
// \$this.off(‘click’).css(‘cursor’, ‘not-allowed’);
} else {
\$bbddStatusDiv.addClass(«notice notice-error»).html(«❌ <strong>Error al publicar noticia:</strong> » + (response.data.message || «Error desconocido.»));
\$this.html(originalButtonHtml); // Restaurar icono original si hay error
}
\$this.prop(‘disabled’, false); // Re-habilitar botón
},
error: function(xhr, status, error) {
console.error(«AJAX Publish Error:», status, error, xhr.responseText);
\$bbddStatusDiv.addClass(«notice notice-error»).html(«❌ <strong>Fallo la solicitud AJAX para publicar.</strong> » + error + » (HTTP Status: » + xhr.status + «). Consulta la consola (F12) para más información.»);
\$this.html(originalButtonHtml); // Restaurar icono original si hay error
\$this.prop(‘disabled’, false); // Re-habilitar botón
}
});
});
// Añadir estilo CSS para la animación de carga (spinning Dashicon)
$(«<style type=’text/css’> .dashicons.spin { -webkit-animation: dashicons-spin 1s infinite linear; animation: dashicons-spin 1s infinite linear; } @-webkit-keyframes dashicons-spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes dashicons-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>»).appendTo(«head»);
});
</script>
JS;
}
} // Fin del if (!function_exists…)

// ==============================
// FUNCIÓN AJAX ESPECÍFICA PARA BORRAR NOTICIA (SERVIDOR)
// Separamos la lógica de borrado para mayor claridad y control.
add_action(‘wp_ajax_ksk_borrar_noticia_ajax’, function() {
check_ajax_referer(‘ksk_generar_noticia_url_nonce’, ‘nonce’); // Usamos el mismo nonce para consistencia

$url_to_delete = isset($_POST[‘url’]) ? esc_url_raw($_POST[‘url’]) : »;

if (empty($url_to_delete)) {
wp_send_json_error([‘message’ => ‘URL no proporcionada para borrar.’]);
return;
}

$noticias = get_option(‘ksk_noticias_guardadas’, []);
if (!is_array($noticias)) {
$noticias = [];
}

$initial_count = count($noticias);
$noticias_filtradas = array_filter($noticias, function ($n) use ($url_to_delete) {
return ($n[‘url’] ?? ») !== $url_to_delete;
});

if (count($noticias_filtradas) < $initial_count) {
update_option(‘ksk_noticias_guardadas’, array_values($noticias_filtradas));
wp_send_json_success([‘message’ => ‘Noticia borrada correctamente.’]);
} else {
wp_send_json_error([‘message’ => ‘La noticia no fue encontrada para borrar.’]);
}
});

El plugin
Scroll al inicio