Add experimental support for using iframes for webviews (#100991)

* Use non-deprecated API version

* Prototype: enable offline iframe based webviews on desktop

Adds a new `vscode-webview` protocol  for loading the wrapper contents of a webview

Still needs cleaning up

* fix loading of electron webview

* Cleanup and add `webview.experimental.useIframes` setting

Co-authored-by: deepak1556 <hop2deep@gmail.com>
This commit is contained in:
Matt Bierner 2020-06-25 14:52:02 -07:00 committed by GitHub
parent 16be2c9885
commit 48c6e3979d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 396 additions and 210 deletions

View file

@ -32,7 +32,7 @@ interface StandardTsServerRequests {
'completions': [Proto.CompletionsRequestArgs, Proto.CompletionsResponse];
'configure': [Proto.ConfigureRequestArguments, Proto.ConfigureResponse];
'definition': [Proto.FileLocationRequestArgs, Proto.DefinitionResponse];
'definitionAndBoundSpan': [Proto.FileLocationRequestArgs, Proto.DefinitionInfoAndBoundSpanReponse];
'definitionAndBoundSpan': [Proto.FileLocationRequestArgs, Proto.DefinitionInfoAndBoundSpanResponse];
'docCommentTemplate': [Proto.FileLocationRequestArgs, Proto.DocCommandTemplateResponse];
'documentHighlights': [Proto.DocumentHighlightsRequestArgs, Proto.DocumentHighlightsResponse];
'format': [Proto.FormatRequestArgs, Proto.FormatResponse];

View file

@ -86,8 +86,9 @@ setCurrentWorkingDirectory();
// Register custom schemes with privileges
protocol.registerSchemesAsPrivileged([
{
scheme: 'vscode-resource',
scheme: 'vscode-webview',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,

View file

@ -62,6 +62,14 @@ export namespace Schemas {
export const webviewPanel = 'webview-panel';
/**
* Scheme used for loading the wrapper html and script in webviews.
*/
export const vscodeWebview = 'vscode-webview';
/**
* Scheme used for loading resources inside of webviews.
*/
export const vscodeWebviewResource = 'vscode-webview-resource';
/**

View file

@ -3,7 +3,7 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
</head>
<body aria-label="">
</body>

View file

@ -174,11 +174,12 @@ export class CodeApplication extends Disposable {
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%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%20role%3D%22document%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') {
return true;
const uri = URI.parse(source);
if (uri.scheme === Schemas.vscodeWebview) {
return uri.path === '/index.html' || uri.path === '/electron-browser/index.html';
}
const srcUri = URI.parse(source).fsPath.toLowerCase();
const srcUri = uri.fsPath.toLowerCase();
const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase();
return srcUri.startsWith(rootUri + sep);

View file

@ -13,7 +13,7 @@ export const IWebviewManagerService = createDecorator<IWebviewManagerService>('w
export interface IWebviewManagerService {
_serviceBrand: unknown;
registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void>;
registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void>;
unregisterWebview(id: string): Promise<void>;
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;

View file

@ -30,7 +30,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
}
public async registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void> {
public async registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void> {
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
this.protocolProvider.registerWebview(id, {

View file

@ -20,7 +20,7 @@ interface PortMappingData {
export class WebviewPortMappingProvider extends Disposable {
private readonly _webviewData = new Map<string, {
readonly webContentsId: number;
readonly webContentsId: number | undefined;
readonly manager: WebviewPortMappingManager;
metadata: PortMappingData;
}>();
@ -56,14 +56,16 @@ export class WebviewPortMappingProvider extends Disposable {
});
}
public async registerWebview(id: string, webContentsId: number, metadata: PortMappingData): Promise<void> {
public async registerWebview(id: string, webContentsId: number | undefined, metadata: PortMappingData): Promise<void> {
const manager = new WebviewPortMappingManager(
() => this._webviewData.get(id)?.metadata.extensionLocation,
() => this._webviewData.get(id)?.metadata.mappings || [],
this._tunnelService);
this._webviewData.set(id, { webContentsId, metadata, manager });
this._webContentsIdsToWebviewIds.set(webContentsId, id);
if (typeof webContentsId === 'number') {
this._webContentsIdsToWebviewIds.set(webContentsId, id);
}
}
public unregisterWebview(id: string): void {
@ -71,7 +73,9 @@ export class WebviewPortMappingProvider extends Disposable {
if (existing) {
existing.manager.dispose();
this._webviewData.delete(id);
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
if (typeof existing.webContentsId === 'number') {
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
}
}
}

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { session } from 'electron';
import { session, protocol } from 'electron';
import { Readable } from 'stream';
import { VSBufferReadableStream } from 'vs/base/common/buffer';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
@ -23,6 +23,13 @@ interface WebviewMetadata {
export class WebviewProtocolProvider extends Disposable {
private static validWebviewFilePaths = new Map([
['/index.html', 'index.html'],
['/electron-browser/index.html', 'index.html'],
['/main.js', 'main.js'],
['/host.js', 'host.js'],
]);
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
constructor(
@ -33,62 +40,22 @@ export class WebviewProtocolProvider extends Disposable {
const sess = session.fromPartition(webviewPartitionId);
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
try {
const uri = URI.parse(request.url);
// Register the protocol loading webview html
const webviewHandler = this.handleWebviewRequest.bind(this);
protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
const id = uri.authority;
const metadata = this.webviewMetadata.get(id);
if (metadata) {
// Register the protocol loading webview resources both inside the webview and at the top level
const webviewResourceHandler = this.handleWebviewResourceRequest.bind(this);
protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
let rewriteUri: undefined | ((uri: URI) => URI);
if (metadata.remoteConnectionData) {
rewriteUri = (uri) => {
if (metadata.remoteConnectionData) {
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
const scheme = metadata.remoteConnectionData.host === 'localhost' || metadata.remoteConnectionData.host === '127.0.0.1' ? 'http' : 'https';
return URI.parse(`${scheme}://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
path: '/vscode-remote-resource',
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
});
}
}
return uri;
};
}
const result = await loadLocalResource(uri, {
extensionLocation: metadata.extensionLocation,
roots: metadata.localResourceRoots,
remoteConnectionData: metadata.remoteConnectionData,
rewriteUri,
}, this.fileService, this.requestService);
if (result.type === WebviewResourceResponse.Type.Success) {
return callback({
statusCode: 200,
data: this.streamToNodeReadable(result.stream),
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
}
});
}
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ data: null, statusCode: 401 });
}
}
} catch {
// noop
}
return callback({ data: null, statusCode: 404 });
});
this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
this._register(toDisposable(() => {
protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
protocol.unregisterProtocol(Schemas.vscodeWebview);
sess.protocol.unregisterProtocol(Schemas.vscodeWebview);
}));
}
private streamToNodeReadable(stream: VSBufferReadableStream): Readable {
@ -152,4 +119,81 @@ export class WebviewProtocolProvider extends Disposable {
});
}
}
private async handleWebviewRequest(request: Electron.Request, callback: any) {
try {
const uri = URI.parse(request.url);
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
if (typeof entry === 'string') {
let url: string;
if (uri.path.startsWith('/electron-browser')) {
url = require.toUrl(`vs/workbench/contrib/webview/electron-browser/pre/${entry}`);
} else {
url = require.toUrl(`vs/workbench/contrib/webview/browser/pre/${entry}`);
}
return callback(url.replace('file://', ''));
}
} catch {
// noop
}
callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ });
}
private async handleWebviewResourceRequest(
request: Electron.Request,
callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void
) {
try {
const uri = URI.parse(request.url);
const id = uri.authority;
const metadata = this.webviewMetadata.get(id);
if (metadata) {
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
let rewriteUri: undefined | ((uri: URI) => URI);
if (metadata.remoteConnectionData) {
rewriteUri = (uri) => {
if (metadata.remoteConnectionData) {
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
const scheme = metadata.remoteConnectionData.host === 'localhost' || metadata.remoteConnectionData.host === '127.0.0.1' ? 'http' : 'https';
return URI.parse(`${scheme}://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
path: '/vscode-remote-resource',
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
});
}
}
return uri;
};
}
const result = await loadLocalResource(uri, {
extensionLocation: metadata.extensionLocation,
roots: metadata.localResourceRoots,
remoteConnectionData: metadata.remoteConnectionData,
rewriteUri,
}, this.fileService, this.requestService);
if (result.type === WebviewResourceResponse.Type.Success) {
return callback({
statusCode: 200,
data: this.streamToNodeReadable(result.stream),
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
}
});
}
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ data: null, statusCode: 401 });
}
}
} catch {
// noop
}
return callback({ data: null, statusCode: 404 });
}
}

View file

@ -5,6 +5,7 @@
// @ts-check
(function () {
const id = document.location.search.match(/\bid=([\w-]+)/)[1];
const onElectron = /platform=electron/.test(document.location.search);
const hostMessaging = new class HostMessaging {
constructor() {
@ -36,6 +37,10 @@
}();
const workerReady = new Promise(async (resolveWorkerReady) => {
if (onElectron) {
return resolveWorkerReady();
}
if (!areServiceWorkersEnabled()) {
console.log('Service Workers are not enabled. Webviews will not work properly');
return resolveWorkerReady();
@ -95,11 +100,15 @@
postMessage: hostMessaging.postMessage.bind(hostMessaging),
onMessage: hostMessaging.onMessage.bind(hostMessaging),
ready: workerReady,
fakeLoad: true,
rewriteCSP: (csp, endpoint) => {
const endpointUrl = new URL(endpoint);
return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin);
}
fakeLoad: !onElectron,
rewriteCSP: onElectron
? (csp) => {
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
}
: (csp, endpoint) => {
const endpointUrl = new URL(endpoint);
return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin);
}
};
(/** @type {any} */ (window)).createWebviewManager(host);

View file

@ -7,9 +7,7 @@ import { addDisposableListener } from 'vs/base/browser/dom';
import { streamToBuffer } from 'vs/base/common/buffer';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
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 { ILogService } from 'vs/platform/log/common/log';
@ -37,7 +35,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
@ITunnelService tunnelService: ITunnelService,
@IFileService private readonly fileService: IFileService,
@IRequestService private readonly requestService: IRequestService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITelemetryService telemetryService: ITelemetryService,
@IEnvironmentService environmentService: IEnvironmentService,
@IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService,
@ -46,10 +43,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
) {
super(id, options, contentOptions, extension, webviewThemeDataProvider, logService, telemetryService, environmentService, _workbenchEnvironmentService);
if (!this.useExternalEndpoint && (!_workbenchEnvironmentService.options || typeof _workbenchEnvironmentService.webviewExternalEndpoint !== 'string')) {
throw new Error('To use iframe based webviews, you must configure `environmentService.webviewExternalEndpoint`');
}
this._portMappingManager = this._register(new WebviewPortMappingManager(
() => this.extension?.location,
() => this.content.options.portMapping || [],
@ -67,8 +60,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
this.localLocalhost(entry.origin);
}));
// The extensionId and purpose in the URL are used for filtering in js-debug:
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
this.initElement(extension, options);
}
protected createElement(options: WebviewOptions, _contentOptions: WebviewContentOptions) {
@ -83,6 +75,11 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
return element;
}
protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
// The extensionId and purpose in the URL are used for filtering in js-debug:
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
}
private get externalEndpoint(): string {
const endpoint = this.workbenchEnvironmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
if (endpoint[endpoint.length - 1] === '/') {
@ -91,10 +88,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
return endpoint;
}
private get useExternalEndpoint(): boolean {
return isWeb || this._configurationService.getValue<boolean>('webview.experimental.useExternalEndpoint');
}
public mountTo(parent: HTMLElement) {
if (this.element) {
parent.appendChild(this.element);
@ -105,7 +98,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
super.html = this.preprocessHtml(value);
}
private preprocessHtml(value: string): string {
protected preprocessHtml(value: string): string {
return value
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
@ -121,7 +114,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
});
}
protected get extraContentOptions() {
protected get extraContentOptions(): any {
return {
endpoint: this.externalEndpoint,
};

View file

@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { IRequestService } from 'vs/platform/request/common/request';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-browser/resourceLoading';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
/**
* Webview backed by an iframe but that uses Electron APIs to power the webview.
*/
export class ElectronIframeWebview extends IFrameWebview {
private readonly _resourceRequestManager: WebviewResourceRequestManager;
private _messagePromise = Promise.resolve();
constructor(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
webviewThemeDataProvider: WebviewThemeDataProvider,
@ITunnelService tunnelService: ITunnelService,
@IFileService fileService: IFileService,
@IRequestService requestService: IRequestService,
@ITelemetryService telemetryService: ITelemetryService,
@IEnvironmentService environmentService: IEnvironmentService,
@IWorkbenchEnvironmentService _workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@ILogService logService: ILogService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super(id, options, contentOptions, extension, webviewThemeDataProvider,
tunnelService, fileService, requestService, telemetryService, environmentService, _workbenchEnvironmentService, _remoteAuthorityResolverService, logService);
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, Promise.resolve(undefined)));
}
protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
// The extensionId and purpose in the URL are used for filtering in js-debug:
this.element!.setAttribute('src', `${Schemas.vscodeWebview}://${this.id}/index.html?id=${this.id}&platform=electron&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
}
public set contentOptions(options: WebviewContentOptions) {
this._resourceRequestManager.update(options);
super.contentOptions = options;
}
public set localResourcesRoot(resources: URI[]) {
this._resourceRequestManager.update({
...this.contentOptions,
localResourceRoots: resources,
});
super.localResourcesRoot = resources;
}
protected get extraContentOptions() {
return {};
}
protected async doPostMessage(channel: string, data?: any): Promise<void> {
this._messagePromise = this._messagePromise
.then(() => this._resourceRequestManager.ensureReady())
.then(() => {
this.element?.contentWindow!.postMessage({ channel, args: data }, '*');
});
}
protected preprocessHtml(value: string): string {
return rewriteVsCodeResourceUrls(this.id, value);
}
}

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%; height: 100%">
<head>
<title>Virtual Document</title>
</head>
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%" role="document">
</body>
</html>

View file

@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals } from 'vs/base/common/arrays';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import * as modes from 'vs/editor/common/modes';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { ILogService } from 'vs/platform/log/common/log';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewContentOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Schemas } from 'vs/base/common/network';
/**
* Try to rewrite `vscode-resource:` urls in html
*/
export function rewriteVsCodeResourceUrls(
id: string,
html: string,
): string {
return html
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${Schemas.vscodeWebviewResource}://${id}/${scheme}${path}${endQuote}`;
}
if (!path.startsWith('//')) {
// Add an empty authority if we don't already have one
path = '//' + path;
}
return `${startQuote}${Schemas.vscodeWebviewResource}://${id}/file${path}${endQuote}`;
});
}
/**
* Manages the loading of resources inside of a webview.
*/
export class WebviewResourceRequestManager extends Disposable {
private readonly _webviewManagerService: IWebviewManagerService;
private _localResourceRoots: ReadonlyArray<URI>;
private _portMappings: ReadonlyArray<modes.IWebviewPortMapping>;
private _ready: Promise<void>;
constructor(
private readonly id: string,
private readonly extension: WebviewExtensionDescription | undefined,
initialContentOptions: WebviewContentOptions,
getWebContentsId: Promise<number | undefined>,
@ILogService private readonly _logService: ILogService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IMainProcessService mainProcessService: IMainProcessService,
) {
super();
this._logService.debug(`WebviewResourceRequestManager(${this.id}): init`);
this._webviewManagerService = createChannelSender<IWebviewManagerService>(mainProcessService.getChannel('webview'));
this._localResourceRoots = initialContentOptions.localResourceRoots || [];
this._portMappings = initialContentOptions.portMapping || [];
const remoteAuthority = environmentService.configuration.remoteAuthority;
const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
this._ready = getWebContentsId.then(async (webContentsId) => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
await this._webviewManagerService.registerWebview(this.id, webContentsId, {
extensionLocation: this.extension?.location.toJSON(),
localResourceRoots: this._localResourceRoots.map(x => x.toJSON()),
remoteConnectionData: remoteConnectionData,
portMappings: this._portMappings,
});
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did register`);
});
if (remoteAuthority) {
this._register(remoteAuthorityResolverService.onDidChangeConnectionData(() => {
const update = this._webviewManagerService.updateWebviewMetadata(this.id, {
remoteConnectionData: remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null,
});
this._ready = this._ready.then(() => update);
}));
}
this._register(toDisposable(() => this._webviewManagerService.unregisterWebview(this.id)));
}
public update(options: WebviewContentOptions) {
const localResourceRoots = options.localResourceRoots || [];
const portMappings = options.portMapping || [];
if (!this.needsUpdate(localResourceRoots, portMappings)) {
return;
}
this._localResourceRoots = localResourceRoots;
this._portMappings = portMappings;
this._logService.debug(`WebviewResourceRequestManager(${this.id}): will update`);
const update = this._webviewManagerService.updateWebviewMetadata(this.id, {
localResourceRoots: localResourceRoots.map(x => x.toJSON()),
portMappings: portMappings,
}).then(() => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did update`);
});
this._ready = this._ready.then(() => update);
}
private needsUpdate(
localResourceRoots: readonly URI[],
portMappings: readonly modes.IWebviewPortMapping[],
): boolean {
return !(
equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString())
&& equals(this._portMappings, portMappings, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort)
);
}
public ensureReady(): Promise<void> {
return this._ready;
}
}

