diff --git a/extensions/markdown/media/csp.js b/extensions/markdown/media/csp.js index f43b14c8d77..192cb907578 100644 --- a/extensions/markdown/media/csp.js +++ b/extensions/markdown/media/csp.js @@ -11,7 +11,7 @@ let didShow = false; - document.addEventListener('securitypolicyviolation', () => { + const showCspWarning = () => { if (didShow) { return; } @@ -28,5 +28,15 @@ notification.setAttribute('href', `command:markdown.showPreviewSecuritySelector?${encodeURIComponent(JSON.stringify(args))}`); document.body.appendChild(notification); + }; + + document.addEventListener('securitypolicyviolation', () => { + showCspWarning(); + }); + + window.addEventListener('message', (event) => { + if (event && event.data && event.data.name === 'vscode-did-block-svg') { + showCspWarning(); + } }); }()); \ No newline at end of file diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index d4b879e1619..aeaeb51962e 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -98,8 +98,8 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerDocumentLinkProvider('markdown', new DocumentLinkProvider())); - context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreview', showPreview)); - context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewToSide', uri => showPreview(uri, true))); + context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreview', (uri) => showPreview(cspArbiter, uri, false))); + context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewToSide', uri => showPreview(cspArbiter, uri, true))); context.subscriptions.push(vscode.commands.registerCommand('markdown.showSource', showSource)); context.subscriptions.push(vscode.commands.registerCommand('_markdown.revealLine', (uri, line) => { @@ -206,7 +206,7 @@ export function activate(context: vscode.ExtensionContext) { } -function showPreview(uri?: vscode.Uri, sideBySide: boolean = false) { +function showPreview(cspArbiter: ExtensionContentSecurityPolicyArbiter, uri?: vscode.Uri, sideBySide: boolean = false) { let resource = uri; if (!(resource instanceof vscode.Uri)) { if (vscode.window.activeTextEditor) { @@ -228,7 +228,10 @@ function showPreview(uri?: vscode.Uri, sideBySide: boolean = false) { getMarkdownUri(resource), getViewColumn(sideBySide), `Preview '${path.basename(resource.fsPath)}'`, - { allowScripts: true, allowSvgs: true }); + { + allowScripts: true, + allowSvgs: cspArbiter.shouldAllowSvgsForResource(resource) + }); if (telemetryReporter) { telemetryReporter.sendTelemetryEvent('openPreview', { diff --git a/extensions/markdown/src/security.ts b/extensions/markdown/src/security.ts index 5011d5feb47..b3776e4087b 100644 --- a/extensions/markdown/src/security.ts +++ b/extensions/markdown/src/security.ts @@ -22,6 +22,8 @@ export interface ContentSecurityPolicyArbiter { getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel; setSecurityLevelForResource(resource: vscode.Uri, level: MarkdownPreviewSecurityLevel): Thenable; + + shouldAllowSvgsForResource(resource: vscode.Uri): void; } export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPolicyArbiter { @@ -50,6 +52,11 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol return this.globalState.update(this.security_level_key + this.getRoot(resource), level); } + public shouldAllowSvgsForResource(resource: vscode.Uri) { + const securityLevel = this.getSecurityLevelForResource(resource); + return securityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent || securityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent; + } + private getRoot(resource: vscode.Uri): vscode.Uri { if (vscode.workspace.workspaceFolders) { const folderForResource = vscode.workspace.getWorkspaceFolder(resource); @@ -123,6 +130,14 @@ export class PreviewSecuritySelector { await this.cspArbiter.setSecurityLevelForResource(resource, selection.level); const sourceUri = getMarkdownUri(resource); + + await vscode.commands.executeCommand('_workbench.htmlPreview.updateOptions', + sourceUri, + { + allowScripts: true, + allowSvgs: this.cspArbiter.shouldAllowSvgsForResource(resource) + }); + this.contentProvider.update(sourceUri); } } diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index ddb472e6a37..40b498fa282 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -21,6 +21,14 @@ import { IEditorGroupService } from 'vs/workbench/services/group/common/groupSer import { MenuRegistry } from "vs/platform/actions/common/actions"; import { WebviewElement } from "vs/workbench/parts/html/browser/webview"; +function getActivePreviewsForResource(accessor: ServicesAccessor, resource: URI | string) { + const uri = resource instanceof URI ? resource : URI.parse(resource); + return accessor.get(IWorkbenchEditorService).getVisibleEditors() + .filter(c => c instanceof HtmlPreviewPart && c.model) + .map(e => e as HtmlPreviewPart) + .filter(e => e.model.uri.scheme === uri.scheme && e.model.uri.fsPath === uri.fsPath); +} + // --- Register Editor (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(HtmlPreviewPart.ID, localize('html.editor.label', "Html Preview"), @@ -70,17 +78,23 @@ CommandsRegistry.registerCommand('_workbench.previewHtml', function ( }); CommandsRegistry.registerCommand('_workbench.htmlPreview.postMessage', (accessor: ServicesAccessor, resource: URI | string, message: any) => { - const uri = resource instanceof URI ? resource : URI.parse(resource); - const activePreviews = accessor.get(IWorkbenchEditorService).getVisibleEditors() - .filter(c => c instanceof HtmlPreviewPart && c.model) - .map(e => e as HtmlPreviewPart) - .filter(e => e.model.uri.scheme === uri.scheme && e.model.uri.fsPath === uri.fsPath); + const activePreviews = getActivePreviewsForResource(accessor, resource); for (const preview of activePreviews) { preview.sendMessage(message); } return activePreviews.length > 0; }); +CommandsRegistry.registerCommand('_workbench.htmlPreview.updateOptions', (accessor: ServicesAccessor, resource: URI | string, options: HtmlInputOptions) => { + const uri = resource instanceof URI ? resource : URI.parse(resource); + const activePreviews = getActivePreviewsForResource(accessor, resource); + for (const preview of activePreviews) { + if (preview.input && preview.input instanceof HtmlInput) { + const input = accessor.get(IInstantiationService).createInstance(HtmlInput, preview.input.getName(), '', uri, options); + preview.setInput(input); + } + } +}); CommandsRegistry.registerCommand('_webview.openDevTools', function () { const elements = document.querySelectorAll('webview.ready'); diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index 3d956d9044f..e982a70facd 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -14,7 +14,7 @@ import { EditorOptions, EditorInput } from 'vs/workbench/common/editor'; import { Position } from 'vs/platform/editor/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; -import { HtmlInput } from 'vs/workbench/parts/html/common/htmlInput'; +import { HtmlInput, HtmlInputOptions, areHtmlInputOptionsEqual } from 'vs/workbench/parts/html/common/htmlInput'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -182,7 +182,10 @@ export class HtmlPreviewPart extends WebviewEditor { return TPromise.as(undefined); } + let oldOptions: HtmlInputOptions | undefined = undefined; + if (this.input instanceof HtmlInput) { + oldOptions = this.input.options; this.saveViewState(this.input.getResource(), { scrollYPercentage: this.scrollYPercentage }); @@ -210,6 +213,10 @@ export class HtmlPreviewPart extends WebviewEditor { return TPromise.wrapError(new Error(localize('html.voidInput', "Invalid editor input."))); } + if (oldOptions && !areHtmlInputOptionsEqual(oldOptions, input.options)) { + this._doSetVisible(false); + } + this._modelChangeSubscription = this.model.onDidChangeContent(() => { if (this.model) { this.scrollYPercentage = 0; diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index 59d06e85c47..44cb9a0bdb6 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -122,6 +122,7 @@ export default class Webview { if (details.url.indexOf('.svg') > 0) { const uri = URI.parse(details.url); if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg') && !this.isAllowedSvg(uri)) { + this.onDidBlockSvg(); return callback({ cancel: true }); } } @@ -133,6 +134,7 @@ export default class Webview { if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { const uri = URI.parse(details.url); if (uri && !this.isAllowedSvg(uri)) { + this.onDidBlockSvg(); return callback({ cancel: true }); } } @@ -195,7 +197,6 @@ export default class Webview { parent.appendChild(this._webviewFindWidget.getDomNode()); parent.appendChild(this._webview); } - } public notifyFindWidgetFocusChanged(isFocused: boolean) { @@ -259,6 +260,12 @@ export default class Webview { this._send('message', data); } + private onDidBlockSvg() { + this.sendMessage({ + name: 'vscode-did-block-svg' + }); + } + style(theme: ITheme): void { const { fontFamily, fontWeight, fontSize } = window.getComputedStyle(this._styleElement); // TODO@theme avoid styleElement diff --git a/src/vs/workbench/parts/html/common/htmlInput.ts b/src/vs/workbench/parts/html/common/htmlInput.ts index a8d21c8fbee..34b00ed9152 100644 --- a/src/vs/workbench/parts/html/common/htmlInput.ts +++ b/src/vs/workbench/parts/html/common/htmlInput.ts @@ -14,6 +14,10 @@ export interface HtmlInputOptions { allowSvgs?: boolean; } +export function areHtmlInputOptionsEqual(left: HtmlInputOptions, right: HtmlInputOptions) { + return left.allowScripts === right.allowScripts && left.allowSvgs === right.allowSvgs; +} + export class HtmlInput extends ResourceEditorInput { constructor( name: string, @@ -24,4 +28,12 @@ export class HtmlInput extends ResourceEditorInput { ) { super(name, description, resource, textModelResolverService); } + + public matches(otherInput: any): boolean { + if (!super.matches(otherInput)) { + return false; + } + + return otherInput instanceof HtmlInput && areHtmlInputOptionsEqual(this.options, otherInput.options); + } }