Merge branch 'MDL-78714-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE

This commit is contained in:
Ilya Tregubov 2023-08-09 12:05:24 +08:00
commit 4732fa0b13
No known key found for this signature in database
GPG Key ID: 0F58186F748E55C1
12 changed files with 101 additions and 47 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -241,8 +241,17 @@ const getStandardConfig = (target, tinyMCE, options, plugins) => {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
a11y_advanced_options: true, a11y_advanced_options: true,
// Ensure that scripts are recognized as valid elements.
// eslint-disable-next-line camelcase
extended_valid_elements: 'script[*]', extended_valid_elements: 'script[*]',
// Disable XSS Sanitisation.
// We do this in PHP.
// https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off
// Note: This feature has been backported from TinyMCE 6.4.0.
// eslint-disable-next-line camelcase
xss_sanitization: false,
// Disable quickbars entirely. // Disable quickbars entirely.
// The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle. // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase

View File

@ -1044,8 +1044,13 @@
}; };
const parseAndSanitize = (editor, context, html) => { const parseAndSanitize = (editor, context, html) => {
const getEditorOption = editor.options.get;
const sanitize = getEditorOption('xss_sanitization');
const validate = shouldFilterHtml(editor); const validate = shouldFilterHtml(editor);
return Parser(editor.schema, { validate }).parse(html, { context }); return Parser(editor.schema, {
sanitize,
validate
}).parse(html, { context });
}; };
const setup$1 = editor => { const setup$1 = editor => {

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,17 @@
# This is a description for the TinyMCE 6 library integration with Moodle. # This is a description for the TinyMCE 6 library integration with Moodle.
Please note that we have a clone of the official TinyMCE repository which contains the working build and branch for each release. This ensures build repeatability and gives us the ability to patch stable versions of Moodle for security fixes where relevant.
The Moodle `master` branch is named as the upcoming STABLE branch name, for example during the development of Moodle 4.2.0, the upcoming STABLE branch name will be MOODLE_402_STABLE.
## Patches included in this release
- MDL-78714: Add support for disabling XSS Sanitisation (TINY-9600)
- MDL-77470: Fix for CVE-2022-23494
Please note: TinyMCE issue numbers are related to bugs in their private issue
tracker. See git history of their repository for relevant information.
## Upgrade procedure for TinyMCE Editor ## Upgrade procedure for TinyMCE Editor
1. Store an environment variable to the Tiny directory in the Moodle repository (the current directory). 1. Store an environment variable to the Tiny directory in the Moodle repository (the current directory).
@ -10,7 +22,7 @@
2. Check out a clean copy of TinyMCE of the target version. 2. Check out a clean copy of TinyMCE of the target version.
``` ```../../
tinymce=`mktemp -d` tinymce=`mktemp -d`
cd "${tinymce}" cd "${tinymce}"
git clone https://github.com/tinymce/tinymce.git git clone https://github.com/tinymce/tinymce.git
@ -25,21 +37,43 @@
sed -i 's/"target.*es.*",/"target": "es2020",/' tsconfig.shared.json sed -i 's/"target.*es.*",/"target": "es2020",/' tsconfig.shared.json
``` ```
4. Rebuild TinyMCE 4. Install dependencies
``` ```
yarn yarn
yarn build
``` ```
5. Remove the old TinyMCE configuration and replace it with the newly built version. 5. Check in the base changes
``` ```
rm -rf "${MOODLEDIR}/js" git commit -m 'MDL: Add build configuration'
cp -r modules/tinymce/js "${MOODLEDIR}/js"
``` ```
6. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/6/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php 6. Apply any necessary security patches.
7. Rebuild TinyMCE
```
yarn
yarn build
```
8. Remove the old TinyMCE configuration and replace it with the newly built version.
```
rm -rf "${MOODLEDIR}/js"
cp -r modules/tinymce/js "${MOODLEDIR}/js"
```
9. Push the build to MoodleHQ for future change support
```
# Tag the next Moodle version.
git tag v4.2.0
git remote add moodlehq --tags
git push moodlehq MOODLE_402_STABLE
```
10. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/6/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php
## Update procedure for included TinyMCE translations ## Update procedure for included TinyMCE translations

View File

@ -26917,7 +26917,6 @@
var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI; var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI;
value = name === 'value' ? attr.value : stringTrim(attr.value); value = name === 'value' ? attr.value : stringTrim(attr.value);
lcName = transformCaseFunc(name); lcName = transformCaseFunc(name);
var initValue = value;
hookEvent.attrName = lcName; hookEvent.attrName = lcName;
hookEvent.attrValue = value; hookEvent.attrValue = value;
hookEvent.keepAttr = true; hookEvent.keepAttr = true;
@ -26927,8 +26926,8 @@
if (hookEvent.forceKeepAttr) { if (hookEvent.forceKeepAttr) {
continue; continue;
} }
_removeAttribute(name, currentNode);
if (!hookEvent.keepAttr) { if (!hookEvent.keepAttr) {
_removeAttribute(name, currentNode);
continue; continue;
} }
if (regExpTest(/\/>/i, value)) { if (regExpTest(/\/>/i, value)) {
@ -26941,19 +26940,16 @@
} }
var lcTag = transformCaseFunc(currentNode.nodeName); var lcTag = transformCaseFunc(currentNode.nodeName);
if (!_isValidAttribute(lcTag, lcName, value)) { if (!_isValidAttribute(lcTag, lcName, value)) {
_removeAttribute(name, currentNode);
continue; continue;
} }
if (value !== initValue) { try {
try { if (namespaceURI) {
if (namespaceURI) { currentNode.setAttributeNS(namespaceURI, name, value);
currentNode.setAttributeNS(namespaceURI, name, value); } else {
} else { currentNode.setAttribute(name, value);
currentNode.setAttribute(name, value);
}
} catch (_) {
_removeAttribute(name, currentNode);
} }
arrayPop(DOMPurify.removed);
} catch (_) {
} }
} }
_executeHook('afterSanitizeAttributes', currentNode, null); _executeHook('afterSanitizeAttributes', currentNode, null);

