Skip to content

Commit a8cf19d

Browse files
committed
use tt policy when rendering html from markdown, microsoft#106396
1 parent 93a1dab commit a8cf19d

3 files changed

Lines changed: 101 additions & 89 deletions

File tree

.vscode/searches/TrustedTypes.code-search

Lines changed: 58 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,27 @@
44
# Excluding: *.test.ts
55
# ContextLines: 3
66

7-
22 results - 14 files
7+
22 results - 15 files
8+
9+
src/vs/base/browser/dom.ts:
10+
26 // this is a workaround for innerHTML not allowing for "asymetric" accessors
11+
27 // see https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393
12+
28 // and https://github.com/microsoft/TypeScript/issues/30024
13+
29: node.innerHTML = value as unknown as string;
14+
30 }
15+
31
16+
32 export function isInDOM(node: Node | null): boolean {
817

918
src/vs/base/browser/markdownRenderer.ts:
10-
161 const strValue = values[0];
11-
162 const span = element.querySelector(`div[data-code="${id}"]`);
12-
163 if (span) {
13-
164: span.innerHTML = strValue;
14-
165 }
15-
166 }).catch(err => {
16-
167 // ignore
17-
18-
243 return true;
19-
244 }
20-
245
21-
246: element.innerHTML = insane(renderedMarkdown, {
22-
247 allowedSchemes,
23-
248 // allowedTags should included everything that markdown renders to.
24-
249 // Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
19+
273 };
20+
274
21+
275 if (_ttpInsane) {
22+
276: element.innerHTML = _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
23+
277 } else {
24+
278: element.innerHTML = insane(renderedMarkdown, insaneOptions);
25+
279 }
26+
280
27+
281 // signal that async code blocks can be now be inserted
2528

2629
src/vs/base/browser/ui/contextview/contextview.ts:
2730
157 this.shadowRootHostElement = DOM.$('.shadow-root-host');
@@ -50,6 +53,15 @@ src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts:
5053
326 document.head.appendChild(styleTag);
5154
327 }
5255

56+
src/vs/editor/browser/core/markdownRenderer.ts:
57+
88
58+
89 const element = document.createElement('span');
59+
90
60+
91: element.innerHTML = MarkdownRenderer._ttpTokenizer
61+
92 ? MarkdownRenderer._ttpTokenizer.createHTML(value, tokenization) as unknown as string
62+
93 : tokenizeToString(value, tokenization);
63+
94
64+
5365
src/vs/editor/browser/view/domLineBreaksComputer.ts:
5466
107 allCharOffsets[i] = tmp[0];
5567
108 allVisibleColumns[i] = tmp[1];
@@ -60,21 +72,21 @@ src/vs/editor/browser/view/domLineBreaksComputer.ts:
6072
113 containerDomNode.style.top = '10000';
6173

6274
src/vs/editor/browser/view/viewLayer.ts:
63-
507 private _finishRenderingNewLines(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, newLinesHTML: string, wasNew: boolean[]): void {
64-
508 const lastChild = <HTMLElement>this.domNode.lastChild;
65-
509 if (domNodeIsEmpty || !lastChild) {
66-
510: this.domNode.innerHTML = newLinesHTML;
67-
511 } else {
68-
512 lastChild.insertAdjacentHTML('afterend', newLinesHTML);
69-
513 }
70-
71-
525 private _finishRenderingInvalidLines(ctx: IRendererContext<T>, invalidLinesHTML: string, wasInvalid: boolean[]): void {
72-
526 const hugeDomNode = document.createElement('div');
73-
527
74-
528: hugeDomNode.innerHTML = invalidLinesHTML;
75-
529
76-
530 for (let i = 0; i < ctx.linesLength; i++) {
77-
531 const line = ctx.lines[i];
75+
512 }
76+
513 const lastChild = <HTMLElement>this.domNode.lastChild;
77+
514 if (domNodeIsEmpty || !lastChild) {
78+
515: this.domNode.innerHTML = newLinesHTML;
79+
516 } else {
80+
517 lastChild.insertAdjacentHTML('afterend', newLinesHTML);
81+
518 }
82+
83+
530 private _finishRenderingInvalidLines(ctx: IRendererContext<T>, invalidLinesHTML: string, wasInvalid: boolean[]): void {
84+
531 const hugeDomNode = document.createElement('div');
85+
532
86+
533: hugeDomNode.innerHTML = invalidLinesHTML;
87+
534
88+
535 for (let i = 0; i < ctx.linesLength; i++) {
89+
536 const line = ctx.lines[i];
7890

7991
src/vs/editor/browser/widget/diffEditorWidget.ts:
8092
2157
@@ -141,23 +153,6 @@ src/vs/editor/test/browser/controller/imeTester.ts:
141153
74
142154
75 let startBtn = document.createElement('button');
143155

144-
src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts:
145-
455
146-
456 private getMarkdownDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement {
147-
457 const dragImageContainer = DOM.$('.cell-drag-image.monaco-list-row.focused.markdown-cell-row');
148-
458: dragImageContainer.innerHTML = templateData.container.outerHTML;
149-
459
150-
460 // Remove all rendered content nodes after the
151-
461 const markdownContent = dragImageContainer.querySelector('.cell.markdown')!;
152-
153-
641 return null;
154-
642 }
155-
643
156-
644: editorContainer.innerHTML = richEditorText;
157-
645
158-
646 return dragImageContainer;
159-
647 }
160-
161156
src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts:
162157
375 addMouseoverListeners(outputNode, outputId);
163158
376 const content = data.content;
@@ -177,18 +172,18 @@ src/vs/workbench/contrib/webview/browser/pre/main.js:
177172
399 applyStyles(newDocument, newDocument.body);
178173

179174
src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts:
180-
281
181-
282 const content = model.main.textEditorModel.getValue(EndOfLinePreference.LF);
182-
283 if (!input.resource.path.endsWith('.md')) {
183-
284: this.content.innerHTML = content;
184-
285 this.updateSizeClasses();
185-
286 this.decorateContent();
186-
287 this.contentDisposables.push(this.keybindingService.onDidUpdateKeybindings(() => this.decorateContent()));
187-
188-
303 const innerContent = document.createElement('div');
189-
304 innerContent.classList.add('walkThroughContent'); // only for markdown files
190-
305 const markdown = this.expandMacros(content);
191-
306: innerContent.innerHTML = marked(markdown, { renderer });
192-
307 this.content.appendChild(innerContent);
193-
308
194-
309 model.snippets.forEach((snippet, i) => {
175+
280
176+
281 const content = model.main;
177+
282 if (!input.resource.path.endsWith('.md')) {
178+
283: this.content.innerHTML = content;
179+
284 this.updateSizeClasses();
180+
285 this.decorateContent();
181+
286 this.contentDisposables.push(this.keybindingService.onDidUpdateKeybindings(() => this.decorateContent()));
182+
183+
302 const innerContent = document.createElement('div');
184+
303 innerContent.classList.add('walkThroughContent'); // only for markdown files
185+
304 const markdown = this.expandMacros(content);
186+
305: innerContent.innerHTML = marked(markdown, { renderer });
187+
306 this.content.appendChild(innerContent);
188+
307
189+
308 model.snippets.forEach((snippet, i) => {

src/vs/base/browser/markdownRenderer.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
99
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
1010
import { defaultGenerator } from 'vs/base/common/idGenerator';
1111
import * as marked from 'vs/base/common/marked/marked';
12-
import { insane } from 'vs/base/common/insane/insane';
12+
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
1313
import { parse } from 'vs/base/common/marshalling';
1414
import { cloneAndChange } from 'vs/base/common/objects';
1515
import { escape } from 'vs/base/common/strings';
@@ -30,6 +30,12 @@ export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
3030
baseUrl?: URI;
3131
}
3232

33+
const _ttpInsane = window.trustedTypes?.createPolicy('insane', {
34+
createHTML(value, options: InsaneOptions): string {
35+
return insane(value, options);
36+
}
37+
});
38+
3339
/**
3440
* Low-level way create a html element from a markdown string.
3541
*
@@ -227,25 +233,16 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
227233
if (value.length > 100_000) {
228234
value = `${value.substr(0, 100_000)}…`;
229235
}
230-
const renderedMarkdown = marked.parse(
231-
markdown.supportThemeIcons ? markdownEscapeEscapedCodicons(value) : value,
232-
markedOptions
233-
);
234-
235-
function filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean {
236-
if (token.tag === 'span' && markdown.isTrusted && (Object.keys(token.attrs).length === 1)) {
237-
if (token.attrs['style']) {
238-
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
239-
} else if (token.attrs['class']) {
240-
// The class should match codicon rendering in src\vs\base\common\codicons.ts
241-
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
242-
}
243-
return false;
244-
}
245-
return true;
236+
// escape theme icons
237+
if (markdown.supportThemeIcons) {
238+
value = markdownEscapeEscapedCodicons(value);
246239
}
247240

248-
element.innerHTML = insane(renderedMarkdown, {
241+
const renderedMarkdown = marked.parse(value, markedOptions);
242+
243+
244+
// sanitize with insane
245+
const insaneOptions = {
249246
allowedSchemes,
250247
// allowedTags should included everything that markdown renders to.
251248
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
@@ -261,9 +258,27 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
261258
'th': ['align'],
262259
'td': ['align']
263260
},
264-
filter
265-
});
261+
filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean {
262+
if (token.tag === 'span' && markdown.isTrusted && (Object.keys(token.attrs).length === 1)) {
263+
if (token.attrs['style']) {
264+
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
265+
} else if (token.attrs['class']) {
266+
// The class should match codicon rendering in src\vs\base\common\codicons.ts
267+
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
268+
}
269+
return false;
270+
}
271+
return true;
272+
}
273+
};
274+
275+
if (_ttpInsane) {
276+
element.innerHTML = _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
277+
} else {
278+
element.innerHTML = insane(renderedMarkdown, insaneOptions);
279+
}
266280

281+
// signal that async code blocks can be now be inserted
267282
signalInnerHTML!();
268283

269284
return element;

src/vs/base/common/insane/insane.d.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
export interface InsaneOptions {
7+
readonly allowedSchemes?: readonly string[],
8+
readonly allowedTags?: readonly string[],
9+
readonly allowedAttributes?: { readonly [key: string]: string[] },
10+
readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean,
11+
}
12+
613
export function insane(
714
html: string,
8-
options?: {
9-
readonly allowedSchemes?: readonly string[],
10-
readonly allowedTags?: readonly string[],
11-
readonly allowedAttributes?: { readonly [key: string]: string[] },
12-
readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean,
13-
},
15+
options?: InsaneOptions,
1416
strict?: boolean,
1517
): string;

0 commit comments

Comments
 (0)