diff --git a/scripts/fix_imported_k2_metas.py b/scripts/fix_imported_k2_metas.py new file mode 100644 index 0000000..04c4036 --- /dev/null +++ b/scripts/fix_imported_k2_metas.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +fix_imported_k2_metas.py + +Asigna metas, categorías y Polylang a los posts importados por import_new_k2_items.py. +Los posts WP ya existen (IDs 43914-44082); este script solo añade los metadatos. + +Mapping: wp_id = k2_id + 26040 +""" + +import json +import subprocess +import sys +import re + +# ── Config ───────────────────────────────────────────────────────────────────── +JOOMLA_SSH_HOST = "134.0.10.170" +JOOMLA_SSH_USER = "feadulta" +JOOMLA_SSH_PASS = "6Rm2qOF@eundwpda" +JOOMLA_DB_HOST = "127.0.0.1" +JOOMLA_DB_USER = "fejoomla3" +JOOMLA_DB_PASS = "5FF-}5^[>7^pK4W9" +JOOMLA_DB_NAME = "fejoomla3" + +WP_DOCKER = "wordpress-mysql" +WP_DB_USER = "wordpress_user" +WP_DB_PASS = "wordpress_pass" +WP_DB_NAME = "wordpress_db" + +LAST_K2_ID = 17873 +WP_ID_OFFSET = 26040 # wp_id = k2_id + WP_ID_OFFSET + +CAT_FEADULTA = 71 +CAT_ARTICULOS = 1650 +CAT_EVANGELIO = 1647 +CAT_EUCARISTIA = 1648 +LANG_MAP = {1: 'es', 2: 'en', 3: 'fr', 4: 'it', 5: 'pt'} +DOMINGO_RE = r'DOMINGO|SEMANA SANTA|SEMANA DE PASCUA|PENTECOST|NAVIDAD|EPIFAN' + +DRY_RUN = '--dry-run' in sys.argv + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def wp_execute(sql: str): + if DRY_RUN: + print(f" [DRY] {sql[:100]}") + return + cmd = ['docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-e', sql] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + err = result.stderr.replace('mysql: [Warning] Using a password on the command line interface can be insecure.\n', '') + if err.strip(): + print(f" [ERR] {err.strip()[:200]}", file=sys.stderr) + + +def wp_mysql(query: str) -> list[dict]: + cmd = ['docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-B', '-e', query] + result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + return [dict(zip(headers, line.split('\t'))) for line in lines[1:] if line] + + +def esc(s: str) -> str: + return s.replace('\\', '\\\\').replace("'", "\\'") + + +def unhex(val: str) -> str: + if not val or val == 'NULL': + return '' + try: + return bytes.fromhex(val).decode('utf-8', errors='replace') + except Exception: + return val + + +def parse_extra_fields(ef_json: str) -> dict: + result = {'lang_val': None, 'has_libro': False} + if not ef_json: + return result + try: + fields = json.loads(ef_json) + except json.JSONDecodeError: + return result + for f in fields: + fid = str(f.get('id', '')) + val = f.get('value') + if fid == '16' and val is not None: + try: + result['lang_val'] = int(val) + except (ValueError, TypeError): + pass + elif fid == '9': + result['has_libro'] = True + return result + + +def determine_categories(ef: dict, title: str) -> list[int]: + lang = ef.get('lang_val') + es = (lang == 1 or lang is None) + cats = [CAT_FEADULTA] + if es and ef.get('has_libro'): + cats.append(CAT_EVANGELIO) + elif es and re.search(DOMINGO_RE, title, re.IGNORECASE): + cats.append(CAT_EUCARISTIA) + else: + cats.append(CAT_ARTICULOS) + return cats + + +# ── Main ─────────────────────────────────────────────────────────────────────── + +def main(): + print(f"=== Fix metas/cats K2 items > {LAST_K2_ID} {'[DRY RUN]' if DRY_RUN else '[LIVE]'} ===\n") + + # Cargar term_taxonomy_ids + term_ids = [CAT_FEADULTA, CAT_ARTICULOS, CAT_EVANGELIO, CAT_EUCARISTIA] + tt_ids = {} + rows = wp_mysql(f"SELECT term_id, term_taxonomy_id FROM wp_term_taxonomy WHERE term_id IN ({','.join(map(str,term_ids))}) AND taxonomy='category'") + for r in rows: + tt_ids[int(r['term_id'])] = int(r['term_taxonomy_id']) + print(f"TT IDs categorías: {tt_ids}") + + pl_ids = {} + rows = wp_mysql("SELECT t.slug, tt.term_taxonomy_id FROM wp_terms t JOIN wp_term_taxonomy tt ON tt.term_id=t.term_id WHERE tt.taxonomy='language' AND t.slug IN ('es','en','fr','it','pt')") + for r in rows: + pl_ids[r['slug']] = int(r['term_taxonomy_id']) + print(f"Polylang TT IDs: {pl_ids}") + + # Verificar que los WP posts existen + rows = wp_mysql(f"SELECT COUNT(*) n FROM wp_posts WHERE ID BETWEEN {LAST_K2_ID+WP_ID_OFFSET+1} AND (SELECT MAX(ID) FROM wp_posts)") + print(f"Posts WP a procesar (aprox): {rows[0]['n'] if rows else '?'}") + + # Obtener K2 items desde Joomla + print("\nObteniendo K2 items de Joomla prod...") + query = ( + f"SELECT id, HEX(title) title, HEX(extra_fields) extra_fields " + f"FROM ew4r_k2_items WHERE published=1 AND id > {LAST_K2_ID} ORDER BY id;" + ) + mysql_cmd = ( + f"mysql -h {JOOMLA_DB_HOST} -u {JOOMLA_DB_USER} " + f"-p'{JOOMLA_DB_PASS}' {JOOMLA_DB_NAME} " + f"--default-character-set=utf8mb4 -B" + ) + cmd = ['sshpass', '-p', JOOMLA_SSH_PASS, 'ssh', f'{JOOMLA_SSH_USER}@{JOOMLA_SSH_HOST}', mysql_cmd] + result = subprocess.run(cmd, input=query, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + print(f"ERROR: {result.stderr[:300]}") + sys.exit(1) + + lines = result.stdout.strip().split('\n') + headers = lines[0].split('\t') + items = [dict(zip(headers, line.split('\t'))) for line in lines[1:] if line] + print(f"Items obtenidos: {len(items)}") + + stats = {'ok': 0, 'skip': 0} + + for item in items: + k2_id = int(item['id']) + wp_id = k2_id + WP_ID_OFFSET + title = unhex(item.get('title', '')) + ef_raw = unhex(item.get('extra_fields', '')) + ef = parse_extra_fields(ef_raw) + lang = LANG_MAP.get(ef.get('lang_val'), 'es') + cats = determine_categories(ef, title) + + # Verificar que el WP post existe + existing = wp_mysql(f"SELECT ID FROM wp_posts WHERE ID={wp_id} LIMIT 1") + if not existing: + print(f" [SKIP] WP post ID={wp_id} no encontrado (k2={k2_id})") + stats['skip'] += 1 + continue + + print(f" [{k2_id}→{wp_id}] {title[:45]} | lang={lang} | cats={cats}") + + # Metas + for meta_key, meta_val in [('_fgj2wp_old_k2_id', str(k2_id)), ('Idioma', str(ef.get('lang_val') or 1))]: + wp_execute( + f"INSERT IGNORE INTO wp_postmeta (post_id, meta_key, meta_value) " + f"VALUES ({wp_id}, '{esc(meta_key)}', '{esc(meta_val)}')" + ) + + # Categorías + for term_id in cats: + tt_id = tt_ids.get(term_id) + if tt_id: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({wp_id}, {tt_id})" + ) + + # Polylang + pl_tt = pl_ids.get(lang) + if pl_tt: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({wp_id}, {pl_tt})" + ) + + stats['ok'] += 1 + + # Actualizar counts + if not DRY_RUN and stats['ok'] > 0: + print("\nActualizando counts de categorías y Polylang...") + all_tt = list(tt_ids.values()) + list(pl_ids.values()) + tt_str = ','.join(str(x) for x in all_tt) + wp_execute( + f"UPDATE wp_term_taxonomy tt SET count = (" + f"SELECT COUNT(*) FROM wp_term_relationships tr WHERE tr.term_taxonomy_id=tt.term_taxonomy_id" + f") WHERE tt.term_taxonomy_id IN ({tt_str})" + ) + + print(f"\n=== Resultado: {stats['ok']} ok, {stats['skip']} skip ===") + + +if __name__ == '__main__': + main() diff --git a/scripts/import_new_cartas.py b/scripts/import_new_cartas.py new file mode 100644 index 0000000..96167bb --- /dev/null +++ b/scripts/import_new_cartas.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +""" +import_new_cartas.py + +Importa las cartas de la semana nuevas de ew4r_content (Joomla prod, id > 9043) +al WordPress local (Docker), y luego asigna _carta_id a los artículos K2 +correspondientes según la fecha (extra_field id 15). + +Categorías WP según catid Joomla: + catid 27 (Carta de la semana) → WP: 6 + 21 + 71 + catid 40 (Cartas de otras sem) → WP: 21 + 71 + catid 41 (Carta semana pasada) → WP: 21 + 22 + 71 +""" + +import json +import subprocess +import sys +import re +from datetime import datetime + +JOOMLA_SSH_HOST = "134.0.10.170" +JOOMLA_SSH_USER = "feadulta" +JOOMLA_SSH_PASS = "6Rm2qOF@eundwpda" +JOOMLA_DB_HOST = "127.0.0.1" +JOOMLA_DB_USER = "fejoomla3" +JOOMLA_DB_PASS = "5FF-}5^[>7^pK4W9" +JOOMLA_DB_NAME = "fejoomla3" + +WP_DOCKER = "wordpress-mysql" +WP_DB_USER = "wordpress_user" +WP_DB_PASS = "wordpress_pass" +WP_DB_NAME = "wordpress_db" + +LAST_CONTENT_ID = 9043 # último ew4r_content.id ya en WP + +# WP term_ids y sus term_taxonomy_ids (se cargan dinámicamente) +CAT_FEADULTA = 71 +CAT_CARTA_SEMANA = 6 +CAT_CARTAS_OTRAS = 21 +CAT_CARTA_PASADA = 22 + +CATID_TO_WP = { + 27: [CAT_CARTA_SEMANA, CAT_CARTAS_OTRAS, CAT_FEADULTA], + 40: [CAT_CARTAS_OTRAS, CAT_FEADULTA], + 41: [CAT_CARTAS_OTRAS, CAT_CARTA_PASADA, CAT_FEADULTA], +} + +DRY_RUN = '--dry-run' in sys.argv + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def joomla_query(query: str) -> list[dict]: + mysql_cmd = (f"mysql -h {JOOMLA_DB_HOST} -u {JOOMLA_DB_USER} " + f"-p'{JOOMLA_DB_PASS}' {JOOMLA_DB_NAME} " + f"--default-character-set=utf8mb4 -B") + cmd = ['sshpass', '-p', JOOMLA_SSH_PASS, + 'ssh', f'{JOOMLA_SSH_USER}@{JOOMLA_SSH_HOST}', mysql_cmd] + result = subprocess.run(cmd, input=query, capture_output=True, + text=True, encoding='utf-8') + if result.returncode != 0: + print(f"[ERR SSH] {result.stderr[:300]}", file=sys.stderr) + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + return [dict(zip(headers, line.split('\t'))) for line in lines[1:] if line] + + +def wp_mysql(query: str) -> list[dict]: + cmd = ['docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-B', '-e', query] + result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + return [dict(zip(headers, line.split('\t'))) for line in lines[1:] if line] + + +def wp_execute(sql: str): + if DRY_RUN: + print(f" [DRY] {sql[:110]}") + return None + cmd = ['docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-e', sql] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + err = result.stderr.replace('mysql: [Warning] Using a password on the command line interface can be insecure.\n', '') + if err.strip(): + print(f" [ERR] {err.strip()[:200]}", file=sys.stderr) + + +def esc(s: str) -> str: + return s.replace('\\', '\\\\').replace("'", "\\'") + + +def unhex(val: str) -> str: + if not val or val == 'NULL': + return '' + try: + return bytes.fromhex(val).decode('utf-8', errors='replace') + except Exception: + return val + + +# ── Main ─────────────────────────────────────────────────────────────────────── + +def main(): + print(f"=== Import nuevas cartas (ew4r_content id > {LAST_CONTENT_ID}) " + f"{'[DRY RUN]' if DRY_RUN else '[LIVE]'} ===\n") + + # Cargar term_taxonomy_ids + all_term_ids = [CAT_FEADULTA, CAT_CARTA_SEMANA, CAT_CARTAS_OTRAS, CAT_CARTA_PASADA] + rows = wp_mysql( + f"SELECT term_id, term_taxonomy_id FROM wp_term_taxonomy " + f"WHERE term_id IN ({','.join(map(str,all_term_ids))}) AND taxonomy='category'" + ) + tt_ids = {int(r['term_id']): int(r['term_taxonomy_id']) for r in rows} + print(f"TT IDs: {tt_ids}") + + # Cargar Polylang ES + pl_rows = wp_mysql( + "SELECT tt.term_taxonomy_id FROM wp_terms t " + "JOIN wp_term_taxonomy tt ON tt.term_id=t.term_id " + "WHERE tt.taxonomy='language' AND t.slug='es' LIMIT 1" + ) + pl_es_tt = int(pl_rows[0]['term_taxonomy_id']) if pl_rows else None + print(f"Polylang ES tt_id: {pl_es_tt}") + + # Cargar user map + user_rows = wp_mysql( + "SELECT um.meta_value jid, u.ID wid FROM wp_users u " + "JOIN wp_usermeta um ON um.user_id=u.ID " + "WHERE um.meta_key='_fgj2wp_old_user_id'" + ) + user_map = {} + for r in user_rows: + try: + user_map[int(r['jid'])] = int(r['wid']) + except ValueError: + pass + + # Obtener cartas nuevas de Joomla (con HEX para texto) + print("\nObteniendo cartas nuevas de Joomla...") + query = ( + f"SELECT id, HEX(title) title, HEX(alias) alias, " + f"HEX(introtext) introtext, HEX(`fulltext`) fulltext_col, " + f"catid, created, created_by " + f"FROM ew4r_content " + f"WHERE state=1 AND id > {LAST_CONTENT_ID} AND catid IN (27,40,41) " + f"ORDER BY id;" + ) + items = joomla_query(query) + print(f"Cartas a importar: {len(items)}") + + # Mapa fecha_carta → wp_id (para asignar _carta_id a artículos K2) + fecha_a_wp_carta = {} + + for item in items: + joomla_id = int(item['id']) + catid = int(item['catid']) + title = unhex(item.get('title','')) + alias = unhex(item.get('alias','')) + intro = unhex(item.get('introtext','')) + full = unhex(item.get('fulltext_col','')) + created = item.get('created','') or datetime.now().strftime('%Y-%m-%d %H:%M:%S') + created_by = int(item.get('created_by', 0) or 0) + + content = intro + ('\n\n' + full if full.strip() else '') + wp_author = user_map.get(created_by, 1) + wp_cats = CATID_TO_WP.get(catid, [CAT_CARTAS_OTRAS, CAT_FEADULTA]) + fecha_carta = created[:10] # YYYY-MM-DD + + print(f"\n [{joomla_id}] {title[:55]} | catid={catid} | fecha={fecha_carta}") + print(f" → WP cats: {wp_cats}") + + # INSERT post + post_slug = esc(alias[:200]) + post_title = esc(title) + post_content = esc(content) + + wp_execute( + f"INSERT INTO wp_posts " + f"(post_author, post_date, post_date_gmt, post_content, post_title, " + f"post_excerpt, post_status, comment_status, ping_status, post_name, " + f"post_type, post_modified, post_modified_gmt, comment_count, " + f"to_ping, pinged, post_content_filtered) VALUES (" + f"{wp_author}, '{created}', '{created}', '{post_content}', " + f"'{post_title}', '', 'publish', 'open', 'open', '{post_slug}', " + f"'post', '{created}', '{created}', 0, '', '', '')" + ) + + if DRY_RUN: + fecha_a_wp_carta[fecha_carta] = f"DRY_WP_ID_for_{joomla_id}" + continue + + new_id_rows = wp_mysql("SELECT MAX(ID) new_id FROM wp_posts") + if not new_id_rows: + print(f" [ERR] No se pudo obtener ID del post", file=sys.stderr) + continue + new_wp_id = int(new_id_rows[0]['new_id']) + print(f" → WP post ID={new_wp_id}") + + fecha_a_wp_carta[fecha_carta] = new_wp_id + + # Metas + wp_execute(f"INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES ({new_wp_id}, '_fgj2wp_old_content_id', '{joomla_id}')") + wp_execute(f"INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES ({new_wp_id}, 'Idioma', '1')") + + # Categorías + for term_id in wp_cats: + tt_id = tt_ids.get(term_id) + if tt_id: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({new_wp_id}, {tt_id})" + ) + + # Polylang ES + if pl_es_tt: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({new_wp_id}, {pl_es_tt})" + ) + + print(f"\nFecha→WP carta map: {fecha_a_wp_carta}") + + # ── Asignar _carta_id a los artículos K2 importados ────────────────────── + if DRY_RUN or not fecha_a_wp_carta: + print("\n[SKIP] Asignación _carta_id (dry-run o sin cartas importadas)") + return + + print("\n=== Asignando _carta_id a artículos K2 ===") + + # Obtener los artículos K2 nuevos con su fecha (id 15) + k2_query = ( + f"SELECT id, HEX(extra_fields) ef " + f"FROM ew4r_k2_items WHERE published=1 AND id > 17873 ORDER BY id;" + ) + k2_items = joomla_query(k2_query) + print(f"Artículos K2 a procesar: {len(k2_items)}") + + assigned = 0 + for k2item in k2_items: + k2_id = int(k2item['id']) + wp_id = k2_id + 26040 # offset conocido + ef_raw = unhex(k2item.get('ef','')) + + # Parsear fecha (id 15) + fecha_art = None + try: + fields = json.loads(ef_raw) + for f in fields: + if str(f.get('id','')) == '15': + fecha_art = str(f.get('value',''))[:10] + break + except Exception: + pass + + if not fecha_art: + continue + + carta_wp_id = fecha_a_wp_carta.get(fecha_art) + if not carta_wp_id: + continue + + # Verificar que el meta no existe ya + existing = wp_mysql( + f"SELECT meta_id FROM wp_postmeta WHERE post_id={wp_id} AND meta_key='_carta_id' LIMIT 1" + ) + if existing: + continue + + wp_execute( + f"INSERT INTO wp_postmeta (post_id, meta_key, meta_value) " + f"VALUES ({wp_id}, '_carta_id', '{carta_wp_id}')" + ) + print(f" K2 {k2_id} (WP {wp_id}) → _carta_id={carta_wp_id} [{fecha_art}]") + assigned += 1 + + print(f"\n_carta_id asignado a {assigned} artículos.") + + # Actualizar counts de categorías + print("\nActualizando counts de categorías...") + tt_str = ','.join(str(v) for v in tt_ids.values()) + wp_execute( + f"UPDATE wp_term_taxonomy tt SET count = (" + f"SELECT COUNT(*) FROM wp_term_relationships tr " + f"WHERE tr.term_taxonomy_id=tt.term_taxonomy_id" + f") WHERE tt.term_taxonomy_id IN ({tt_str})" + ) + + print("\nListo.") + + +if __name__ == '__main__': + main() diff --git a/scripts/import_new_content.py b/scripts/import_new_content.py new file mode 100644 index 0000000..d0f309b --- /dev/null +++ b/scripts/import_new_content.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +import_new_content.py + +Importa los ew4r_content items no-carta nuevos (id > 9043, catid NOT IN 27,40,41) +al WordPress local (Docker). + +Mapping catid → WP term_ids: + 54 (Índice multimedia) → 26 + 77 (Videos) → 58 + 64 (Noticias de alcance) → 41 + 52 (Tablón de anuncios) → 1 (uncategorized / sin categoría) + 63 (Fechas) → 40 + 61 (Lista completa de autores) → 38 + 65 (Cantoral Salomé Arricibita) → 31 + otro → 1 (uncategorized) +""" + +import subprocess +import sys +from datetime import datetime + +JOOMLA_SSH_HOST = "134.0.10.170" +JOOMLA_SSH_USER = "feadulta" +JOOMLA_SSH_PASS = "6Rm2qOF@eundwpda" +JOOMLA_DB_HOST = "127.0.0.1" +JOOMLA_DB_USER = "fejoomla3" +JOOMLA_DB_PASS = "5FF-}5^[>7^pK4W9" +JOOMLA_DB_NAME = "fejoomla3" + +WP_DOCKER = "wordpress-mysql" +WP_DB_USER = "wordpress_user" +WP_DB_PASS = "wordpress_pass" +WP_DB_NAME = "wordpress_db" + +LAST_CONTENT_ID = 9043 +CARTA_CATIDS = {27, 40, 41} + +CATID_TO_WP = { + 54: [26], + 77: [58], + 64: [41], + 52: [1], + 63: [40], + 61: [38], + 65: [31], +} + +DRY_RUN = '--dry-run' in sys.argv + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def joomla_query(query: str) -> list[dict]: + mysql_cmd = (f"mysql -h {JOOMLA_DB_HOST} -u {JOOMLA_DB_USER} " + f"-p'{JOOMLA_DB_PASS}' {JOOMLA_DB_NAME} " + f"--default-character-set=utf8mb4 -B") + cmd = ['sshpass', '-p', JOOMLA_SSH_PASS, + 'ssh', f'{JOOMLA_SSH_USER}@{JOOMLA_SSH_HOST}', mysql_cmd] + result = subprocess.run(cmd, input=query, capture_output=True, + text=True, encoding='utf-8') + if result.returncode != 0: + print(f"[ERR SSH] {result.stderr[:300]}", file=sys.stderr) + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + return [dict(zip(headers, line.split('\t'))) for line in lines[1:] if line] + + +def wp_mysql(query: str) -> list[dict]: + cmd = ['docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-B', '-e', query] + result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + return [dict(zip(headers, line.split('\t'))) for line in lines[1:] if line] + + +def wp_execute(sql: str): + if DRY_RUN: + print(f" [DRY] {sql[:110]}") + return + cmd = ['docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-e', sql] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + err = result.stderr.replace('mysql: [Warning] Using a password on the command line interface can be insecure.\n', '') + if err.strip(): + print(f" [ERR] {err.strip()[:200]}", file=sys.stderr) + + +def esc(s: str) -> str: + return s.replace('\\', '\\\\').replace("'", "\\'") + + +def unhex(val: str) -> str: + if not val or val == 'NULL': + return '' + try: + return bytes.fromhex(val).decode('utf-8', errors='replace') + except Exception: + return val + + +def main(): + print(f"=== Import ew4r_content no-cartas (id > {LAST_CONTENT_ID}) " + f"{'[DRY RUN]' if DRY_RUN else '[LIVE]'} ===\n") + + # Cargar user map + user_rows = wp_mysql( + "SELECT um.meta_value jid, u.ID wid FROM wp_users u " + "JOIN wp_usermeta um ON um.user_id=u.ID " + "WHERE um.meta_key='_fgj2wp_old_user_id'" + ) + user_map = {} + for r in user_rows: + try: + user_map[int(r['jid'])] = int(r['wid']) + except ValueError: + pass + print(f"Usuarios mapeados: {len(user_map)}") + + # Cargar term_taxonomy_ids + all_term_ids = sorted({t for cats in CATID_TO_WP.values() for t in cats} | {1}) + rows = wp_mysql( + f"SELECT term_id, term_taxonomy_id FROM wp_term_taxonomy " + f"WHERE term_id IN ({','.join(map(str,all_term_ids))}) AND taxonomy='category'" + ) + tt_ids = {int(r['term_id']): int(r['term_taxonomy_id']) for r in rows} + print(f"TT IDs: {tt_ids}") + + # Polylang ES + pl_rows = wp_mysql( + "SELECT tt.term_taxonomy_id FROM wp_terms t " + "JOIN wp_term_taxonomy tt ON tt.term_id=t.term_id " + "WHERE tt.taxonomy='language' AND t.slug='es' LIMIT 1" + ) + pl_es_tt = int(pl_rows[0]['term_taxonomy_id']) if pl_rows else None + + # IDs ya en WP + existing_rows = wp_mysql( + f"SELECT meta_value FROM wp_postmeta " + f"WHERE meta_key='_fgj2wp_old_content_id' AND meta_value+0 > {LAST_CONTENT_ID}" + ) + existing_ids = {int(r['meta_value']) for r in existing_rows} + print(f"IDs ya importados con id > {LAST_CONTENT_ID}: {len(existing_ids)}") + + # Obtener items de Joomla + print("\nObteniendo items de Joomla...") + catids_excl = ','.join(str(c) for c in CARTA_CATIDS) + query = ( + f"SELECT id, HEX(title) title, HEX(alias) alias, " + f"HEX(introtext) introtext, HEX(`fulltext`) fulltext_col, " + f"catid, created, created_by " + f"FROM ew4r_content " + f"WHERE state=1 AND id > {LAST_CONTENT_ID} AND catid NOT IN ({catids_excl}) " + f"ORDER BY id;" + ) + items = joomla_query(query) + print(f"Items a importar: {len(items)}") + + stats = {'ok': 0, 'skip': 0, 'err': 0} + + for item in items: + joomla_id = int(item['id']) + catid = int(item['catid']) + title = unhex(item.get('title', '')) + alias = unhex(item.get('alias', '')) + intro = unhex(item.get('introtext', '')) + full = unhex(item.get('fulltext_col', '')) + created = item.get('created', '') or datetime.now().strftime('%Y-%m-%d %H:%M:%S') + created_by = int(item.get('created_by', 0) or 0) + + if joomla_id in existing_ids: + print(f" [SKIP] id={joomla_id} ya existe") + stats['skip'] += 1 + continue + + content = intro + ('\n\n' + full if full.strip() else '') + wp_author = user_map.get(created_by, 1) + wp_cats = CATID_TO_WP.get(catid, [1]) + + print(f" [{joomla_id}] catid={catid} | {title[:50]}") + + wp_execute( + f"INSERT INTO wp_posts " + f"(post_author, post_date, post_date_gmt, post_content, post_title, " + f"post_excerpt, post_status, comment_status, ping_status, post_name, " + f"post_type, post_modified, post_modified_gmt, comment_count, " + f"to_ping, pinged, post_content_filtered) VALUES (" + f"{wp_author}, '{created}', '{created}', '{esc(content)}', " + f"'{esc(title)}', '', 'publish', 'open', 'open', '{esc(alias[:200])}', " + f"'post', '{created}', '{created}', 0, '', '', '')" + ) + + if DRY_RUN: + stats['ok'] += 1 + continue + + new_id_rows = wp_mysql("SELECT MAX(ID) new_id FROM wp_posts") + if not new_id_rows: + stats['err'] += 1 + continue + new_wp_id = int(new_id_rows[0]['new_id']) + print(f" → WP ID={new_wp_id}") + + # Metas + wp_execute(f"INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES ({new_wp_id}, '_fgj2wp_old_content_id', '{joomla_id}')") + + # Categorías + for term_id in wp_cats: + tt_id = tt_ids.get(term_id) + if tt_id: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({new_wp_id}, {tt_id})" + ) + + # Polylang ES + if pl_es_tt: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({new_wp_id}, {pl_es_tt})" + ) + + stats['ok'] += 1 + + if not DRY_RUN and stats['ok'] > 0: + print("\nActualizando counts de categorías...") + tt_str = ','.join(str(v) for v in tt_ids.values()) + wp_execute( + f"UPDATE wp_term_taxonomy tt SET count = (" + f"SELECT COUNT(*) FROM wp_term_relationships tr " + f"WHERE tr.term_taxonomy_id=tt.term_taxonomy_id" + f") WHERE tt.term_taxonomy_id IN ({tt_str})" + ) + + print(f"\n=== Resultado: {stats['ok']} ok, {stats['skip']} skip, {stats['err']} err ===") + + +if __name__ == '__main__': + main() diff --git a/scripts/import_new_k2_items.py b/scripts/import_new_k2_items.py new file mode 100644 index 0000000..5aeac6f --- /dev/null +++ b/scripts/import_new_k2_items.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +""" +import_new_k2_items.py + +Importa los K2 items nuevos de Joomla prod (id > 17873) al WordPress local (Docker). +Conexión a Joomla: SSH + MySQL en feadulta@134.0.10.170 +Conexión a WP: Docker exec wordpress-mysql + +Categorías WP asignadas según extra_fields: + - ES + tiene "libro de la biblia" (id 9) → Comentarios al evangelio (1647) + Feadulta (71) + - ES + no id9 + título "DOMINGO/SEMANA SANTA/etc." → Eucaristía (1648) + Feadulta (71) + - ES + no id9 + otro → Artículos (1650) + Feadulta (71) + - No ES → Artículos (1650) + Feadulta (71) + +Idioma Polylang asignado según extra_field id 16: + 1=es, 2=en, 3=fr, 4=it, 5=pt +""" + +import json +import subprocess +import sys +from datetime import datetime + +# ── Configuración ────────────────────────────────────────────────────────────── + +JOOMLA_SSH_HOST = "134.0.10.170" +JOOMLA_SSH_USER = "feadulta" +JOOMLA_SSH_PASS = "6Rm2qOF@eundwpda" +JOOMLA_DB_HOST = "127.0.0.1" +JOOMLA_DB_USER = "fejoomla3" +JOOMLA_DB_PASS = "5FF-}5^[>7^pK4W9" +JOOMLA_DB_NAME = "fejoomla3" + +WP_DOCKER = "wordpress-mysql" +WP_DB_USER = "wordpress_user" +WP_DB_PASS = "wordpress_pass" +WP_DB_NAME = "wordpress_db" +WP_DB_HOST = "wordpress-mysql" # dentro del container + +LAST_K2_ID = 17873 # último ID importado en WP + +# WP term_taxonomy_ids (obtenidos con SELECT tt.term_taxonomy_id FROM wp_term_taxonomy tt WHERE tt.term_id=N) +# Precalculados: +CAT_FEADULTA = 71 # term_id (se convertirá a term_taxonomy_id abajo) +CAT_ARTICULOS = 1650 +CAT_EVANGELIO = 1647 +CAT_EUCARISTIA = 1648 + +LANG_MAP = {1: 'es', 2: 'en', 3: 'fr', 4: 'it', 5: 'pt'} +DOMINGO_RE = r'DOMINGO|SEMANA SANTA|SEMANA DE PASCUA|PENTECOST|NAVIDAD|EPIFAN' + +DRY_RUN = '--dry-run' in sys.argv + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def ssh_mysql(query: str) -> list[dict]: + """Ejecuta una query en el MySQL de Joomla prod vía sshpass.""" + cmd = [ + 'sshpass', '-p', JOOMLA_SSH_PASS, + 'ssh', f'{JOOMLA_SSH_USER}@{JOOMLA_SSH_HOST}', + f'mysql -h {JOOMLA_DB_HOST} -u {JOOMLA_DB_USER} ' + f'-p{repr(JOOMLA_DB_PASS)} {JOOMLA_DB_NAME} ' + f'--default-character-set=utf8mb4 -B -e "{query}"' + ] + result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + print(f"[ERROR SSH] {result.stderr[:300]}", file=sys.stderr) + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + rows = [] + for line in lines[1:]: + if line: + vals = line.split('\t') + rows.append(dict(zip(headers, vals))) + return rows + + +def wp_mysql(query: str) -> list[dict]: + """Ejecuta una query en el MySQL del WP local vía Docker exec.""" + cmd = [ + 'docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-B', '-e', query + ] + result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + print(f"[ERROR WP] {result.stderr[:300]}", file=sys.stderr) + return [] + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return [] + headers = lines[0].split('\t') + rows = [] + for line in lines[1:]: + if line: + vals = line.split('\t') + rows.append(dict(zip(headers, vals))) + return rows + + +def wp_execute(sql: str): + """Ejecuta un INSERT/UPDATE en WP MySQL.""" + if DRY_RUN: + print(f" [DRY] {sql[:120]}") + return + cmd = [ + 'docker', 'exec', WP_DOCKER, + 'mysql', '-u', WP_DB_USER, f'-p{WP_DB_PASS}', WP_DB_NAME, + '--default-character-set=utf8mb4', '-e', sql + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"[ERROR INSERT] {result.stderr[:300]}", file=sys.stderr) + + +def esc(s: str) -> str: + """Escapa una string para SQL.""" + return s.replace('\\', '\\\\').replace("'", "\\'") + + +# ── Cargar datos auxiliares ──────────────────────────────────────────────────── + +def load_user_map() -> dict: + """Devuelve {joomla_user_id: wp_user_id}.""" + rows = wp_mysql( + "SELECT um.meta_value jid, u.ID wid FROM wp_users u " + "JOIN wp_usermeta um ON um.user_id=u.ID " + "WHERE um.meta_key='_fgj2wp_old_user_id'" + ) + m = {} + for r in rows: + try: + m[int(r['jid'])] = int(r['wid']) + except ValueError: + pass + return m + + +def load_term_taxonomy_ids() -> dict: + """Devuelve {term_id: term_taxonomy_id} para las categorías relevantes.""" + term_ids = [CAT_FEADULTA, CAT_ARTICULOS, CAT_EVANGELIO, CAT_EUCARISTIA] + ids_str = ','.join(str(x) for x in term_ids) + rows = wp_mysql( + f"SELECT term_id, term_taxonomy_id FROM wp_term_taxonomy " + f"WHERE term_id IN ({ids_str}) AND taxonomy='category'" + ) + return {int(r['term_id']): int(r['term_taxonomy_id']) for r in rows} + + +def load_polylang_term_ids() -> dict: + """Devuelve {'es': tt_id, 'en': tt_id, ...} para los términos de idioma de Polylang.""" + rows = wp_mysql( + "SELECT t.slug, tt.term_taxonomy_id FROM wp_terms t " + "JOIN wp_term_taxonomy tt ON tt.term_id=t.term_id " + "WHERE tt.taxonomy='language' AND t.slug IN ('es','en','fr','it','pt')" + ) + return {r['slug']: int(r['term_taxonomy_id']) for r in rows} + + +# ── Parsear extra_fields ─────────────────────────────────────────────────────── + +def parse_extra_fields(ef_json: str) -> dict: + """Devuelve dict con claves: lang_val, has_libro, cita_biblica.""" + result = {'lang_val': None, 'has_libro': False, 'cita_biblica': None} + if not ef_json or ef_json == 'NULL': + return result + try: + fields = json.loads(ef_json) + except json.JSONDecodeError: + return result + for f in fields: + fid = str(f.get('id', '')) + val = f.get('value') + if fid == '16' and val is not None: + try: + result['lang_val'] = int(val) + except (ValueError, TypeError): + pass + elif fid == '9': + result['has_libro'] = True + elif fid == '14': + if isinstance(val, list): + result['cita_biblica'] = ','.join(str(v) for v in val) + else: + result['cita_biblica'] = str(val) if val else None + return result + + +def determine_categories(ef: dict, title: str) -> list[int]: + """Devuelve lista de term_ids de categoría para el post.""" + import re + lang = ef.get('lang_val') + es = (lang == 1 or lang is None) + cats = [CAT_FEADULTA] + if es and ef.get('has_libro'): + cats.append(CAT_EVANGELIO) + elif es and re.search(DOMINGO_RE, title, re.IGNORECASE): + cats.append(CAT_EUCARISTIA) + else: + cats.append(CAT_ARTICULOS) + return cats + + +# ── Import principal ─────────────────────────────────────────────────────────── + +def main(): + print(f"=== Import K2 items > {LAST_K2_ID} → WP local {'[DRY RUN]' if DRY_RUN else '[LIVE]'} ===\n") + + user_map = load_user_map() + print(f"Usuarios mapeados: {len(user_map)}") + + tt_ids = load_term_taxonomy_ids() + print(f"Categorías TT IDs: {tt_ids}") + + pl_ids = load_polylang_term_ids() + print(f"Polylang idiomas: {pl_ids}") + + # Verificar que los K2 IDs ya en WP no se reimportan + existing = wp_mysql( + f"SELECT meta_value FROM wp_postmeta WHERE meta_key='_fgj2wp_old_k2_id' " + f"AND meta_value+0 > {LAST_K2_ID}" + ) + existing_ids = {int(r['meta_value']) for r in existing} + print(f"K2 IDs > {LAST_K2_ID} ya en WP: {len(existing_ids)}") + + # Obtener items de Joomla vía SSH+MySQL (query por stdin para evitar escape de shell) + print("\nObteniendo K2 items de Joomla prod...") + # HEX encoding para campos de texto (evita que el HTML con saltos de línea + # rompa el parsing TSV) + query = ( + f"SELECT id, HEX(title) title, HEX(alias) alias, " + f"HEX(introtext) introtext, HEX(`fulltext`) fulltext_col, " + f"created, created_by, HEX(extra_fields) extra_fields, publish_up " + f"FROM ew4r_k2_items " + f"WHERE published=1 AND id > {LAST_K2_ID} ORDER BY id;" + ) + mysql_cmd = ( + f"mysql -h {JOOMLA_DB_HOST} -u {JOOMLA_DB_USER} " + f"-p'{JOOMLA_DB_PASS}' {JOOMLA_DB_NAME} " + f"--default-character-set=utf8mb4 -B" + ) + cmd = [ + 'sshpass', '-p', JOOMLA_SSH_PASS, + 'ssh', f'{JOOMLA_SSH_USER}@{JOOMLA_SSH_HOST}', + mysql_cmd + ] + result = subprocess.run(cmd, input=query, capture_output=True, text=True, encoding='utf-8') + if result.returncode != 0: + print(f"ERROR: {result.stderr[:500]}") + sys.exit(1) + + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + print("No se encontraron items nuevos.") + return + + headers = lines[0].split('\t') + items = [] + for line in lines[1:]: + if line: + vals = line.split('\t') + items.append(dict(zip(headers, vals))) + + print(f"Items a importar: {len(items)}") + + stats = {'ok': 0, 'skip': 0, 'err': 0} + + for item in items: + k2_id = int(item['id']) + + if k2_id in existing_ids: + print(f" [SKIP] K2 id={k2_id} ya existe en WP") + stats['skip'] += 1 + continue + + def unhex(val: str) -> str: + if not val or val == 'NULL': + return '' + try: + return bytes.fromhex(val).decode('utf-8', errors='replace') + except Exception: + return val + + title = unhex(item.get('title', '')) + alias = unhex(item.get('alias', '')) + intro = unhex(item.get('introtext', '')) + full = unhex(item.get('fulltext_col', '')) + ef_json = unhex(item.get('extra_fields', '')) or '[]' + created = item.get('created', '') or datetime.now().strftime('%Y-%m-%d %H:%M:%S') + if not created or created == 'NULL': + created = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + created_by_raw = item.get('created_by', '0') + created_by = int(created_by_raw) if created_by_raw and created_by_raw != 'NULL' else 0 + + # Contenido combinado + if full and full.strip(): + content = intro + '\n\n' + full + else: + content = intro + + # Autor WP + wp_author = user_map.get(created_by, 1) # fallback: admin + + # Extra fields + ef = parse_extra_fields(ef_json) + lang_code = LANG_MAP.get(ef.get('lang_val'), 'es') + cats = determine_categories(ef, title) + + print(f" [{k2_id}] {title[:50]} | lang={lang_code} | cats={cats}") + + # INSERT post + post_slug = esc(alias[:200]) if alias else '' + post_title = esc(title) + post_content = esc(content) + post_date = created + post_date_gmt = created # simplificado (no ajuste TZ) + + insert_post = ( + f"INSERT INTO wp_posts " + f"(post_author, post_date, post_date_gmt, post_content, post_title, " + f"post_excerpt, post_status, comment_status, ping_status, post_name, " + f"post_type, post_modified, post_modified_gmt, comment_count, " + f"to_ping, pinged, post_content_filtered) VALUES (" + f"{wp_author}, '{post_date}', '{post_date_gmt}', '{post_content}', " + f"'{post_title}', '', 'publish', 'open', 'open', '{post_slug}', " + f"'post', '{post_date}', '{post_date_gmt}', 0, '', '', '')" + ) + wp_execute(insert_post) + + if DRY_RUN: + stats['ok'] += 1 + continue + + # Obtener el ID del post recién insertado + new_id_rows = wp_mysql("SELECT LAST_INSERT_ID() as new_id") + if not new_id_rows: + print(f" [ERROR] No se pudo obtener LAST_INSERT_ID para k2_id={k2_id}") + stats['err'] += 1 + continue + new_wp_id = int(new_id_rows[0]['new_id']) + print(f" → WP post ID={new_wp_id}") + + # INSERT metas + metas = [ + ('_fgj2wp_old_k2_id', str(k2_id)), + ('Idioma', str(ef.get('lang_val') or 1)), + ] + for meta_key, meta_val in metas: + wp_execute( + f"INSERT INTO wp_postmeta (post_id, meta_key, meta_value) " + f"VALUES ({new_wp_id}, '{esc(meta_key)}', '{esc(meta_val)}')" + ) + + # Categorías + for term_id in cats: + tt_id = tt_ids.get(term_id) + if tt_id: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({new_wp_id}, {tt_id})" + ) + + # Polylang language + pl_tt = pl_ids.get(lang_code) + if pl_tt: + wp_execute( + f"INSERT IGNORE INTO wp_term_relationships (object_id, term_taxonomy_id) " + f"VALUES ({new_wp_id}, {pl_tt})" + ) + + stats['ok'] += 1 + + # Actualizar counts de categorías + if not DRY_RUN and stats['ok'] > 0: + print("\nActualizando counts de categorías...") + tt_ids_list = ','.join(str(v) for v in tt_ids.values()) + wp_execute( + f"UPDATE wp_term_taxonomy tt SET count = (" + f"SELECT COUNT(*) FROM wp_term_relationships tr WHERE tr.term_taxonomy_id=tt.term_taxonomy_id" + f") WHERE tt.term_taxonomy_id IN ({tt_ids_list})" + ) + + print(f"\n=== Resultado: {stats['ok']} ok, {stats['skip']} skip, {stats['err']} err ===") + + +if __name__ == '__main__': + main() diff --git a/wordpress/wp-content/mu-plugins/fea-homepage.php b/wordpress/wp-content/mu-plugins/fea-homepage.php index e52846a..a8a2e72 100644 --- a/wordpress/wp-content/mu-plugins/fea-homepage.php +++ b/wordpress/wp-content/mu-plugins/fea-homepage.php @@ -749,6 +749,80 @@ add_shortcode('fea_multimedia', function($atts) { return $html . ''; }); +// ── Shortcode: [fea_prefooter] — 3 columnas sobre el footer ────────────────── +add_shortcode('fea_prefooter', function() { + $uploads = wp_upload_dir()['baseurl']; + + // Columna centro: última noticia de alcance (cat term_id=41) + $latest_noticias = get_posts([ + 'posts_per_page' => 1, + 'category__in' => [fea_cat(41)], + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + ]); + $cat_noticias_url = get_category_link(fea_cat(41)); + $noticia_title = !empty($latest_noticias) ? fea_title($latest_noticias[0]->post_title) : ''; + $noticia_url = !empty($latest_noticias) ? get_permalink($latest_noticias[0]->ID) : $cat_noticias_url; + + // Columna derecha: último vídeo (cat term_id=58) + $latest_video = get_posts([ + 'posts_per_page' => 1, + 'category__in' => [fea_cat(58)], + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + ]); + $video_url = !empty($latest_video) ? get_permalink($latest_video[0]->ID) : get_category_link(fea_cat(58)); + + $effa_url = get_permalink(42726); + $suma_url = get_permalink(18036); + $libros_url = get_category_link(1616); + $videos_escuela_url = get_permalink(21921); + + ob_start(); ?> +
+
+ + Cómo adquirir nuestros libros + + + Últimos vídeos de la escuela + +
+
+ + Noticias de alcance + + +

+ +

+ +
+
+ + Este portal + + + Acceso a EFFA + + + Vídeo de la semana + +
+
+ +.fea-prefooter{display:flex;gap:1.5rem;justify-content:center;align-items:flex-start;padding:2rem 1rem;border-top:2px solid #e5e5e5;margin-top:3rem;} +.fea-prefooter-col{flex:1;max-width:320px;text-align:center;} +.fea-prefooter-noticia{margin-top:0.6rem;font-size:0.85rem;font-weight:600;color:#0000cc;line-height:1.3;} +.fea-prefooter-noticia a{color:#0000cc;} +@media(max-width:640px){.fea-prefooter{flex-direction:column;align-items:center;}.fea-prefooter-col{max-width:100%;}} +'; + return $css . ob_get_clean(); +}); + // ── Reescribir links internos al idioma activo (Polylang) ───────────────── add_filter('the_content', function($content) { if (!function_exists('pll_current_language') || !function_exists('pll_get_post')) return $content;