File diff suppressed because one or more lines are too long

View File

@ -1281,6 +1281,7 @@ interface DomParserSettings {
preserve_cdata?: boolean; preserve_cdata?: boolean;
remove_trailing_brs?: boolean; remove_trailing_brs?: boolean;
root_name?: string; root_name?: string;
sanitize?: boolean;
validate?: boolean; validate?: boolean;
inline_styles?: boolean; inline_styles?: boolean;
blob_cache?: BlobCache; blob_cache?: BlobCache;
@ -1870,6 +1871,7 @@ interface BaseEditorOptions {
visual_anchor_class?: string; visual_anchor_class?: string;
visual_table_class?: string; visual_table_class?: string;
width?: number | string; width?: number | string;
xss_sanitization?: boolean;
disable_nodechange?: boolean; disable_nodechange?: boolean;
forced_plugins?: string | string[]; forced_plugins?: string | string[];
plugin_base_urls?: Record<string, string>; plugin_base_urls?: Record<string, string>;
@ -1954,6 +1956,7 @@ interface EditorOptions extends NormalizedEditorOptions {
visual_anchor_class: string; visual_anchor_class: string;
visual_table_class: string; visual_table_class: string;
width: number | string; width: number | string;
xss_sanitization: boolean;
} }
declare type StyleMap = Record<string, string | number>; declare type StyleMap = Record<string, string | number>;
interface StylesSettings { interface StylesSettings {

View File

@ -7046,6 +7046,10 @@
processor: 'boolean', processor: 'boolean',
default: true default: true
}); });
registerOption('xss_sanitization', {
processor: 'boolean',
default: true
});
editor.on('ScriptsLoaded', () => { editor.on('ScriptsLoaded', () => {
registerOption('directionality', { registerOption('directionality', {
processor: 'string', processor: 'string',
@ -7145,6 +7149,7 @@
const getEditableClass = option('editable_class'); const getEditableClass = option('editable_class');
const getNonEditableRegExps = option('noneditable_regexp'); const getNonEditableRegExps = option('noneditable_regexp');
const shouldPreserveCData = option('preserve_cdata'); const shouldPreserveCData = option('preserve_cdata');
const shouldSanitizeXss = option('xss_sanitization');
const hasTextPatternsLookup = editor => editor.options.isSet('text_patterns_lookup'); const hasTextPatternsLookup = editor => editor.options.isSet('text_patterns_lookup');
const getFontStyleValues = editor => Tools.explode(editor.options.get('font_size_style_values')); const getFontStyleValues = editor => Tools.explode(editor.options.get('font_size_style_values'));
const getFontSizeClasses = editor => Tools.explode(editor.options.get('font_size_classes')); const getFontSizeClasses = editor => Tools.explode(editor.options.get('font_size_classes'));
@ -15463,7 +15468,6 @@
var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI; var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI;
value = name === 'value' ? attr.value : stringTrim(attr.value); value = name === 'value' ? attr.value : stringTrim(attr.value);
lcName = transformCaseFunc(name); lcName = transformCaseFunc(name);
var initValue = value;
hookEvent.attrName = lcName; hookEvent.attrName = lcName;
hookEvent.attrValue = value; hookEvent.attrValue = value;
hookEvent.keepAttr = true; hookEvent.keepAttr = true;
@ -15473,8 +15477,8 @@
if (hookEvent.forceKeepAttr) { if (hookEvent.forceKeepAttr) {
continue; continue;
} }
_removeAttribute(name, currentNode);
if (!hookEvent.keepAttr) { if (!hookEvent.keepAttr) {
_removeAttribute(name, currentNode);
continue; continue;
} }
if (regExpTest(/\/>/i, value)) { if (regExpTest(/\/>/i, value)) {
@ -15487,19 +15491,16 @@
} }
var lcTag = transformCaseFunc(currentNode.nodeName); var lcTag = transformCaseFunc(currentNode.nodeName);
if (!_isValidAttribute(lcTag, lcName, value)) { if (!_isValidAttribute(lcTag, lcName, value)) {
_removeAttribute(name, currentNode);
continue; continue;
} }
if (value !== initValue) { try {
try { if (namespaceURI) {
if (namespaceURI) { currentNode.setAttributeNS(namespaceURI, name, value);
currentNode.setAttributeNS(namespaceURI, name, value); } else {
} else { currentNode.setAttribute(name, value);
currentNode.setAttribute(name, value);
}
} catch (_) {
_removeAttribute(name, currentNode);
} }
arrayPop(DOMPurify.removed);
} catch (_) {
} }
} }
_executeHook('afterSanitizeAttributes', currentNode, null); _executeHook('afterSanitizeAttributes', currentNode, null);
@ -16596,6 +16597,7 @@
const defaultedSettings = { const defaultedSettings = {
validate: true, validate: true,
root_name: 'body', root_name: 'body',
sanitize: true,
...settings ...settings
}; };
const parser = new DOMParser(); const parser = new DOMParser();
@ -16606,8 +16608,10 @@
const content = isSpecialRoot ? `<${ rootName }>${ html }</${ rootName }>` : html; const content = isSpecialRoot ? `<${ rootName }>${ html }</${ rootName }>` : html;
const wrappedHtml = format === 'xhtml' ? `<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>${ content }</body></html>` : `<body>${ content }</body>`; const wrappedHtml = format === 'xhtml' ? `<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>${ content }</body></html>` : `<body>${ content }</body>`;
const body = parser.parseFromString(wrappedHtml, mimeType).body; const body = parser.parseFromString(wrappedHtml, mimeType).body;
purify.sanitize(body, getPurifyConfig(defaultedSettings, mimeType)); if (defaultedSettings.sanitize) {
purify.removed = []; purify.sanitize(body, getPurifyConfig(defaultedSettings, mimeType));
purify.removed = [];
}
return isSpecialRoot ? body.firstChild : body; return isSpecialRoot ? body.firstChild : body;
}; };
const addNodeFilter = nodeFilterRegistry.addFilter; const addNodeFilter = nodeFilterRegistry.addFilter;
@ -16720,7 +16724,7 @@
}; };
const serializeContent = content => isTreeNode(content) ? HtmlSerializer({ validate: false }).serialize(content) : content; const serializeContent = content => isTreeNode(content) ? HtmlSerializer({ validate: false }).serialize(content) : content;
const withSerializedContent = (content, fireEvent) => { const withSerializedContent = (content, fireEvent, sanitize) => {
const serializedContent = serializeContent(content); const serializedContent = serializeContent(content);
const eventArgs = fireEvent(serializedContent); const eventArgs = fireEvent(serializedContent);
if (eventArgs.isDefaultPrevented()) { if (eventArgs.isDefaultPrevented()) {
@ -16729,7 +16733,8 @@
if (eventArgs.content !== serializedContent) { if (eventArgs.content !== serializedContent) {
const rootNode = DomParser({ const rootNode = DomParser({
validate: false, validate: false,
forced_root_block: false forced_root_block: false,
sanitize
}).parse(eventArgs.content, { context: content.name }); }).parse(eventArgs.content, { context: content.name });
return { return {
...eventArgs, ...eventArgs,
@ -16764,10 +16769,10 @@
if (args.no_events) { if (args.no_events) {
return content; return content;
} else { } else {
const processedEventArgs = withSerializedContent(content, c => fireGetContent(editor, { const processedEventArgs = withSerializedContent(content, content => fireGetContent(editor, {
...args, ...args,
content: c content
})); }), shouldSanitizeXss(editor));
return processedEventArgs.content; return processedEventArgs.content;
} }
}; };
@ -16778,7 +16783,7 @@
const processedEventArgs = withSerializedContent(args.content, content => fireBeforeSetContent(editor, { const processedEventArgs = withSerializedContent(args.content, content => fireBeforeSetContent(editor, {
...args, ...args,
content content
})); }), shouldSanitizeXss(editor));
if (processedEventArgs.isDefaultPrevented()) { if (processedEventArgs.isDefaultPrevented()) {
fireSetContent(editor, processedEventArgs); fireSetContent(editor, processedEventArgs);
return Result.error(undefined); return Result.error(undefined);
@ -24409,7 +24414,7 @@
}; };
const preProcess = (editor, html) => { const preProcess = (editor, html) => {
const parser = DomParser({}, editor.schema); const parser = DomParser({ sanitize: shouldSanitizeXss(editor) }, editor.schema);
parser.addNodeFilter('meta', nodes => { parser.addNodeFilter('meta', nodes => {
Tools.each(nodes, node => { Tools.each(nodes, node => {
node.remove(); node.remove();
@ -26655,6 +26660,7 @@
remove_trailing_brs: getOption('remove_trailing_brs'), remove_trailing_brs: getOption('remove_trailing_brs'),
inline_styles: getOption('inline_styles'), inline_styles: getOption('inline_styles'),
root_name: getRootName(editor), root_name: getRootName(editor),
sanitize: getOption('xss_sanitization'),
validate: true, validate: true,
blob_cache: blobCache, blob_cache: blobCache,
document: editor.getDoc() document: editor.getDoc()

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
For instructions on how to import TinyMCE into Moodle, see js/tinymce/readme_moodle.md.