Add vscode-webview-resource protocol (#97777)

* Add vscode-webview-resource protocol

Adds a new protocol (`vscode-webview-resource`) for loading resources insides of webviews. This replaces the existing `vscode-resource` protocol and is registered on the main thread instead of in each renderer

This change also adds some rewriting logic to update any `vscode-resource:` references found in the main html to use `vscode-webview-resource` instead.

* Move webview protcol provider to own file

* Remove registration of vscode-resource scheme

* Remove use or parition for each webview

Now that we have a single shared protocol handler, we do not need to run each webview in its own partition

* Fix rewriting csp to use new protocol

* Update src/vs/code/electron-main/app.ts

Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>

Co-authored-by: Benjamin Pasero <benjpas@microsoft.com>
This commit is contained in:
Matt Bierner 2020-05-15 11:34:08 -07:00 committed by GitHub
parent 946d994d1b
commit 307cb32f30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 220 additions and 130 deletions

View file

@ -79,7 +79,15 @@ setCurrentWorkingDirectory();
// Register custom schemes with privileges
protocol.registerSchemesAsPrivileged([
{ scheme: 'vscode-resource', privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } }
{
scheme: 'vscode-webview-resource',
privileges: {
secure: true,
standard: true,
supportFetchAPI: true,
corsEnabled: true,
}
},
]);
// Global app listeners

View file