View file

@ -5,22 +5,19 @@
import { FindInPageOptions, WebviewTag } from 'electron';
import { addDisposableListener } from 'vs/base/browser/dom';
import { equals } from 'vs/base/common/arrays';
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 { 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 { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import * as modes from 'vs/editor/common/modes';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { ILogService } from 'vs/platform/log/common/log';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
@ -29,97 +26,7 @@ import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/t
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget';
class WebviewResourceRequestManager extends Disposable {
private readonly _webviewManagerService: IWebviewManagerService;
private _localResourceRoots: ReadonlyArray<URI>;
private _portMappings: ReadonlyArray<modes.IWebviewPortMapping>;
private _ready?: Promise<void>;
constructor(
private readonly id: string,
private readonly extension: WebviewExtensionDescription | undefined,
webview: WebviewTag,
initialContentOptions: WebviewContentOptions,
@ILogService private readonly _logService: ILogService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IMainProcessService mainProcessService: IMainProcessService,
) {
super();
this._logService.debug(`WebviewResourceRequestManager(${this.id}): init`);
this._webviewManagerService = createChannelSender<IWebviewManagerService>(mainProcessService.getChannel('webview'));
this._localResourceRoots = initialContentOptions.localResourceRoots || [];
this._portMappings = initialContentOptions.portMapping || [];
const remoteAuthority = environmentService.configuration.remoteAuthority;
const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
this._ready = new Promise(resolve => {
this._register(addDisposableListener(webview!, 'did-start-loading', once(() => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
const webContentsId = webview.getWebContentsId();
this._webviewManagerService.registerWebview(this.id, webContentsId, {
extensionLocation: this.extension?.location.toJSON(),
localResourceRoots: this._localResourceRoots.map(x => x.toJSON()),
remoteConnectionData: remoteConnectionData,
portMappings: this._portMappings,
}).then(() => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did register`);
}).finally(() => resolve());
})));
});
if (remoteAuthority) {
this._register(remoteAuthorityResolverService.onDidChangeConnectionData(() => {
const update = this._webviewManagerService.updateWebviewMetadata(this.id, {
remoteConnectionData: remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null,
});
this._ready = this._ready?.then(() => update);
}));
}
this._register(toDisposable(() => this._webviewManagerService.unregisterWebview(this.id)));
}
public update(options: WebviewContentOptions) {
const localResourceRoots = options.localResourceRoots || [];
const portMappings = options.portMapping || [];
if (
equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString())
&& equals(this._portMappings, portMappings, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort)
) {
return;
}
this._localResourceRoots = localResourceRoots;
this._portMappings = portMappings;
this._logService.debug(`WebviewResourceRequestManager(${this.id}): will update`);
const update = this._webviewManagerService.updateWebviewMetadata(this.id, {
localResourceRoots: localResourceRoots.map(x => x.toJSON()),
portMappings: portMappings,
}).then(() => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did update`);
});
this._ready = this._ready?.then(() => update);
}
async synchronize(): Promise<void> {
return this._ready;
}
}
import { WebviewResourceRequestManager, rewriteVsCodeResourceUrls } from './resourceLoading';
class WebviewKeyboardHandler {
@ -222,7 +129,17 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
this._myLogService.debug(`Webview(${this.id}): init`);
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.element!, this.content.options));
const webviewId = new Promise<number | undefined>((resolve, reject) => {
const sub = this._register(addDisposableListener(this.element!, 'did-start-loading', once(() => {
if (!this.element) {
reject();
throw new Error('No element');
}
resolve(this.element.getWebContentsId());
sub.dispose();
})));
});
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, webviewId));
this._register(addDisposableListener(this.element!, 'did-start-loading', once(() => {
this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!));
@ -289,7 +206,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
}
this.element!.preload = require.toUrl('./pre/electron-index.js');
this.element!.src = '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%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%20role%3D%22document%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E';
this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser/index.html`;
}
protected createElement(options: WebviewOptions) {
@ -301,6 +218,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
element.focus = () => {
this.doFocus();
};
element.setAttribute('partition', webviewPartitionId);
element.setAttribute('webpreferences', 'contextIsolation=yes');
element.className = `webview ${options.customClasses || ''}`;
@ -332,24 +250,9 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
public set html(value: string) {
this._myLogService.debug(`Webview(${this.id}): will set html`);
super.html = this.preprocessHtml(value);
super.html = rewriteVsCodeResourceUrls(this.id, 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}`;
}
if (!path.startsWith('//')) {
// Add an empty authority if we don't already have one
path = '//' + path;
}
return `${startQuote}${Schemas.vscodeWebviewResource}://${this.id}/file${path}${endQuote}`;
});
}
public mountTo(parent: HTMLElement) {
if (!this.element) {
return;
@ -365,7 +268,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
this._myLogService.debug(`Webview(${this.id}): will post message on '${channel}'`);
this._messagePromise = this._messagePromise
.then(() => this._resourceRequestManager.synchronize())
.then(() => this._resourceRequestManager.ensureReady())
.then(() => {
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
return this.element?.send(channel, data);

View file

@ -6,10 +6,10 @@
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, 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/browser/themeing';
import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewIconManager } from 'vs/workbench/contrib/webview/browser/webviewIconManager';
import { ElectronIframeWebview } from 'vs/workbench/contrib/webview/electron-browser/iframeWebviewElement';
import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
export class ElectronWebviewService implements IWebviewService {
@ -32,12 +32,8 @@ export class ElectronWebviewService implements IWebviewService {
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, extension, this._webviewThemeDataProvider);
} else {
return this._instantiationService.createInstance(ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
}
const useIframes = this._configService.getValue<string>('webview.experimental.useIframes');
return this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
}
createWebviewOverlay(