Skip to Content

Chapitre 4.1 — Trouver l’API

⏱️ TL;DR — Le LMS ne t’envoie pas l’API : il la pose dans une fenêtre ancêtre de ton SCO, et c’est à ton contenu d’aller la chercher. L’algorithme, identique depuis 2001, s’appelle « find the API » : on remonte la chaîne des window.parent, puis, si rien, on tente window.opener et ses parents, jusqu’à trouver un objet nommé API (SCORM 1.2) ou API_1484_11 (SCORM 2004). Tant que ce scan n’a pas abouti, aucun appel n’est possible et ton module est aveugle. Les trois pièges qui font échouer le scan : le cross-domain (SCO hébergé sur un autre domaine que le LMS), une imbrication d’iframes inattendue, et une pop-up bloquée. Ce chapitre te donne une fonction findAPI() réelle, commentée et défensive.

🎯 Objectifs

  • Comprendre pourquoi l’API se trouve dans une fenêtre ancêtre, et pas dans ton document.
  • Écrire un findAPI() qui remonte window.parent puis window.opener, avec garde-fou anti-boucle et try/catch cross-domain.
  • Chercher le bon nom d’objet selon la version : API en 1.2, API_1484_11 en 2004.
  • Diagnostiquer les trois échecs classiques : cross-domain, iframe imbriquée, pop-up bloquée.

Le LMS pose l’API dans un ancêtre

Quand un LMS lance un SCO, il ne charge pas ta page « à nu ». Il la place dans un conteneur : le plus souvent une iframe à l’intérieur de sa propre page de lecture (le player), parfois une fenêtre pop-up ouverte depuis cette page. Puis il attache un objet JavaScript à cette fenêtre conteneur — un objet qui expose les méthodes du runtime.

Ton SCO, lui, tourne dans l’iframe. Pour dialoguer, il doit remonter jusqu’à la fenêtre qui porte l’objet. La spec définit précisément où chercher et dans quel ordre : c’est l’algorithme find the API, que tout contenu SCORM embarque (les outils auteurs — Storyline, Rise, Captivate — le génèrent pour toi ; mais dès que tu codes un SCO à la main, tu l’écris).

