/**
* Afficheur générique pour l'affichage de toutes les ressources
* appelé avant les plugins (c'est sa fct load qui chargera le bon)
*
* Son chargement déclenche celui de init qui ajoute en global nos méthodes utilitaires, cf {@link namespace:sesamath}
*/
import sjt from 'sesajstools'
import dom from 'sesajstools/dom'
import log from 'sesajstools/utils/log'
import sjtUrl from 'sesajstools/http/url'
import xhr from 'sesajstools/http/xhr'
import { addSesatheques, getBaseUrl } from 'sesatheque-client/src/sesatheques'
import config from 'server/config'
import page from '../page'
import xhrPostSync from '../page/xhrPostSync'
import Resultat from '../../constructors/Resultat'
import getDisplay from 'plugins/displays'
import crypto from '../../utils/crypto'
import 'client-react/styles/display.scss'
const { hasProp, isFunction, isString, isUrlAbsolute, stringify } = sjt
const { sesatheques } = config
const { decrypt } = crypto
const wd = window.document
addSesatheques(sesatheques)
/**
* Le timeout des requêtes ajax. 10s c'est bcp mais certains clients ont des BP catastrophiques
* @private
* @type {number}
*/
const ajaxTimeout = 10000
const debugMode = sjtUrl.getParameter('debug')
/**
* Ajoute une méthode resultatCallback aux options si besoin
* @private
* @param {Object} options L'objet sur lequel on ajoutera la methode resultatCallback
*/
function addResultatCallback (ressource, options) {
// appelle resultatListener avec le résultat formaté (ou rapporte une erreur)
function processResult (result, resultatListener) {
if (result && result instanceof Error) {
log.error(result)
feedback({ success: false, error: result.toString() }, divFeedback)
notifyBugsnag(result)
} else if (result) {
const resultat = getResultat(result, ressource, options)
if (isDebugMode) console.log('resultatCallback va envoyer', resultat)
resultatListener(resultat)
} else {
notifyBugsnag(new Error('callback de résultat appelée sans erreur ni résultat'))
}
}
const divFeedback = wd.getElementById('pictoFeedback')
const isDebugMode = ['all', 'resultat'].includes(debugMode)
let resultatListener
// pour envoyer les résultats, on regarde si on nous fourni une url ou une fct ou un nom de message
// on prend en callback par ordre de priorité resultatCallback, urlResultatCallback, resultatMessageAction+
if (options.resultatCallback && isFunction(options.resultatCallback)) {
resultatListener = options.resultatCallback
} else if (options.urlResultatCallback && isUrlAbsolute(options.urlResultatCallback)) {
// callback ajax
resultatListener = (resultat) => {
const url = options.urlResultatCallback
if (resultat.deferSync) {
delete resultat.deferSync
xhrPostSync(url, resultat, alertIfError)
} else {
xhr.post(url, resultat, { timeout: ajaxTimeout }, (error, retour) => {
if (!retour) retour = {}
if (error) {
console.error(error)
if (!retour.error) retour.error = error.toString()
}
feedback(retour, divFeedback)
})
}
}
} else if (options && options.resultatMessageAction && isString(options.resultatMessageAction)) {
// callback message
resultatListener = (resultat) => sendResultatMessage(options, resultat)
} else if (isDebugMode) {
log('activation de la récup du résultat en console pour débug')
resultatListener = (resultat) => console.log('[DEBUG] resultat qui aurait été envoyé', resultat)
}
// on s'occupe des résultats si qqun écoute
if (resultatListener) {
// on ajoute dans options des infos qu'on nous demanderait de mettre dans tous les résultats via l'url
['resultatId', 'tokenId', 'userId'].forEach(function (paramName) {
const paramValue = sjtUrl.getParameter(paramName)
if (paramValue) options[paramName] = paramValue
})
options.resultatCallback = (result) => processResult(result, resultatListener)
}
} // addResultatCallback
/**
* Callback pour l'envoi au unload
* @param {Error} error
*/
function alertIfError (error) {
if (!error) return
console.error(error)
/* global bugsnagClient */
if (typeof bugsnagClient !== 'undefined') bugsnagClient.notify(error)
alert(error)
}
/**
* Gère l'affichage du feedback puis appelle next(retour)
* @private
* @type feedbackCallback
* @param {retour} retour Le retour de l'envoi du résultat
* @param {HTMLElement} divFeedback
*/
function feedback (retour, divFeedback) {
log('feedback', retour)
if (retour && retour.error) {
feedbackKo(divFeedback)
page.addError(retour.error)
} else if ((retour && (retour.ok && retour.ok === true)) || (retour.success && retour.success === true)) {
feedbackOk(divFeedback)
} else if (retour && (hasProp(retour, 'ok') || hasProp(retour, 'success'))) {
feedbackKo(divFeedback)
page.addError("Une erreur est survenue dans l'enregistrement du résultat")
} else {
// else on en sait rien on fait rien
log.error(new Error('feedback appellé sans argument intelligible'), retour)
}
}
// Éteint le feedback */
function feedbackOff (divFeedback) {
divFeedback.className = 'feedbackOff'
}
// Allume le feedback OK pour 4s
function feedbackOk (divFeedback) {
divFeedback.className = 'feedbackOk'
setTimeout(() => feedbackOff(divFeedback), 4000)
}
// Allume le feedback KO pour 4s
function feedbackKo (divFeedback) {
divFeedback.className = 'feedbackKo'
setTimeout(() => feedbackOff(divFeedback), 4000)
}
/**
* Retourne l'objet Resultat (ou affiche une erreur en feedback si result en est une)
* @private
* @param result
* @return {Resultat|undefined}
*/
function getResultat (result, ressource, options) {
// on reçoit parfois des refs circulaires, faut nettoyer… ça clone au passage
result = JSON.parse(stringify(result))
const resultat = new Resultat(result)
// pour l'envoi au unload on ajoute ça
if (options.urlResultatCallback && result.deferSync) resultat.deferSync = result.deferSync
// on impose date et durée
resultat.date = new Date()
// le plugin peut imposer sa mesure, on ne met la durée que s'il ne l'a pas fourni
if (!resultat.duree && options.startDate) {
resultat.duree = Math.floor(((new Date()).getTime() - options.startDate.getTime()) / 1000)
}
// on impose ça d'après la ressource
resultat.type = ressource.type
resultat.rid = ressource.rid
// on regarde si on nous a demandé d'ajouter des propriétés au résultat
// (via options, ou querystring qui a été mis dans options à l'init)
;['resultatId', 'tokenId', 'userId'].forEach((p) => {
if (options[p]) resultat[p] = options[p]
})
return resultat
}
/**
* Fait le chargement proprement dit après page.init
* @private
* @param ressource
* @param options
* @param next
*/
function load (ressource, options, next) {
log('load avec la ressource', ressource)
log('et les options après page.init', options)
// le display du plugin
const pluginName = ressource.type
const loadDisplay = getDisplay(pluginName)
loadDisplay().then(({ display: pluginDisplay }) => {
if (options.container) dom.empty(options.container)
else throw new Error("L'initialisation a échoué, pas de conteneur pour la ressource")
if (!options.errorsContainer) throw new Error("L'initialisation a échoué, pas de conteneur pour afficher les erreurs")
if (!pluginDisplay) {
switch (pluginName) {
case 'ec2': throw new Error('Cette ressource calcul@tice en flash n’est pas gérée dans la bibliothèque, vous devez utiliser son équivalent javascript (icône avec un "C" bleu)')
case 'sequenceModele': throw Error('Ce contenu ne peut être affiché ici, il ne peut être utilisé que dans Labomep (le glisser dans "Mes séquences" pour l’y dupliquer puis le modifier pour y ajouter des élèves)')
default: throw new Error(`L'affichage des ressources de type ${pluginName} n'est pas encore implémenté`)
}
}
// On vire le titre si on nous le demande via les options ou un param dans l'url
if (
(hasProp(options, 'showTitle') && !options.showTitle) ||
/\?.*showTitle=0/.test(wd.URL) ||
/\/apercevoir\//.test(wd.URL) ||
/\?(.+&)?layout=iframe/.test(wd.URL)
) {
page.hideTitle()
}
if (ressource.parametres && ressource.parametres.correction) {
const { rid, parametres } = ressource
const { correction } = parametres
ressource = {
...ressource,
parametres: {
...parametres,
correction: JSON.parse(decrypt(correction, rid))
}
}
}
// on regarde s'il faut ajouter une fct de sauvegarde des résultats
addResultatCallback(ressource, options)
// ajout du chemin du plugin aux options
options.pluginBase = options.base + 'plugins/' + pluginName + '/'
// on peut afficher
pluginDisplay(ressource, options, function (error) {
options.startDate = new Date()
if (error) {
log("le display a terminé mais renvoyé l'erreur", error)
} else {
log('le display a terminé sans renvoyer d’erreur')
}
next(error)
})
}).catch(next)
} // load
/**
* Log en console.error si error
* @param {Error} error
*/
function logIfError (error) {
if (error) console.error(error)
}
/**
* Notifie busgnag (si on a un client) et sort l'erreur en console
* @param {Error} error
*/
function notifyBugsnag (error) {
console.error(error)
if (window.bugsnagClient) window.bugsnagClient.notify(error)
}
/**
* page.addError si error (inclue console.error)
* @param {Error} error
*/
function printIfError (error) {
if (error) page.addError(error)
}
/**
* Envoie le résultat au dom parent (à priori sesalab) avec un postMessage mais ne gère pas de feedback
* (faudrait passer dans le message une action de retour et un id, et ajouter un écouteur dessus)
* c'est le parent qui affichera le feedback
*/
function sendResultatMessage (options, resultat) {
const chunks = options.resultatMessageAction.split('::')
const action = options.resultatMessageAction
// le nom de la propriété attendue par celui qui écoute
const resultatProp = chunks[1] || 'resultat'
const message = {
action,
[resultatProp]: resultat
}
// on envoie
window.top.postMessage(message, '*')
}
/**
* Module d'une seule fonction pour afficher une ressource quelconque.
* Il chargera le bon afficheur en lui passant les options attendues,
* en créant si besoin les contereurs dans le dom courant, avec un appel de page.init(options).
* Il créera aussi éventuellement un wrapper pour appeler une callback de résultat éventuelle
* @service display
* @param {Ressource} ressource La ressource à afficher
* @param {initOptions} [options] Les options éventuelles (passer base si ce js est chargé sur un autre domaine)
* @param {errorCallback} [next] Fct appelée à la fin du chargement avec une erreur ou undefined
*/
export default function display (ressource, options, next) {
/**
* Envoie une erreur à /api/notifyError (ajoute le rid de la ressource courante)
* @param {string|object} infos
*/
function notifyError (infos) {
const type = typeof infos
if (type === 'string') infos = { error: infos }
else if (typeof infos !== 'object') return console.error(new Error(`paramètre invalide dans notifyError (type ${type}, il faut string ou object)`))
infos.rid = ressource.rid
// et on envoie
xhr.post('/api/notifyError', infos, logIfError)
}
try {
// init params
if (typeof options === 'function') {
next = options
options = {}
} else {
if (typeof next !== 'function') {
if (next) console.error(new Error('paramètre next invalide'), next)
next = printIfError
}
if (!options || typeof options !== 'object') {
if (options) console.error(new Error('options invalides'), options)
options = {}
}
}
const loadedMessageAction = sjtUrl.getParameter('loadedMessageAction')
if (loadedMessageAction) {
// qqun veut être prévenu en plus de next, on wrap next
const message = {
action: loadedMessageAction,
rid: ressource.rid
}
const originalNext = next
next = (error) => {
if (error) return originalNext(error)
window.top.postMessage(message, '*')
originalNext()
}
}
// activation du log en debug
if (['all', 'display'].includes(debugMode)) log.enable()
// log('options avant page.init', options)
// on accepte des baseId dans options.base
if (typeof options.base === 'string' && options.base.substr(0, 4) !== 'http') {
try {
options.base = getBaseUrl(options.base)
} catch (error) {
return next(error)
}
}
// on ajoute notifyError à options
options.notifyError = notifyError
// on peut lancer l'init
page.init(options, function (error) {
if (error) return next(error)
// ajout de metadata pour bugsnag
if (window.bugsnagClient) {
window.bugsnagClient.addMetadata('exo', {
rid: ressource.rid,
type: ressource.type,
url: location.href // déjà dispo dans l'onglet request mais plus pratique de l'avoir là aussi
})
}
load(ressource, options, next)
})
} catch (error) {
next(error)
}
}
/**
* Options à passer à display (celles de initOptions plus d'autres)
* display ajoutera une propriété pluginBase pour appeler les méthodes display des plugins
* @typedef displayOptions
* @type {Object}
* @property {string} [base=/] Le préfixe de chemin vers la racine de la sésathèque.
* Si ce module est utilisé sur un autre domaine que la sésathèque
* il faut passer une baseId connue de sesatheque-client
* ou un chemin http://… complet
*
* @property {Element} [container] L'élément html qui servira de conteneur au plugin pour afficher sa ressource, créé si non fourni
* @property {Element} [errorsContainer] L'élément html pour afficher des erreurs éventuelles, créé si non fourni
* @property {boolean} [verbose=false] Passer true pour ajouter des log en console
* @property {boolean} [isDev=false] Passer true pour initialiser le dom source en sesamath.dev (pour certains plugins)
* @property {string} [urlResultatCallback] Une url vers laquelle poster le résultat
* (idem si la page de la ressource contient ?urlScoreCallback=http…)
* @property {string} [resultatMessageAction] Un nom d'action pour passer le résultat en postMessage
* @property {resultatCallback} [resultatCallback] Une fonction pour recevoir un objet Resultat (si y'a pas de urlScoreCallback)
* @property {string} [sesatheque] Sera ajoutée en propriété du résultat (peut être passé en param du GET de la page),
* le nom de la sésathèque pour un client qui récupère des résultats de plusieurs sésatheques
* @property {boolean} [showTitle=true] Passer '0' ou 'false' via l'url ou false via options pour cacher le titre
* @property {string} [userOrigine] Sera ajoutée en propriété du résultat (peut être passé en param du GET de la page)
* @property {string} [userId] Sera ajoutée en propriété du résultat (peut être passé en param du GET de la page)
* @property {object} [flashvars] Pour les plugins qui chargent du swf, sera passé en flashvars en plus
* @property {Resultat} [lastResultat] Un éventuel Resultat
*/
/**
* @callback resultatCallback
* @param {Resultat} Un objet Resultat (en cas d'erreur la callback n'est pas appelée)
* @param {feedbackCallback}
*/
/**
* @callback feedbackCallback
* @param {feedbackArg} retour Le retour de resultatCallback (pour confirmer une sauvegarde)
*/
/**
* @typedef feedbackArg
* @type {object}
* @param {boolean} success
* @param {string} [error]
*/