versions
This commit is contained in:
parent
3eb2267424
commit
9a8c6c5c01
5 changed files with 173 additions and 6 deletions
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
68
gfd/sfd.py
Normal 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'}")
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue