1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-29 22:48:10 +02:00

Desktop: Rich Text Editor: Disallow inline event handlers (#12106)

This commit is contained in:
Henry Heino
2025-04-17 05:02:35 -07:00
committed by GitHub
parent 3ffcf065fc
commit 56e2d3da89
13 changed files with 123 additions and 89 deletions

View File

@@ -43,6 +43,7 @@ import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
import useDocument from '../../../hooks/useDocument';
import useEditDialog from './utils/useEditDialog';
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';
import Setting from '@joplin/lib/models/Setting';
import useTextPatternsLookup from './utils/useTextPatternsLookup';
const logger = Logger.create('TinyMCE');
@@ -728,6 +729,25 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
toolbar: toolbar.join(' '),
localization_function: _,
// See https://www.tiny.cloud/docs/tinymce/latest/tinymce-and-csp/#content_security_policy
content_security_policy: Setting.value('featureFlag.richText.useStrictContentSecurityPolicy') ? [
// Media: *: Allow users to include images and videos from the internet (e.g. ![](http://example.com/image.png)).
// Media: blob: Allow loading images/videos/audio from blob URLs. The Rich Text Editor
// replaces certain base64 URLs with blob URLs.
// Media: data: Allow loading images and other media from data: URLs
'default-src \'self\'',
'img-src \'self\' blob: data: *', // Images
'media-src \'self\' blob: data: *', // Audio and video players
// Disallow certain unused features
'child-src \'none\'', // Should not contain sub-frames
'object-src \'none\'', // Objects can be used for script injection
'form-action \'none\'', // No submitting forms
// Styles: unsafe-inline: TinyMCE uses inline style="" styles.
// Styles: *: Allow users to include styles from the internet (e.g. <style src="https://example.com/style.css">)
'style-src \'self\' \'unsafe-inline\' * data:',
].join(' ; ') : undefined,
contextmenu: false,
browser_spellcheck: true,

View File

@@ -2,72 +2,37 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import { useEffect } from 'react';
import { Editor } from 'tinymce';
const useWebViewApi = (editor: Editor, window: Window) => {
interface WebViewApi {
postMessage: (contentScriptId: string, message: unknown)=> Promise<unknown>;
}
interface ExtendedWindow extends Window {
webviewApi: WebViewApi;
}
const useWebViewApi = (editor: Editor, containerWindow: Window) => {
useEffect(() => {
if (!editor) return ()=>{};
if (!window) return ()=>{};
if (!containerWindow) return ()=>{};
const scriptElement = window.document.createElement('script');
const channelId = `plugin-post-message-${Math.random()}`;
scriptElement.appendChild(window.document.createTextNode(`
window.webviewApi = {
postMessage: (contentScriptId, message) => {
const channelId = ${JSON.stringify(channelId)};
const messageId = Math.random();
window.parent.postMessage({
channelId,
messageId,
contentScriptId,
message,
}, '*');
const waitForResponse = async () => {
while (true) {
const messageEvent = await new Promise(resolve => {
window.addEventListener('message', event => {
resolve(event);
}, {once: true});
});
if (messageEvent.source !== window.parent || messageEvent.data.messageId !== messageId) {
continue;
}
const data = messageEvent.data;
return data.response;
}
};
return waitForResponse();
},
};
`));
const editorWindow = editor.getWin();
editorWindow.document.head.appendChild(scriptElement);
const onMessageHandler = async (event: MessageEvent) => {
if (event.source !== editorWindow || event.data.channelId !== channelId) {
return;
}
const contentScriptId = event.data.contentScriptId;
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
const result = await plugin.emitContentScriptMessage(contentScriptId, event.data.message);
editorWindow.postMessage({
messageId: event.data.messageId,
response: result,
}, '*');
const editorWindow = editor.getWin() as ExtendedWindow;
const webviewApi: WebViewApi = {
postMessage: async (contentScriptId: string, message: unknown) => {
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
return await plugin.emitContentScriptMessage(contentScriptId, message);
},
};
window.addEventListener('message', onMessageHandler);
editorWindow.webviewApi = webviewApi;
return () => {
window.removeEventListener('message', onMessageHandler);
scriptElement.remove();
if (editorWindow.webviewApi === webviewApi) {
editorWindow.webviewApi = undefined;
}
};
}, [editor, window]);
}, [editor, containerWindow]);
};
export default useWebViewApi;