From 9a8c6c5c01d34634f4921053e2c45a5128989048 Mon Sep 17 00:00:00 2001 From: tavo Date: Tue, 7 Oct 2025 00:57:44 -0600 Subject: [PATCH] versions --- gfd/__init__.py | 97 +++++++++++++++++++++++++++++++++++++++++++++--- gfd/i18n/en.py | 3 ++ gfd/i18n/es.py | 3 ++ gfd/sfd.py | 68 +++++++++++++++++++++++++++++++++ requirements.txt | 8 ++++ 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 gfd/sfd.py diff --git a/gfd/__init__.py b/gfd/__init__.py index a533607..0e0b578 100644 --- a/gfd/__init__.py +++ b/gfd/__init__.py @@ -1,23 +1,108 @@ from PySide6 import QtWidgets, QtCore import gfd.i18n as i18n +from gfd import sfd + class GFDWidget(QtWidgets.QWidget): def __init__(self, tr): super().__init__() self.tr = tr 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"{name}
MD5: {md5 or 'N/A'}") + + +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"): import sys - app = QtWidgets.QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) tr = i18n.Translator(lang) w = GFDWidget(tr) w.show() - sys.exit(app.exec()) diff --git a/gfd/i18n/en.py b/gfd/i18n/en.py index 3017bc8..9ae74e5 100644 --- a/gfd/i18n/en.py +++ b/gfd/i18n/en.py @@ -1,4 +1,7 @@ translations = { "hello_world": "Hello, World!", "window_title": "Digital Signature Manager", + "no_installers_found": "Could not update installer list", + "loading_installers": "Loading Installers", + "refresh": "Refresh", } diff --git a/gfd/i18n/es.py b/gfd/i18n/es.py index e57ed7a..20d5330 100644 --- a/gfd/i18n/es.py +++ b/gfd/i18n/es.py @@ -1,4 +1,7 @@ translations = { "hello_world": "¡Hola, Mundo!", "window_title": "Gestor de Firma Digital", + "no_installers_found": "No se pudo actualizar la lista de instaladores", + "loading_installers": "Cargando instaladores", + "refresh": "Buscar", } diff --git a/gfd/sfd.py b/gfd/sfd.py new file mode 100644 index 0000000..c082ccb --- /dev/null +++ b/gfd/sfd.py @@ -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': , 'md5': } + """ + 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'}") diff --git a/requirements.txt b/requirements.txt index 91c79fa..87cbcfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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_Addons==6.9.2 PySide6_Essentials==6.9.2 +requests==2.32.5 shiboken6==6.9.2 +soupsieve==2.8 +typing_extensions==4.15.0 +urllib3==2.5.0