window (ton SCO, dans l'iframe) └─ window.parent ──► la page "player" du LMS ← l'objet API est souvent ICI └─ window.parent ──► éventuel cadre de plus haut niveau

L’objet porte un nom imposé par la spec, et ce nom change avec la version :

Version SCORMNom de l’objet à chercherMéthode d’ouverture
SCORM 1.2APIAPI.LMSInitialize("")
SCORM 2004API_1484_11API_1484_11.Initialize("")

⚠️ Piège — Chercher window.API dans ta propre fenêtre et conclure « l’API n’existe pas » quand window.API est undefined. Elle n’est presque jamais dans ta fenêtre : elle est chez un ancêtre. Ne teste jamais uniquement window.API ; remonte la chaîne.

L’algorithme « find the API »

La logique tient en deux temps :

  1. Remonter les parents : tant qu’on n’a pas trouvé l’objet et qu’il reste un parent au-dessus, on grimpe d’un cran (win = win.parent).
  2. Basculer sur l’opener : si la remontée des parents n’a rien donné et que la fenêtre a été ouverte par une autre (window.opener, cas de la pop-up), on relance la même remontée depuis l’opener.

Voici une implémentation réelle, défensive, qui gère les deux versions et les erreurs cross-domain :

// findAPI.js — recherche de l'objet API fourni par le LMS (SCORM 1.2 & 2004) // Nom de l'objet selon la version. On tente 2004 d'abord si on cible 2004, // sinon 1.2. Ici on rend la fonction paramétrable. function scanUp(startWin, apiName) { let win = startWin; let attempts = 0; // On remonte la chaîne des window.parent. // Conditions d'arrêt : on a trouvé l'objet, OU il n'y a plus de parent // (une fenêtre racine a window.parent === elle-même). while (win) { try { // L'accès à win[apiName] peut LEVER une exception si la fenêtre // appartient à un autre domaine (Same-Origin Policy). D'où le try/catch. if (win[apiName]) { return win[apiName]; // trouvé } } catch (e) { // Cross-domain : on ne peut pas inspecter cette fenêtre. On s'arrête // proprement pour cette branche plutôt que de laisser planter le SCO. return null; } // Garde-fou : jamais plus de N crans, pour ne pas boucler à l'infini // sur des conteneurs mal formés. if (attempts++ > 50) return null; // A-t-on atteint le sommet ? (racine : parent === soi-même) if (win.parent == null || win.parent === win) break; win = win.parent; } return null; } // Recherche complète : parents d'abord, puis l'opener et SES parents. function findAPIObject(apiName) { // 1) Chercher en remontant depuis la fenêtre courante. let api = scanUp(window, apiName); // 2) Rien ? Tenter la fenêtre qui nous a ouverts (cas pop-up), // et remonter ses parents à elle aussi. if (!api && window.opener) { try { api = scanUp(window.opener, apiName); } catch (e) { api = null; // opener cross-domain } } return api; // objet API, ou null si introuvable } // Point d'entrée : on essaie 2004, puis on retombe sur 1.2. // (Adapte l'ordre selon ce que ton contenu cible.) function getScormAPI() { const api2004 = findAPIObject("API_1484_11"); if (api2004) return { api: api2004, version: "2004" }; const api12 = findAPIObject("API"); if (api12) return { api: api12, version: "1.2" }; return { api: null, version: null }; // aucun LMS détecté }

Quelques points de cette implémentation qui font la différence entre un findAPI() de tutoriel et un findAPI() de production :

  • Le try/catch autour de win[apiName] : sans lui, le premier accès à une fenêtre d’un autre domaine lève une SecurityError non catchée qui casse tout le script du SCO. Avec lui, on échoue proprement et on peut afficher un message utile.
  • Le garde-fou attempts : une chaîne de frames mal formée (rare, mais réel sur certains vieux LMS ou ENT) peut piéger une boucle. On plafonne.
  • La double stratégie parents puis opener : la spec impose les deux, dans cet ordre. Un player en iframe → on trouve via parent. Un player qui ouvre le SCO en pop-up → on trouve via opener.
  • Détecter la version en même temps que l’objet : c’est le bon moment pour savoir si on parle 1.2 ou 2004, et adapter tout le reste du code (LMSInitialize vs Initialize, cmi.core.lesson_status vs cmi.completion_status…).

💡 Réflexe — Fais le scan une seule fois, au chargement, et mémorise l’objet et la version dans une variable de module. Rappeler findAPI() à chaque SetValue est inutile et masque les vrais bugs. Trouve l’API tôt ; si getScormAPI() renvoie null, arrête-toi et signale, ne fais pas semblant de tracer.

Les trois pièges qui font échouer le scan

1. Le cross-domain (le tueur silencieux)

La Same-Origin Policy du navigateur interdit à ton SCO de lire les propriétés d’une fenêtre servie depuis un autre domaine. Si le LMS est sur moodle.ecole.fr et que ton SCO est chargé depuis cdn.formacampus.fr, l’accès win.API sur la fenêtre du player lève une exception : le scan s’arrête, l’API reste introuvable, et le module est muet — alors que le code est correct.

moodle.ecole.fr (fenêtre parente, porte l'objet API) └─ iframe: cdn.formacampus.fr ← ton SCO ne peut PAS lire window.parent.API

C’est pour ça que SCORM veut, par principe, que le contenu soit servi par le LMS lui-même (le .zip est décompressé côté LMS et servi depuis son domaine). Si tu héberges le contenu ailleurs (CDN, autre sous-domaine), tu casses le pont — sauf montage spécifique (proxy, produit type Rustici Dispatch qui gère précisément ce cas).

🔌 Côté intégration — Quand une école t’importe un SCORM, elle s’attend à ce que tout tourne dans son domaine. Un SCO qui appelle en douce un CDN externe pour ses assets peut fonctionner ; mais un SCO qui espère trouver l’API à travers une frontière de domaine ne fonctionnera jamais. Livre un paquet autoportant : c’est ce que vérifie, de fait, tout LMS conforme.

2. L’iframe imbriquée (l’ordre de remontée)

Certains players enveloppent le SCO dans plusieurs niveaux d’iframe (cadre de navigation + cadre de contenu). Un findAPI() naïf qui ne regarde que window.parent (un seul cran) rate l’objet posé deux crans plus haut. La remontée en boucle de scanUp() règle ça : elle grimpe jusqu’à trouver ou jusqu’au sommet.

3. La pop-up bloquée (l’opener absent)

Si le LMS lance le SCO en nouvelle fenêtre et que le navigateur bloque la pop-up (ou que l’utilisateur la rouvre à la main, cassant le lien opener), alors window.opener est null : la branche « opener » du scan ne trouve rien. Résultat : le SCO s’affiche seul, sans lien vers le LMS. Symptôme typique : « ça marche en prévisualisation, ça ne trace pas en vrai ».

⚠️ Piège — Confondre « le SCO s’affiche » et « le SCO est connecté ». L’affichage n’exige aucune API ; le tracking, si. Un module peut être parfaitement visible et totalement déconnecté du LMS. La seule preuve de connexion, c’est un getScormAPI() qui renvoie un objet non nul.

Que faire quand l’API est introuvable ?

Deux écoles. La mauvaise : continuer comme si de rien n’était, laisser l’apprenant terminer, et perdre toute sa progression. La bonne : détecter tôt et prévenir.

const { api, version } = getScormAPI(); if (!api) { // Pas de LMS : on est en prévisualisation locale, ou le scan a échoué. // On journalise et on avertit — on ne fait PAS semblant de tracer. console.warn("[SCORM] API introuvable : aucun tracking ne sera remonté."); // Option : mode dégradé (le contenu reste consultable) + bandeau visible. } else { console.info(`[SCORM] API ${version} détectée.`); // ... on peut initialiser (chapitre 4.2). }

Un bon SCO sait qu’il n’est pas connecté et le dit. C’est la première ligne de défense contre le « SCO muet » qu’on débusquera au chapitre 4.5.

🧭 Sur FormaCampus — Premier réflexe de diagnostic sur le module qui ne remonte rien dans le Moodle de l’école : ouvrir la console dans l’iframe du SCO et vérifier que getScormAPI() renvoie bien un objet. Ici, il en renvoie un (version: "1.2") — l’API est trouvée. Bonne nouvelle : le problème n’est pas le scan ni le cross-domain. On garde ce résultat en tête : le bug est plus loin, dans le cycle de vie ou le statut (chapitres 4.2 et 4.4). Éliminer l’hypothèse « API introuvable » dès la première marche, c’est déjà la moitié du diagnostic.

📚 La spec — L’algorithme find the API et le nom des objets (API pour SCORM 1.2, API_1484_11 pour SCORM 2004) sont définis dans le livre RTE de chaque version, publié par ADL (adlnet.gov). Le pseudo-code de référence y remonte window.parent puis window.opener ; les implémentations réelles ne font qu’ajouter les garde-fous (cross-domain, boucle) que la spec suppose gérés.

✏️ Exercices

Exercice 1 — Repérer le mauvais scan. Un dev livre ce code dans son SCO SCORM 1.2 :

function getAPI() { return window.API || window.parent.API || null; }

Cite deux situations réelles où ce code échoue à trouver l’API pourtant présente, et corrige.

✅ Solution

Ce code ne regarde que deux niveaux (la fenêtre courante et son parent direct) et ne tente jamais window.opener. Il échoue notamment :

  1. Iframe imbriquée : si le player pose API deux crans au-dessus (cadre de navigation + cadre de contenu), window.parent.API est undefined et l’objet, plus haut, n’est jamais atteint.
  2. Lancement en pop-up : si le SCO est ouvert en nouvelle fenêtre, l’objet est chez window.opener (ou un de ses parents), branche que ce code ignore totalement.

Bonus : window.parent.API peut lever une exception cross-domain non catchée qui casse le script. La correction est un vrai scan en boucle avec try/catch et repli sur opener — voir scanUp() / findAPIObject() du chapitre.

Exercice 2 — Cross-domain. Le SCO de FormaCampus est servi depuis contenus.formacampus.fr et embarqué dans un LMS hébergé sur lms.ecole.fr. Le scan lève une SecurityError. Explique la cause et donne deux façons de régler le problème sans casser le standard.

✅ Solution

Cause : la Same-Origin Policy interdit de lire les propriétés (dont API) d’une fenêtre d’un autre domaine. contenus.formacampus.fr et lms.ecole.fr sont des origines différentes → l’accès win.API lève une exception, le scan s’arrête, l’API reste introuvable.

Deux solutions conformes :

  1. Servir le contenu depuis le LMS lui-même : livrer un paquet autoportant (assets inclus) que l’école décompresse et sert depuis son domaine. C’est le mode nominal de SCORM.
  2. Passer par un dispositif de diffusion prévu pour ça, type Rustici Dispatch : le LMS importe un mince paquet « proxy » et le contenu réel reste centralisé, la connexion à l’API étant gérée dans le domaine attendu.

Ce qu’il ne faut pas faire : bricoler un postMessage maison hors spec en espérant que le LMS coopère — il n’a aucune raison de le faire.

🧠 Quiz de révision

1. Où se trouve, en général, l’objet API d’un SCO lancé en iframe ?

Dans une fenêtre ancêtre — le plus souvent la page « player » du LMS accessible via window.parent (parfois plusieurs crans plus haut). Presque jamais dans la fenêtre du SCO lui-même. D’où la nécessité de remonter la chaîne des parents.

2. Quel nom d’objet cherche-t-on en SCORM 1.2 ? Et en SCORM 2004 ?

API en SCORM 1.2, API_1484_11 en SCORM 2004. Le nom est imposé par la spec et détermine aussi les méthodes disponibles (LMSInitialize vs Initialize, etc.).

3. Après avoir remonté les parents sans succès, où le scan doit-il chercher ensuite ?

Sur window.opener (la fenêtre qui a ouvert le SCO, cas d’un lancement en pop-up) — et sur les parents de cet opener. C’est la seconde branche imposée par l’algorithme find the API.

4. Pourquoi entourer l’accès à win[apiName] d’un try/catch ?

Parce que lire une propriété d’une fenêtre d’un autre domaine lève une SecurityError (Same-Origin Policy). Sans try/catch, cette exception casse tout le script du SCO ; avec, on échoue proprement et on peut prévenir l’utilisateur.

5. Le SCO s’affiche correctement mais ne trace rien. Est-ce forcément un problème d’API introuvable ?

Non. L’affichage ne dépend d’aucune API. Une API introuvable est une cause possible (cross-domain, pop-up bloquée, iframe imbriquée), mais le module peut aussi trouver l’API et échouer plus loin (jamais initialisé, statut jamais posé, Terminate oublié). Le test décisif : getScormAPI() renvoie-t-il un objet non nul ? Si oui, chercher ailleurs (chapitres 4.2, 4.4, 4.5).


Chapitre suivant : 4.2 — Le cycle Initialize → Commit → Terminate — une fois l’API en main, comment ouvrir, alimenter et fermer proprement une session.

Last updated on