Skip to content

Commit

Permalink
Upgrade to MathJax v3 (#5818)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasr8 committed Jun 21, 2023
1 parent d4ca966 commit ff5d7e2
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 385 deletions.
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ Version 3.2.5

*Unreleased*

Security fixes
^^^^^^^^^^^^^^

- Fix an XSS vulnerability in the LaTeX ``\href`` macro when rendering it client-side.
Previously, it was possible to embed arbitrary JavaScript there using the ``javascript:``
protocol. The underlying MathJax library has now been updated to version 3 which allows
blacklisting certain protocols, thus allowing only ``http``, ``https`` and ``mailto``
links in ``\href`` macros :pr:`5818`

Improvements
^^^^^^^^^^^^

Expand Down
4 changes: 2 additions & 2 deletions indico/util/mathjax.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from flask import current_app, render_template
from flask import current_app


class MathjaxMixin:
def _get_head_content(self):
return render_template('mathjax_config.html') + str(current_app.manifest['mathjax.js'])
return str(current_app.manifest['mathjax.js'])
125 changes: 117 additions & 8 deletions indico/web/client/js/jquery/compat/mathjax.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,120 @@
// modify it under the terms of the MIT License; see the
// LICENSE file for more details.

import 'mathjax';
import '../utils/pagedown_mathjax';

// pretend this is not an "unpacked" setup
MathJax.isPacked = true;
// the default values for these paths use isPacked before we can set it
MathJax.OutputJax.fontDir = '[MathJax]/fonts';
MathJax.OutputJax.imageDir = '[MathJax]/images';
/* eslint-disable import/unambiguous, import/no-commonjs */

function getMathJaxSource() {
const dist = 'dist/js/mathjax/es5';
const isStatic = JSON.parse(document.documentElement.dataset.staticSite);
return isStatic ? `static/${dist}` : `${Indico.Urls.Base}/${dist}`;
}

window.MathJax = {
loader: {
paths: {
// path from which MathJax dynamically loads components at runtime
mathjax: getMathJaxSource(),
},
},
startup: {
typeset: false,
elements: [],
},
options: {
enableMenu: true,
menuOptions: {
settings: {
zoom: 'None',
ctrl: false,
alt: false,
cmd: false,
shift: false,
zscale: '200%',
texHints: true,
},
},
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
ignoreHtmlClass: 'asciimath2jax_ignore',
processHtmlClass: 'asciimath2jax_process',
safeOptions: {
allow: {
URLs: 'safe',
classes: 'none',
cssIDs: 'none',
styles: 'none',
},
safeProtocols: {
http: true,
https: true,
mailto: true,
file: false,
javascript: false,
data: false,
},
},
},
tex: {
packages: {
'[+]': ['ams', 'html'],
'[-]': ['require', 'autoload'], // Prevent loading packages which we did not explicitly add
},
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
processEscapes: false,
processEnvironments: true,
processRefs: true,
tagSide: 'right',
tagIndent: '.8em',
multlineWidth: '85%',
tags: 'none',
useLabelIds: true,
},
};

// Based on this tutorial:
// https://github.com/mathjax/MathJax-demos-web/blob/master/custom-component/custom-component.html.md

//
// Initialize the MathJax startup code
//
require('mathjax-full/components/src/startup/lib/startup.js');

//
// Get the loader module and indicate the modules that
// will be loaded by hand below
//
const {Loader} = require('mathjax-full/js/components/loader.js');
Loader.preLoad(
'loader',
'startup',
'core',
'input/tex-base',
'[tex]/ams',
'[tex]/html',
'output/chtml',
'output/chtml/fonts/tex.js',
'ui/menu',
'ui/safe' // https://docs.mathjax.org/en/latest/web/typeset.html#typesetting-user-supplied-content
);

//
// Load the components that we want to combine into one component
// (the ones listed in the preLoad() call above)
//
require('mathjax-full/components/src/core/core.js');

require('mathjax-full/components/src/input/tex-base/tex-base.js');
require('mathjax-full/components/src/input/tex/extensions/ams/ams.js');
require('mathjax-full/components/src/input/tex/extensions/html/html.js');
require('mathjax-full/components/src/output/chtml/chtml.js');
require('mathjax-full/components/src/output/chtml/fonts/tex/tex.js');
require('mathjax-full/components/src/ui/menu/menu.js');
require('mathjax-full/components/src/ui/safe/safe.js');

//
// Loading this component will cause all the normal startup
// operations to be performed when this component is loaded
//
require('mathjax-full/components/src/startup/startup.js');

require('../utils/pagedown_mathjax');
158 changes: 19 additions & 139 deletions indico/web/client/js/jquery/utils/pagedown_mathjax.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,17 @@
// modify it under the terms of the MIT License; see the
// LICENSE file for more details.

/* eslint-disable camelcase, one-var, no-lonely-if */
/* eslint-disable camelcase, one-var, no-lonely-if, import/unambiguous */
/* global Markdown:false, MathJax:false, PageDownMathJax:false */

// General MathJax configuration
(function() {
MathJax.Ajax.config.root =
($('html').data('static-site') ? 'static/' : Indico.Urls.Base) + '/dist/js/mathjax';
})();

(function() {
var DELIMITERS = [['$', '$'], ['$$', '$$']];

window.PageDownMathJax = function() {
var ready = false; // true after initial typeset is complete
var pending = false; // true when MathJax has been requested
var $preview = null; // the preview container
var ready_listeners = []; // the inline math delimiter

var blocks, start, end, last, braces; // used in searching for math
var math; // stores math until markdone is done
var HUB = MathJax.Hub;

//
// Runs after initial typeset
//
HUB.Queue(function() {
ready = true;
HUB.processUpdateTime = 50; // reduce update time so that we can cancel easier
HUB.Config({
'HTML-CSS': {
EqnChunk: 10,
EqnChunkFactor: 1,
}, // reduce chunk for more frequent updates
'SVG': {
EqnChunk: 10,
EqnChunkFactor: 1,
},
});

ready_listeners.forEach(listener => {
listener();
});
});

//
// The pattern for math delimiters and special symbols
Expand All @@ -70,9 +38,6 @@
.replace(/&/g, '&') // use HTML entity for &
.replace(/</g, '&lt;') // use HTML entity for <
.replace(/>/g, '&gt;'); // use HTML entity for >
if (HUB.Browser.isMSIE) {
block = block.replace(/(%[^\n]*)\n/g, '$1<br/>\n');
}
while (j > i) {
blocks[j] = '';
j--;
Expand Down Expand Up @@ -161,42 +126,28 @@
return text;
}

//
// This is run to restart MathJax after it has finished
// the previous run (that may have been canceled)
//
function restartMJ(cb) {
pending = false;
HUB.cancelTypeset = false; // won't need to do this in the future

if ($preview) {
typeset($preview.get(0));
}

if (cb) {
HUB.Queue(cb);
}
}

//
// When the preview changes, cancel MathJax and restart,
// if we haven't done that already.
//
function updateMJ(elem, cb) {
var mathjaxEnabled = $(elem).data('no-mathjax') === undefined;
if (!mathjaxEnabled) {
if (elem.dataset.noMathjax !== undefined) {
cb();
} else if (!pending && ready) {
pending = true;
HUB.cancelTypeset = false;
HUB.Queue(restartMJ, cb);
} else {
typeset($preview.get(0)).then(cb);
}
}

function typeset(elem) {
if ($(elem).data('no-mathjax') === undefined) {
HUB.Queue(['Typeset', HUB, elem]);
async function typeset(elem) {
if (elem.dataset.noMathjax !== undefined) {
return;
}

// https://docs.mathjax.org/en/latest/web/typeset.html#handling-asynchronous-typesetting
MathJax.startup.promise = MathJax.startup.promise
.then(() => MathJax.typesetPromise([elem]))
.catch(err => console.log(`[MathJax] Typeset failed: ${err.message}`));
return MathJax.startup.promise;
}

function createPreview(elem, editorObject) {
Expand All @@ -222,8 +173,7 @@
}

editorObject.hooks.chain('onPreviewRefresh', preview);
typeset($preview.get(0));
addListener(preview);
MathJax.startup.promise.then(preview);
}

function createEditor(elem) {
Expand Down Expand Up @@ -256,11 +206,7 @@
}

function mathJax(elem) {
typeset(elem);
}

function addListener(listener) {
ready_listeners.push(listener);
return typeset(elem);
}

return {
Expand All @@ -280,77 +226,11 @@
return this;
};

$.fn.pagedown = function(arg1, arg2) {
$.fn.pagedown = function() {
function _pagedown(elem) {
var options = {},
pd_context = elem.data('pagedown'),
last_change = null,
$save_button = elem.siblings('.wmd-button-bar').find('.save-button');

function _set_saving() {
$save_button
.prop('disabled', true)
.addClass('saved')
.removeClass('saving waiting-save')
.text($T('Saving...'));
// the 'save' function will trigger a callback that sets
// the final state
options.save(elem.val(), function() {
$save_button
.text($T('Saved'))
.addClass('saved')
.removeClass('saving waiting-save');
});
}

function _save_cycle(my_time) {
return function() {
// if there's been a change meanwhile, don't do anything
if (last_change <= my_time) {
// otherwise, start saving
_set_saving();
}
};
}

if (pd_context) {
if (arg1 === 'auto-save' && $save_button.length) {
/*
* This is the 'auto-save' feature
* options:
* - 'wait_time' - time to wait after a change, before saving
* - save - function to be called on save (passed current data and callback)
*
* This feature also requires a '.save-button' button element to exist
* inside '.wmd-button-bar'
*/

_.extend(options, arg2 || {});
elem.on('input', function() {
$save_button
.addClass('waiting-save')
.removeClass('saving saved')
.text($T('Save'))
.prop('disabled', false);

// let handlers (_save_cyle) know that they're out-of-date
// by updating last_change with the current time
last_change = new Date().getTime();
setTimeout(_save_cycle(last_change), options.wait_time || 2000);
});

$save_button.on('click', function() {
// let's kill handlers that may have been triggered
// by setting last_change to now
last_change = new Date().getTime();
_set_saving();
});
}
} else {
pd_context = PageDownMathJax();
elem.data('pagedown', pd_context);
pd_context.createEditor(elem.get(0));
}
const pd_context = PageDownMathJax();
elem.data('pagedown', pd_context);
pd_context.createEditor(elem.get(0));
}

$(this).each(function(i, elem) {
Expand Down
Loading

0 comments on commit ff5d7e2

Please sign in to comment.