diff --git a/services/web/scripts/translations/checkSanitizeOptions.js b/services/web/scripts/translations/checkSanitizeOptions.js new file mode 100644 index 0000000000..49bed23ed7 --- /dev/null +++ b/services/web/scripts/translations/checkSanitizeOptions.js @@ -0,0 +1,42 @@ +const Path = require('path') +const fs = require('fs') +const { sanitize } = require('./sanitize') + +async function main() { + let ok = true + const base = Path.join(__dirname, '/../../locales') + for (const name of await fs.promises.readdir(base)) { + if (name === 'README.md') continue + const blob = await fs.promises.readFile( + Path.join(__dirname, '/../../locales', name), + 'utf-8' + ) + const locales = JSON.parse(blob) + + for (const key of Object.keys(locales)) { + const want = locales[key] + const got = sanitize(locales[key]) + if (got !== want) { + if (want === 'Editor & PDF' && got === 'Editor & PDF') { + // Ignore this mismatch. React cannot handle escaped labels. + continue + } + ok = false + console.warn(`${name}: ${key}: want: ${want}`) + console.warn(`${name}: ${key}: got: ${got}`) + } + } + } + if (!ok) { + throw new Error('Check the logs, some values changed.') + } +} + +main() + .then(() => { + process.exit(0) + }) + .catch(error => { + console.error(error) + process.exit(1) + }) diff --git a/services/web/scripts/translations/download.js b/services/web/scripts/translations/download.js index 6f6cf0fa84..76dd4382cb 100644 --- a/services/web/scripts/translations/download.js +++ b/services/web/scripts/translations/download.js @@ -1,7 +1,7 @@ const path = require('path') const { promises: fs } = require('fs') const oneSky = require('@brainly/onesky-utils') -const sanitizeHtml = require('sanitize-html') +const { sanitize } = require('./sanitize') const { withAuth } = require('./config') async function run() { @@ -46,45 +46,3 @@ async function run() { } } run() - -/** - * Sanitize a translation string to prevent injection attacks - * - * @param {string} input - * @returns {string} - */ -function sanitize(input) { - // Block Angular XSS - // Ticket: https://github.com/overleaf/issues/issues/4478 - input = input.replace(/'/g, '’') - // Use left quote where (likely) appropriate. - input.replace(/ ’/g, ' ‘') - - return sanitizeHtml(input, { - // Allow "replacement" tags (in the format <0>, <1>, <2>, etc) used by - // react-i18next to allow for HTML insertion via the Trans component. - // See: https://github.com/overleaf/developer-manual/blob/master/code/translations.md - // Unfortunately the sanitizeHtml library does not accept regexes or a - // function for the allowedTags option, so we are limited to a hard-coded - // number of "replacement" tags. - allowedTags: ['b', 'strong', 'a', 'code', ...range(10)], - allowedAttributes: { - a: ['href', 'class'], - }, - textFilter(text) { - return text - .replace(/\{\{/, '{{') - .replace(/\}\}/, '}}') - }, - }) -} - -/** - * Generate a range of numbers as strings up to the given size - * - * @param {number} size Size of range - * @returns {string[]} - */ -function range(size) { - return Array.from(Array(size).keys()).map(n => n.toString()) -} diff --git a/services/web/scripts/translations/sanitize.js b/services/web/scripts/translations/sanitize.js new file mode 100644 index 0000000000..223feb801d --- /dev/null +++ b/services/web/scripts/translations/sanitize.js @@ -0,0 +1,45 @@ +const sanitizeHtml = require('sanitize-html') + +/** + * Sanitize a translation string to prevent injection attacks + * + * @param {string} input + * @returns {string} + */ +function sanitize(input) { + // Block Angular XSS + // Ticket: https://github.com/overleaf/issues/issues/4478 + input = input.replace(/'/g, '’') + // Use left quote where (likely) appropriate. + input.replace(/ ’/g, ' ‘') + + return sanitizeHtml(input, { + // Allow "replacement" tags (in the format <0>, <1>, <2>, etc) used by + // react-i18next to allow for HTML insertion via the Trans component. + // See: https://github.com/overleaf/developer-manual/blob/master/code/translations.md + // Unfortunately the sanitizeHtml library does not accept regexes or a + // function for the allowedTags option, so we are limited to a hard-coded + // number of "replacement" tags. + allowedTags: ['b', 'strong', 'a', 'code', ...range(10)], + allowedAttributes: { + a: ['href', 'class'], + }, + textFilter(text) { + return text + .replace(/\{\{/, '{{') + .replace(/\}\}/, '}}') + }, + }) +} + +/** + * Generate a range of numbers as strings up to the given size + * + * @param {number} size Size of range + * @returns {string[]} + */ +function range(size) { + return Array.from(Array(size).keys()).map(n => n.toString()) +} + +module.exports = { sanitize }