diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ca4a569c0f6..faffd00b753 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -126,8 +126,16 @@ export class CodeApplication { } }); - const isValidWebviewSource = (source: string) => - !source || (URI.parse(source.toLowerCase()).toString() as any).startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString()); + const isValidWebviewSource = (source: string): boolean => { + if (!source) { + return false; + } + if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') { + return true; + } + const srcUri: any = URI.parse(source.toLowerCase()).toString(); + return srcUri.startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString()); + }; app.on('web-contents-created', (_event: any, contents) => { contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => { diff --git a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts index 95a51e0fadc..43a5d963e96 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts @@ -52,6 +52,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Color } from 'vs/base/common/color'; import { WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; /** A context key that is set when an extension editor webview has focus. */ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS = new RawContextKey('extensionEditorWebviewFocus', undefined); @@ -62,13 +63,17 @@ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED = new /** A context key that is set when the find widget find input in extension editor webview is not focused. */ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED.toNegated(); -function renderBody(body: string): string { +function renderBody( + body: string, + environmentService: IEnvironmentService +): string { + const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://' + environmentService.appRoot, 'vscode-core-resource://'); return ` - - + + @@ -194,7 +199,9 @@ export class ExtensionEditor extends BaseEditor { @IPartService private partService: IPartService, @IContextViewService private contextViewService: IContextViewService, @IContextKeyService private contextKeyService: IContextKeyService, - @IExtensionTipsService private extensionTipsService: IExtensionTipsService + @IExtensionTipsService private extensionTipsService: IExtensionTipsService, + @IEnvironmentService private environmentService: IEnvironmentService + ) { super(ExtensionEditor.ID, telemetryService, themeService); this.disposables = []; @@ -408,12 +415,12 @@ export class ExtensionEditor extends BaseEditor { private openMarkdown(content: TPromise, noContentCopy: string) { return this.loadContents(() => content .then(marked.parse) - .then(renderBody) + .then(content => renderBody(content, this.environmentService)) .then(removeEmbeddedSVGs) .then(body => { const allowedBadgeProviders = this.extensionsWorkbenchService.allowedBadgeProviders; - const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : undefined; - this.activeWebview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions); + const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : {}; + this.activeWebview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions, false); const removeLayoutParticipant = arrays.insert(this.layoutParticipants, this.activeWebview); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index 9674b857ced..edee199e773 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -25,6 +25,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import Webview, { WebviewOptions } from './webview'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { WebviewEditor } from './webviewEditor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; /** @@ -46,13 +47,14 @@ export class HtmlPreviewPart extends WebviewEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, - @ITextModelService private textModelResolverService: ITextModelService, @IThemeService themeService: IThemeService, - @IOpenerService private readonly openerService: IOpenerService, - @IPartService private partService: IPartService, @IStorageService storageService: IStorageService, - @IContextViewService private _contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @IOpenerService private readonly openerService: IOpenerService, + @IPartService private readonly partService: IPartService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService ) { super(HtmlPreviewPart.ID, telemetryService, themeService, storageService, contextKeyService); } @@ -84,7 +86,7 @@ export class HtmlPreviewPart extends WebviewEditor { webviewOptions = this.input.options; } - this._webview = new Webview(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._contextViewService, this.contextKey, this.findInputFocusContextKey, webviewOptions); + this._webview = new Webview(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._environmentService, this._contextViewService, this.contextKey, this.findInputFocusContextKey, webviewOptions, true); if (this.input && this.input instanceof HtmlInput) { const state = this.loadViewState(this.input.getResource()); this.scrollYPercentage = state ? state.scrollYPercentage : 0; diff --git a/src/vs/workbench/parts/html/browser/webview-pre.js b/src/vs/workbench/parts/html/browser/webview-pre.js index ebb69c9bd46..17c09cdc32f 100644 --- a/src/vs/workbench/parts/html/browser/webview-pre.js +++ b/src/vs/workbench/parts/html/browser/webview-pre.js @@ -264,7 +264,7 @@ contentWindow.addEventListener('scroll', handleInnerScroll); pendingMessages.forEach(function (data) { - contentWindow.postMessage(data, document.location.origin); + contentWindow.postMessage(data, '*'); }); pendingMessages = []; } @@ -303,7 +303,7 @@ } else { const target = getActiveFrame(); if (target) { - target.contentWindow.postMessage(data, document.location.origin); + target.contentWindow.postMessage(data, '*'); } } }); diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index c4954e1f376..b46ee754598 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -14,6 +14,9 @@ import { ITheme, LIGHT, DARK } from 'vs/platform/theme/common/themeService'; import { WebviewFindWidget } from './webviewFindWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { normalize, join } from 'vs/base/common/paths'; +import { startsWith } from 'vs/base/common/strings'; export interface WebviewElementFindInPageOptions { forward?: boolean; @@ -39,8 +42,6 @@ export interface WebviewOptions { } export default class Webview { - private static index: number = 0; - private readonly _webview: Electron.WebviewTag; private _ready: Promise; private _disposables: IDisposable[] = []; @@ -55,13 +56,15 @@ export default class Webview { constructor( private readonly parent: HTMLElement, private readonly _styleElement: Element, - @IContextViewService private readonly _contextViewService: IContextViewService, + private readonly _environmentService: IEnvironmentService, + private readonly _contextViewService: IContextViewService, private readonly _contextKey: IContextKey, private readonly _findInputContextKey: IContextKey, - private _options: WebviewOptions = {}, + private _options: WebviewOptions, + useSameOriginForRoot: boolean ) { this._webview = document.createElement('webview'); - this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Webview.index++}`); + this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Date.now()}`); // disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick) this._webview.setAttribute('disableblinkfeatures', 'Auxclick'); @@ -75,7 +78,7 @@ export default class Webview { this._webview.style.outline = '0'; this._webview.preload = require.toUrl('./webview-pre.js'); - this._webview.src = require.toUrl('./webview.html'); + this._webview.src = useSameOriginForRoot ? require.toUrl('./webview.html') : 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E'; this._ready = new Promise(resolve => { const subscription = addDisposableListener(this._webview, 'ipc-message', (event) => { @@ -89,9 +92,24 @@ export default class Webview { }); }); + if (!useSameOriginForRoot) { + let loaded = false; + this._disposables.push(addDisposableListener(this._webview, 'did-start-loading', () => { + if (loaded) { + return; + } + loaded = true; + + const contents = this._webview.getWebContents(); + if (contents && !contents.isDestroyed()) { + registerFileProtocol(contents, 'vscode-core-resource', this._environmentService.appRoot); + } + })); + } + if (!this._options.allowSvgs) { let loaded = false; - const subscription = addDisposableListener(this._webview, 'did-start-loading', () => { + this._disposables.push(addDisposableListener(this._webview, 'did-start-loading', () => { if (loaded) { return; } @@ -124,9 +142,7 @@ export default class Webview { } return callback({ cancel: false, responseHeaders: details.responseHeaders }); }); - }); - - this._disposables.push(subscription); + })); } this._disposables.push( @@ -397,3 +413,24 @@ export default class Webview { this._webviewFindWidget.showPreviousFindTerm(); } } + +function registerFileProtocol( + contents: Electron.WebContents, + protocol: string, + root: string +) { + contents.session.protocol.registerFileProtocol(protocol, (request, callback: any) => { + const requestPath = URI.parse(request.url).path; + const normalizedPath = normalize(join(root, requestPath)); + if (startsWith(normalizedPath, root)) { + callback({ path: normalizedPath }); + } else { + callback({ error: 'Cannot load resource outside of protocol root' }); + } + }, (error) => { + if (error) { + console.error('Failed to register protocol ' + protocol); + } + }); +} + diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts index 27632cdde54..53e1440adbd 100644 --- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts @@ -29,14 +29,19 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; -function renderBody(body: string, css: string): string { +function renderBody( + body: string, + css: string, + environmentService: IEnvironmentService +): string { + const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://' + environmentService.appRoot, 'vscode-core-resource://'); return ` - - + + ${body} @@ -52,14 +57,14 @@ export class ReleaseNotesEditor extends WebviewEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, - @IEnvironmentService private environmentService: IEnvironmentService, @IThemeService protected themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IContextKeyService contextKeyService: IContextKeyService, + @IEnvironmentService private environmentService: IEnvironmentService, @IOpenerService private openerService: IOpenerService, @IModeService private modeService: IModeService, @IPartService private partService: IPartService, - @IStorageService storageService: IStorageService, - @IContextViewService private _contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextViewService private _contextViewService: IContextViewService ) { super(ReleaseNotesEditor.ID, telemetryService, themeService, storageService, contextKeyService); } @@ -99,8 +104,8 @@ export class ReleaseNotesEditor extends WebviewEditor { const colorMap = TokenizationRegistry.getColorMap(); const css = generateTokensCSSForColorMap(colorMap); - const body = renderBody(marked(text, { renderer }), css); - this._webview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._contextViewService, this.contextKey, this.findInputFocusContextKey); + const body = renderBody(marked(text, { renderer }), css, this.environmentService); + this._webview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this._contextViewService, this.contextKey, this.findInputFocusContextKey, {}, false); if (this.input && this.input instanceof ReleaseNotesInput) { const state = this.loadViewState(this.input.version); if (state) {