69e849d38e
Este repo local tenia origin apuntando al Gitea local (localhost:3000), que Rafa
declaro archivado el 2026-06-28 (commit 962f33a, desarrollo movido a
gitea.feadulta.com). Ese memo nunca llego a este checkout: main quedo congelado
y todo el trabajo real de las ultimas 3+ semanas se fue commiteando solo en la
rama fix/multiidioma-portada-132 (ya fusionada a main sin perdida, commit
2504666), mientras que ademas se acumulaban 78 cambios sin commitear en el
working tree que nunca llegaron a NINGUN historial de git.
Este commit consolida esos cambios sueltos: TTS multi-voz (tts_*.py), scripts
de traduccion (translate_haiku.py, pretranslate_en_haiku.py, sync_translations_to_prod.py),
mu-plugins nuevos desplegados a prod (fea-beta-feedback, fea-cloudflare-realip,
fea-legacy-redirect, fea-gsc-verification, fea-support-campaign, fea-ui, etc.),
scripts de mantenimiento de enlaces/cartas, capturas E2E (tools/e2e/shot_*.cjs)
y documentacion de sesiones recientes.
Excluido deliberadamente (no es codigo versionable): tts-voices/ (3.3GB de
muestras de audio para clonacion de voz, anadido a .gitignore), logs/ (logs de
ejecucion, anadido a .gitignore), y 2 ficheros vacios accidentales + 2 copias
duplicadas sueltas en la raiz que ya existen en su ubicacion correcta.
229 lines
8.1 KiB
PHP
229 lines
8.1 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Fe Adulta — Carta → Portada
|
|
* Description: Parser de la carta semanal. Extrae los links de cada sección de la
|
|
* carta y los expone para que los shortcodes de portada los rendericen.
|
|
* Version: 1.0
|
|
*
|
|
* Modelo: cada carta semanal es un post HTML con secciones encabezadas
|
|
* (Evangelio, Artículos, Eucaristía, Multimedia, EFFA). Los links DENTRO de
|
|
* cada sección son lo que la portada debe mostrar en su shortcode equivalente.
|
|
*
|
|
* Ver issue rafa/feadulta#38.
|
|
*/
|
|
|
|
/**
|
|
* Devuelve el post-carta vigente para un idioma (más reciente en cat 6).
|
|
* Si Polylang está activo y hay traducción del idioma, devuelve la traducida.
|
|
*/
|
|
function fea_get_current_carta_id($lang = null) {
|
|
static $cache = [];
|
|
$lang = $lang ?: (function_exists('pll_current_language') ? pll_current_language() : 'es');
|
|
if ($lang === false || $lang === null) $lang = 'es';
|
|
if (isset($cache[$lang])) return $cache[$lang];
|
|
|
|
$cat_es = 6;
|
|
$cat = function_exists('fea_cat') ? fea_cat($cat_es) : $cat_es;
|
|
|
|
$cartas = get_posts([
|
|
'posts_per_page' => 1,
|
|
'category__in' => [$cat],
|
|
'post_status' => 'publish',
|
|
'orderby' => 'date',
|
|
'order' => 'DESC',
|
|
'suppress_filters' => false,
|
|
]);
|
|
if (!$cartas) return $cache[$lang] = 0;
|
|
|
|
$carta_id = (int) $cartas[0]->ID;
|
|
|
|
if ($lang !== 'es' && function_exists('pll_get_post')) {
|
|
$trans = pll_get_post($carta_id, $lang);
|
|
if ($trans) $carta_id = (int) $trans;
|
|
}
|
|
|
|
return $cache[$lang] = $carta_id;
|
|
}
|
|
|
|
/**
|
|
* Parsea el HTML de la carta y devuelve los post_ids agrupados por sección.
|
|
*/
|
|
function fea_parse_carta_sections($carta_id) {
|
|
static $mem = [];
|
|
$carta_id = (int) $carta_id;
|
|
if (!$carta_id) return [];
|
|
if (isset($mem[$carta_id])) return $mem[$carta_id];
|
|
|
|
$tk = 'fea_carta_sections_' . $carta_id;
|
|
$cached = get_transient($tk);
|
|
if (is_array($cached)) return $mem[$carta_id] = $cached;
|
|
|
|
$post = get_post($carta_id);
|
|
if (!$post) return $mem[$carta_id] = [];
|
|
|
|
$sections = fea_extract_sections_from_html($post->post_content);
|
|
|
|
set_transient($tk, $sections, 15 * MINUTE_IN_SECONDS);
|
|
return $mem[$carta_id] = $sections;
|
|
}
|
|
|
|
/**
|
|
* Extrae secciones del HTML. Pública para tests/CLI.
|
|
*/
|
|
function fea_extract_sections_from_html($html) {
|
|
$section_patterns = [
|
|
'evangelio' => '/Evangelio\s+y\s+comentarios\s+al\s+Evangelio/iu',
|
|
'articulos' => '/Art[ií]culos\s+seleccionados\s+para\s+la\s+semana/iu',
|
|
'eucaristia' => '/Para\s+unas\s+eucarist[ií]as\s+m[áa]s\s+participativas/iu',
|
|
'multimedia' => '/Material\s+multimedia/iu',
|
|
'effa' => '/Escuela\s+EFFA/iu',
|
|
];
|
|
|
|
$positions = [];
|
|
foreach ($section_patterns as $slug => $regex) {
|
|
if (preg_match($regex, $html, $m, PREG_OFFSET_CAPTURE)) {
|
|
$positions[$slug] = $m[0][1];
|
|
}
|
|
}
|
|
if (empty($positions)) return [];
|
|
asort($positions);
|
|
|
|
$slugs = array_keys($positions);
|
|
$offsets = array_values($positions);
|
|
$offsets[] = strlen($html);
|
|
|
|
$sections = [];
|
|
for ($i = 0; $i < count($slugs); $i++) {
|
|
$segment = substr($html, $offsets[$i], $offsets[$i+1] - $offsets[$i]);
|
|
$ids = fea_resolve_links_in_html($segment);
|
|
if ($ids) $sections[$slugs[$i]] = $ids;
|
|
}
|
|
return $sections;
|
|
}
|
|
|
|
/**
|
|
* Extrae los href de un fragmento HTML y los resuelve a wp_posts.ID.
|
|
*/
|
|
function fea_resolve_links_in_html($html) {
|
|
if (!preg_match_all('/href=["\']([^"\']+)["\']/i', $html, $m)) return [];
|
|
$ids = [];
|
|
$seen = [];
|
|
foreach ($m[1] as $url) {
|
|
$pid = fea_url_to_post_id($url);
|
|
if ($pid && !isset($seen[$pid])) {
|
|
$seen[$pid] = true;
|
|
$ids[] = $pid;
|
|
}
|
|
}
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* Resuelve una URL (WP o Joomla legacy) a wp_posts.ID o null.
|
|
*/
|
|
function fea_url_to_post_id($url) {
|
|
global $wpdb;
|
|
|
|
// Joomla legacy: /item/<k2id>-...html
|
|
if (preg_match('~/item/(\d+)-[^/"]+\.html~i', $url, $m)) {
|
|
$k2 = (int) $m[1];
|
|
if ($k2 > 0) {
|
|
$pid = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='_fgj2wp_old_k2_id' AND meta_value=%s LIMIT 1",
|
|
(string) $k2
|
|
));
|
|
if ($pid) return (int) $pid;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Enlace interno WP: deriva el slug del path, relativo al home. Agnóstico al
|
|
// entorno → funciona en local (home en .../fea) y en prod (home en la raíz).
|
|
// No depende de un prefijo /fea/ hardcodeado (issue #91).
|
|
//
|
|
// Durante el cutover de dominio (issue #158) el `home_url()` de WP sigue en
|
|
// wp-nuevo.feadulta.com pero el contenido ya enlaza a www.feadulta.com. Para
|
|
// no romper la resolución mientras el cutover no esté cerrado, se acepta
|
|
// también una lista fija de hosts propios del sitio, no solo home_url().
|
|
// TODO: cuando #158 quede resuelto y home_url() sea www.feadulta.com, esta
|
|
// lista se puede simplificar a solo home_url().
|
|
$own_hosts = array_filter(array_unique([
|
|
strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST)),
|
|
'www.feadulta.com',
|
|
'feadulta.com',
|
|
'wp-nuevo.feadulta.com',
|
|
]));
|
|
$host = wp_parse_url($url, PHP_URL_HOST);
|
|
if ($host && !in_array(strtolower($host), $own_hosts, true)) {
|
|
return null; // host externo → no es un artículo nuestro
|
|
}
|
|
|
|
$path = (string) wp_parse_url($url, PHP_URL_PATH);
|
|
if ($path === '') return null;
|
|
$home_path = rtrim((string) wp_parse_url(home_url('/'), PHP_URL_PATH), '/');
|
|
if ($home_path !== '' && strpos($path, $home_path . '/') === 0) {
|
|
$path = substr($path, strlen($home_path));
|
|
}
|
|
$seg = explode('/', trim($path, '/'));
|
|
$slug = $seg[0] ?? '';
|
|
if ($slug === '' || in_array($slug, ['wp-admin','wp-content','category','tag','author','page','en','fr','it','pt'], true)) {
|
|
return null;
|
|
}
|
|
$pid = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT ID FROM {$wpdb->posts}
|
|
WHERE post_name=%s AND post_status='publish' AND post_type='post'
|
|
ORDER BY post_date DESC LIMIT 1",
|
|
$slug
|
|
));
|
|
return $pid ? (int) $pid : null;
|
|
}
|
|
|
|
/**
|
|
* Devuelve los WP_Post objects de una sección de la carta vigente,
|
|
* o array vacío si no hay carta o no hay links resueltos en esa sección.
|
|
*/
|
|
function fea_carta_section_posts($section_slug, $lang = null) {
|
|
$lang = $lang ?: (function_exists('fea_current_lang') ? fea_current_lang() : 'es');
|
|
|
|
// El parser de secciones reconoce las cabeceras SOLO en español
|
|
// (fea_extract_sections_from_html). Las cartas traducidas tienen las
|
|
// cabeceras en su idioma → 0 secciones. Por eso parseamos SIEMPRE la
|
|
// carta ES y luego mapeamos cada link a su traducción del idioma destino.
|
|
$carta_id = fea_get_current_carta_id($lang);
|
|
if (!$carta_id) return [];
|
|
$carta_es = $carta_id;
|
|
if ($lang !== 'es' && function_exists('pll_get_post')) {
|
|
$es = pll_get_post($carta_id, 'es');
|
|
if ($es) $carta_es = (int) $es;
|
|
}
|
|
|
|
$sections = fea_parse_carta_sections($carta_es);
|
|
$ids = $sections[$section_slug] ?? [];
|
|
if (!$ids) return [];
|
|
$posts = [];
|
|
foreach ($ids as $pid) {
|
|
// Mapear el artículo ES a su traducción del idioma de la portada.
|
|
// Si no hay traducción, se mantiene el ES (degradación elegante).
|
|
if ($lang !== 'es' && function_exists('pll_get_post')) {
|
|
$tr = pll_get_post((int) $pid, $lang);
|
|
if ($tr) $pid = (int) $tr;
|
|
}
|
|
$p = get_post($pid);
|
|
if ($p && $p->post_status === 'publish') $posts[] = $p;
|
|
}
|
|
return $posts;
|
|
}
|
|
|
|
/**
|
|
* Invalida transients de secciones al guardar/editar un post.
|
|
*/
|
|
add_action('save_post_post', function($post_id, $post, $update) {
|
|
$cats = wp_get_post_categories($post_id);
|
|
$watch = [6, 21, 22];
|
|
if (array_intersect($cats, $watch)) {
|
|
global $wpdb;
|
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_fea_carta_sections_%'");
|
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_fea_carta_sections_%'");
|
|
}
|
|
}, 10, 3);
|