$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 '
';
echo '';
}
echo '';
// Firmantes antes del título
echo $firm_html;
// Título
echo '
';
// 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;
}
}