CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
308 lines
13 KiB
Python
Executable File
308 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
scripts/verifica-vazamentos.py
|
|
Verificação permanente de ausência de vazamento de português em rotas inglesas (en).
|
|
Analisa tanto o texto visível da página quanto todos os blocos <script type="application/ld+json">.
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
import json
|
|
import html
|
|
import urllib.request
|
|
|
|
base = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:3998"
|
|
|
|
series = [
|
|
"ESP32", "ESP32-S2", "ESP32-S3", "ESP32-S31", "ESP32-C2", "ESP32-C3",
|
|
"ESP32-C5", "ESP32-C6", "ESP32-C61", "ESP32-P4", "ESP32-H2", "ESP32-H4"
|
|
]
|
|
|
|
frameworks = [
|
|
"ESP-IDF", "ESP-RainMaker", "ESP-Matter", "ESP-ADF", "ESP-SR", "ESP-IoT-Solution",
|
|
"ESP-NOW", "ESP-Zigbee-SDK", "ESP-WHO", "ESP-NN", "ESP-DSP", "Rust-ESP",
|
|
"Arduino", "MicroPython"
|
|
]
|
|
|
|
fixas = [
|
|
"/en", "/en/series", "/en/compare", "/en/diagnostics", "/en/selector",
|
|
"/en/frameworks", "/en/components", "/en/glossary", "/en/catalog", "/en/about"
|
|
]
|
|
|
|
urls = fixas + [f"/en/series/{s}" for s in series] + [f"/en/frameworks/{f}" for f in frameworks]
|
|
|
|
# Regex rigoroso para detecção de vazamentos em português
|
|
pt_regex = re.compile(
|
|
r"\b(não|você|para o|para a|com o|uma|são|também|então|código|sobre|pinagem|"
|
|
r"série|séries|livro|ção|ções|mais|muito|já|está|estão|é um|que|dos|das|"
|
|
r"nos|nas|pelo|pela|especificações|especificação|guia|comparativo|catálogo|"
|
|
r"seletor|recursos|projeto|independente|núcleos|frequência máxima|fabricante|"
|
|
r"arquitetura|encapsulamento|início|artigo)\b",
|
|
re.I
|
|
)
|
|
|
|
# Regex estrito para vazamentos de português em páginas em espanhol
|
|
# Palavras/sufixos que existem em português mas são incorretos em espanhol
|
|
pt_in_es_regex = re.compile(
|
|
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|"
|
|
r"série|séries|livro|ção|ções|muito|já|estão|é um|é uma|"
|
|
r"pelo|pela|especificações|especificação|início|artigo)\b",
|
|
re.I
|
|
)
|
|
|
|
# Termos técnicos legítimos aceitos em links/IDs/nomes
|
|
TECHNICAL_EXEMPT_PATHS = {"@id", "@type", "@context", "url", "image", "logo", "sameAs"}
|
|
|
|
def check_jsonld(obj, path="", regex=pt_regex, is_es=False):
|
|
problems = []
|
|
if isinstance(obj, str):
|
|
key = path.split(".")[-1].split("[")[0]
|
|
if key in TECHNICAL_EXEMPT_PATHS:
|
|
# Garante que âncoras de id não vazem português
|
|
if (path.endswith("@id") or path.endswith("url")) and ("#artigo" in obj or "#sobre" in obj):
|
|
problems.append((path, f"Anchor leak in {obj}"))
|
|
return problems
|
|
if regex.search(obj):
|
|
problems.append((path, obj))
|
|
elif isinstance(obj, list):
|
|
for i, val in enumerate(obj):
|
|
problems.extend(check_jsonld(val, f"{path}[{i}]", regex, is_es))
|
|
elif isinstance(obj, dict):
|
|
for k, val in obj.items():
|
|
problems.extend(check_jsonld(val, f"{path}.{k}" if path else k, regex, is_es))
|
|
return problems
|
|
|
|
falhas = 0
|
|
|
|
print(f"ESPDocs · Varredura de i18n nas 36 rotas em inglês ({base})")
|
|
|
|
for u in urls:
|
|
try:
|
|
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
|
|
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
|
|
except Exception as e:
|
|
print(f" ✖ ERRO ao acessar {u}: {e}")
|
|
falhas += 1
|
|
continue
|
|
|
|
# 1. Verifica blocos ld+json
|
|
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
|
|
for i, s in enumerate(scripts):
|
|
try:
|
|
data = json.loads(s)
|
|
issues = check_jsonld(data, f"script[{i}]", pt_regex, False)
|
|
for p, v in issues:
|
|
print(f" ✖ JSON-LD vazamento em {u} -> {p}: \"{v}\"")
|
|
falhas += 1
|
|
except Exception as e:
|
|
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
|
|
falhas += 1
|
|
|
|
# 2. Verifica texto visível
|
|
clean = re.sub(r"<(script|style)[\s\S]*?</\1>", "", raw)
|
|
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
|
|
hits = [l.strip() for l in clean.split("\n") if l.strip() and pt_regex.search(l)]
|
|
for h in hits:
|
|
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
|
falhas += 1
|
|
|
|
# Varredura das rotas em espanhol
|
|
fixas_es = [
|
|
"/es", "/es/series", "/es/comparacion", "/es/diagnostico", "/es/selector",
|
|
"/es/frameworks", "/es/componentes", "/es/glosario", "/es/catalogo", "/es/acerca"
|
|
]
|
|
urls_es = fixas_es + [f"/es/series/{s}" for s in series] + [f"/es/frameworks/{f}" for f in frameworks]
|
|
|
|
print(f"\nESPDocs · Varredura de i18n nas 36 rotas em espanhol ({base})")
|
|
|
|
for u in urls_es:
|
|
try:
|
|
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
|
|
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
|
|
except Exception as e:
|
|
print(f" ✖ ERRO ao acessar {u}: {e}")
|
|
falhas += 1
|
|
continue
|
|
|
|
# 1. Verifica blocos ld+json
|
|
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
|
|
for i, s in enumerate(scripts):
|
|
try:
|
|
data = json.loads(s)
|
|
issues = check_jsonld(data, f"script[{i}]", pt_in_es_regex, True)
|
|
for p, v in issues:
|
|
print(f" ✖ JSON-LD vazamento em {u} -> {p}: \"{v}\"")
|
|
falhas += 1
|
|
except Exception as e:
|
|
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
|
|
falhas += 1
|
|
|
|
# 2. Verifica texto visível
|
|
clean = re.sub(r"<(script|style)[\s\S]*?</\1>", "", raw)
|
|
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
|
|
hits = [l.strip() for l in clean.split("\n") if l.strip() and pt_in_es_regex.search(l)]
|
|
for h in hits:
|
|
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
|
falhas += 1
|
|
|
|
# Varredura das rotas em francês
|
|
pt_in_fr_regex = re.compile(
|
|
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|livro|"
|
|
r"ção|ções|muito|já|estão|é um|é uma|pelo|pela|especificações|especificação|início|artigo|seletor)\b",
|
|
re.I
|
|
)
|
|
|
|
fixas_fr = [
|
|
"/fr", "/fr/series", "/fr/comparaison", "/fr/diagnostic", "/fr/selecteur",
|
|
"/fr/frameworks", "/fr/composants", "/fr/glossaire", "/fr/catalogue", "/fr/a-propos"
|
|
]
|
|
urls_fr = fixas_fr + [f"/fr/series/{s}" for s in series] + [f"/fr/frameworks/{f}" for f in frameworks]
|
|
|
|
print(f"\nESPDocs · Varredura de i18n nas {len(urls_fr)} rotas em francês ({base})")
|
|
|
|
for u in urls_fr:
|
|
try:
|
|
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
|
|
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
|
|
except Exception as e:
|
|
print(f" ✖ ERRO ao acessar {u}: {e}")
|
|
falhas += 1
|
|
continue
|
|
|
|
# 1. Verifica blocos ld+json
|
|
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
|
|
for i, s in enumerate(scripts):
|
|
try:
|
|
data = json.loads(s)
|
|
issues = check_jsonld(data, f"script[{i}]", pt_in_fr_regex, True)
|
|
for p, v in issues:
|
|
print(f" ✖ JSON-LD vazamento em {u} -> {p}: \"{v}\"")
|
|
falhas += 1
|
|
except Exception as e:
|
|
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
|
|
falhas += 1
|
|
|
|
# 2. Verifica texto visível
|
|
clean = re.sub(r"<(script|style)[\s\S]*?</\1>", "", raw)
|
|
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
|
|
hits = [l.strip() for l in clean.split("\n") if l.strip() and pt_in_fr_regex.search(l)]
|
|
for h in hits:
|
|
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
|
falhas += 1
|
|
|
|
# Varredura das rotas em alemão
|
|
pt_in_de_regex = re.compile(
|
|
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|série|séries|livro|"
|
|
r"ção|ções|muito|já|estão|é um|é uma|pelo|pela|especificações|especificação|início|artigo|sobre|seletor)\b",
|
|
re.I
|
|
)
|
|
|
|
fixas_de = [
|
|
"/de", "/de/serien", "/de/vergleich", "/de/diagnose", "/de/auswahl",
|
|
"/de/frameworks", "/de/komponenten", "/de/glossar", "/de/katalog", "/de/ueber-uns"
|
|
]
|
|
urls_de = fixas_de + [f"/de/serien/{s}" for s in series] + [f"/de/frameworks/{f}" for f in frameworks]
|
|
|
|
print(f"\nESPDocs · Varredura de i18n nas {len(urls_de)} rotas em alemão ({base})")
|
|
|
|
for u in urls_de:
|
|
try:
|
|
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
|
|
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
|
|
except Exception as e:
|
|
print(f" ✖ ERRO ao acessar {u}: {e}")
|
|
falhas += 1
|
|
continue
|
|
|
|
# 1. Verifica blocos ld+json
|
|
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
|
|
for i, s in enumerate(scripts):
|
|
try:
|
|
data = json.loads(s)
|
|
issues = check_jsonld(data, f"script[{i}]", pt_in_de_regex, True)
|
|
for p, v in issues:
|
|
print(f" ✖ JSON-LD vazamento em {u} -> {p}: \"{v}\"")
|
|
falhas += 1
|
|
except Exception as e:
|
|
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
|
|
falhas += 1
|
|
|
|
# 2. Verifica texto visível
|
|
clean = re.sub(r"<(script|style)[\s\S]*?</\1>", "", raw)
|
|
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
|
|
hits = [l.strip() for l in clean.split("\n") if l.strip() and pt_in_de_regex.search(l)]
|
|
for h in hits:
|
|
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
|
falhas += 1
|
|
|
|
# Varredura das rotas em chinês simplificado: português OU inglês solto (fora de blocos de código)
|
|
pt_in_zh_regex = re.compile(
|
|
r"\b(não|você|para o|para a|com o|com a|são|também|então|pinagem|série|séries|livro|"
|
|
r"ção|ções|muito|já|estão|é um|é uma|pelo|pela|especificações|especificação|início|artigo|sobre|seletor)\b",
|
|
re.I
|
|
)
|
|
# Frase em inglês solta: palavra funcional entre palavras minúsculas, ou expressões de interface.
|
|
# Nomes de produto em Title Case ("Arduino Core for ESP32", "Google Home") não casam.
|
|
en_in_zh_regex = re.compile(
|
|
r"\b[a-z]{2,}\s+(?:the|and|with|for|your|this|that|from|are|you|will|not|how|what)\s+[a-z]{2,}\b"
|
|
r"|(?i:\b(?:skip to content|click here|learn more|read more|back to|please)\b)"
|
|
)
|
|
|
|
fixas_zh = [
|
|
"/zh-CN", "/zh-CN/series", "/zh-CN/compare", "/zh-CN/diagnostics", "/zh-CN/selector",
|
|
"/zh-CN/frameworks", "/zh-CN/components", "/zh-CN/glossary", "/zh-CN/catalog", "/zh-CN/about"
|
|
]
|
|
urls_zh = fixas_zh + [f"/zh-CN/series/{s}" for s in series] + [f"/zh-CN/frameworks/{f}" for f in frameworks]
|
|
|
|
print(f"\nESPDocs · Varredura de i18n nas {len(urls_zh)} rotas em chinês ({base})")
|
|
|
|
# Mensagens reais de log/erro da ferramenta: aparecem iguais em qualquer idioma.
|
|
ASSINATURAS_DE_LOG = {"Failed to connect to ESP32: Timed out waiting for packet header"}
|
|
|
|
def vazou_zh(texto):
|
|
if texto.strip() in ASSINATURAS_DE_LOG:
|
|
return None
|
|
return pt_in_zh_regex.search(texto) or en_in_zh_regex.search(texto)
|
|
|
|
for u in urls_zh:
|
|
try:
|
|
req = urllib.request.Request(base + u, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
|
|
raw = urllib.request.urlopen(req, timeout=10).read().decode("utf-8")
|
|
except Exception as e:
|
|
print(f" ✖ ERRO ao acessar {u}: {e}")
|
|
falhas += 1
|
|
continue
|
|
|
|
# <html lang> correto
|
|
if not re.search(r"<html[^>]*\blang=\"zh-CN\"", raw):
|
|
print(f" ✖ <html lang> diferente de zh-CN em {u}")
|
|
falhas += 1
|
|
|
|
# 1. Verifica blocos ld+json (strings de texto, não URLs/ids)
|
|
scripts = re.findall(r"<script[^>]*type=[\"']application/ld\+json[\"'][^>]*>(.*?)</script>", raw, re.DOTALL)
|
|
for i, s in enumerate(scripts):
|
|
try:
|
|
data = json.loads(s)
|
|
issues = check_jsonld(data, f"script[{i}]", pt_in_zh_regex, True)
|
|
for p_, v in issues:
|
|
print(f" ✖ JSON-LD vazamento em {u} -> {p_}: \"{v}\"")
|
|
falhas += 1
|
|
except Exception as e:
|
|
print(f" ✖ Erro no parse de JSON-LD em {u}: {e}")
|
|
falhas += 1
|
|
|
|
# 2. Texto visível, sem blocos de código (exemplos têm logs e comentários em inglês por desenho)
|
|
clean = re.sub(r"<(script|style|pre|code)[\s\S]*?</\1>", "", raw)
|
|
clean = html.unescape(re.sub(r"<[^>]+>", "\n", clean))
|
|
hits = [l.strip() for l in clean.split("\n") if l.strip() and vazou_zh(l)]
|
|
for h in hits:
|
|
print(f" ✖ Texto visível vazamento em {u}: \"{h[:120]}\"")
|
|
falhas += 1
|
|
|
|
if falhas == 0:
|
|
print(f" ✔ Todas as {len(urls)} rotas en, {len(urls_es)} rotas es, {len(urls_fr)} rotas fr, {len(urls_de)} rotas de e {len(urls_zh)} rotas zh verificadas com ZERO vazamentos (texto e JSON-LD).")
|
|
sys.exit(0)
|
|
else:
|
|
print(f" ✖ Total de falhas detectadas: {falhas}")
|
|
sys.exit(1)
|