From 3866c3553be8b268c8a7f8c0482c0c0177aa8bfa Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 18 Aug 2021 16:34:31 -0700 Subject: [PATCH] Use insane with notebook markdown content (#131134) Runs insane against markdown content. Also requires hooking up a way for renderers to detect if the workspace is trusted or not --- .../notebook/index.ts | 48 +++++++++++++++- .../notebook/insane.d.ts | 19 +++++++ .../markdown-language-features/package.json | 1 + .../markdown-language-features/yarn.lock | 18 ++++++ .../view/renderers/backLayerWebView.ts | 17 +++++- .../browser/view/renderers/webviewMessages.ts | 8 ++- .../browser/view/renderers/webviewPreloads.ts | 56 +++++++++++++++---- 7 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 extensions/markdown-language-features/notebook/insane.d.ts diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index 699bd2129ad7c..81eae8963955e 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -4,8 +4,48 @@ *--------------------------------------------------------------------------------------------*/ const MarkdownIt = require('markdown-it'); +const insane = require('insane'); +import type { InsaneOptions } from 'insane'; + +function _extInsaneOptions(opts: InsaneOptions, allowedAttributesForAll: string[]): InsaneOptions { + const allowedAttributes: Record = opts.allowedAttributes ?? {}; + if (opts.allowedTags) { + for (const tag of opts.allowedTags) { + let array = allowedAttributes[tag]; + if (!array) { + array = allowedAttributesForAll; + } else { + array = array.concat(allowedAttributesForAll); + } + allowedAttributes[tag] = array; + } + } + + return { ...opts, allowedAttributes }; +} -export function activate() { +const insaneOptions: InsaneOptions = _extInsaneOptions({ + allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], + allowedAttributes: { + 'a': ['href', 'x-dispatch'], + 'button': ['data-href', 'x-dispatch'], + 'img': ['src'], + 'input': ['type', 'placeholder', 'checked', 'required'], + 'label': ['for'], + 'select': ['required'], + 'span': ['data-command', 'role'], + 'textarea': ['name', 'placeholder', 'required'], + }, + allowedSchemes: ['http', 'https'] +}, [ + 'align', + 'class', + 'id', + 'style', + 'aria-hidden', +]); + +export function activate(ctx: { workspace: { isTrusted: boolean } }) { let markdownIt = new MarkdownIt({ html: true }); @@ -172,8 +212,10 @@ export function activate() { } else { previewNode.classList.remove('emptyMarkdownCell'); - const rendered = markdownIt.render(text); - previewNode.innerHTML = rendered; + const unsanitizedRenderedMarkdown = markdownIt.render(text); + previewNode.innerHTML = ctx.workspace.isTrusted + ? unsanitizedRenderedMarkdown + : insane(unsanitizedRenderedMarkdown, insaneOptions); } }, extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { diff --git a/extensions/markdown-language-features/notebook/insane.d.ts b/extensions/markdown-language-features/notebook/insane.d.ts new file mode 100644 index 0000000000000..84db6f6ecff11 --- /dev/null +++ b/extensions/markdown-language-features/notebook/insane.d.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'insane' { + export interface InsaneOptions { + readonly allowedSchemes?: readonly string[], + readonly allowedTags?: readonly string[], + readonly allowedAttributes?: { readonly [key: string]: string[] }, + readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean, + } + + export function insane( + html: string, + options?: InsaneOptions, + strict?: boolean, + ): string; +} diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f9750857b35ef..969f6edf80fae 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -353,6 +353,7 @@ }, "dependencies": { "highlight.js": "^10.4.1", + "insane": "^2.6.2", "markdown-it": "^12.0.3", "markdown-it-front-matter": "^0.2.1", "vscode-extension-telemetry": "0.2.6", diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 1ecfce6a73bee..2d35c914c618e 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -36,16 +36,34 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +assignment@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.0.0.tgz#ffd17b21bf5d6b22e777b989681a815456a3dd3e" + integrity sha1-/9F7Ib9dayLnd7mJaBqBVFaj3T4= + entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +he@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" + integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI= + highlight.js@*, highlight.js@^10.4.1: version "10.4.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== +insane@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/insane/-/insane-2.6.2.tgz#c2ab68bb3e006ab451560d1b446917329c0a8120" + integrity sha1-wqtouz4AarRRVg0bRGkXMpwKgSA= + dependencies: + assignment "2.0.0" + he "0.5.0" + linkify-it@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 5853965776485..0ae0871b060e7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -24,6 +24,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; @@ -102,6 +103,7 @@ export class BackLayerWebView extends Disposable { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, ) { super(); @@ -127,6 +129,13 @@ export class BackLayerWebView extends Disposable { return Promise.resolve(true); }; } + + this._register(workspaceTrustManagementService.onDidChangeTrust(e => { + this._sendMessageToWebview({ + type: 'updateWorkspaceTrust', + isTrusted: e, + }); + })); } updateOptions(options: { @@ -182,6 +191,12 @@ export class BackLayerWebView extends Disposable { private generateContent(baseUrl: string) { const renderersData = this.getRendererData(); + const preloadScript = preloadsScriptStr( + this.options, + { dragAndDropEnabled: this.options.dragAndDropEnabled }, + renderersData, + this.workspaceTrustManagementService.isWorkspaceTrusted()); + return html` @@ -302,7 +317,7 @@ export class BackLayerWebView extends Disposable {
- + `; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index ba9f9488d7076..a5f070af6b0a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -326,6 +326,11 @@ export interface INotebookOptionsMessage { readonly options: PreloadOptions; } +export interface INotebookUpdateWorkspaceTrust { + readonly type: 'updateWorkspaceTrust'; + readonly isTrusted: boolean; +} + export type FromWebviewMessage = WebviewIntialized | IDimensionMessage | IMouseEnterMessage | @@ -373,6 +378,7 @@ export type ToWebviewMessage = IClearMessage | IUpdateSelectedMarkupCellsMessage | IInitializeMarkupCells | INotebookStylesMessage | - INotebookOptionsMessage; + INotebookOptionsMessage | + INotebookUpdateWorkspaceTrust; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index a3b61d4168889..4b102e176245a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -43,10 +43,18 @@ export interface PreloadOptions { dragAndDropEnabled: boolean; } +interface PreloadContext { + readonly style: PreloadStyles; + readonly options: PreloadOptions; + readonly rendererData: readonly RendererMetadata[]; + readonly isWorkspaceTrusted: boolean; +} + declare function __import(path: string): Promise; -async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, rendererData: readonly RendererMetadata[]) { - let currentOptions = options; +async function webviewPreloads(ctx: PreloadContext) { + let currentOptions = ctx.options; + let isWorkspaceTrusted = ctx.isWorkspaceTrusted; const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); @@ -133,6 +141,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re getRenderer(id: string): Promise; postMessage?(message: unknown): void; onDidReceiveMessage?: Event; + readonly workspace: { readonly isTrusted: boolean }; } interface ScriptModule { @@ -207,7 +216,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re if (entry.target.id === observedElementInfo.id && entry.contentRect) { if (observedElementInfo.output) { if (entry.contentRect.height !== 0) { - entry.target.style.padding = `${style.outputNodePadding}px 0 ${style.outputNodePadding}px 0`; + entry.target.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`; } else { entry.target.style.padding = `0px`; } @@ -549,12 +558,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re if (offsetHeight !== 0 && cps.padding === '0px') { // we set padding to zero if the output height is zero (then we can have a zero-height output DOM node) // thus we need to ensure the padding is accounted when updating the init height of the output - dimensionUpdater.updateHeight(outputId, offsetHeight + style.outputNodePadding * 2, { + dimensionUpdater.updateHeight(outputId, offsetHeight + ctx.style.outputNodePadding * 2, { isOutput: true, init: true, }); - outputNode.style.padding = `${style.outputNodePadding}px 0 ${style.outputNodePadding}px 0`; + outputNode.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`; } else { dimensionUpdater.updateHeight(outputId, outputNode.offsetHeight, { isOutput: true, @@ -657,6 +666,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re currentOptions = event.data.options; viewModel.toggleDragDropEnabled(currentOptions.dragAndDropEnabled); break; + + case 'updateWorkspaceTrust': { + isWorkspaceTrusted = event.data.isTrusted; + viewModel.rerenderMarkupCells(); + break; + } } }); @@ -700,6 +715,9 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re // TODO: This is async so that we can return a promise to the API in the future. // Currently the API is always resolved before we call `createRendererContext`. getRenderer: async (id: string) => renderers.getRenderer(id)?.api, + workspace: { + get isTrusted() { return isWorkspaceTrusted; } + } }; if (messaging) { @@ -722,7 +740,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re // Squash any errors extends errors. They won't prevent the renderer // itself from working, so just log them. - await Promise.all(rendererData + await Promise.all(ctx.rendererData .filter(d => d.extends === this.data.id) .map(d => this.loadExtension(d.id).catch(console.error)), ); @@ -807,7 +825,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re private readonly _renderers = new Map(); constructor() { - for (const renderer of rendererData) { + for (const renderer of ctx.rendererData) { this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => { const ext = this._renderers.get(extensionId); if (!ext) { @@ -936,6 +954,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re cell?.unhide(); } + public rerenderMarkupCells() { + for (const cell of this._markupCells.values()) { + cell.rerender(); + } + } + private getExpectedMarkupCell(id: string): MarkupCell | undefined { const cell = this._markupCells.get(id); if (!cell) { @@ -1166,6 +1190,10 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re } } + public rerender() { + this.updateContentAndRender(this._content); + } + public hide() { this.element.style.visibility = 'hidden'; } @@ -1409,14 +1437,18 @@ export interface RendererMetadata { readonly messaging: boolean; } -export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[]) { - // TS will try compiling `import()` in webviePreloads, so use an helper function instead +export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[], isWorkspaceTrusted: boolean) { + const ctx: PreloadContext = { + style: styleValues, + options, + rendererData: renderers, + isWorkspaceTrusted + }; + // TS will try compiling `import()` in webviewPreloads, so use an helper function instead // of using `import(...)` directly return ` const __import = (x) => import(x); (${webviewPreloads})( - JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(styleValues))}")), - JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(options))}")), - JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(renderers))}")) + JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")) )\n//# sourceURL=notebookWebviewPreloads.js\n`; }