This commit is contained in:
tavo 2025-10-07 00:57:44 -06:00
parent 3eb2267424
commit 9a8c6c5c01
5 changed files with 173 additions and 6 deletions

View file

@ -1,23 +1,108 @@
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
import gfd.i18n as i18n import gfd.i18n as i18n
from gfd import sfd
class GFDWidget(QtWidgets.QWidget): class GFDWidget(QtWidgets.QWidget):
def __init__(self, tr): def __init__(self, tr):
super().__init__() super().__init__()
self.tr = tr self.tr = tr
self.setWindowTitle(self.tr("window_title")) self.setWindowTitle(self.tr("window_title"))
self.resize(300, 100) self.resize(400, 220)
self.label = QtWidgets.QLabel(
self.tr("hello_world"),
alignment=QtCore.Qt.AlignmentFlag.AlignCenter,
)
self.combo = QtWidgets.QComboBox()
self.combo.addItem(self.tr("loading_installers") + "")
self.refresh_btn = QtWidgets.QPushButton("" + self.tr("refresh"))
self.refresh_btn.clicked.connect(self.reload_installers)
combo_layout = QtWidgets.QHBoxLayout()
combo_layout.addWidget(self.combo)
combo_layout.addWidget(self.refresh_btn)
self.md5_label = QtWidgets.QLabel(
"", alignment=QtCore.Qt.AlignmentFlag.AlignCenter
)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.label)
layout.addLayout(combo_layout)
layout.addWidget(self.md5_label)
self.combo.currentIndexChanged.connect(self.on_selection_changed)
QtCore.QTimer.singleShot(0, self.load_installers_async)
def reload_installers(self):
"""Triggered by the refresh button."""
self.refresh_btn.setEnabled(False)
self.combo.clear()
self.combo.addItem(self.tr("loading_installers") + "")
self.load_installers_async()
def load_installers_async(self):
"""Fetch installer options without freezing the UI."""
self.thread = QtCore.QThread()
self.worker = SFDWorker()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.result_ready.connect(self.populate_installers)
self.thread.start()
@QtCore.Slot(list)
def populate_installers(self, installers):
self.refresh_btn.setEnabled(True)
self.combo.clear()
if not installers:
self.combo.addItem(self.tr("no_installers_found"))
self.md5_label.setText("")
return
self.installers = installers
for item in installers:
self.combo.addItem(item["name"], item["md5"])
self.combo.setCurrentIndex(0)
self.on_selection_changed(0)
@QtCore.Slot(int)
def on_selection_changed(self, index):
if not hasattr(self, "installers") or not self.installers:
return
md5 = self.combo.currentData()
name = self.combo.currentText()
self.md5_label.setText(f"<b>{name}</b><br>MD5: <code>{md5 or 'N/A'}</code>")
class SFDWorker(QtCore.QObject):
"""Background worker to fetch installers safely."""
finished = QtCore.Signal()
result_ready = QtCore.Signal(list)
@QtCore.Slot()
def run(self):
data = sfd.fetchInstallerOptions()
self.result_ready.emit(data)
self.finished.emit()
label = QtWidgets.QLabel(self.tr("hello_world"), alignment=QtCore.Qt.AlignCenter)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(label)
def run(lang="es"): def run(lang="es"):
import sys import sys
app = QtWidgets.QApplication(sys.argv)
app = QtWidgets.QApplication(sys.argv)
tr = i18n.Translator(lang) tr = i18n.Translator(lang)
w = GFDWidget(tr) w = GFDWidget(tr)
w.show() w.show()
sys.exit(app.exec()) sys.exit(app.exec())

View file

@ -1,4 +1,7 @@
translations = { translations = {
"hello_world": "Hello, World!", "hello_world": "Hello, World!",
"window_title": "Digital Signature Manager", "window_title": "Digital Signature Manager",
"no_installers_found": "Could not update installer list",
"loading_installers": "Loading Installers",
"refresh": "Refresh",
} }

View file

@ -1,4 +1,7 @@
translations = { translations = {
"hello_world": "¡Hola, Mundo!", "hello_world": "¡Hola, Mundo!",
"window_title": "Gestor de Firma Digital", "window_title": "Gestor de Firma Digital",
"no_installers_found": "No se pudo actualizar la lista de instaladores",
"loading_installers": "Cargando instaladores",
"refresh": "Buscar",
} }

68
gfd/sfd.py Normal file
View file

@ -0,0 +1,68 @@
import re
import unicodedata
import requests
from bs4 import BeautifulSoup
SFD_URL = "https://soportefirmadigital.com/sfdj/dl.aspx"
def _normalize_text(text: str) -> str:
"""Internal: remove accents, extra spaces, and lowercase."""
text = unicodedata.normalize("NFKD", text)
text = "".join(c for c in text if not unicodedata.combining(c))
return re.sub(r"\s+", " ", text).strip().lower()
def fetchInstallerOptions(url: str = SFD_URL):
"""
Fetch the list of installer options from the given Soporte Firma Digital page.
Args:
url (str, optional): Page URL to fetch. Defaults to SFD_URL.
Returns:
list[dict]: Each item is {'name': <str>, 'md5': <str or None>}
"""
try:
r = requests.get(url, timeout=8)
r.raise_for_status()
except requests.RequestException:
return []
soup = BeautifulSoup(r.text, "html.parser")
installers = [
opt.text.strip()
for opt in soup.select("#ctl00_certContents_ddlInstaladores option")
]
if not installers:
return []
# Extract valid MD5 hashes from embedded JavaScript
md5s = {}
for name, md5 in re.findall(
r"text\s*==\s*'([^']+)'.*?MD5\s*[:=]\s*([A-Za-z0-9]+)",
r.text,
re.DOTALL,
):
md5 = md5.lower()
if re.fullmatch(r"[0-9a-f]{32}", md5): # only valid 32-char hex hashes
md5s[_normalize_text(name)] = md5
results = []
for inst in installers:
n = _normalize_text(inst)
md5 = md5s.get(n)
if not md5:
for key, val in md5s.items():
if key in n or n in key:
md5 = val
break
results.append({"name": inst, "md5": md5})
return results
if __name__ == "__main__":
for item in fetchInstallerOptions():
print(f"{item['name']:<70} MD5={item['md5'] or 'N/A'}")

View file

@ -1,4 +1,12 @@
beautifulsoup4==4.14.2
certifi==2025.10.5
charset-normalizer==3.4.3
idna==3.10
PySide6==6.9.2 PySide6==6.9.2
PySide6_Addons==6.9.2 PySide6_Addons==6.9.2
PySide6_Essentials==6.9.2 PySide6_Essentials==6.9.2
requests==2.32.5
shiboken6==6.9.2 shiboken6==6.9.2
soupsieve==2.8
typing_extensions==4.15.0
urllib3==2.5.0