uiCommands/javascriptDriver.js

import { ge } from 'src/kernel/dom.js'

export function getCodeFromDom (javascriptCodeId) {
  const elt = ge(javascriptCodeId, true)
  if (elt) {
    return elt.textContent
  }
  console.error(`Aucun élément #${javascriptCodeId} => option javascriptCodeId ignorée`)
}

export function safeEval (mtgApp, jsCode, console) {
  if (typeof jsCode !== 'string') throw TypeError(`type ${typeof jsCode} invalide pour safeEval`)
  // on pourrait virer les commentaires, mais ça va fausser le calcul du n° de ligne
  // on regarde si en virant les /* */ il reste un /*,
  // si oui on les vire, sinon on laisse tout
  const tmpCode = jsCode
    // les /* */ en multi-ligne
    .replace(/\/\*.*?\*\//msg, '')
  const tmpCode2 = tmpCode
    // mais aussi après un éventuel /* qui resterait (non fermé ça fait planter notre eval)
    .replace(/\/\*.*$/ms, '')
  if (tmpCode2 !== tmpCode) {
    // il y a un /* non fermé, on garde la version nettoyée
    jsCode = tmpCode2
  }

  // @see https://blog.risingstack.com/writing-a-javascript-framework-sandboxed-code-evaluation/
  // On utilise pas le weakMap qu'il propose car on n'évalue chaque expression qu'une seule fois
  // (son exemple est pour compiler le code d'une fonction qui sera appelée de nombreuses fois)
  // On crée le proxy qui va intercepter tous les appels de variables/fonctions
  const proxyHandler = {
    // si has retourne toujours true, le `in sandbox` sera toujours true et le code évalué ne remontera jamais au scope global
    // car toutes les variables sont vues comme existante dans le scope local (mis avec le with)
    has: () => true,
    // le Symbol.unscopables est là pour les propriétés qui ne sont pas contraintes par le with => on empêche ainsi de remonter au scope global pour celles-là
    get: (target, key) => key === Symbol.unscopables ? undefined : target[key]
  }
  // on autorise mtgApp, Math, Number et console dans le code js évalué, … rien d'autre (pas window)
  const globalProxied = { Math, Number, console, mtgApp }
  // et on ajoute toutes les méthodes de mtgApp
  for (const meth of Object.keys(mtgApp)) {
    if (typeof mtgApp[meth] !== 'function') continue
    globalProxied[meth] = mtgApp[meth].bind(mtgApp)
  }
  const sandboxProxy = new Proxy(globalProxied, proxyHandler)
  // et on wrap l'expression avec un with pour limiter le scope au sandbox qui sera passé à la fct
  const src = `with (sandbox) {
  try {
    ${jsCode}
  } catch (error) {
    console.error(error) 
  } 
}`
  // ça ressemble à de l'eval, mais ici c'est très encadré
  // eslint-disable-next-line no-new-func
  const runner = new Function('sandbox', src)
  runner(sandboxProxy)
}

/**
 * Lance le code js, sans éditeur js
 * @param {MtgAppLecteurApi} mtgAppLecteurApi
 * @param {MtgOptions} mtgOptions
 * @param {MtgOptions} mtgOptions.mtgContainer Le conteneur pour la figure
 * @param {MtgOptions} mtgOptions.javascriptCode Le code javascript a exécuter
 * @param {Array<string|FigDef>} [mtgOptions.commandsFigs] Une liste éventuelle de figures initiales (sinon ce sera figure vide et repère orthonormé)
 */
async function loadJavascriptDriver (mtgAppLecteurApi, mtgOptions) {
  if (!mtgOptions.mtgContainer) throw Error('Il faut fournir un mtgContainer dans les options')
  const code = mtgOptions.javascriptCodeId
    ? getCodeFromDom(mtgOptions.javascriptCodeId)
    : mtgOptions.javascriptCode
  if (!code) throw Error('Il faut fournir du code js dans les options (javascriptCode ou javascriptCodeId)')

  safeEval(mtgAppLecteurApi, code, console)
}

export default loadJavascriptDriver