$val) { $out[ sanitize_key($k) ] = sanitize_text_field($val); } return $out; }; $sanitize_cache_enum = function($v){ $allowed = [21600, 43200, 86400]; // 6h, 12h, 1 día $val = (int)$v; return in_array($val, $allowed, true) ? $val : 43200; // por defecto 12h }; $sanitize_num_posts = function($v){ $n = (int) $v; if ($n < 1) $n = 1; if ($n > 12) $n = 12; return $n; }; register_setting('aceprensa-posts-settings','aceprensa_site_url',[ 'type'=>'string', 'sanitize_callback'=>$sanitize_url ]); register_setting('aceprensa-posts-settings','aceprensa_cache_seconds',[ 'type'=>'integer', 'sanitize_callback'=>$sanitize_cache_enum, 'default'=>43200 ]); register_setting('aceprensa-posts-settings','aceprensa_num_posts',[ 'type'=>'integer', 'sanitize_callback'=>$sanitize_num_posts, 'default'=>3 ]); register_setting('aceprensa-posts-settings','aceprensa_image_size',[ 'type'=>'string', 'sanitize_callback'=>$sanitize_text, 'default'=>'medium' ]); register_setting('aceprensa-posts-settings','aceprensa_un_click',[ 'type'=>'boolean', 'sanitize_callback'=>$sanitize_bool, 'default'=>0 ]); register_setting('aceprensa-posts-settings','aceprensa_username',[ 'type'=>'string', 'sanitize_callback'=>$sanitize_text ]); register_setting('aceprensa-posts-settings','aceprensa_selected_categories',[ 'type'=>'array', 'sanitize_callback'=>$sanitize_array_kv, 'default'=>[] ]); register_setting('aceprensa-posts-settings','aceprensa_post_types',[ 'type'=>'array', 'sanitize_callback'=>$sanitize_array_kv, 'default'=>['post'=>'post'] ]); // --- DISEÑO: sanitizadores específicos --- $sanitize_enum = function($v, $allowed){ $v = is_string($v) ? $v : ''; return in_array($v, $allowed, true) ? $v : $allowed[0]; }; $sanitize_cols = function($v, $min, $max){ $n = (int)$v; return max($min, min($max, $n)); }; $sanitize_words = function($v){ $n = (int)$v; return max(5, min(120, $n ?: 30)); }; add_settings_section('aceprensa_main', __('Parámetros', 'aceprensa-posts'), function(){ echo '

'.esc_html__('Configura la conexión y el comportamiento del listado.', 'aceprensa-posts').'

'; }, ACEPRENSA_POSTS_SLUG); add_settings_field('aceprensa_site_url','URL origen', [__CLASS__,'field_site_url'], ACEPRENSA_POSTS_SLUG, 'aceprensa_main', ['class' => 'aceprensa_hide']); add_settings_field('aceprensa_un_click','Aceprensa a un click', [__CLASS__,'field_un_click'], ACEPRENSA_POSTS_SLUG, 'aceprensa_main'); add_settings_field('aceprensa_username','Nombre de usuario', [__CLASS__,'field_username'], ACEPRENSA_POSTS_SLUG, 'aceprensa_main'); add_settings_field('aceprensa_selected_categories','Categorías', [__CLASS__,'field_categories'], ACEPRENSA_POSTS_SLUG, 'aceprensa_main'); add_settings_field('aceprensa_post_types','Tipos de artículo', [__CLASS__,'field_post_types'], ACEPRENSA_POSTS_SLUG, 'aceprensa_main'); add_settings_field('aceprensa_cache_seconds','Caché (seg.)', [__CLASS__,'field_cache'], ACEPRENSA_POSTS_SLUG, 'aceprensa_main'); // ---------- DISEÑO ---------- // Registros (todos dentro del mismo group 'aceprensa-posts-settings') register_setting('aceprensa-posts-settings','aceprensa_layout',[ 'type' => 'string', 'sanitize_callback' => function($v){ $v = (string)$v; return in_array($v, ['cards','minimal'], true) ? $v : 'cards'; }, 'default' => 'cards', ]); // Columnas: límites por breakpoint register_setting('aceprensa-posts-settings','aceprensa_cols_m',[ 'type'=>'integer','default'=>1, 'sanitize_callback'=>function($v){ $n=(int)$v; return max(1,min(2,$n)); } ]); register_setting('aceprensa-posts-settings','aceprensa_cols_t',[ 'type'=>'integer','default'=>2, 'sanitize_callback'=>function($v){ $n=(int)$v; return max(1,min(3,$n)); } ]); register_setting('aceprensa-posts-settings','aceprensa_cols_d',[ 'type'=>'integer','default'=>3, 'sanitize_callback'=>function($v){ $n=(int)$v; return max(1,min(4,$n)); } // ← ahora hasta 4 ]); register_setting('aceprensa-posts-settings','aceprensa_show_image',[ 'type'=>'boolean', 'sanitize_callback'=>$sanitize_bool, 'default'=>1 ]); // Imagen: posición (top|left|right) register_setting('aceprensa-posts-settings','aceprensa_image_position',[ 'type'=>'string', 'sanitize_callback'=>function($v) use($sanitize_enum){ return $sanitize_enum($v, ['top','left','right']); }, 'default'=>'top' ]); // Imagen: orientación (landscape|portrait) register_setting('aceprensa-posts-settings','aceprensa_image_orientation',[ 'type'=>'string', 'sanitize_callback'=>function($v) use($sanitize_enum){ return $sanitize_enum($v, ['landscape','portrait']); }, 'default'=>'landscape' ]); // Imagen: ratio (añadimos verticales) register_setting('aceprensa-posts-settings','aceprensa_aspect_ratio',[ 'type'=>'string', 'sanitize_callback'=>function($v) use($sanitize_enum){ return $sanitize_enum($v, ['16-9','4-3','1-1','3-4','2-3','4-5']); }, 'default'=>'16-9' ]); // Ancho de imagen cuando la posición es left/right: narrow|normal|wide register_setting('aceprensa-posts-settings', 'aceprensa_image_width', [ 'type' => 'string', 'sanitize_callback' => function($v){ $v=(string)$v; return in_array($v, ['narrow','normal','wide'], true) ? $v : 'normal'; }, 'default' => 'normal' ]); // Gap interno de la tarjeta: compact|normal|spacious register_setting('aceprensa-posts-settings', 'aceprensa_card_gap', [ 'type' => 'string', 'sanitize_callback' => function($v){ $v=(string)$v; return in_array($v, ['compact','normal','spacious'], true) ? $v : 'normal'; }, 'default' => 'normal' ]); register_setting('aceprensa-posts-settings','aceprensa_show_author',['type'=>'boolean','sanitize_callback'=>$sanitize_bool,'default'=>1]); register_setting('aceprensa-posts-settings','aceprensa_upper_author',['type'=>'boolean','sanitize_callback'=>$sanitize_bool,'default'=>1]); register_setting('aceprensa-posts-settings','aceprensa_show_date',['type'=>'boolean','sanitize_callback'=>$sanitize_bool,'default'=>1]); register_setting('aceprensa-posts-settings','aceprensa_date_format',[ 'type'=>'string','sanitize_callback'=>function($v) use($sanitize_enum){ return $sanitize_enum($v,['j M, Y','d/m/Y','d M Y']); },'default'=>'j M, Y' ]); register_setting('aceprensa-posts-settings','aceprensa_date_upper',['type'=>'boolean','sanitize_callback'=>$sanitize_bool,'default'=>0]); register_setting('aceprensa-posts-settings','aceprensa_excerpt_words',[ 'type'=>'integer','sanitize_callback'=>$sanitize_words,'default'=>30 ]); register_setting('aceprensa-posts-settings','aceprensa_excerpt_mode',[ 'type'=>'string', 'sanitize_callback'=>function($v){ $v=(string)$v; return in_array($v,['none','full','clamp'],true)?$v:'clamp'; }, 'default'=>'clamp' ]); register_setting('aceprensa-posts-settings','aceprensa_excerpt_lines',[ 'type'=>'integer', 'sanitize_callback'=>function($v){ $n=(int)$v; return max(1, min(8, $n ?: 3)); }, 'default'=>3 ]); register_setting('aceprensa-posts-settings','aceprensa_title_clamp',[ 'type'=>'boolean', 'sanitize_callback'=>function($v){ return (int)!empty($v); }, 'default'=>1 ]); register_setting('aceprensa-posts-settings','aceprensa_title_lines',[ 'type'=>'integer', 'sanitize_callback'=>function($v){ $n=(int)$v; return max(1, min(4, $n ?: 2)); }, 'default'=>2 ]); // Sección y campos en la "página" de diseño add_settings_section('aceprensa_design', __('Diseño', 'aceprensa-posts'), function(){ echo '

'.esc_html__('Ajusta cómo se mostrarán los artículos en el frontend (y en la previsualización).','aceprensa-posts').'

'; }, 'aceprensa-posts-design'); add_settings_field('aceprensa_layout','Estilo', [__CLASS__,'field_layout'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_columns','Columnas', [__CLASS__,'field_columns'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_num_posts','Nº de artículos', [__CLASS__,'field_num_posts'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_card_gap','Separación interna (gap)', [__CLASS__,'field_card_gap'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_image_opts','Imagen', [__CLASS__,'field_image_opts'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_author_opts','Firmantes', [__CLASS__,'field_author_opts'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_date_opts','Fecha', [__CLASS__,'field_date_opts'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_title_opts','Título', [__CLASS__,'field_title_opts'], 'aceprensa-posts-design', 'aceprensa_design'); add_settings_field('aceprensa_excerpt_opts','Extracto', [__CLASS__,'field_excerpt_opts'], 'aceprensa-posts-design', 'aceprensa_design'); } /** Carga de assets en admin */ public static function enqueue_admin_assets($hook) { if ($hook !== 'toplevel_page_'.ACEPRENSA_POSTS_SLUG) return; // Select2 con handles propios para evitar colisiones wp_register_script('aceprensa-select2', ACEPRENSA_POSTS_URL.'assets/js/select2.min.js', ['jquery'], '4.0.13', true); wp_register_style('aceprensa-select2', ACEPRENSA_POSTS_URL.'assets/css/select2.min.css', [], '4.0.13'); wp_enqueue_style('aceprensa-posts-admin', ACEPRENSA_POSTS_URL.'assets/css/admin.css', [], ACEPRENSA_POSTS_VERSION); wp_enqueue_style('aceprensa-select2'); wp_enqueue_script('aceprensa-posts-admin', ACEPRENSA_POSTS_URL.'assets/js/admin.js', ['jquery','aceprensa-select2'], ACEPRENSA_POSTS_VERSION, true); // CSS del frontend del shortcode wp_enqueue_style('aceprensa-posts-frontend', ACEPRENSA_POSTS_URL.'assets/css/frontend.css', [], ACEPRENSA_POSTS_VERSION); wp_localize_script('aceprensa-posts-admin','AceprensaPosts', [ 'ajax' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aceprensa_posts'), 'strings' => [ 'loading' => __('Cargando...', 'aceprensa-posts'), 'error' => __('No se pudo obtener la previsualización.', 'aceprensa-posts'), ] ]); } /** Carga de estilos del frontend para evitar conflictos **/ public static function enqueue_frontend_last() { wp_enqueue_style( 'aceprensa-posts-frontend', ACEPRENSA_POSTS_URL.'assets/css/frontend.css', [], // sin dependencias para que quede lo más al final posible ACEPRENSA_POSTS_VERSION ); } /** Página de ajustes con pestañas */ public static function render_settings_page() { if ( ! current_user_can(ACEPRENSA_POSTS_CAP) ) { wp_die(__('No tienes permisos para acceder.', 'aceprensa-posts')); } if ( isset($_GET['settings-updated']) && $_GET['settings-updated'] ) { self::force_refresh_now(); } // textos de ayuda $shortcode = '[aceprensa_posts]'; $php_call = ""; ?>

'; } public static function field_un_click() { $v = (int) get_option('aceprensa_un_click', 0); echo ''; } public static function field_username() { $v = esc_attr(get_option('aceprensa_username', '')); echo ''; } public static function field_categories() { $saved = (array) get_option('aceprensa_selected_categories', []); echo ''; echo ''; } public static function field_post_types() { $saved = (array) get_option('aceprensa_post_types', ['post'=>'post']); $choices = [ 'post' => __('Artículos','aceprensa-posts'), 'cine_y_series' => __('Reseñas de cine y series','aceprensa-posts'), 'libros' => __('Reseñas de libros','aceprensa-posts'), 'juegos' => __('Reseñas de juegos','aceprensa-posts'), 'resenas-espectaculos' => __('Reseñas de espectáculos','aceprensa-posts'), ]; foreach ($choices as $key=>$label) { $checked = isset($saved[$key]) ? 'checked' : ''; echo ''; } } public static function field_cache() { $v = (int) get_option('aceprensa_cache_seconds', 43200); $opts = [ 21600 => __('6 horas','aceprensa-posts'), 43200 => __('12 horas','aceprensa-posts'), 86400 => __('1 día','aceprensa-posts'), ]; echo ''; } /** ----- Campos de diseño ----- */ public static function field_num_posts() { $v = (int) get_option('aceprensa_num_posts', 3); echo ''; } public static function field_layout(){ $v = get_option('aceprensa_layout','cards'); // cards|minimal $opts = [ 'cards' => __('Tarjetas','aceprensa-posts'), 'minimal' => __('Minimal','aceprensa-posts'), ]; echo '
'; foreach ($opts as $k=>$label){ $active = ($v===$k) ? ' is-active' : ''; echo ''; } echo '
'; } public static function field_columns(){ $m = (int)get_option('aceprensa_cols_m',1); $t = (int)get_option('aceprensa_cols_t',2); $d = (int)get_option('aceprensa_cols_d',3); echo '
'; echo ''; echo ''; echo ''; echo '
'; } public static function field_image_opts() { $show = (int) get_option('aceprensa_show_image', 1); $pos = get_option('aceprensa_image_position', 'top'); // top|left|right $orient = get_option('aceprensa_image_orientation', 'landscape'); // landscape|portrait $ratio = get_option('aceprensa_aspect_ratio', '16-9'); $imgw = get_option('aceprensa_image_width', 'normal'); // narrow|normal|wide // Tamaño WP $size_val = esc_attr( get_option('aceprensa_image_size', 'medium') ); $size_opts = [ 'thumbnail' => __('Pequeño', 'aceprensa-posts'), 'medium' => __('Mediano', 'aceprensa-posts'), 'large' => __('Grande', 'aceprensa-posts'), ]; echo '

'; echo '
'; /* Tamaño */ echo '
'; echo '' . esc_html__('Tamaño de imagen', 'aceprensa-posts') . ' '; echo ''; echo '
'; /* Orientación */ echo '
'; echo '' . esc_html__('Orientación', 'aceprensa-posts') . ' '; foreach (['landscape' => __('Horizontal', 'aceprensa-posts'), 'portrait' => __('Vertical', 'aceprensa-posts')] as $k => $label) { echo ''; } echo '
'; /* Ratio */ $ratios = [ '16-9' => '16:9 (apaisado)', '4-3' => '4:3', '1-1' => '1:1', '3-4' => '3:4 (vertical)', '2-3' => '2:3 (cartel)', '4-5' => '4:5 (vertical)', ]; echo '
'; echo ''; echo '
'; /* Posición (ojo: DIV, no

) */ echo '

'; echo '' . esc_html__('Posición de la imagen', 'aceprensa-posts') . ''; echo '
'; $pos_opts = [ 'top' => ['label' => __('Arriba', 'aceprensa-posts'), 'svg' => ''], 'left' => ['label' => __('Izquierda', 'aceprensa-posts'), 'svg' => ''], 'right' => ['label' => __('Derecha', 'aceprensa-posts'), 'svg' => ''], ]; $val = get_option('aceprensa_image_position', 'top'); foreach ($pos_opts as $key => $o) { $active = checked($val, $key, false) ? ' is-active' : ''; echo ''; } echo '
'; // #ace-img-position echo '
'; // fin fila posición /* Anchura (solo left/right) */ echo '
'; echo '' . esc_html__('Anchura de la imagen (left/right)', 'aceprensa-posts') . ' '; echo '
'; foreach (['narrow' => __('Estrecha', 'aceprensa-posts'), 'normal' => __('Normal', 'aceprensa-posts'), 'wide' => __('Ancha', 'aceprensa-posts')] as $key => $label) { $active = ($imgw === $key) ? ' is-active' : ''; echo ''; } echo '
'; echo '' . esc_html__('Solo aplica con imagen izquierda/derecha.', 'aceprensa-posts') . ''; echo '
'; // <-- AQUÍ faltaba un cierre en tu versión echo '
'; // .ace-image-opts } public static function field_card_gap() { $v = get_option('aceprensa_card_gap','normal'); // compact|normal|spacious $opts = [ 'compact' => __('Compacto','aceprensa-posts'), 'normal' => __('Normal','aceprensa-posts'), 'spacious' => __('Amplio','aceprensa-posts'), ]; echo ''; echo ' '.esc_html__('Afecta al hueco interno entre la imagen y el bloque de texto.','aceprensa-posts').''; } public static function field_author_opts() { $show = (int)get_option('aceprensa_show_author',1); $up = (int)get_option('aceprensa_upper_author',1); echo ''; echo '
'; echo ''; echo '
'; } public static function field_date_opts() { $show = (int)get_option('aceprensa_show_date',1); $fmt = get_option('aceprensa_date_format','j M, Y'); $up = (int)get_option('aceprensa_date_upper',0); $opts = ['j M, Y'=>'15 Ago, 2025','d/m/Y'=>'15/08/2025','d M Y'=>'15 Ago 2025']; echo ''; echo '
'; echo ' '; echo ''; echo '
'; } public static function field_title_opts() { $clamp = (int)get_option('aceprensa_title_clamp',1); $lines = (int)get_option('aceprensa_title_lines',2); echo ' '; echo ''; } public static function field_excerpt_opts() { $mode = get_option('aceprensa_excerpt_mode','clamp'); // none|full|clamp $lines = (int)get_option('aceprensa_excerpt_lines',3); ?>
'forbidden'], 403); check_ajax_referer('aceprensa_posts','nonce'); $term = isset($_POST['term']) ? sanitize_text_field(wp_unslash($_POST['term'])) : ''; $page = isset($_POST['page']) ? max(1, (int) $_POST['page']) : 1; $per_page = isset($_POST['per_page']) ? max(1, (int) $_POST['per_page']) : 20; $qs = [ 'search' => $term, 'per_page' => $per_page, 'page' => $page, 'orderby' => 'count', 'order' => 'desc', ]; $items = self::remote_get('/wp/v2/categories', $qs, false); if ( is_wp_error($items) ) { wp_send_json_error(['message'=>$items->get_error_message()], 500); } // Heurística: si devuelve exactamente per_page, asumimos que hay más páginas $more = (is_array($items) && count($items) >= $per_page); wp_send_json_success([ 'items' => $items, 'more' => $more, 'page' => $page, 'per_page' => $per_page, ]); } /** Ajax: previsualización forzada (no usa caché) */ public static function ajax_preview() { if ( ! current_user_can(ACEPRENSA_POSTS_CAP) ) wp_send_json_error(['message'=>'forbidden'], 403); check_ajax_referer('aceprensa_posts','nonce'); $data = self::build_posts_array(true); // fresh if ( is_wp_error($data) ) { wp_send_json_error(['message'=>$data->get_error_message()], 500); } ob_start(); self::render_list_html($data); $html = ob_get_clean(); wp_send_json_success(['html'=>$html]); } /** Refrescar caché y devolver HTML */ public static function ajax_refresh_cache() { if ( ! current_user_can(ACEPRENSA_POSTS_CAP) ) wp_send_json_error(['message'=>'forbidden'], 403); check_ajax_referer('aceprensa_posts','nonce'); $data = self::build_posts_array(true, true); // fresh + store if ( is_wp_error($data) ) { wp_send_json_error(['message'=>$data->get_error_message()], 500); } ob_start(); self::render_list_html($data); $html = ob_get_clean(); wp_send_json_success(['html'=>$html]); } /** ----- Shortcode & Render ----- */ /** Shortcode principal */ public static function shortcode_render($atts = []) { $data = self::build_posts_array(false); // usar SWR if ( is_wp_error($data) ) { return '
'. esc_html($data->get_error_message()) .'
'; } if ( empty($data) ) { return '
'. esc_html__('No hay contenidos disponibles en este momento.','aceprensa-posts') .'
'; } ob_start(); self::render_list_html($data); return ob_get_clean(); } /** Renderiza la lista de posts en HTML. **/ protected static function render_list_html($posts) { // Opciones de diseño $layout = get_option('aceprensa_layout','card'); $cols_m = (int)get_option('aceprensa_cols_m',1); $cols_t = (int)get_option('aceprensa_cols_t',2); $cols_d = (int)get_option('aceprensa_cols_d',3); $show_img = (int)get_option('aceprensa_show_image',1); $ratio = get_option('aceprensa_aspect_ratio','16-9'); $show_firm = (int)get_option('aceprensa_show_author',1); $upper_firm = (int)get_option('aceprensa_upper_author',1); $show_date = (int)get_option('aceprensa_show_date',1); $date_fmt = get_option('aceprensa_date_format','j M, Y'); $date_upper = (int)get_option('aceprensa_date_upper',0); $excerpt_mode = get_option('aceprensa_excerpt_mode','clamp'); // none|full|clamp if (!in_array($excerpt_mode,['none','full','clamp'],true)) $excerpt_mode = 'clamp'; // compat $title_clamp = (int)get_option('aceprensa_title_clamp',1); $title_lines = (int)get_option('aceprensa_title_lines',2); $excerpt_lines = (int)get_option('aceprensa_excerpt_lines',3); $pos = get_option('aceprensa_image_position','top'); // top|left|right $orient = get_option('aceprensa_image_orientation','landscape'); // landscape|portrait $ratio = get_option('aceprensa_aspect_ratio','16-9'); $imgw = get_option('aceprensa_image_width','normal'); // narrow|normal|wide $gap = get_option('aceprensa_card_gap','normal'); // compact|normal|spacious $container_classes = [ 'aceprensa-posts', 'ace-layout-'.$layout, 'ace-cols-m-'.$cols_m, 'ace-cols-t-'.$cols_t, 'ace-cols-d-'.$cols_d, 'ace-imgpos-'.$pos, 'ace-orient-'.$orient, 'ace-ratio-'.$ratio, 'ace-imgw-' . sanitize_key($imgw), 'ace-gap-' . sanitize_key($gap), ]; // clases de clamp para título y excerpt if ($title_clamp) { $container_classes[] = 'ace-title-clamp'; $container_classes[] = 'ace-title-lines-'.$title_lines; } if ($excerpt_mode === 'clamp') { $container_classes[] = 'ace-excerpt-clamp'; $container_classes[] = 'ace-excerpt-lines-'.$excerpt_lines; } echo '
'; foreach ($posts as $p) { $img = !empty($p['image']) ? esc_url($p['image']) : ''; $title = isset($p['title']) ? wp_kses_post($p['title']) : ''; $link = isset($p['link']) ? esc_url($p['link']) : '#'; $exc = isset($p['excerpt']) ? wp_kses_post($p['excerpt']) : ''; $dateR = isset($p['date']) ? $p['date'] : ''; $firs = isset($p['firmantes']) && is_array($p['firmantes']) ? $p['firmantes'] : []; // Fecha $date_html = ''; if ($show_date && $dateR) { $ts = self::parse_date_to_ts($dateR); if ($ts) { $date_html = date_i18n($date_fmt, $ts); if ($date_upper) { $date_html = mb_strtoupper($date_html); } } else { // fallback (poco frecuente ya) $date_html = esc_html($dateR); } } // Firmantes (0..n) $firm_html = ''; if ($show_firm && !empty($firs)) { $pieces = []; foreach ($firs as $f) { $name = esc_html($f['name'] ?? ''); $url = !empty($f['url']) ? esc_url($f['url']) : ''; if ($name === '') continue; $pieces[] = $url ? ''.$name.'' : $name; } if (!empty($pieces)) { $cls = $upper_firm ? 'aceprensa-firmantes upper' : 'aceprensa-firmantes'; $firm_html = '
'. implode(', ', $pieces) .'
'; } } echo '
'; if ($show_img && $img) { echo ''; echo ''.esc_attr(wp_strip_all_tags($title)).''; echo ''; } echo '
'; // Firmantes antes del título echo $firm_html; // Título echo '

