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
|
||||
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"<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"):
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
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_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
|
||||
|
|
|
|||
Loading…
Reference in a new issue