mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge vscode source through 1.62 release (#19981)
* Build breaks 1 * Build breaks * Build breaks * Build breaks * More build breaks * Build breaks (#2512) * Runtime breaks * Build breaks * Fix dialog location break * Update typescript * Fix ASAR break issue * Unit test breaks * Update distro * Fix breaks in ADO builds (#2513) * Bump to node 16 * Fix hygiene errors * Bump distro * Remove reference to node type * Delete vscode specific extension * Bump to node 16 in CI yaml * Skip integration tests in CI builds (while fixing) * yarn.lock update * Bump moment dependency in remote yarn * Fix drop-down chevron style * Bump to node 16 * Remove playwrite from ci.yaml * Skip building build scripts in hygine check
This commit is contained in:
@@ -4,16 +4,19 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as dompurify from 'vs/base/browser/dompurify/dompurify';
|
||||
import { DomEmitter } from 'vs/base/browser/event';
|
||||
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
import { raceCancellation } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels';
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import * as marked from 'vs/base/common/marked/marked';
|
||||
import { parse } from 'vs/base/common/marshalling';
|
||||
import { FileAccess, Schemas } from 'vs/base/common/network';
|
||||
@@ -27,24 +30,23 @@ export interface MarkedOptions extends marked.MarkedOptions {
|
||||
}
|
||||
|
||||
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
|
||||
codeBlockRenderer?: (modeId: string, value: string) => Promise<HTMLElement>;
|
||||
codeBlockRenderer?: (languageId: string, value: string) => Promise<HTMLElement>;
|
||||
asyncRenderCallback?: () => void;
|
||||
baseUrl?: URI;
|
||||
}
|
||||
|
||||
const _ttpInsane = window.trustedTypes?.createPolicy('insane', {
|
||||
createHTML(value, options: InsaneOptions): string {
|
||||
return insane(value, options);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Low-level way create a html element from a markdown string.
|
||||
*
|
||||
* **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/browser/core/markdownRenderer.ts)
|
||||
* which comes with support for pretty code block rendering and which uses the default way of handling links.
|
||||
*/
|
||||
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement {
|
||||
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): { element: HTMLElement, dispose: () => void } {
|
||||
const disposables = new DisposableStore();
|
||||
let isDisposed = false;
|
||||
|
||||
const cts = disposables.add(new CancellationTokenSource());
|
||||
|
||||
const element = createElement(options);
|
||||
|
||||
const _uriMassage = function (part: string): string {
|
||||
@@ -74,6 +76,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
}
|
||||
let uri = URI.revive(data);
|
||||
if (isDomUri) {
|
||||
if (href.startsWith(Schemas.data + ':')) {
|
||||
return href;
|
||||
}
|
||||
// this URI will end up as "src"-attribute of a dom node
|
||||
// and because of that special rewriting needs to be done
|
||||
// so that the URI uses a protocol that's understood by
|
||||
@@ -155,10 +160,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
}
|
||||
};
|
||||
renderer.paragraph = (text): string => {
|
||||
if (markdown.supportThemeIcons) {
|
||||
const elements = renderLabelWithIcons(text);
|
||||
text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
|
||||
}
|
||||
return `<p>${text}</p>`;
|
||||
};
|
||||
|
||||
@@ -168,26 +169,23 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
// when code-block rendering is async we return sync
|
||||
// but update the node with the real result later.
|
||||
const id = defaultGenerator.nextId();
|
||||
// {{SQL CARBON EDIT}} - Promise.all not returning the strValue properly in original code? @todo anthonydresser 4/12/19 investigate a better way to do this.
|
||||
const promise = value.then(strValue => {
|
||||
withInnerHTML.then(e => {
|
||||
raceCancellation(Promise.all([value, withInnerHTML]), cts.token).then(values => {
|
||||
if (!isDisposed && values) {
|
||||
const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
|
||||
if (span) {
|
||||
DOM.reset(span, strValue);
|
||||
DOM.reset(span, values[0]);
|
||||
}
|
||||
}).catch(err => {
|
||||
// ignore
|
||||
});
|
||||
options.asyncRenderCallback?.();
|
||||
}
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
});
|
||||
|
||||
if (options.asyncRenderCallback) {
|
||||
promise.then(options.asyncRenderCallback);
|
||||
}
|
||||
|
||||
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (options.actionHandler) {
|
||||
const onClick = options.actionHandler.disposables.add(new DomEmitter(element, 'click'));
|
||||
const onAuxClick = options.actionHandler.disposables.add(new DomEmitter(element, 'auxclick'));
|
||||
@@ -217,17 +215,21 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
}));
|
||||
}
|
||||
|
||||
// Use our own sanitizer so that we can let through only spans.
|
||||
// Otherwise, we'd be letting all html be rendered.
|
||||
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
|
||||
// We always pass the output through insane after this so that we don't rely on
|
||||
// marked for sanitization.
|
||||
markedOptions.sanitizer = (html: string): string => {
|
||||
const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
|
||||
return match ? html : '';
|
||||
};
|
||||
markedOptions.sanitize = true;
|
||||
markedOptions.silent = true;
|
||||
if (!markdown.supportHtml) {
|
||||
// TODO: Can we deprecated this in favor of 'supportHtml'?
|
||||
|
||||
// Use our own sanitizer so that we can let through only spans.
|
||||
// Otherwise, we'd be letting all html be rendered.
|
||||
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
|
||||
// We always pass the output through dompurify after this so that we don't rely on
|
||||
// marked for sanitization.
|
||||
markedOptions.sanitizer = (html: string): string => {
|
||||
const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
|
||||
return match ? html : '';
|
||||
};
|
||||
markedOptions.sanitize = true;
|
||||
markedOptions.silent = true;
|
||||
}
|
||||
|
||||
markedOptions.renderer = renderer;
|
||||
|
||||
@@ -241,10 +243,15 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
value = markdownEscapeEscapedIcons(value);
|
||||
}
|
||||
|
||||
const renderedMarkdown = marked.parse(value, markedOptions);
|
||||
let renderedMarkdown = marked.parse(value, markedOptions);
|
||||
|
||||
// sanitize with insane
|
||||
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as string;
|
||||
// Rewrite theme icons
|
||||
if (markdown.supportThemeIcons) {
|
||||
const elements = renderLabelWithIcons(renderedMarkdown);
|
||||
renderedMarkdown = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
|
||||
}
|
||||
|
||||
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as unknown as string;
|
||||
|
||||
// signal that async code blocks can be now be inserted
|
||||
signalInnerHTML!();
|
||||
@@ -252,26 +259,69 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
// signal size changes for image tags
|
||||
if (options.asyncRenderCallback) {
|
||||
for (const img of element.getElementsByTagName('img')) {
|
||||
const listener = DOM.addDisposableListener(img, 'load', () => {
|
||||
const listener = disposables.add(DOM.addDisposableListener(img, 'load', () => {
|
||||
listener.dispose();
|
||||
options.asyncRenderCallback!();
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return element;
|
||||
return {
|
||||
element,
|
||||
dispose: () => {
|
||||
isDisposed = true;
|
||||
cts.cancel();
|
||||
disposables.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeRenderedMarkdown(
|
||||
options: { isTrusted?: boolean },
|
||||
renderedMarkdown: string,
|
||||
): string | TrustedHTML {
|
||||
const insaneOptions = getInsaneOptions(options);
|
||||
return _ttpInsane?.createHTML(renderedMarkdown, insaneOptions) ?? insane(renderedMarkdown, insaneOptions);
|
||||
): TrustedHTML {
|
||||
const { config, allowedSchemes } = getSanitizerOptions(options);
|
||||
dompurify.addHook('uponSanitizeAttribute', (element, e) => {
|
||||
if (e.attrName === 'style' || e.attrName === 'class') {
|
||||
if (element.tagName === 'SPAN') {
|
||||
if (e.attrName === 'style') {
|
||||
e.keepAttr = /^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/.test(e.attrValue);
|
||||
return;
|
||||
} else if (e.attrName === 'class') {
|
||||
e.keepAttr = /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(e.attrValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// build an anchor to map URLs to
|
||||
const anchor = document.createElement('a');
|
||||
|
||||
// https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html
|
||||
dompurify.addHook('afterSanitizeAttributes', (node) => {
|
||||
// check all href/src attributes for validity
|
||||
for (const attr of ['href', 'src']) {
|
||||
if (node.hasAttribute(attr)) {
|
||||
anchor.href = node.getAttribute(attr) as string;
|
||||
if (!allowedSchemes.includes(anchor.protocol.replace(/:$/, ''))) {
|
||||
node.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true });
|
||||
} finally {
|
||||
dompurify.removeHook('uponSanitizeAttribute');
|
||||
dompurify.removeHook('afterSanitizeAttributes');
|
||||
}
|
||||
}
|
||||
|
||||
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
|
||||
function getSanitizerOptions(options: { readonly isTrusted?: boolean }): { config: dompurify.Config, allowedSchemes: string[] } {
|
||||
const allowedSchemes = [
|
||||
Schemas.http,
|
||||
Schemas.https,
|
||||
@@ -288,33 +338,16 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti
|
||||
}
|
||||
|
||||
return {
|
||||
allowedSchemes,
|
||||
// allowedTags should included everything that markdown renders to.
|
||||
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
|
||||
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
|
||||
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
|
||||
allowedTags: ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'name', 'target', 'data-href'],
|
||||
'img': ['src', 'title', 'alt', 'width', 'height'],
|
||||
'div': ['class', 'data-code'],
|
||||
'span': ['class', 'style'],
|
||||
// https://github.com/microsoft/vscode/issues/95937
|
||||
'th': ['align'],
|
||||
'td': ['align']
|
||||
config: {
|
||||
// allowedTags should included everything that markdown renders to.
|
||||
// Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure.
|
||||
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
|
||||
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
|
||||
ALLOWED_TAGS: ['ul', 'li', 'p', 'b', 'i', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
|
||||
ALLOWED_ATTR: ['href', 'data-href', 'target', 'title', 'src', 'alt', 'class', 'style', 'data-code', 'width', 'height', 'align'],
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
},
|
||||
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
|
||||
if (token.tag === 'span' && options.isTrusted) {
|
||||
if (token.attrs['style'] && (Object.keys(token.attrs).length === 1)) {
|
||||
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
|
||||
} else if (token.attrs['class']) {
|
||||
// The class should match codicon rendering in src\vs\base\common\codicons.ts
|
||||
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
allowedSchemes
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user