?php /** * Plugin Name: Diario de Conciertos - Core Semántico * Plugin URI: https://diariodeconciertos.com * Description: Arquitectura Core v10.0.0 - Motor de Grafo de Conocimiento Global con compatibilidad total de CPTs en RAM. * Version: 10.0.0 * Author: Ingeniería Avanzada (José Antonio + AI) * License: GPL2 */ if (!defined('ABSPATH')) { exit; } /** * CLASE CENTRAL: ARQUITECTURA CORE DIARIO DE CONCIERTOS */ class DiarioConciertosCore { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { // Disparadores de guardado para regenerar la matriz de adyacencia del grafo add_action('save_post', array($this, 'purificar_y_reconstruir_grafo_on_save'), 10, 2); // Aduana AJAX para el SEO Auditor saltándose bloqueos de constructores visuales add_action('wp_ajax_ddc_seo_auditor_aduana', array($this, 'aduana_ajax_seo_auditor')); // Inyección del Grafo de Conocimiento al final del contenido (Fichas Egoístas) add_filter('the_content', array($this, 'inyectar_grafo_semantico_hub'), 99); // Forzado incondicional de limpieza vía URL (?clean=1) add_action('wp_loaded', array($this, 'controlador_disparador_limpieza_incondicional')); } /** * 1. PURIFICACIÓN EN IONOS: CONSULTAS SEPARADAS DE METAS Y TAXONOMÍAS GEO * Mapea de forma masiva tolerando tanto los CPTs nativos de la BD como los limpios. */ public function purificar_y_reconstruir_grafo_on_save($post_id, $post) { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (!current_user_can('edit_post', $post_id)) return; if (in_array($post->post_type, array('revision', 'nav_menu_item', 'attachment'))) return; global $wpdb; // CONSULTA 1: Extracción limpia de contenidos bajo matriz de CPTs duales $sql_posts = " SELECT p.ID, p.post_title, p.post_type, p.post_date, m_strength.meta_value as strength FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} m_strength ON (p.ID = m_strength.post_id AND m_strength.meta_key = '_ddc_relationship_strength') WHERE p.post_status = 'publish' AND p.post_type IN ('post', 'artista', 'ddc_artist', 'sala', 'ddc_venue', 'festival', 'ddc_festival', 'video', 'galeria') ORDER BY p.post_date DESC LIMIT 2000 "; $raw_posts = $wpdb->get_results($sql_posts, ARRAY_A); if (empty($raw_posts)) return; $master_posts = []; $artists_transient = []; $venues_transient = []; $festivals_transient = []; foreach ($raw_posts as $r_post) { $p_id = intval($r_post['ID']); $p_type = $r_post['post_type']; // Normalización interna del tipo de nodo para el motor de renders (links-view.php) $normalized_type = $p_type; if ($p_type === 'ddc_artist') $normalized_type = 'artista'; if ($p_type === 'ddc_venue') $normalized_type = 'sala'; if ($p_type === 'ddc_festival') $normalized_type = 'festival'; // CONSULTA 2 SEPARADA: Evita el desborde de GROUP_CONCAT en la base de datos de IONOS $related_artists = $wpdb->get_col($wpdb->prepare(" SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_ddc_artists_ids' ", $p_id)); // CONSULTA 3 SEPARADA: Taxonomías e Interlinking Geográfico por separado $geo_terms = $wpdb->get_col($wpdb->prepare(" SELECT t.name FROM {$wpdb->terms} t INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id INNER JOIN {$wpdb->term_relationships} tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tr.object_id = %d AND tt.taxonomy IN ('category', 'post_tag', 'ciudades', 'salas_tax') ", $p_id)); $node = [ 'id' => $p_id, 'title' => $r_post['post_title'], 'type' => $normalized_type, // Siempre expone el tipo normalizado al grafo 'date' => $r_post['post_date'], 'permalink' => get_permalink($p_id), 'relationship_strength' => !empty($r_post['strength']) ? $r_post['strength'] : 'mencionado', 'artists_ids' => !empty($related_artists) ? array_map('intval', $related_artists) : [], 'geo_tags' => $geo_terms ]; $master_posts[] = $node; // Enrutamiento y fragmentación en memoria RAM estática por contenedores if ($normalized_type === 'artista') $artists_transient[] = $node; if ($normalized_type === 'sala') $venues_transient[] = $node; if ($normalized_type === 'festival') $festivals_transient[] = $node; } // Almacenamiento directo en los 4 Transients Fragmentados de IONOS set_transient('ddc_master_posts', $master_posts, DAY_IN_SECONDS * 7); set_transient('_artists', $artists_transient, DAY_IN_SECONDS * 7); set_transient('_venues', $venues_transient, DAY_IN_SECONDS * 7); set_transient('_festivals', $festivals_transient, DAY_IN_SECONDS * 7); } /** * 2. INTERCEPCIÓN DEL CONTENIDO E INYECCIÓN DE LA VISTA SEMÁNTICA MODULAR */ public function inyectar_grafo_semantico_hub($content) { // Ejecutar de forma segura tolerando todos los identificadores de post types implicados $allowed_singular = array('post', 'artista', 'ddc_artist', 'sala', 'ddc_venue', 'festival', 'ddc_festival', 'video', 'galeria'); if (!is_singular($allowed_singular)) { return $content; } // Carga la lógica de los 5 filtros en cascada desde links-view.php consumiendo RAM a coste cero SQL $path_view = plugin_dir_path(__FILE__) . 'links-view.php'; if (file_exists($path_view)) { ob_start(); include $path_view; $grafo_html = ob_get_clean(); // Reemplaza por completo el módulo de inyección rígida e inyecta la rejilla $content .= $grafo_html; } return $content; } /** * 3. DISPARADOR INCONDICIONAL DE LIMPIEZA MÁSTER (?clean=1) */ public function controlador_disparador_limpieza_incondicional() { if (isset($_GET['clean']) && $_GET['clean'] == '1' && current_user_can('manage_options')) { delete_transient('ddc_master_posts'); delete_transient('_artists'); delete_transient('_venues'); delete_transient('_festivals'); $post_id = get_the_ID(); if ($post_id) { $post = get_post($post_id); $this->purificar_y_reconstruir_grafo_on_save($post_id, $post); } wp_safe_redirect(remove_query_arg('clean')); exit; } } /** * 4. ADUANA AJAX PARA EL SEO AUDITOR (Ignora los bloqueos estructurales de Extra) */ public function aduana_ajax_seo_auditor() { check_ajax_referer('ddc_seo_aduana_nonce', 'nonce'); $target_post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; if (!$target_post_id) { wp_send_json_error('ID de nodo no válido.'); } $content = get_post_field('post_content', $target_post_id); $word_count = str_word_count(strip_tags($content)); $master_posts = get_transient('ddc_master_posts') ?: []; $total_relations = 0; foreach ($master_posts as $node) { if (isset($node['artists_ids']) && in_array($target_post_id, $node['artists_ids'])) { $total_relations++; } } wp_send_json_success([ 'palabras_editor' => $word_count, 'relaciones_grafo' => $total_relations, 'densidad_semantica' => ($total_relations * 75) . ' palabras indexables calculadas por Schema Graph' ]); } } // Inicialización del objeto Singleton en la carga del ecosistema de plugins add_action('plugins_loaded', array('DiarioConciertosCore', 'get_instance'));