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>
This commit is contained in:
william
2026-09-15 16:06:15 +02:00
co-authored by Claude Sonnet 5
parent 7db4803b93
commit c57420c8c9
454 changed files with 16448 additions and 6967 deletions
+77 -60
View File
@@ -15,15 +15,25 @@
//
// Best-effort partout : un LRS injoignable/mal configuré ne doit JAMAIS
// bloquer ni ralentir la partie du joueur.
//
// NOSONAR S5332 (toutes occurrences "http://adlnet.gov/..." de ce fichier
// + les 2 assertions de __tests__/xapi-client.test.js) : identifiants du
// vocabulaire xAPI standard ADL, jamais dérérencés/appelés en réseau (de
// simples chaînes comparées/embarquées) — le "http://" fait partie de la
// chaîne fixée par la spec, jamais "https://" (changer casserait
// l'interopérabilité avec n'importe quel LRS). Le VRAI endpoint réseau
// (voir forgeXapiSendStatement plus bas) est toujours `config.endpoint`,
// saisi par le créateur (routes/publish/xapi_settings.py), jamais un
// littéral de ce fichier.
// Statut Forge -> verbe xAPI ADL standard, uniquement pour les statuts
// qui marquent une VRAIE fin de partie — "non_commence"/"en_cours" ne
// génèrent rien ici (bruit inutile pour un suivi volontairement léger,
// voir le plan).
var FORGE_TO_XAPI_VERB = {
termine: { id: "http://adlnet.gov/expapi/verbs/completed" },
reussi: { id: "http://adlnet.gov/expapi/verbs/passed" },
echoue: { id: "http://adlnet.gov/expapi/verbs/failed" },
const FORGE_TO_XAPI_VERB = {
termine: { id: 'http://adlnet.gov/expapi/verbs/completed' }, // NOSONAR S5332
reussi: { id: 'http://adlnet.gov/expapi/verbs/passed' }, // NOSONAR S5332
echoue: { id: 'http://adlnet.gov/expapi/verbs/failed' }, // NOSONAR S5332
};
// Libellé humain par verbe — l'id xAPI (http://adlnet.gov/expapi/verbs/...)
@@ -33,18 +43,18 @@ var FORGE_TO_XAPI_VERB = {
// français ?" — un LRS qui préfère le français (ou n'importe quelle
// langue ici) affichera la bonne entrée au lieu de rester bloqué sur
// l'anglais.
var FORGE_XAPI_VERB_DISPLAY = {
"http://adlnet.gov/expapi/verbs/initialized": { "en-US": "initialized", "fr-FR": "initialisé" },
"http://adlnet.gov/expapi/verbs/terminated": { "en-US": "terminated", "fr-FR": "quitté" },
"http://adlnet.gov/expapi/verbs/completed": { "en-US": "completed", "fr-FR": "terminé" },
"http://adlnet.gov/expapi/verbs/passed": { "en-US": "passed", "fr-FR": "réussi" },
"http://adlnet.gov/expapi/verbs/failed": { "en-US": "failed", "fr-FR": "échoué" },
"http://adlnet.gov/expapi/verbs/scored": { "en-US": "scored", "fr-FR": "noté" },
"http://adlnet.gov/expapi/verbs/attempted": { "en-US": "attempted", "fr-FR": "commencé" },
"http://adlnet.gov/expapi/verbs/answered": { "en-US": "answered", "fr-FR": "répondu" },
const FORGE_XAPI_VERB_DISPLAY = {
'http://adlnet.gov/expapi/verbs/initialized': { 'en-US': 'initialized', 'fr-FR': 'initialisé' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/terminated': { 'en-US': 'terminated', 'fr-FR': 'quitté' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/completed': { 'en-US': 'completed', 'fr-FR': 'terminé' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/passed': { 'en-US': 'passed', 'fr-FR': 'réussi' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/failed': { 'en-US': 'failed', 'fr-FR': 'échoué' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/scored': { 'en-US': 'scored', 'fr-FR': 'noté' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/attempted': { 'en-US': 'attempted', 'fr-FR': 'commencé' }, // NOSONAR S5332
'http://adlnet.gov/expapi/verbs/answered': { 'en-US': 'answered', 'fr-FR': 'répondu' }, // NOSONAR S5332
};
var forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si le statut ne change pas réellement
let forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si le statut ne change pas réellement
// Identité de l'apprenant : réutilise l'API SCORM déjà trouvée par
// scorm-api.js (window.forgeScormApi, posée sur "load" — voir plus bas,
@@ -52,23 +62,23 @@ var forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si
// si le paquet tourne dans un LMS ; sinon, un acteur anonyme générique.
function forgeXapiActor() {
try {
if (typeof forgeScormApi !== "undefined" && forgeScormApi) {
var name = forgeScormApi.LMSGetValue("cmi.core.student_name");
var id = forgeScormApi.LMSGetValue("cmi.core.student_id");
if (typeof forgeScormApi !== 'undefined' && forgeScormApi) {
const name = forgeScormApi.LMSGetValue('cmi.core.student_name');
const id = forgeScormApi.LMSGetValue('cmi.core.student_id');
if (id) {
return { objectType: "Agent", name: name || id, account: { homePage: "urn:forge-engine", name: id } };
return { objectType: 'Agent', name: name || id, account: { homePage: 'urn:forge-engine', name: id } };
}
}
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ }
return { objectType: "Agent", name: "Apprenant", mbox: "mailto:anonymous@forge-engine.local" };
return { objectType: 'Agent', name: 'Apprenant', mbox: 'mailto:anonymous@forge-engine.local' };
}
function forgeXapiObject() {
var config = window.FORGE_XAPI_CONFIG;
const config = window.FORGE_XAPI_CONFIG;
return {
objectType: "Activity",
objectType: 'Activity',
id: config.activity_id,
definition: { name: { "fr-FR": config.activity_name } },
definition: { name: { 'fr-FR': config.activity_name } },
};
}
@@ -80,24 +90,24 @@ function forgeXapiObject() {
// de scène qui a déclenché ce dialogue (voir "ℹ️ Informations") — plus de
// "quête" pour le porter.
function forgeXapiDialogueObject(dialogueId, objectName) {
var config = window.FORGE_XAPI_CONFIG;
const config = window.FORGE_XAPI_CONFIG;
return {
objectType: "Activity",
id: config.activity_id + ":dialogue:" + dialogueId,
definition: { name: { "fr-FR": objectName || "Dialogue" }, type: "http://adlnet.gov/expapi/activities/simulation" },
objectType: 'Activity',
id: `${config.activity_id}:dialogue:${dialogueId}`,
definition: { name: { 'fr-FR': objectName || 'Dialogue' }, type: 'http://adlnet.gov/expapi/activities/simulation' }, // NOSONAR S5332
};
}
function forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText) {
var config = window.FORGE_XAPI_CONFIG;
const config = window.FORGE_XAPI_CONFIG;
return {
objectType: "Activity",
id: config.activity_id + ":dialogue:" + dialogueId + ":question:" + questionIndex,
objectType: 'Activity',
id: `${config.activity_id}:dialogue:${dialogueId}:question:${questionIndex}`,
definition: {
name: { "fr-FR": "Question " + (questionIndex + 1) + " — " + (objectName || "Dialogue") },
description: { "fr-FR": questionText || "" },
type: "http://adlnet.gov/expapi/activities/cmi.interaction",
interactionType: "choice",
name: { 'fr-FR': `Question ${questionIndex + 1} — ${objectName || 'Dialogue'}` },
description: { 'fr-FR': questionText || '' },
type: 'http://adlnet.gov/expapi/activities/cmi.interaction', // NOSONAR S5332
interactionType: 'choice',
},
};
}
@@ -110,27 +120,27 @@ function forgeXapiQuestionObject(dialogueId, objectName, questionIndex, question
// (forgeXapiObject), ou une sous-activité quête/question
// (forgeXapiQuestObject/forgeXapiQuestionObject) pour un suivi plus fin.
function forgeXapiSendStatement(verbId, result, object) {
var config = window.FORGE_XAPI_CONFIG;
const config = window.FORGE_XAPI_CONFIG;
if (!config || !config.endpoint) return;
var statement = {
const statement = {
actor: forgeXapiActor(),
verb: { id: verbId, display: FORGE_XAPI_VERB_DISPLAY[verbId] || { "en-US": verbId } },
verb: { id: verbId, display: FORGE_XAPI_VERB_DISPLAY[verbId] || { 'en-US': verbId } },
object: object || forgeXapiObject(),
timestamp: new Date().toISOString(),
};
if (result) statement.result = result;
var endpoint = config.endpoint.replace(/\/+$/, "") + "/statements";
const endpoint = `${config.endpoint.replace(/\/+$/, '')}/statements`;
try {
fetch(endpoint, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
"X-Experience-API-Version": "1.0.3",
"Authorization": "Basic " + btoa(config.login + ":" + config.password),
'Content-Type': 'application/json',
'X-Experience-API-Version': '1.0.3',
Authorization: `Basic ${btoa(`${config.login}:${config.password}`)}`,
},
body: JSON.stringify(statement),
}).catch(function (err) {
console.warn("xAPI: envoi du statement échoué (LRS injoignable ?)", err);
}).catch((err) => {
console.warn('xAPI: envoi du statement échoué (LRS injoignable ?)', err);
});
} catch (err) {
console.warn("xAPI: impossible d'envoyer le statement", err);
@@ -143,8 +153,8 @@ function forgeXapiSendStatement(verbId, result, object) {
function forgeXapiNotifyScoreChanged(newScore) {
if (!window.FORGE_XAPI_CONFIG) return;
forgeXapiSendStatement(
"http://adlnet.gov/expapi/verbs/scored",
{ score: { raw: newScore } }
'http://adlnet.gov/expapi/verbs/scored', // NOSONAR S5332
{ score: { raw: newScore } },
);
}
@@ -156,44 +166,51 @@ function forgeXapiNotifyScoreChanged(newScore) {
function forgeXapiNotifyDialogueCompleted(dialogueId, objectName) {
if (!window.FORGE_XAPI_CONFIG) return;
forgeXapiSendStatement(
"http://adlnet.gov/expapi/verbs/completed",
{ completion: true }, forgeXapiDialogueObject(dialogueId, objectName)
'http://adlnet.gov/expapi/verbs/completed', // NOSONAR S5332
{ completion: true },
forgeXapiDialogueObject(dialogueId, objectName),
);
}
function forgeXapiNotifyQuestionAnswered(dialogueId, objectName, questionIndex, questionText, correct, scoreAwarded) {
if (!window.FORGE_XAPI_CONFIG) return;
forgeXapiSendStatement(
"http://adlnet.gov/expapi/verbs/answered",
'http://adlnet.gov/expapi/verbs/answered', // NOSONAR S5332
{ success: correct, score: { raw: scoreAwarded || 0 } },
forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText)
forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText),
);
}
function forgeXapiNotifyStatusChanged(newStatus) {
if (!window.FORGE_XAPI_CONFIG) return;
var verb = FORGE_TO_XAPI_VERB[newStatus];
const verb = FORGE_TO_XAPI_VERB[newStatus];
if (!verb || newStatus === forgeXapiLastStatusSent) return;
forgeXapiLastStatusSent = newStatus;
forgeXapiSendStatement(verb.id, {
completion: true,
success: newStatus !== "echoue",
success: newStatus !== 'echoue',
});
}
if (typeof window !== "undefined") {
window.addEventListener("load", function () {
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/initialized");
if (typeof window !== 'undefined') {
window.addEventListener('load', () => {
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement('http://adlnet.gov/expapi/verbs/initialized'); // NOSONAR S5332
});
window.addEventListener("beforeunload", function () {
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/terminated");
window.addEventListener('beforeunload', () => {
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement('http://adlnet.gov/expapi/verbs/terminated'); // NOSONAR S5332
});
}
if (typeof module !== "undefined" && module.exports) {
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
forgeXapiSendStatement, forgeXapiNotifyScoreChanged, forgeXapiNotifyStatusChanged, FORGE_TO_XAPI_VERB,
forgeXapiNotifyDialogueCompleted, forgeXapiNotifyQuestionAnswered,
forgeXapiDialogueObject, forgeXapiQuestionObject, FORGE_XAPI_VERB_DISPLAY,
forgeXapiSendStatement,
forgeXapiNotifyScoreChanged,
forgeXapiNotifyStatusChanged,
FORGE_TO_XAPI_VERB,
forgeXapiNotifyDialogueCompleted,
forgeXapiNotifyQuestionAnswered,
forgeXapiDialogueObject,
forgeXapiQuestionObject,
FORGE_XAPI_VERB_DISPLAY,
};
}