This commit is contained in:
tavo 2025-10-08 20:31:25 -06:00
parent b0eea9d62e
commit b44bcd4810
12 changed files with 44 additions and 606 deletions

View file

@ -1 +0,0 @@
from .app import run

View file

@ -1,13 +0,0 @@
from PySide6 import QtWidgets
from gfd.ui.main_window import GFDWidget
from gfd.i18n import Translator
def run(lang="es"):
import sys
app = QtWidgets.QApplication(sys.argv)
tr = Translator(lang)
window = GFDWidget(tr)
window.show()
sys.exit(app.exec())

View file

@ -1,98 +0,0 @@
import time
import re
import unicodedata
from gfd.core import sfd
SUPPORTED_INSTALLERS = [
{
"os_type": "ubuntu24",
"name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
"md5": "bdc871e15f2096f930b285f0ed799aa0",
},
{
"os_type": "debian",
"name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
"md5": "bdc871e15f2096f930b285f0ed799aa0",
},
]
def _normalize(t):
return re.sub(r"\s+", " ", unicodedata.normalize("NFKD", t).lower()).strip()
def _fetch(max_attempts=3, delay=2):
for i in range(max_attempts):
data = sfd.fetchInstallerOptions()
if data:
return data
if i < max_attempts - 1:
time.sleep(delay)
return []
def get_available_installers(os_type, installers=None):
"""
Return a list of confirmed available installers for the given OS type.
If an installer is listed, it means that for a given OS type:
- There is a supported installation routine.
- There is a downloadable archive from Soporte Firma Digital.
"""
src = installers or SUPPORTED_INSTALLERS
locals_ = [i for i in src if i["os_type"] == os_type]
if not locals_:
return []
remote = _fetch()
if not remote:
return []
rmap = {_normalize(r["name"]): r.get("md5") for r in remote}
confirmed = []
for local in locals_:
lname, lmd5 = (
_normalize(local["name"]),
(local.get("md5") or "").lower() or None,
)
rmd5 = rmap.get(lname)
if (lmd5 == rmd5) or (lmd5 is None and rmd5 is None):
confirmed.append((local["name"], lmd5))
return confirmed
# TODO: Routine to check for installed version
def get_installed_version():
"""
Return the current installed version data, or empty if not installed.
"""
return [] # Return always empty for now
if __name__ == "__main__":
os_type = "debian"
print(f"=== TEST: {os_type.upper()} Installer Validation ===")
confirmed = get_available_installers(os_type)
print(
f"{len(confirmed)} confirmed installer(s):"
if confirmed
else "No confirmed installers found."
)
for n, m in confirmed:
print(f" - {n} (MD5={m or 'N/A'})")
installed = get_installed_version()
if not installed:
print("Status: NOT INSTALLED")
else:
print(f"Installed: {installed[0]} MD5={installed[1]}")

View file

@ -1,74 +0,0 @@
import platform
import re
from pathlib import Path
def _read_os_release():
"""Read and parse /etc/os-release or /usr/lib/os-release if available."""
paths = [Path("/etc/os-release"), Path("/usr/lib/os-release")]
data = {}
for path in paths:
if path.exists():
try:
for line in path.read_text(encoding="utf-8").splitlines():
if "=" in line:
key, val = line.split("=", 1)
data[key.strip()] = val.strip().strip('"')
break
except Exception:
pass
return data
def get_os_type():
"""
Detect and return a simple OS type string.
Returns one of:
'macos', 'windows', 'ubuntu24', 'ubuntu22', 'ubuntu20',
'debian', 'arch', 'rpm', or None if unsupported.
"""
system = platform.system().lower()
os_type = None
if system == "darwin":
os_type = "macos"
elif system == "windows":
os_type = "windows"
elif system == "linux":
info = _read_os_release()
id_name = info.get("ID", "").lower()
version_id = info.get("VERSION_ID", "")
# Ubuntu
if id_name == "ubuntu":
if version_id.startswith("24"):
os_type = "ubuntu24"
elif version_id.startswith("22"):
os_type = "ubuntu22"
elif version_id.startswith("20"):
os_type = "ubuntu20"
# Debian (11 or newer supported)
elif id_name == "debian":
try:
version_num = int(re.findall(r"\d+", version_id or "0")[0])
except IndexError:
version_num = 0
if version_num >= 11:
os_type = "debian"
# Arch / RPM-based
elif "arch" in id_name:
os_type = "arch"
elif id_name in ["fedora", "rhel", "centos", "rocky", "alma", "opensuse"]:
os_type = "rpm"
return os_type
if __name__ == "__main__":
print("Detected OS type:", get_os_type() or "NOT SUPPORTED")

View file

@ -1,52 +0,0 @@
"""
routines.py - OS-specific installer routines
Defines a registry of install routines by OS type.
If the current OS is supported and has an associated installer,
the matching routine can be executed to perform installation steps.
"""
from gfd.core.osinfo import get_os_type
def install_ubuntu24():
"""Simulated install routine for Ubuntu 24.04."""
print("Running Ubuntu 24.04 (DEB) installation routine...")
def install_debian():
"""Simulated install routine for Debian."""
print("Running Debian installation routine (using Ubuntu 24 package)...")
SUPPORTED_ROUTINES = {
"ubuntu24": install_ubuntu24,
"debian": install_debian,
}
def run_install_routine(os_type=None):
"""
Run the appropriate install routine for the given OS type.
Args:
os_type (str | None): OS key (e.g. 'ubuntu24', 'macos').
If None, automatically detects current OS.
Returns:
bool: True if the OS is supported and a routine was executed, else False.
"""
os_type = os_type or get_os_type()
if not os_type:
return False
routine = SUPPORTED_ROUTINES.get(os_type)
if not routine:
return False
routine()
return True
if __name__ == "__main__":
run_install_routine()

View file

@ -1,116 +0,0 @@
"""
sfd.py - Installer List Parser for Soporte Firma Digital
This module is responsible for fetching and parsing the list of available
digital signature installers from the official Soporte Firma Digital website.
It retrieves both the visible installer names (from the pages <select> element)
and the associated MD5 hashes (embedded within inline JavaScript).
The parsed results are used by the UI layer to display which installers
are available and to match system OS types to the appropriate installer.
"""
import re
import unicodedata
import requests
from bs4 import BeautifulSoup
SFD_URL = "https://soportefirmadigital.com/sfdj/dl.aspx"
def _normalize_text(text: str) -> str:
"""
Normalize text for consistent matching.
Steps:
1. Remove accents and diacritics using Unicode normalization.
2. Replace consecutive spaces with a single space.
3. Convert to lowercase and strip leading/trailing whitespace.
Args:
text (str): Raw string to normalize.
Returns:
str: Cleaned, normalized string suitable for fuzzy matching.
"""
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 and parse the installer list from the Soporte Firma Digital website.
Example output:
[
{"name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
"md5": "bdc871e15f2096f930b285f0ed799aa0"},
{"name": "Usuarios Windows JCOP3 - 189 MB",
"md5": "596c347e409d00388c3c1ee0a72b96c0"},
...
]
Args:
url (str, optional): Target URL to fetch. Defaults to SFD_URL.
Returns:
list[dict]: Each dict contains:
- "name" (str): Installer display name.
- "md5" (str | None): MD5 checksum (if found), else None.
"""
try:
r = requests.get(url, timeout=8)
r.raise_for_status()
except requests.RequestException:
# Network error, timeout, or unreachable server
return []
soup = BeautifulSoup(r.text, "html.parser")
# Extract installer names from the dropdown list
installers = [
opt.text.strip()
for opt in soup.select("#ctl00_certContents_ddlInstaladores option")
]
if not installers:
return []
# Parse embedded JavaScript to extract name, MD5 mapping
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-character MD5
md5s[_normalize_text(name)] = md5
# Merge name list with MD5 map
results = []
for inst in installers:
n = _normalize_text(inst)
md5 = md5s.get(n)
# Try fuzzy match if exact normalization key not found
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__":
"""
Fetch and print all installers to stdout.
"""
installers = fetchInstallerOptions()
print(f"Fetched {len(installers)} installers from {SFD_URL}\n")
for item in installers:
print(f"{item['name']:<70} MD5={item['md5'] or 'N/A'}")

View file

@ -1,11 +1,3 @@
translations = {
"window_title": "Digital Signature Manager",
"no_install_message": "Digital signature not installed",
"recommended_version": "Recommended version:",
"install": "Install",
"installed_version": "Installed version:",
"latest_version_installed": "Latest version installed",
"update_digital_signature": "Update Digital Signature",
"no_installers_found": "Could not fetch the installer list",
"loading_installers": "Loading installers",
}

View file

@ -1,11 +1,3 @@
translations = {
"window_title": "Gestor de Firma Digital",
"no_install_message": "No tiene la firma digital instalada",
"recommended_version": "Versión recomendada:",
"install": "Instalar",
"installed_version": "Versión instalada:",
"latest_version_installed": "Última versión instalada",
"update_digital_signature": "Actualizar Firma Digital",
"no_installers_found": "No se pudo obtener la lista de instaladores", # ES
"loading_installers": "Cargando instaladores",
}

44
gfd/sfd.py Normal file
View file

@ -0,0 +1,44 @@
import requests as req
from bs4 import BeautifulSoup as bs
from typing import cast
SFD_INSTALLERS_URL = "https://soportefirmadigital.com/sfdj/dl.aspx"
SFD_INSTALLERS_SELECT_ID = "#ctl00_certContents_ddlInstaladores"
def get_installers(
url: str = SFD_INSTALLERS_URL,
select: str = SFD_INSTALLERS_SELECT_ID,
timeout: int = 10,
) -> dict[str, str]:
try:
r = req.get(url, timeout=timeout)
r.raise_for_status()
except req.exceptions.RequestException as e:
raise RuntimeError(f"Error fetching installer list: {e}")
html = bs(r.text, "html.parser")
opts = html.select(f"{select} option")
if not opts:
raise RuntimeError("Empty options")
return {
cast(str, key): value
for opt in opts
if (key := opt.get("value")) and (value := opt.text.strip())
}
if __name__ == "__main__":
import json
try:
data = get_installers()
if data:
print(json.dumps(data, indent=2, ensure_ascii=False), flush=True)
except RuntimeError as e:
raise SystemExit(e)

View file

@ -1,222 +0,0 @@
from PySide6 import QtWidgets, QtCore
from gfd.core.osinfo import get_os_type
from gfd.core.installers import get_available_installers, get_installed_version
def check_installation_status():
"""
Determine system support and installer availability.
Returns:
(installed: [version, md5] or [],
available: list[[version, md5]],
recommended: [version, md5] or None)
"""
os_type = get_os_type()
if not os_type:
return [], [], None
available = get_available_installers(os_type)
installed = get_installed_version()
if not available:
return installed, [], None
# Assume the first installer in the list is the recommended one
recommended = available[0]
return installed, available, recommended
class GFDWidget(QtWidgets.QWidget):
def __init__(self, tr):
super().__init__()
self.tr = tr
self.setWindowTitle(self.tr("window_title"))
self.resize(600, 400)
self.main_layout = QtWidgets.QVBoxLayout(self)
self.main_layout.setContentsMargins(20, 20, 20, 20)
# Card container
self.card = QtWidgets.QFrame()
self.card_layout = QtWidgets.QVBoxLayout(self.card)
self.card_layout.setSpacing(12)
# Title
self.title = QtWidgets.QLabel(self.tr("window_title"))
self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.title.setStyleSheet("font-weight: bold; font-size: 16px;")
self.card_layout.addWidget(self.title)
# Content area
self.content_area = QtWidgets.QWidget()
self.content_layout = QtWidgets.QVBoxLayout(self.content_area)
self.content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.card_layout.addWidget(self.content_area)
# Footer
self.footer = QtWidgets.QHBoxLayout()
self.footer.addStretch()
self.refresh_btn = QtWidgets.QPushButton("")
self.refresh_btn.setFixedWidth(36)
self.refresh_btn.clicked.connect(self.refresh_state)
self.footer.addWidget(self.refresh_btn)
self.card_layout.addLayout(self.footer)
self.main_layout.addWidget(self.card)
self.setStyleSheet("""
QPushButton {
border-radius: 8px;
padding: 6px 12px;
font-weight: bold;
}
QPushButton[primary="true"] {
background-color: #2f80ed;
color: white;
}
QPushButton[primary="true"]:disabled {
background-color: #a0c4ff;
}
QLabel, QComboBox {
font-size: 16px;
}
""")
self.states = ["not_installed", "installed", "update_available"]
self.current_state = 0
# Run initial refresh
self.refresh_state()
# --- LOGIC ---
def refresh_state(self):
"""Fetch installer list and determine status."""
self.refresh_btn.setEnabled(False)
self.clear_content()
loading_label = QtWidgets.QLabel(self.tr("loading_installers") + "")
loading_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.content_layout.addWidget(loading_label)
QtCore.QTimer.singleShot(150, self._refresh_check)
def _refresh_check(self):
"""Evaluate OS type, installed version, and available installers."""
self.installed, self.available, self.recommended = check_installation_status()
print("\n=== INSTALLER STATUS DEBUG ===")
if not self.available:
print("No available installers for this OS.")
else:
for i, (name, md5) in enumerate(self.available, start=1):
print(f"{i:2d}. {name:<70} MD5={md5 or 'N/A'}")
print("==============================\n")
# 🧩 Print simulated installed state
if not self.installed:
print("→ Status: NOT INSTALLED")
else:
print(f"→ Installed: {self.installed[0]} MD5={self.installed[1]}")
if not self.available:
self.show_error()
elif not self.recommended:
print("→ Decision: OS NOT SUPPORTED")
self.show_error()
elif not self.installed:
print(f"→ Decision: NOT INSTALLED (Recommended: {self.recommended[0]})")
self.set_state("not_installed")
else:
installed_tuple = tuple(self.installed)
available_tuples = [tuple(a) for a in self.available]
if installed_tuple in available_tuples:
print("→ Decision: INSTALLED (latest version matches)")
self.set_state("installed")
else:
print("→ Decision: UPDATE AVAILABLE (installed not in available list)")
self.set_state("update_available")
self.refresh_btn.setEnabled(True)
def clear_content(self):
"""Remove all widgets from content area."""
for i in reversed(range(self.content_layout.count())):
widget = self.content_layout.itemAt(i).widget()
if widget:
widget.setParent(None)
# --- UI STATES ---
def update_state(self):
"""Render the current UI state."""
self.clear_content()
state = self.states[self.current_state]
if state == "not_installed":
self.show_not_installed()
elif state == "installed":
self.show_installed()
elif state == "update_available":
self.show_update_available()
def show_error(self):
"""Shown when no installer data is available."""
self.clear_content()
lbl = QtWidgets.QLabel(self.tr("no_installers_found"))
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.content_layout.addWidget(lbl)
def show_not_installed(self):
recommended_name = self.recommended[0] if self.recommended else "?"
lbl = QtWidgets.QLabel(
f"{self.tr('no_install_message')}<br>"
f"{self.tr('recommended_version')}<br><b>{recommended_name}</b>"
)
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.content_layout.addWidget(lbl)
btn = QtWidgets.QPushButton(self.tr("install"))
btn.setProperty("primary", True)
self.content_layout.addWidget(btn)
combo = QtWidgets.QComboBox()
for version, md5 in self.available:
combo.addItem(f"{version} (MD5: {md5 or 'N/A'})")
self.content_layout.addWidget(combo)
def show_installed(self):
version, md5 = self.installed
lbl = QtWidgets.QLabel(
f"{self.tr('installed_version')}<br><b>{version}</b><br>MD5: {md5 or 'N/A'}"
)
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.content_layout.addWidget(lbl)
btn = QtWidgets.QPushButton(self.tr("latest_version_installed"))
btn.setEnabled(False)
self.content_layout.addWidget(btn)
def show_update_available(self):
version, md5 = self.installed
lbl = QtWidgets.QLabel(
f"{self.tr('installed_version')}<br><b>{version}</b><br>MD5: {md5 or 'N/A'}"
)
lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.content_layout.addWidget(lbl)
btn = QtWidgets.QPushButton(self.tr("update_digital_signature"))
btn.setProperty("primary", True)
self.content_layout.addWidget(btn)
combo = QtWidgets.QComboBox()
for version, md5 in self.available:
combo.addItem(f"{version} (MD5: {md5 or 'N/A'})")
self.content_layout.addWidget(combo)
def set_state(self, state):
if state in self.states:
self.current_state = self.states.index(state)
self.update_state()

View file

@ -1,4 +0,0 @@
import gfd
if __name__ == "__main__":
gfd.run("es")

View file

@ -1,12 +1,2 @@
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