'.$title.'

'; // EXTRACTO if ($excerpt_mode === 'full' && $exc) { echo '
'. wp_kses_post($exc) .'
'; } elseif ($excerpt_mode === 'clamp' && $exc) { echo '
'. wp_kses_post($exc) .'
'; // CSS lo recorta por líneas } if ($date_html) { echo '
'; } echo '
'; // .aceprensa-content echo '
'; } echo '
'; } /** ----- Datos & Caché (SWR) ----- */ /** * Construye el array de posts aplicando estrategia SWR. * @param bool $fresh Si true, hace fetch fresco. * @param bool $force_store Si true, guarda en caché siempre. * @return array|\WP_Error */ protected static function build_posts_array($fresh = false, $force_store = false) { $query = self::current_query_args(); $cache_key = self::cache_key($query); $cache_s = (int) get_option('aceprensa_cache_seconds', 43200); $cached = get_transient($cache_key); if (!$fresh && $cached && is_array($cached) && isset($cached['data'], $cached['fetched_at'], $cached['ttl'])) { $age = time() - (int)$cached['fetched_at']; if ($age <= (int)$cached['ttl']) { return $cached['data']; } else { if ( ! wp_next_scheduled(self::CRON_HOOK, [$cache_key, $query]) ) { wp_schedule_single_event(time(), self::CRON_HOOK, [$cache_key, $query]); } return $cached['data']; } } $items = self::remote_get('/custom/v2/all-posts', $query, true); if ( is_wp_error($items) ) { if (!$fresh && $cached && isset($cached['data'])) return $cached['data']; return $items; } $out = self::normalize_items($items); if ($cache_s > 0 || $force_store) { set_transient($cache_key, [ 'data' => $out, 'fetched_at' => time(), 'ttl' => $cache_s, ], $cache_s * 2 ); } return $out; } /** Refresco inmediato tras guardar */ protected static function force_refresh_now() { $query = self::current_query_args(); $cache_key = self::cache_key($query); $items = self::remote_get('/custom/v2/all-posts', $query, true); if ( is_wp_error($items) ) return; $out = self::normalize_items($items); $cache_s = (int) get_option('aceprensa_cache_seconds', 43200); set_transient($cache_key, [ 'data' => $out, 'fetched_at' => time(), 'ttl' => $cache_s, ], $cache_s * 2 ); } /** Cron: refresco en background */ public static function cron_refresh($cache_key, $query) { $items = self::remote_get('/custom/v2/all-posts', $query, true); if ( is_wp_error($items) ) return; $out = self::normalize_items($items); $cache_s = (int) get_option('aceprensa_cache_seconds', 43200); set_transient($cache_key, [ 'data' => $out, 'fetched_at' => time(), 'ttl' => $cache_s, ], $cache_s * 2 ); } /** Construye args de query para el endpoint remoto */ protected static function current_query_args() { $num = (int) get_option('aceprensa_num_posts', 3); $imgsize = get_option('aceprensa_image_size', 'medium'); $cats = (array) get_option('aceprensa_selected_categories', []); $types = (array) get_option('aceprensa_post_types', ['post'=>'post']); $args = [ 'status' => 'publish', 'per_page' => $num, ]; if ($cats) { $args['categories'] = implode(',', array_map('intval', array_keys($cats))); $args['tax_relation'] = 'OR'; } if ($types) { $args['post_types'] = implode(',', array_map('sanitize_key', array_keys($types))); } if ($imgsize) { $args['imagesize'] = sanitize_text_field($imgsize); } return $args; } /** Key de caché determinista */ protected static function cache_key($args) { return 'acep_posts_' . md5( wp_json_encode($args) ); } /** Normaliza items, firmantes y aplica UTM + a un click */ protected static function normalize_items($items) { $un_click = (int) get_option('aceprensa_un_click', 0); $user = get_option('aceprensa_username', ''); $out = []; foreach ($items as $r) { // Link artículo + UTM + a un click $link = isset($r['permalink']) ? $r['permalink'] : ( $r['link'] ?? '' ); if ($link) { $link = self::apply_tracking_to_url($link, true); } // ---- Firmantes (arrays paralelos firmante[] y firmante_link[]) ---- $names = []; if (!empty($r['firmante']) && is_array($r['firmante'])) { $names = array_values(array_map('sanitize_text_field', $r['firmante'])); } $links = []; if (!empty($r['firmante_link']) && is_array($r['firmante_link'])) { $links = array_values(array_map('sanitize_text_field', $r['firmante_link'])); } $firmantes = []; $max = max(count($names), count($links)); for ($i = 0; $i < $max; $i++) { $n = $names[$i] ?? ''; $l = $links[$i] ?? ''; if ($n === '' && $l === '') continue; $firmantes[] = [ 'name' => $n, 'url' => self::make_firmante_url($l), // make_firmante_url ya devolverá con tracking ]; } // Fallback si no vienen arrays de firmantes if (empty($firmantes)) { $fallback = isset($r['author_name']) ? $r['author_name'] : ( $r['author'] ?? '' ); if (!empty($fallback)) { $firmantes[] = [ 'name' => sanitize_text_field($fallback), 'url' => '' ]; } } $out[] = [ 'date' => isset($r['date']) ? sanitize_text_field($r['date']) : '', 'title' => isset($r['title']) ? wp_kses_post($r['title']) : '', 'link' => esc_url_raw($link), 'excerpt' => isset($r['excerpt']) ? wp_kses_post($r['excerpt']) : '', 'image' => isset($r['featured_image']) ? esc_url_raw($r['featured_image']) : '', 'firmantes' => $firmantes, // ← array de {name,url} ]; } return $out; } /** * Aplica UTMs (y ?user= si “a un click” está activo) a una URL. */ protected static function apply_tracking_to_url($url, $include_user = true) { $url = trim((string)$url); if ($url === '') return ''; $utm = [ 'utm_medium' => 'plugin_aceprensa_posts', 'utm_campaign' => 'widget_posts', 'utm_source' => 'Aceprensa posts', ]; $url = add_query_arg($utm, $url); if ($include_user) { $un_click = (int) get_option('aceprensa_un_click', 0); $user = get_option('aceprensa_username', ''); if ($un_click && !empty($user)) { $url = add_query_arg(['user' => $user], $url); } } return esc_url_raw($url); } /** * Construye la URL del firmante a partir de un slug o una URL absoluta. * Filtrable para adaptar la ruta base del sitio remoto. * * Filtros: * - aceprensa_make_firmante_url( $url, $value ): si devuelve no vacío, se usa tal cual. * - aceprensa_firmante_url_base( $base ): por defecto, {site}/autor/ */ protected static function make_firmante_url($value) { $value = trim((string)$value); if ($value === '') return ''; // Permite override completo $custom = apply_filters('aceprensa_make_firmante_url', '', $value); if (!empty($custom)) return esc_url_raw($custom); // Si ya es URL absoluta, devuélvela if (preg_match('#^https?://#i', $value)) { return esc_url_raw($value); } // Construir desde slug con base filtrable $site = untrailingslashit(get_option('aceprensa_site_url','')); $base = trailingslashit(apply_filters('aceprensa_firmante_url_base', $site . '/firmantes/')); return self::apply_tracking_to_url($base . ltrim($value, '/'), true); } /** * Convierte una fecha textual (varios formatos) a timestamp. * Soporta: DD/MM/YYYY, DD-MM-YYYY, YYYY-MM-DD, * y "4 Oct 2025" / "4 Octubre 2025" (ES/EN abreviado/completo). * Devuelve int|false. */ protected static function parse_date_to_ts($value) { $s = trim((string)$value); if ($s === '') return false; // 1) DD/MM/YYYY o DD-MM-YYYY if (preg_match('~^(?\d{1,2})[/-](?\d{1,2})[/-](?\d{4})$~', $s, $m)) { return strtotime(sprintf('%04d-%02d-%02d', (int)$m['y'], (int)$m['m'], (int)$m['d'])); } // 2) YYYY-MM-DD if (preg_match('~^(?\d{4})-(?\d{1,2})-(?\d{1,2})$~', $s, $m)) { return strtotime(sprintf('%04d-%02d-%02d', (int)$m['y'], (int)$m['m'], (int)$m['d'])); } // 3) "4 Oct 2025", "4 Octubre 2025", "04 Oct, 2025", etc. // Normalizamos meses ES -> EN para que strtotime los entienda siempre. $map = [ // abreviados 'ene'=>'jan','feb'=>'feb','mar'=>'mar','abr'=>'apr','may'=>'may','jun'=>'jun', 'jul'=>'jul','ago'=>'aug','sept'=>'sep','sep'=>'sep','set'=>'sep','oct'=>'oct','nov'=>'nov','dic'=>'dec', // completos 'enero'=>'january','febrero'=>'february','marzo'=>'march','abril'=>'april','mayo'=>'may','junio'=>'june', 'julio'=>'july','agosto'=>'august','septiembre'=>'september','setiembre'=>'september', 'octubre'=>'october','noviembre'=>'november','diciembre'=>'december', ]; $s_norm = preg_replace_callback('~[a-záéíóúñ]+~iu', function($m) use($map){ $k = strtolower($m[0]); return $map[$k] ?? $m[0]; }, $s); $ts = strtotime($s_norm); if ($ts !== false) return $ts; // 4) Último intento con strtotime “tal cual” return strtotime($s); } /** * Llamada remota segura contra el WP REST del sitio de origen. * @param string $endpoint Ruta dentro de /wp-json * @param array $args Query string * @param bool $fresh Ignorado a nivel HTTP, reservado para futuras mejoras * @return array|\WP_Error */ protected static function remote_get($endpoint, $args = [], $fresh = false) { $base = untrailingslashit(get_option('aceprensa_site_url','')); if (!$base) return new \WP_Error('no_url', __('No hay URL de origen configurada.', 'aceprensa-posts')); $url = $base . '/wp-json' . $endpoint; if (!empty($args)) { $url = add_query_arg($args, $url); } $headers = []; $salt = defined('ACEPRENSA_BASIC_AUTH') ? ACEPRENSA_BASIC_AUTH : apply_filters('aceprensa_basic_auth', ''); if ($salt) { $headers['Authorization'] = 'Basic ' . $salt; } $resp = wp_safe_remote_get($url, [ 'headers' => $headers, 'timeout' => 10, 'redirection'=> 3, 'user-agent' => 'AceprensaPosts/'.ACEPRENSA_POSTS_VERSION.'; '.home_url('/'), ]); if (is_wp_error($resp)) { return new \WP_Error('remote_error', __('Error de conexión con el servidor remoto.', 'aceprensa-posts')); } $code = wp_remote_retrieve_response_code($resp); $body = wp_remote_retrieve_body($resp); if ($code !== 200) { return new \WP_Error('bad_status', sprintf(__('Respuesta no válida (%d): %s', 'aceprensa-posts'), $code, wp_strip_all_tags($body))); } $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { return new \WP_Error('bad_json', __('JSON inválido recibido.', 'aceprensa-posts')); } return $data; } }