lib/outils/mathlive/internals.js

/**
 * Echappe tous les tags html (et les &xx;) autorisés et vire le reste
 * Il faudra passer le résultat à resumeHtml avant de l'utiliser
 * @param {string} ch
 * @return {string}
 */
export function escHtml (ch) {
  if (!ch) return ''
  return ch
    // lui n’a pas besoin d’un tag car le \n est géré
    .replace(/<br *\/?>/gi, '\n')
    // il y a des tonnes de sections qui envoient du html, qu’il faut conserver… on liste qq tags autorisés
    // tag auto fermant
    .replace(/<hr(\s+\/)?>/gi, '¿¿¿¿hr¡¡¡¡')
    // tags sans attributes (*? pour être "non gourmand" => s’arrêter au premier </tagFermant> trouvé et pas au dernier)
    // on utilise [\s\S] pour faire du .* avec /s, car les navigateurs comprennent pas tous le flag s (pour que . match aussi \n)
    .replace(/<(b|em|i|strong|sub|sup|u)>([\s\S]*?)<\/\1>/gi, (match, tag, content) => `¿¿¿¿${tag}¡¡¡¡${content}¿¿¿¿/${tag}¡¡¡¡`)
    // on autorise img|p|span avec attributs, mais pas n’importe lesquels (surtout pas onload par ex, ça permettrait d’exécuter du js arbitraire)
    .replace(/<img +((?:[^>"']|"[^"]*"|'[^']*')*)\/?>/gi, (match, strAttrs) => {
      if (strAttrs) return `¿¿¿¿img ${cleanAttributes(strAttrs)}/¡¡¡¡`
      console.error(Error('tag <img /> sans attributs'))
      return ''
    })
    .replace(/<(p|span) +((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1>/gi, (match, tag, strAttrs, content) => {
      if (strAttrs) return `¿¿¿¿${tag} ${cleanAttributes(strAttrs)}¡¡¡¡${content}¿¿¿¿/${tag}¡¡¡¡`
      return `¿¿¿¿${tag}¡¡¡¡${content}¿¿¿¿/${tag}¡¡¡¡`
    })
    .replace(/&(nbsp|lt|gt|dollar);/g, (match, chunk) => '¿¿¿' + chunk + '¡¡¡')
    .replace(/&#([0-9]+);/g, (match, chunk) => '¿¿¡' + chunk + '¡¡¡')
}

/**
 * Remet les tags et codes html autorisés
 * @param {string} ch
 * @return {string}
 */
export function resumeHtml (ch) {
  if (!ch) return ''
  return ch
    .replace(/¿¿¿¿/g, '<')
    .replace(/¡¡¡¡/g, '>')
    .replace(/¿¿¿/g, '&')
    .replace(/¿¿¡/g, '&#')
    .replace(/¡¡¡/g, ';')
    .replace(/\n/g, '<br>') // on remplace les éventuels \n mis dans contenu au départ
}

/**
 * Contrôle les attributs qu’on autorise (class|width|height) et supprime les autres (faut pas de style ni de onload="js arbitraire")
 * @private
 * @param {string} strAttrs liste d’attributs x="y"
 * @returns {string} les attributs autorisés
 */
function cleanAttributes (strAttrs) {
  if (!strAttrs?.trim?.()) return ''

  let cleanAttrs = ''

  // Une seule passe : on enlève du "reste" tout ce qu'on accepte,
  // et on construit cleanAttrs au fil de l'eau.
  const allowed = /\b(alt|class|height|src|tabindex|title|width)\s*=\s*(["'])(.*?)\2/gi
  const reste = strAttrs.replace(allowed, (match, name, quote, value) => {
    const attrName = String(name).toLowerCase()

    // Filtrage minimal anti-XSS sur src
    if (attrName === 'src') {
      const v = String(value).trim()
      if (/^(?:javascript|vbscript|data):/i.test(v)) {
        console.warn(Error(`attribut src refusé (schéma interdit) : ${v}`))
        return ''
      }
    }

    // On garde exactement match (ça préserve les quotes originales au cas où le contenu contiendrait l'autre quote)
    cleanAttrs += ` ${match.trim()}`
    return ''
  })

  // On ignore les espaces, et on tolère juste un "/" résiduel (cas <img ... />)
  const resteCompact = reste.replace(/\s+/g, '')
  if (resteCompact && resteCompact !== '/') {
    console.warn(Error(`attributs ignorés : ${resteCompact}`))
  }

  return cleanAttrs
}