@ -59,6 +59,8 @@ export namespace Schemas {
export const vscodeSettings = 'vscode-settings';
export const webviewPanel = 'webview-panel';
export const vscodeWebviewResource = 'vscode-webview-resource';
}
class RemoteAuthoritiesImpl {

View file

@ -81,6 +81,8 @@ import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common
import { StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels';
import { IFileService } from 'vs/platform/files/common/files';
import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider';
export class CodeApplication extends Disposable {
private windowsMainService: IWindowsMainService | undefined;
@ -89,6 +91,7 @@ export class CodeApplication extends Disposable {
constructor(
private readonly mainIpcServer: Server,
private readonly userEnv: IProcessEnvironment,
@IFileService fileService: IFileService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ -99,6 +102,8 @@ export class CodeApplication extends Disposable {
super();
this.registerListeners();
this._register(new WebviewProtocolProvider(fileService));
}
private registerListeners(): void {

View file

@ -8,10 +8,9 @@ import { sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { getWebviewContentMimeType } from 'vs/workbench/contrib/webview/common/mimeTypes';
import { isUNC } from 'vs/base/common/extpath';
export const WebviewResourceScheme = 'vscode-resource';
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
import { Schemas } from 'vs/base/common/network';
export namespace WebviewResourceResponse {
export enum Type { Success, Failed, AccessDenied }
@ -77,7 +76,7 @@ export async function loadLocalResource(
}
function normalizeRequestPath(requestUri: URI) {
if (requestUri.scheme !== WebviewResourceScheme) {
if (requestUri.scheme !== Schemas.vscodeWebviewResource) {
return requestUri;
}

View file

@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcMain as ipc, IpcMainEvent, MimeTypedBuffer, protocol } from 'electron';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
export interface RegisterWebviewMetadata {
readonly extensionLocation: URI | undefined;
readonly localResourceRoots: readonly URI[];
}
type ErrorCallback = (response: MimeTypedBuffer | { error: number }) => void;
export class WebviewProtocolProvider extends Disposable {
private readonly webviewMetadata = new Map<string, {
readonly extensionLocation: URI | undefined;
readonly localResourceRoots: URI[];
}>();
constructor(
@IFileService private readonly fileService: IFileService,
) {
super();
ipc.on('vscode:registerWebview', (event: IpcMainEvent, id: string, data: RegisterWebviewMetadata) => {
this.webviewMetadata.set(id, {
extensionLocation: data.extensionLocation ? URI.from(data.extensionLocation) : undefined,
localResourceRoots: data.localResourceRoots.map((x: UriComponents) => URI.from(x)),
});
event.sender.send(`vscode:didRegisterWebview-${id}`);
});
ipc.on('vscode:unregisterWebview', (_event: IpcMainEvent, id: string) => {
this.webviewMetadata.delete(id);
});
protocol.registerBufferProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
try {
const uri = URI.parse(request.url);
const resource = URI.parse(uri.path.replace(/^\/(\w+)/, '$1:'));
const id = uri.authority;
const metadata = this.webviewMetadata.get(id);
if (metadata) {
const result = await loadLocalResource(resource, this.fileService, metadata.extensionLocation, () => metadata.localResourceRoots);
if (result.type === WebviewResourceResponse.Type.Success) {
return callback({
data: Buffer.from(result.data.buffer),
mimeType: result.mimeType
});
}
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
console.error('Webview: Cannot load resource outside of protocol root');
return (callback as ErrorCallback)({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
}
}
} catch {
// noop
}
return (callback as ErrorCallback)({ error: -2 });
});
this._register(toDisposable(() => protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
}
}

2
src/vs/vscode.d.ts vendored
View file

@ -6610,7 +6610,7 @@ declare module 'vscode' {
readonly enableCommandUris?: boolean;
/**
* Root paths from which the webview can load local (filesystem) resources using the `vscode-resource:` scheme.
* Root paths from which the webview can load local (filesystem) resources using uris from `asWebviewUri`
*
* Default to the root folders of the current workspace plus the extension's install directory.
*

View file

@ -94,8 +94,7 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
}, {
allowScripts: options.enableScripts,
localResourceRoots: options.localResourceRoots ? options.localResourceRoots.map(uri => URI.revive(uri)) : undefined
});
webview.extension = { id: extensionId, location: URI.revive(extensionLocation) };
}, { id: extensionId, location: URI.revive(extensionLocation) });
const webviewZone = new EditorWebviewZone(editor, line, height, webview);

View file

@ -97,9 +97,8 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory {
const webview = webviewService.createWebviewOverlay(data.id, {
enableFindWidget: data.options.enableFindWidget,
retainContextWhenHidden: data.options.retainContextWhenHidden
}, data.options);
}, data.options, data.extension);
webview.state = data.state;
webview.extension = data.extension;
return webview;
});
}

View file

@ -231,7 +231,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
const id = generateUuid();
const webview = new Lazy(() => {
return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {});
return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {}, undefined);
});
const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, {});
if (typeof group !== 'undefined') {

View file

@ -588,7 +588,7 @@ export class ExtensionEditor extends BaseEditor {
const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', {
enableFindWidget: true,
}, {}));
}, {}, undefined));
webview.claim(this);
webview.layoutWebviewOverElement(template.content);

View file

@ -19,7 +19,8 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod
import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader';
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export interface IDimensionMessage {
__vscode_notebook_message: boolean;
@ -144,7 +145,8 @@ export class BackLayerWebView extends Disposable {
@IWebviewService readonly webviewService: IWebviewService,
@IOpenerService readonly openerService: IOpenerService,
@INotebookService private readonly notebookService: INotebookService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
) {
super();
this.element = document.createElement('div');
@ -155,7 +157,7 @@ export class BackLayerWebView extends Disposable {
this.element.style.margin = `0px 0 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px`;
const pathsPath = getPathFromAmdModule(require, 'vs/loader.js');
const loader = URI.file(pathsPath).with({ scheme: WebviewResourceScheme });
const loader = asWebviewUri(this.workbenchEnvironmentService, this.id, URI.file(pathsPath));
let coreDependencies = '';
let resolveFunc: () => void;
@ -560,13 +562,14 @@ ${loaderJs}
private _createInset(webviewService: IWebviewService, content: string) {
const rootPath = URI.file(path.dirname(getPathFromAmdModule(require, '')));
this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), rootPath];
const webview = webviewService.createWebviewElement(this.id, {
enableFindWidget: false,
}, {
allowMultipleAPIAcquire: true,
allowScripts: true,
localResourceRoots: this.localResourceRootsCache
});
}, undefined);
webview.html = content;
return webview;
}
@ -742,7 +745,7 @@ ${loaderJs}
if (this.environmentService.isExtensionDevelopment && (preloadResource.scheme === 'http' || preloadResource.scheme === 'https')) {
return preloadResource;
}
return preloadResource.with({ scheme: WebviewResourceScheme });
return asWebviewUri(this.workbenchEnvironmentService, this.id, preloadResource);
});
extensionLocations.push(rendererInfo.extensionLocation);
preloadResources.forEach(e => {

View file

@ -59,13 +59,13 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
protected content: WebviewContent;
public extension: WebviewExtensionDescription | undefined;
constructor(
// TODO: matb, this should not be protected. The only reason it needs to be is that the base class ends up using it in the call to createElement
protected readonly id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
public readonly extension: WebviewExtensionDescription | undefined,
private readonly webviewThemeDataProvider: WebviewThemeDataProvider,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IEnvironmentService private readonly _environementService: IEnvironmentService,
@ -79,7 +79,7 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
state: undefined
};
this._element = this.createElement(options);
this._element = this.createElement(options, contentOptions);
this._ready = new Promise(resolve => {
const subscription = this._register(this.on(WebviewMessageChannels.webviewReady, () => {
@ -187,7 +187,7 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
protected abstract readonly extraContentOptions: { readonly [key: string]: string };
protected abstract createElement(options: WebviewOptions): T;
protected abstract createElement(options: WebviewOptions, contentOptions: WebviewContentOptions): T;
protected abstract on<T = unknown>(channel: string, handler: (data: T) => void): IDisposable;

View file

@ -28,7 +28,6 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
private _html: string = '';
private _initialScrollProgress: number = 0;
private _state: string | undefined = undefined;
private _extension: WebviewExtensionDescription | undefined;
private _contentOptions: WebviewContentOptions;
private _options: WebviewOptions;
@ -42,6 +41,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
private readonly id: string,
initialOptions: WebviewOptions,
initialContentOptions: WebviewContentOptions,
public readonly extension: WebviewExtensionDescription | undefined,
@ILayoutService private readonly _layoutService: ILayoutService,
@IWebviewService private readonly _webviewService: IWebviewService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService
@ -109,11 +109,10 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
private show() {
if (!this._webview.value) {
const webview = this._webviewService.createWebviewElement(this.id, this._options, this._contentOptions);
const webview = this._webviewService.createWebviewElement(this.id, this._options, this._contentOptions, this.extension);
this._webview.value = webview;
webview.state = this._state;
webview.html = this._html;
webview.extension = this._extension;
if (this._options.tryRestoreScrollPosition) {
webview.initialScrollProgress = this._initialScrollProgress;
}
@ -175,12 +174,6 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
this.withWebview(webview => webview.contentOptions = value);
}
public get extension() { return this._extension; }
public set extension(value) {
this._extension = value;
this.withWebview(webview => webview.extension = value);
}
private readonly _onDidFocus = this._register(new Emitter<void>());
public readonly onDidFocus: Event<void> = this._onDidFocus.event;

View file

@ -94,6 +94,10 @@
postMessage: hostMessaging.postMessage.bind(hostMessaging),
onMessage: hostMessaging.onMessage.bind(hostMessaging),
ready: workerReady,
fakeLoad: true
fakeLoad: true,
rewriteCSP: (csp, endpoint) => {
const endpointUrl = new URL(endpoint);
csp.setAttribute('content', csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin));
}
});
}());
}());

View file

@ -11,7 +11,8 @@
* focusIframeOnCreate?: boolean,
* ready?: Promise<void>,
* onIframeLoaded?: (iframe: HTMLIFrameElement) => void,
* fakeLoad: boolean
* fakeLoad: boolean,
* rewriteCSP: (existingCSP: string, endpoint?: string) => string,
* }} WebviewHost
*/
@ -360,14 +361,10 @@
if (!csp) {
host.postMessage('no-csp-found');
} else {
// Rewrite vscode-resource in csp
if (data.endpoint) {
try {
const endpointUrl = new URL(data.endpoint);
csp.setAttribute('content', csp.getAttribute('content').replace(/vscode-resource:(?=(\s|;|$))/g, endpointUrl.origin));
} catch (e) {
console.error('Could not rewrite csp');
}
try {
csp.setAttribute('content', host.rewriteCSP(csp.getAttribute('content'), data.endpoint));
} catch (e) {
console.error('Could not rewrite csp');
}
}

View file

@ -40,12 +40,14 @@ export interface IWebviewService {
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement;
createWebviewOverlay(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewOverlay;
setIcons(id: string, value: WebviewIcons | undefined): void;

View file

@ -5,19 +5,19 @@
import { addDisposableListener } from 'vs/base/browser/dom';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isWeb } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping';
import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/common/resourceLoader';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isWeb } from 'vs/base/common/platform';
export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Webview {
private readonly _portMappingManager: WebviewPortMappingManager;
@ -26,6 +26,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
webviewThemeDataProvider: WebviewThemeDataProvider,
@ITunnelService tunnelService: ITunnelService,
@IFileService private readonly fileService: IFileService,
@ -34,7 +35,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
@IEnvironmentService environementService: IEnvironmentService,
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService,
) {
super(id, options, contentOptions, webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
super(id, options, contentOptions, extension, webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
if (!this.useExternalEndpoint && (!workbenchEnvironmentService.options || typeof workbenchEnvironmentService.webviewExternalEndpoint !== 'string')) {
throw new Error('To use iframe based webviews, you must configure `environmentService.webviewExternalEndpoint`');
@ -58,7 +59,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
}));
}
protected createElement(options: WebviewOptions) {
protected createElement(options: WebviewOptions, contentOptions: WebviewContentOptions) {
const element = document.createElement('iframe');
element.className = `webview ${options.customClasses || ''}`;
element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms');
@ -93,7 +94,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
private preprocessHtml(value: string): string {
return value
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
.replace(/(["'])(?:vscode-resource|vscode-webview-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}

View file

@ -5,7 +5,7 @@
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
import { DynamicWebviewEditorOverlay } from './dynamicWebviewEditorOverlay';
@ -27,17 +27,19 @@ export class WebviewService implements IWebviewService {
createWebviewElement(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement {
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, this._webviewThemeDataProvider);
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
}
createWebviewOverlay(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewOverlay {
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions);
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension);
}
setIcons(id: string, iconPath: WebviewIcons | undefined): void {

View file

@ -307,14 +307,12 @@ export class WebviewEditorService implements IWebviewWorkbenchService {
private createWebviewElement(
id: string,
extension: WebviewExtensionDescription | undefined,
options: WebviewInputOptions
options: WebviewInputOptions,
) {
const webview = this._webviewService.createWebviewOverlay(id, {
return this._webviewService.createWebviewOverlay(id, {
enableFindWidget: options.enableFindWidget,
retainContextWhenHidden: options.retainContextWhenHidden
}, options);
webview.extension = extension;
return webview;
}, options, extension);
}
}

View file

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export function asWebviewUri(
environmentService: IWorkbenchEnvironmentService,
uuid: string,
resource: URI,
): URI {
const uri = environmentService.webviewResourceRoot
// Make sure we preserve the scheme of the resource but convert it into a normal path segment
// The scheme is important as we need to know if we are requesting a local or a remote resource.
.replace('{{resource}}', resource.scheme + withoutScheme(resource))
.replace('{{uuid}}', uuid);
return URI.parse(uri);
}
function withoutScheme(resource: URI): string {
return resource.toString().replace(/^\S+?:/, '');
}

View file

@ -62,7 +62,10 @@
isMouseDown = false;
});
newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent);
}
},
rewriteCSP: (csp) => {
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
},
};
host.onMessage('devtools-opened', () => {

View file

@ -3,12 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FindInPageOptions, OnBeforeRequestListenerDetails, OnHeadersReceivedListenerDetails, Response, WebContents, WebviewTag } from 'electron';
import { FindInPageOptions, OnBeforeRequestListenerDetails, OnHeadersReceivedListenerDetails, Response, WebContents, WebviewTag, ipcRenderer } from 'electron';
import { addDisposableListener } from 'vs/base/browser/dom';
import { ThrottledDelayer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isMacintosh } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
@ -21,9 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping';
import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
import { registerFileProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget';
@ -116,32 +115,29 @@ class WebviewSession extends Disposable {
class WebviewProtocolProvider extends Disposable {
private _resolve!: () => void;
private _reject!: () => void;
public readonly ready: Promise<void>;
constructor(
handle: WebviewTagHandle,
getExtensionLocation: () => URI | undefined,
private readonly id: string,
extension: WebviewExtensionDescription | undefined,
getLocalResourceRoots: () => ReadonlyArray<URI>,
fileService: IFileService,
) {
super();
this.ready = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
ipcRenderer.send('vscode:registerWebview', id, {
extensionLocation: extension?.location.toJSON(),
localResourceRoots: getLocalResourceRoots().map(x => x.toJSON()),
});
this._register(handle.onFirstLoad(contents => {
try {
registerFileProtocol(contents, WebviewResourceScheme, fileService, getExtensionLocation(), getLocalResourceRoots);
this._resolve();
} catch {
this._reject();
}
}));
this.ready = new Promise((resolve) => {
ipcRenderer.once(`vscode:didRegisterWebview-${id}`, () => resolve());
});
}
dispose() {
super.dispose();
ipcRenderer.send('vscode:unregisterWebview', this.id);
}
}
@ -236,7 +232,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
private _webviewFindWidget: WebviewFindWidget | undefined;
private _findStarted: boolean = false;
public extension: WebviewExtensionDescription | undefined;
public readonly extension: WebviewExtensionDescription | undefined;
private readonly _protocolProvider: WebviewProtocolProvider;
private readonly _domReady: Promise<void>;
@ -247,6 +243,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
private readonly _webviewThemeDataProvider: WebviewThemeDataProvider,
@IInstantiationService instantiationService: IInstantiationService,
@IFileService fileService: IFileService,
@ -256,15 +253,12 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(id, options, contentOptions, _webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
super(id, options, contentOptions, extension, _webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService);
const webviewAndContents = this._register(new WebviewTagHandle(this.element!));
const session = this._register(new WebviewSession(webviewAndContents));
this._protocolProvider = new WebviewProtocolProvider(webviewAndContents,
() => this.extension?.location,
() => (this.content.options.localResourceRoots || []),
fileService);
this._protocolProvider = new WebviewProtocolProvider(id, extension, () => (this.content.options.localResourceRoots || []));
this._register(this._protocolProvider);
this._register(new WebviewPortMappingProvider(
@ -345,7 +339,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
element.focus = () => {
this.doFocus();
};
element.setAttribute('partition', `webview${Date.now()}`);
element.setAttribute('webpreferences', 'contextIsolation=yes');
element.className = `webview ${options.customClasses || ''}`;
@ -362,6 +355,21 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
protected readonly extraContentOptions = {};
public set html(value: string) {
super.html = this.preprocessHtml(value);
}
private preprocessHtml(value: string): string {
return value
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${Schemas.vscodeWebviewResource}:${this.id}//${scheme}${path}${endQuote}`;
}
return `${startQuote}${Schemas.vscodeWebviewResource}:${this.id}//file${path}${endQuote}`;
});
}
public mountTo(parent: HTMLElement) {
if (!this.element) {
return;

View file

@ -1,37 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as electron from 'electron';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/common/resourceLoader';
export function registerFileProtocol(
contents: electron.WebContents,
protocol: string,
fileService: IFileService,
extensionLocation: URI | undefined,
getRoots: () => ReadonlyArray<URI>
) {
contents.session.protocol.registerBufferProtocol(protocol, async (request, callback: any) => {
try {
const result = await loadLocalResource(URI.parse(request.url), fileService, extensionLocation, getRoots);
if (result.type === WebviewResourceResponse.Type.Success) {
return callback({
data: Buffer.from(result.data.buffer),
mimeType: result.mimeType
});
}
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
}
} catch {
// noop
}
return callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
});
}

View file

@ -6,7 +6,7 @@
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay';
import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
import { WebviewIconManager } from 'vs/workbench/contrib/webview/browser/webviewIconManager';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing';
@ -29,13 +29,14 @@ export class ElectronWebviewService implements IWebviewService {
createWebviewElement(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement {
const useExternalEndpoint = this._configService.getValue<string>('webview.experimental.useExternalEndpoint');
if (useExternalEndpoint) {
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, this._webviewThemeDataProvider);
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
} else {
return this._instantiationService.createInstance(ElectronWebviewBasedWebview, id, options, contentOptions, this._webviewThemeDataProvider);
return this._instantiationService.createInstance(ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
}
}
@ -43,8 +44,9 @@ export class ElectronWebviewService implements IWebviewService {
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewOverlay {
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions);
return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension);
}
setIcons(id: string, iconPath: WebviewIcons | undefined): void {

View file

@ -42,10 +42,10 @@ export class NativeWorkbenchEnvironmentService extends EnvironmentService implem
}
@memoize
get webviewResourceRoot(): string { return 'vscode-resource://{{resource}}'; }
get webviewResourceRoot(): string { return `${Schemas.vscodeWebviewResource}://{{uuid}}/{{resource}}`; }
@memoize
get webviewCspSource(): string { return 'vscode-resource:'; }
get webviewCspSource(): string { return `${Schemas.vscodeWebviewResource}:`; }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }