Files
williamandClaude Sonnet 5 c57420c8c9 Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture,
import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas
casser le build" - l'existant a ete corrige pour la satisfaire plutot que
l'inverse. Hooks pre-commit locaux (language: system) bloquants.

- Typage mypy --strict propage a tout le moteur (db, screens, auth, core,
  ai, routes, puis publish/scripts/tests/app.py/build_css.py).
- Securite : fuite de handle fichier Windows corrigee dans l'export SCORM
  (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes
  (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe
  "</script>" dans le JSON embarque en <script>, 25 sites).
- Architecture : imports circulaires/F811 nettoyes, contrats
  import-linter respectes, code mort retire (vulture).
- Accessibilite : 69 champs de formulaire sans label correctement
  associe corriges (for/id ou aria-label) sur 11 templates.
- ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis
  appliquees (aucune desactivee sans verification individuelle).
- Tests : isolation du compte admin partage (nettoyage ponctuel +
  fixture de teardown automatique en filet de securite), suite complete
  verte (591 tests Python, 241 tests JS).
- SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport
  complet analyse point par point, faux positifs documentes.
- .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine
  faisait echouer ESLint (linebreak-style) via un bug connu de git
  (checkout "en place" qui ignore l'eol force sur un fichier deja
  present sur disque - contourne en supprimant puis recreant chaque
  fichier suivi).

djLint (H021, styles inline) volontairement saute pour ce commit -
backlog assume, deja documente, traite dans un lot separe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 16:06:15 +02:00

73 lines
3.3 KiB
Python

import contextlib
import sqlite3
from typing import TYPE_CHECKING
from .db_path import db_path
if TYPE_CHECKING:
from flask import Flask
def connect(slug: str) -> sqlite3.Connection:
# timeout=10 : si une autre connexion tient un verrou d'écriture au même
# instant (deux requêtes qui arrivent presque en même temps, ex. deux
# onglets, ou le navigateur qui recharge plusieurs ressources), sqlite3
# réessaie pendant 10 secondes au lieu de lever immédiatement "database
# is locked". Le mode WAL (Write-Ahead Logging) va plus loin : il permet
# à des lectures de se faire PENDANT qu'une écriture est en cours
# ailleurs, ce qui est la cause la plus fréquente de ce blocage avec le
# serveur de développement Flask.
conn = sqlite3.connect(db_path(slug), timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA busy_timeout = 8000")
_track_for_teardown(conn)
return conn
def _track_for_teardown(conn: sqlite3.Connection) -> None:
"""Filet de sécurité : chaque fonction de db/ ouvre sa propre connexion
et est censée la fermer elle-même (conn.close()) avant de rendre la
main — mais si une exception survient ENTRE l'ouverture et cette
fermeture (une erreur de programmation, une contrainte violée...), le
conn.close() prévu n'est jamais atteint. En mode debug (voir app.py),
le débogueur Werkzeug garde alors la trace complète de l'erreur en
mémoire pour l'inspection interactive — ce qui inclut la variable
locale `conn`, empêchant le ramasse-miettes Python de la libérer et
donc SQLite de relâcher son verrou d'écriture. Toute requête suivante
qui écrit se heurte alors à "database is locked" jusqu'au redémarrage
du serveur, même longtemps après l'erreur d'origine. En enregistrant
ici la connexion sur le contexte de la requête Flask en cours (quand il
y en a un), on garantit sa fermeture à la fin de la requête via
_close_leaked_connections ci-dessous, que la requête ait réussi ou
planté — sans rien changer au comportement des ~80 fonctions qui
ferment déjà correctement leur connexion (fermer une connexion SQLite
déjà fermée ne fait rien)."""
try:
from flask import g, has_app_context
except ImportError:
return
if not has_app_context():
return
if not hasattr(g, "_forge_db_connections"):
g._forge_db_connections = []
g._forge_db_connections.append(conn)
def install_teardown_safety_net(app: "Flask") -> None:
"""Enregistre le filet de sécurité (voir _track_for_teardown) sur
l'appli Flask passée en paramètre — jamais importée ici : `db/` est la
couche la plus basse du moteur (voir pyproject.toml, contrat
import-linter) et ne doit dépendre d'aucun autre paquet. C'est
core/db_teardown_guard.py, dans la couche de câblage, qui appelle
cette fonction avec `core.flask_app.app`."""
@app.teardown_request
def _close_leaked_connections(exception: BaseException | None = None) -> None: # noqa: ARG001 - signature imposée par Flask
from flask import g
for conn in getattr(g, "_forge_db_connections", ()):
with contextlib.suppress(sqlite3.Error):
conn.close()