Mirror del Joomla antiguo: versionar los scripts y reponer los assets que faltaban

Los scripts del mirror (00-90) vivian solo en el disco. Van al repo; los datos que
generan no (16 GB entre crawl, snapshot del origen y Joomla restaurado) -> .gitignore.

Nuevo 91-repone-assets404.sh: repone los ficheros que el crawl no capturo porque se
referencian SOLO desde CSS y el crawler seguia enlaces HTML (system.css, los fondos de
fe_adulta_1, ratingstars.gif de K2). Salian como 404 en los logs de nginx del Hetzner.

Descarga por HTTP desde el Joomla local aislado, nunca del filesystem -- mismo principio
que el crawl, para no arrastrar los .php comprometidos del #183 -- y escanea PHP embebido
antes de copiar a site/.

Resultado sobre las 286 rutas unicas con 404 del log: 196 repuestas y verificadas en
produccion (196/196 en 200 tras el rsync), 82 que dan 301->404 tambien en el origen (ya
estaban rotas en la web original) y 8 rutas basura /%22/... de HTML mal formado.

Refs #180

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:04:31 -04:00
parent 69e849d38e
commit 275aff1430
71 changed files with 2667 additions and 0 deletions
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""§4.3, último punto del plan: contrastar páginas capturadas contra Internet Archive.
No compara el texto (una instantánea de hace años difiere por fuerza: fechas, barras laterales,
bloques rotativos). Compara lo que de verdad delata una inyección: **el conjunto de hosts externos
a los que la página carga scripts o iframes**. Si nuestra captura referencia hosts que la versión
histórica no tenía, hay que mirarlo.
"""
import os, re, json, sys, urllib.request, random
from collections import Counter
from urllib.parse import urlsplit, unquote
BASE = "/home/rafa/joomla-migration/mirror-antiguo"
RUN = open(os.path.join(BASE, "CURRENT_RUN")).read().strip()
DIR = os.path.join(BASE, "runs", RUN)
SITE = os.path.join(DIR, "site", "antiguo.feadulta.com")
N = int(sys.argv[1]) if len(sys.argv) > 1 else 10
SRC = re.compile(r'<(?:script|iframe)[^>]+src=["\']((?:https?:)?//[^"\'/]+)', re.I)
UA = {"User-Agent": "feadulta-archiver/1.0 (verificacion de integridad; incident-183)"}
def hosts(html):
out = set()
for m in SRC.findall(html):
h = m.split("//", 1)[-1].lower()
# web.archive.org reescribe los recursos: nos quedamos con el host original
if h.startswith("web.archive.org"):
continue
out.add(h)
return out
# candidatas: URLs /es/ que Wayback conoce Y que tenemos capturadas
cand = []
for fn in ("wayback-antiguo.txt", "wayback-feadulta.txt"):
for line in open(os.path.join(BASE, "inventory", fn), errors="replace"):
u = line.strip()
s = urlsplit(u)
if s.query or not s.path.startswith("/es/") or not s.path.endswith(".html"):
continue
p = unquote(s.path).lstrip("/")
if os.path.isfile(os.path.join(SITE, p)):
cand.append((u, p))
random.seed(20260730)
sample = random.sample(cand, min(N, len(cand)))
print("candidatas:", len(cand), "- muestra:", len(sample), "\n")
res, extra_total = [], Counter()
for url, rel in sample:
local = open(os.path.join(SITE, rel), encoding="utf-8", errors="replace").read()
hl = hosts(local)
try:
req = urllib.request.Request("https://web.archive.org/web/2id_/" + url, headers=UA)
arch = urllib.request.urlopen(req, timeout=90).read().decode("utf-8", "replace")
ha = hosts(arch)
estado = "ok"
except Exception as e:
ha, estado = set(), "sin snapshot (%s)" % type(e).__name__
extra = hl - ha
if estado == "ok":
for h in extra:
extra_total[h] += 1
print("%-70s %s" % (rel[-68:], estado))
if estado == "ok" and extra:
print(" hosts solo en nuestra captura:", ", ".join(sorted(extra)))
res.append({"url": url, "estado": estado, "hosts_mirror": sorted(hl),
"hosts_wayback": sorted(ha), "solo_en_mirror": sorted(extra)})
print("\n--- hosts presentes solo en nuestra captura (agregado) ---")
if extra_total:
for h, c in extra_total.most_common():
print("%4d %s" % (c, h))
else:
print("ninguno")
json.dump(res, open(os.path.join(DIR, "wayback-contraste.json"), "w"), indent=2, ensure_ascii=False)
print("\ninforme:", os.path.join(DIR, "wayback-contraste.json"))