未验证 提交 d05ded6d 编写于 作者: M Matt Bierner 提交者: GitHub

Use service workers for loading webview resources on desktop (#120654)

This switches us from using a custom protocol to using a service worker to load resources inside webviews. Previously we had only been using service workers on web (since custom protocols are not supported on web). The service worker based approach is much cleaner to than our custom protocol work and lets us avoid some extra roundtrips between the main process and the renderer

We were previously blocked from using service workers on desktop due to an electron issue. However this has now been resolved
上级 c19bae2d
......@@ -59,7 +59,7 @@ if (portable && portable.isPortable) {
protocol.registerSchemesAsPrivileged([
{
scheme: 'vscode-webview',
privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true }
privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, allowServiceWorkers: true, }
}, {
scheme: 'vscode-webview-resource',
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true }
......
......@@ -186,7 +186,7 @@ export class CodeApplication extends Disposable {
const uri = URI.parse(source);
if (uri.scheme === Schemas.vscodeWebview) {
return uri.path === '/index.html' || uri.path === '/electron-browser/index.html';
return uri.path === '/index.html' || uri.path === '/electron-browser-index.html';
}
const srcUri = uri.fsPath.toLowerCase();
......
......@@ -3,11 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import { UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping';
export const IWebviewManagerService = createDecorator<IWebviewManagerService>('webviewManagerService');
......@@ -19,32 +15,8 @@ export interface WebviewWindowId {
readonly windowId: number;
}
export type WebviewManagerDidLoadResourceResponse =
VSBuffer
| 'not-modified'
| 'access-denied'
| 'not-found';
export interface WebviewManagerDidLoadResourceResponseDetails {
readonly etag?: string;
}
export interface IWebviewManagerService {
_serviceBrand: unknown;
registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise<void>;
unregisterWebview(id: string): Promise<void>;
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
/** Note: the VSBuffer must be a top level argument so that it can be serialized and deserialized properly */
didLoadResource(requestId: number, response: WebviewManagerDidLoadResourceResponse, responseDetails?: WebviewManagerDidLoadResourceResponseDetails): void;
setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise<void>;
}
export interface RegisterWebviewMetadata {
readonly extensionLocation: UriComponents | undefined;
readonly localResourceRoots: readonly UriComponents[];
readonly remoteConnectionData: IRemoteConnectionData | null;
readonly portMappings: readonly IWebviewPortMapping[];
}
......@@ -5,14 +5,9 @@
import { session, WebContents, webContents } from 'electron';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { IRequestService } from 'vs/platform/request/common/request';
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewManagerService, RegisterWebviewMetadata, WebviewManagerDidLoadResourceResponse, WebviewManagerDidLoadResourceResponseDetails, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider';
import { IWebviewManagerService, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
......@@ -20,19 +15,12 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
declare readonly _serviceBrand: undefined;
private readonly protocolProvider: WebviewProtocolProvider;
private readonly portMappingProvider: WebviewPortMappingProvider;
constructor(
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@IRequestService requestService: IRequestService,
@ITunnelService tunnelService: ITunnelService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
) {
super();
this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, logService, requestService, windowsMainService));
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
this._register(new WebviewProtocolProvider());
const sess = session.fromPartition(webviewPartitionId);
sess.setPermissionRequestHandler((_webContents, permission, callback) => {
......@@ -48,43 +36,6 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
});
}
public async registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise<void> {
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
this.protocolProvider.registerWebview(id, {
...metadata,
windowId: windowId,
extensionLocation,
localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x))
});
this.portMappingProvider.registerWebview(id, {
extensionLocation,
mappings: metadata.portMappings,
resolvedAuthority: metadata.remoteConnectionData,
});
}
public async unregisterWebview(id: string): Promise<void> {
this.protocolProvider.unregisterWebview(id);
this.portMappingProvider.unregisterWebview(id);
}
public async updateWebviewMetadata(id: string, metaDataDelta: Partial<RegisterWebviewMetadata>): Promise<void> {
const extensionLocation = metaDataDelta.extensionLocation ? URI.from(metaDataDelta.extensionLocation) : undefined;
this.protocolProvider.updateWebviewMetadata(id, {
...metaDataDelta,
extensionLocation,
localResourceRoots: metaDataDelta.localResourceRoots?.map(x => URI.from(x)),
});
this.portMappingProvider.updateWebviewMetadata(id, {
...metaDataDelta,
extensionLocation,
});
}
public async setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise<void> {
let contents: WebContents | undefined;
......@@ -107,12 +58,4 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
contents.setIgnoreMenuShortcuts(enabled);
}
}
public async didLoadResource(
requestId: number,
response: WebviewManagerDidLoadResourceResponse,
responseDetails?: WebviewManagerDidLoadResourceResponseDetails,
): Promise<void> {
this.protocolProvider.didLoadResource(requestId, response, responseDetails);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnBeforeRequestListenerDetails, session, webContents } from 'electron';
import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewPortMapping, WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
interface OnBeforeRequestListenerDetails_Extended extends OnBeforeRequestListenerDetails {
readonly lastCommittedOrigin?: string;
}
interface PortMappingData {
readonly extensionLocation: URI | undefined;
readonly mappings: readonly IWebviewPortMapping[];
readonly resolvedAuthority: IAddress | null | undefined;
}
interface WebviewData {
readonly manager: WebviewPortMappingManager;
readonly metadata: PortMappingData;
}
export class WebviewPortMappingProvider extends Disposable {
private readonly _webviewData = new Map<string, WebviewData>();
constructor(
@ITunnelService private readonly _tunnelService: ITunnelService,
) {
super();
const sess = session.fromPartition(webviewPartitionId);
sess.webRequest.onBeforeRequest({
urls: [
'*://localhost:*/*',
'*://127.0.0.1:*/*',
'*://0.0.0.0:*/*',
]
}, async (details: OnBeforeRequestListenerDetails_Extended, callback) => {
let webviewId: string | undefined;
try {
if (details.lastCommittedOrigin) {
const origin = URI.parse(details.lastCommittedOrigin);
webviewId = origin.authority;
} else if (typeof details.webContentsId === 'number') {
const contents = webContents.fromId(details.webContentsId);
const url = URI.parse(contents.getURL());
if (url.scheme === Schemas.vscodeWebview) {
webviewId = url.authority;
}
}
} catch {
return callback({});
}
if (!webviewId) {
return callback({});
}
const entry = this._webviewData.get(webviewId);
if (!entry) {
return callback({});
}
const redirect = await entry.manager.getRedirect(entry.metadata.resolvedAuthority, details.url);
return callback(redirect ? { redirectURL: redirect } : {});
});
}
public async registerWebview(id: string, 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, { metadata, manager });
}
public unregisterWebview(id: string): void {
const existing = this._webviewData.get(id);
if (existing) {
existing.manager.dispose();
this._webviewData.delete(id);
}
}
public async updateWebviewMetadata(id: string, metadataDelta: Partial<PortMappingData>): Promise<void> {
const entry = this._webviewData.get(id);
if (entry) {
this._webviewData.set(id, {
...entry,
...metadataDelta,
});
}
}
}
......@@ -4,53 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import { protocol, session } from 'electron';
import { Readable } from 'stream';
import { bufferToStream, VSBufferReadableStream } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { listenStream } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRequestService } from 'vs/platform/request/common/request';
import { loadLocalResource, readFileStream, WebviewFileReadResponse, webviewPartitionId, WebviewResourceFileReader, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { WebviewManagerDidLoadResourceResponse, WebviewManagerDidLoadResourceResponseDetails } from 'vs/platform/webview/common/webviewManagerService';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
interface WebviewMetadata {
readonly windowId: number;
readonly extensionLocation: URI | undefined;
readonly localResourceRoots: readonly URI[];
readonly remoteConnectionData: IRemoteConnectionData | null;
}
interface PendingResourceResult {
readonly response: WebviewManagerDidLoadResourceResponse;
readonly responseDetails?: WebviewManagerDidLoadResourceResponseDetails;
}
export class WebviewProtocolProvider extends Disposable {
private static validWebviewFilePaths = new Map([
['/index.html', 'index.html'],
['/electron-browser/index.html', 'index.html'],
['/fake.html', 'fake.html'],
['/electron-browser-index.html', 'index.html'],
['/main.js', 'main.js'],
['/host.js', 'host.js'],
['/service-worker.js', 'service-worker.js'],
]);
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
private requestIdPool = 1;
private readonly pendingResourceReads = new Map<number, { resolve: (content: PendingResourceResult) => void }>();
constructor(
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
@IRequestService private readonly requestService: IRequestService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
) {
constructor() {
super();
const sess = session.fromPartition(webviewPartitionId);
......@@ -59,79 +30,6 @@ export class WebviewProtocolProvider extends Disposable {
const webviewHandler = this.handleWebviewRequest.bind(this);
protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
// 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);
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 {
return new class extends Readable {
private listening = false;
_read(size?: number): void {
if (!this.listening) {
this.listening = true;
listenStream(stream, {
onData: data => {
try {
if (!this.push(data.buffer)) {
stream.pause(); // pause the stream if we should not push anymore
}
} catch (error) {
this.emit(error);
}
},
onError: error => {
this.emit('error', error);
},
onEnd: () => {
try {
this.push(null); // signal EOS
} catch (error) {
this.emit(error);
}
}
});
}
// ensure the stream is flowing
stream.resume();
}
_destroy(error: Error | null, callback: (error: Error | null) => void): void {
stream.destroy();
callback(null);
}
};
}
public async registerWebview(id: string, metadata: WebviewMetadata): Promise<void> {
this.webviewMetadata.set(id, metadata);
}
public unregisterWebview(id: string): void {
this.webviewMetadata.delete(id);
}
public async updateWebviewMetadata(id: string, metadataDelta: Partial<WebviewMetadata>): Promise<void> {
const entry = this.webviewMetadata.get(id);
if (entry) {
this.webviewMetadata.set(id, {
...entry,
...metadataDelta,
});
}
}
private async handleWebviewRequest(
......@@ -143,8 +41,8 @@ export class WebviewProtocolProvider extends Disposable {
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
if (typeof entry === 'string') {
const relativeResourcePath = uri.path.startsWith('/electron-browser')
? `vs/workbench/contrib/webview/electron-browser/pre/${entry}`
: `vs/workbench/contrib/webview/browser/pre/${entry}`;
? `vs/workbench/contrib/webview/electron-browser/pre/${entry} `
: `vs/workbench/contrib/webview/browser/pre/${entry} `;
const url = FileAccess.asFileUri(relativeResourcePath, require);
return callback(decodeURIComponent(url.fsPath));
......@@ -154,164 +52,4 @@ export class WebviewProtocolProvider extends Disposable {
}
callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ });
}
private async handleWebviewResourceRequest(
request: Electron.ProtocolRequest,
callback: (stream: NodeJS.ReadableStream | Electron.ProtocolResponse) => void
) {
try {
const uri = URI.parse(request.url);
const ifNoneMatch = request.headers['If-None-Match'];
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 === Schemas.vscodeRemote)) {
let host = metadata.remoteConnectionData.host;
if (host && host.indexOf(':') !== -1) { // IPv6 address
host = `[${host}]`;
}
return URI.parse(`http://${host}:${metadata.remoteConnectionData.port}`).with({
path: '/vscode-remote-resource',
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
});
}
}
return uri;
};
}
const fileReader: WebviewResourceFileReader = {
readFileStream: async (resource: URI, etag: string | undefined): Promise<WebviewFileReadResponse.Response> => {
if (resource.scheme === Schemas.file) {
return readFileStream(this.fileService, resource, etag);
}
// Unknown uri scheme. Try delegating the file read back to the renderer
// process which should have a file system provider registered for the uri.
const window = this.windowsMainService.getWindowById(metadata.windowId);
if (!window) {
throw new FileOperationError('Could not find window for resource', FileOperationResult.FILE_NOT_FOUND);
}
const requestId = this.requestIdPool++;
const p = new Promise<PendingResourceResult>(resolve => {
this.pendingResourceReads.set(requestId, { resolve });
});
window.send(`vscode:loadWebviewResource-${id}`, requestId, uri, etag);
const result = await p;
switch (result.response) {
case 'access-denied':
throw new FileOperationError('Could not read file', FileOperationResult.FILE_PERMISSION_DENIED);
case 'not-found':
throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND);
case 'not-modified':
return WebviewFileReadResponse.NotModified;
default:
return new WebviewFileReadResponse.StreamSuccess(bufferToStream(result.response), result.responseDetails?.etag);
}
}
};
const result = await loadLocalResource(uri, ifNoneMatch, {
extensionLocation: metadata.extensionLocation,
roots: metadata.localResourceRoots,
remoteConnectionData: metadata.remoteConnectionData,
rewriteUri,
}, fileReader, this.requestService, this.logService, CancellationToken.None);
switch (result.type) {
case WebviewResourceResponse.Type.Success:
{
const cacheHeaders: Record<string, string> = result.etag ? {
'ETag': result.etag,
'Cache-Control': 'no-cache'
} : {};
const ifNoneMatch = request.headers['If-None-Match'];
if (ifNoneMatch && result.etag === ifNoneMatch) {
/*
* Note that the server generating a 304 response MUST
* generate any of the following header fields that would
* have been sent in a 200 (OK) response to the same request:
* Cache-Control, Content-Location, Date, ETag, Expires, and Vary.
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
*/
return callback({
statusCode: 304, // not modified
data: undefined, // The request fails if `data` is not set
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
...cacheHeaders
}
});
}
return callback({
statusCode: 200,
data: this.streamToNodeReadable(result.stream),
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
...cacheHeaders
}
});
}
case WebviewResourceResponse.Type.NotModified:
{
/*
* Note that the server generating a 304 response MUST
* generate any of the following header fields that would
* have been sent in a 200 (OK) response to the same request:
* Cache-Control, Content-Location, Date, ETag, Expires, and Vary.
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
*/
return callback({
statusCode: 304, // not modified
data: undefined, // The request fails if `data` is not set
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
}
});
}
case WebviewResourceResponse.Type.AccessDenied:
{
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ data: undefined, statusCode: 401 });
}
}
}
} catch {
// noop
}
return callback({ data: undefined, statusCode: 404 });
}
public didLoadResource(
requestId: number,
response: WebviewManagerDidLoadResourceResponse,
responseDetails?: WebviewManagerDidLoadResourceResponseDetails,
) {
const pendingRead = this.pendingResourceReads.get(requestId);
if (!pendingRead) {
throw new Error('Unknown request');
}
this.pendingResourceReads.delete(requestId);
pendingRead.resolve({ response, responseDetails });
}
}
......@@ -4,14 +4,23 @@
*--------------------------------------------------------------------------------------------*/
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { streamToBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
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 { loadLocalResource, readFileStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { areWebviewContentOptionsEqual, WebviewContentOptions, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
......@@ -78,25 +87,55 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
protected content: WebviewContent;
private readonly _portMappingManager: WebviewPortMappingManager;
private readonly _fileService: IFileService;
private readonly _logService: ILogService;
private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService;
private readonly _requestService: IRequestService;
private readonly _telemetryService: ITelemetryService;
private readonly _tunnelService: ITunnelService;
protected readonly _environmentService: IWorkbenchEnvironmentService;
constructor(
public readonly id: string,
private readonly options: WebviewOptions,
contentOptions: WebviewContentOptions,
public extension: WebviewExtensionDescription | undefined,
private readonly webviewThemeDataProvider: WebviewThemeDataProvider,
@INotificationService notificationService: INotificationService,
@ILogService private readonly _logService: ILogService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService
services: {
environmentService: IWorkbenchEnvironmentService,
fileService: IFileService,
logService: ILogService,
notificationService: INotificationService,
remoteAuthorityResolverService: IRemoteAuthorityResolverService,
requestService: IRequestService,
telemetryService: ITelemetryService,
tunnelService: ITunnelService,
}
) {
super();
this._environmentService = services.environmentService;
this._fileService = services.fileService;
this._logService = services.logService;
this._remoteAuthorityResolverService = services.remoteAuthorityResolverService;
this._requestService = services.requestService;
this._telemetryService = services.telemetryService;
this._tunnelService = services.tunnelService;
this.content = {
html: '',
options: contentOptions,
state: undefined
};
this._portMappingManager = this._register(new WebviewPortMappingManager(
() => this.extension?.location,
() => this.content.options.portMapping || [],
this._tunnelService
));
this._element = this.createElement(options, contentOptions);
const subscription = this._register(this.on(WebviewMessageChannels.webviewReady, () => {
......@@ -153,7 +192,7 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
}));
this._register(this.on<{ message: string }>(WebviewMessageChannels.fatalError, (e) => {
notificationService.error(localize('fatalErrorMessage', "Error loading webview: {0}", e.message));
services.notificationService.error(localize('fatalErrorMessage', "Error loading webview: {0}", e.message));
}));
this._register(this.on('did-keydown', (data: KeyboardEvent) => {
......@@ -167,6 +206,17 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
this.handleKeyEvent('keyup', data);
}));
this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => {
const rawPath = entry.path;
const normalizedPath = decodeURIComponent(rawPath);
const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path));
this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch);
}));
this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => {
this.localLocalhost(entry.id, entry.origin);
}));
this.style();
this._register(webviewThemeDataProvider.onThemeDataChanged(this.style, this));
}
......@@ -240,7 +290,7 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
this._hasAlertedAboutMissingCsp = true;
if (this.extension && this.extension.id) {
if (this.environmentService.isExtensionDevelopment) {
if (this._environmentService.isExtensionDevelopment) {
this._onMissingCsp.fire(this.extension.id);
}
......@@ -391,4 +441,90 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
this._send('execCommand', command);
}
}
private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) {
try {
const remoteAuthority = this._environmentService.remoteAuthority;
const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
const extensionLocation = this.extension?.location;
// If we are loading a file resource from a remote extension, rewrite the uri to go remote
let rewriteUri: undefined | ((uri: URI) => URI);
if (extensionLocation?.scheme === Schemas.vscodeRemote) {
rewriteUri = (uri) => {
if (uri.scheme === Schemas.file && extensionLocation?.scheme === Schemas.vscodeRemote) {
return URI.from({
scheme: Schemas.vscodeRemote,
authority: extensionLocation.authority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: uri.path
})
});
}
return uri;
};
}
const result = await loadLocalResource(uri, ifNoneMatch, {
extensionLocation: extensionLocation,
roots: this.content.options.localResourceRoots || [],
remoteConnectionData,
rewriteUri,
}, {
readFileStream: (resource, etag) => readFileStream(this._fileService, resource, etag),
}, this._requestService, this._logService, CancellationToken.None);
switch (result.type) {
case WebviewResourceResponse.Type.Success:
{
const { buffer } = await streamToBuffer(result.stream);
return this._send('did-load-resource', {
id,
status: 200,
path: requestPath,
mime: result.mimeType,
data: buffer,
etag: result.etag,
});
}
case WebviewResourceResponse.Type.NotModified:
{
return this._send('did-load-resource', {
id,
status: 304, // not modified
path: requestPath,
mime: result.mimeType,
});
}
case WebviewResourceResponse.Type.AccessDenied:
{
return this._send('did-load-resource', {
id,
status: 401, // unauthorized
path: requestPath,
});
}
}
} catch {
// noop
}
return this._send('did-load-resource', {
id,
status: 404,
path: requestPath
});
}
private async localLocalhost(id: string, origin: string) {
const authority = this._environmentService.remoteAuthority;
const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined;
const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined;
return this._send('did-load-localhost', {
id,
origin,
location: redirect
});
}
}
......@@ -47,75 +47,6 @@
}
}();
function fatalError(/** @type {string} */ message) {
console.error(`Webview fatal error: ${message}`);
hostMessaging.postMessage('fatal-error', { message });
}
/** @type {Promise<void>} */
const workerReady = new Promise(async (resolveWorkerReady) => {
if (onElectron) {
return resolveWorkerReady();
}
if (!areServiceWorkersEnabled()) {
fatalError('Service Workers are not enabled in browser. Webviews will not work.');
return resolveWorkerReady();
}
const expectedWorkerVersion = 1;
navigator.serviceWorker.register('service-worker.js').then(
async registration => {
await navigator.serviceWorker.ready;
const versionHandler = (event) => {
if (event.data.channel !== 'version') {
return;
}
navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolveWorkerReady();
} else {
// If we have the wrong version, try once to unregister and re-register
return registration.update()
.then(() => navigator.serviceWorker.ready)
.finally(resolveWorkerReady);
}
};
navigator.serviceWorker.addEventListener('message', versionHandler);
registration.active.postMessage({ channel: 'version' });
},
error => {
fatalError(`Could not register service workers: ${error}.`);
resolveWorkerReady();
});
const forwardFromHostToWorker = (channel) => {
hostMessaging.onMessage(channel, event => {
navigator.serviceWorker.ready.then(registration => {
registration.active.postMessage({ channel: channel, data: event.data.args });
});
});
};
forwardFromHostToWorker('did-load-resource');
forwardFromHostToWorker('did-load-localhost');
navigator.serviceWorker.addEventListener('message', event => {
if (['load-resource', 'load-localhost'].includes(event.data.channel)) {
hostMessaging.postMessage(event.data.channel, event.data);
}
});
});
function areServiceWorkersEnabled() {
try {
return !!navigator.serviceWorker;
} catch (e) {
return false;
}
}
const unloadMonitor = new class {
......@@ -175,21 +106,15 @@
const host = {
postMessage: hostMessaging.postMessage.bind(hostMessaging),
onMessage: hostMessaging.onMessage.bind(hostMessaging),
ready: workerReady,
fakeLoad: !onElectron,
onElectron: onElectron,
useParentPostMessage: false,
onIframeLoaded: (/** @type {HTMLIFrameElement} */ frame) => {
unloadMonitor.onIframeLoaded(frame);
},
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);
}
rewriteCSP: (csp, endpoint) => {
const endpointUrl = new URL(endpoint);
return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin);
}
};
(/** @type {any} */ (window)).createWebviewManager(host);
......
......@@ -11,7 +11,6 @@
* focusIframeOnCreate?: boolean,
* ready?: Promise<void>,
* onIframeLoaded?: (iframe: HTMLIFrameElement) => void,
* fakeLoad?: boolean,
* rewriteCSP: (existingCSP: string, endpoint?: string) => string,
* onElectron?: boolean,
* useParentPostMessage: boolean,
......@@ -204,6 +203,68 @@
initialScrollProgress: undefined,
};
function fatalError(/** @type {string} */ message) {
console.error(`Webview fatal error: ${message}`);
host.postMessage('fatal-error', { message });
}
/** @type {Promise<void>} */
const workerReady = new Promise(async (resolveWorkerReady) => {
// if (onElectron) {
// return resolveWorkerReady();
// }
if (!areServiceWorkersEnabled()) {
fatalError('Service Workers are not enabled in browser. Webviews will not work.');
return resolveWorkerReady();
}
const expectedWorkerVersion = 1;
navigator.serviceWorker.register(`service-worker.js${self.location.search}`).then(
async registration => {
await navigator.serviceWorker.ready;
const versionHandler = (event) => {
if (event.data.channel !== 'version') {
return;
}
navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolveWorkerReady();
} else {
// If we have the wrong version, try once to unregister and re-register
return registration.update()
.then(() => navigator.serviceWorker.ready)
.finally(resolveWorkerReady);
}
};
navigator.serviceWorker.addEventListener('message', versionHandler);
registration.active.postMessage({ channel: 'version' });
},
error => {
fatalError(`Could not register service workers: ${error}.`);
resolveWorkerReady();
});
const forwardFromHostToWorker = (channel) => {
host.onMessage(channel, (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
registration.active.postMessage({ channel, data });
});
});
};
forwardFromHostToWorker('did-load-resource');
forwardFromHostToWorker('did-load-localhost');
navigator.serviceWorker.addEventListener('message', event => {
if (['load-resource', 'load-localhost'].includes(event.data.channel)) {
host.postMessage(event.data.channel, event.data);
}
});
});
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body
......@@ -489,7 +550,7 @@
let updateId = 0;
host.onMessage('content', async (_event, data) => {
const currentUpdateId = ++updateId;
await host.ready;
await workerReady;
if (currentUpdateId !== updateId) {
return;
}
......@@ -534,42 +595,33 @@
newFrame.setAttribute('frameborder', '0');
newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin allow-pointer-lock allow-downloads' : 'allow-same-origin allow-pointer-lock');
newFrame.setAttribute('allow', options.allowScripts ? 'clipboard-read; clipboard-write;' : '');
if (host.fakeLoad) {
// We should just be able to use srcdoc, but I wasn't
// seeing the service worker applying properly.
// Fake load an empty on the correct origin and then write real html
// into it to get around this.
newFrame.src = `./fake.html?id=${ID}`;
} else {
newFrame.src = `about:blank?webviewFrame`;
}
// We should just be able to use srcdoc, but I wasn't
// seeing the service worker applying properly.
// Fake load an empty on the correct origin and then write real html
// into it to get around this.
newFrame.src = `./fake.html?id=${ID}`;
newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
document.body.appendChild(newFrame);
if (!host.fakeLoad) {
// write new content onto iframe
newFrame.contentDocument.open();
}
/**
* @param {Document} contentDocument
*/
function onFrameLoaded(contentDocument) {
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325
setTimeout(() => {
if (host.fakeLoad) {
contentDocument.open();
contentDocument.write(newDocument);
contentDocument.close();
hookupOnLoadHandlers(newFrame);
}
contentDocument.open();
contentDocument.write(newDocument);
contentDocument.close();
hookupOnLoadHandlers(newFrame);
if (contentDocument) {
applyStyles(contentDocument, contentDocument.body);
}
}, 0);
}
if (host.fakeLoad && !options.allowScripts && isSafari) {
if (!options.allowScripts && isSafari) {
// On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired.
// Use polling instead.
const interval = setInterval(() => {
......@@ -666,15 +718,6 @@
}
}
if (!host.fakeLoad) {
hookupOnLoadHandlers(newFrame);
}
if (!host.fakeLoad) {
newFrame.contentDocument.write(newDocument);
newFrame.contentDocument.close();
}
host.postMessage('did-set-content', undefined);
});
......@@ -722,6 +765,14 @@
});
}
function areServiceWorkersEnabled() {
try {
return !!navigator.serviceWorker;
} catch (e) {
return false;
}
}
if (typeof module !== 'undefined') {
module.exports = createWebviewManager;
} else {
......
......@@ -15,11 +15,19 @@ const resourceCacheName = `vscode-resource-cache-${VERSION}`;
const rootPath = sw.location.pathname.replace(/\/service-worker.js$/, '');
const searchParams = new URL(location.toString()).searchParams;
/**
* Origin used for resources
*/
const resourceOrigin = searchParams.get('vscode-resource-origin') ?? sw.origin;
/**
* Root path for resources
*/
const resourceRoot = rootPath + '/vscode-resource';
const resolveTimeout = 30000;
/**
......@@ -163,12 +171,12 @@ sw.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// See if it's a resource request
if (requestUrl.origin === sw.origin && requestUrl.pathname.startsWith(resourceRoot + '/')) {
if (requestUrl.origin === resourceOrigin && requestUrl.pathname.startsWith(resourceRoot + '/')) {
return event.respondWith(processResourceRequest(event, requestUrl));
}
// See if it's a localhost request
if (requestUrl.origin !== sw.origin && requestUrl.host.match(/^localhost:(\d+)$/)) {
if (requestUrl.origin !== sw.origin && requestUrl.host.match(/^(localhost|127.0.0.1|0.0.0.0):(\d+)$/)) {
return event.respondWith(processLocalhostRequest(event, requestUrl));
}
});
......@@ -308,6 +316,7 @@ async function getOuterIframeClient(webviewId) {
const allClients = await sw.clients.matchAll({ includeUncontrolled: true });
return allClients.find(client => {
const clientUrl = new URL(client.url);
return (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`) && clientUrl.search.match(new RegExp('\\bid=' + webviewId));
const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html` || clientUrl.pathname === `${rootPath}/electron-browser-index.html`);
return hasExpectedPathName && clientUrl.search.match(new RegExp('\\bid=' + webviewId));
});
}
......@@ -5,11 +5,7 @@
import { addDisposableListener } from 'vs/base/browser/dom';
import { ThrottledDelayer } from 'vs/base/common/async';
import { streamToBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
......@@ -18,8 +14,6 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot
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 { loadLocalResource, readFileStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
......@@ -27,7 +21,6 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Webview {
private readonly _portMappingManager: WebviewPortMappingManager;
private _confirmBeforeClose: string;
private readonly _focusDelayer = this._register(new ThrottledDelayer(10));
......@@ -39,17 +32,26 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
webviewThemeDataProvider: WebviewThemeDataProvider,
@IConfigurationService configurationService: IConfigurationService,
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@INotificationService notificationService: INotificationService,
@ITunnelService tunnelService: ITunnelService,
@IFileService private readonly fileService: IFileService,
@IRequestService private readonly requestService: IRequestService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IRequestService requestService: IRequestService,
@ITelemetryService telemetryService: ITelemetryService,
@ITunnelService tunnelService: ITunnelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@ILogService private readonly logService: ILogService,
) {
super(id, options, contentOptions, extension, webviewThemeDataProvider, notificationService, logService, telemetryService, environmentService);
super(id, options, contentOptions, extension, webviewThemeDataProvider, {
notificationService,
logService,
telemetryService,
environmentService,
requestService,
fileService,
tunnelService,
remoteAuthorityResolverService
});
/* __GDPR__
"webview.createWebview" : {
......@@ -62,28 +64,11 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
webviewElementType: 'iframe',
});
this._portMappingManager = this._register(new WebviewPortMappingManager(
() => this.extension?.location,
() => this.content.options.portMapping || [],
tunnelService
));
this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => {
const rawPath = entry.path;
const normalizedPath = decodeURIComponent(rawPath);
const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path));
this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch);
}));
this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => {
this.localLocalhost(entry.id, entry.origin);
}));
this._confirmBeforeClose = this._configurationService.getValue<string>('window.confirmBeforeClose');
this._confirmBeforeClose = configurationService.getValue<string>('window.confirmBeforeClose');
this._register(this._configurationService.onDidChangeConfiguration(e => {
this._register(configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('window.confirmBeforeClose')) {
this._confirmBeforeClose = this._configurationService.getValue('window.confirmBeforeClose');
this._confirmBeforeClose = configurationService.getValue('window.confirmBeforeClose');
this._send(WebviewMessageChannels.setConfirmBeforeClose, this._confirmBeforeClose);
}
}));
......@@ -119,20 +104,24 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
} as const;
const queryString = (Object.keys(params) as Array<keyof typeof params>)
.map((key) => `${key}=${params[key]}`)
.map((key) => `${key}=${encodeURIComponent(params[key]!)}`)
.join('&');
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?${queryString}`);
this.element!.setAttribute('src', `${this.webviewContentEndpoint}/index.html?${queryString}`);
}
private get externalEndpoint(): string {
const endpoint = this.environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
protected get webviewContentEndpoint(): string {
const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
if (endpoint[endpoint.length - 1] === '/') {
return endpoint.slice(0, endpoint.length - 1);
}
return endpoint;
}
protected get webviewResourceEndpoint(): string {
return this.webviewContentEndpoint;
}
public mountTo(parent: HTMLElement) {
if (this.element) {
parent.appendChild(this.element);
......@@ -147,21 +136,21 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
return value
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}
return `${startQuote}${this.externalEndpoint}/vscode-resource/file${path}${endQuote}`;
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/file${path}${endQuote}`;
})
.replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}
return `${startQuote}${this.externalEndpoint}/vscode-resource/file${path}${endQuote}`;
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/file${path}${endQuote}`;
});
}
protected get extraContentOptions(): any {
return {
endpoint: this.externalEndpoint,
endpoint: this.webviewContentEndpoint,
confirmBeforeClose: this._confirmBeforeClose,
};
}
......@@ -178,92 +167,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
throw new Error('Method not implemented.');
}
private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) {
try {
const remoteAuthority = this.environmentService.remoteAuthority;
const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
const extensionLocation = this.extension?.location;
// If we are loading a file resource from a remote extension, rewrite the uri to go remote
let rewriteUri: undefined | ((uri: URI) => URI);
if (extensionLocation?.scheme === Schemas.vscodeRemote) {
rewriteUri = (uri) => {
if (uri.scheme === Schemas.file && extensionLocation?.scheme === Schemas.vscodeRemote) {
return URI.from({
scheme: Schemas.vscodeRemote,
authority: extensionLocation.authority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: uri.path
})
});
}
return uri;
};
}
const result = await loadLocalResource(uri, ifNoneMatch, {
extensionLocation: extensionLocation,
roots: this.content.options.localResourceRoots || [],
remoteConnectionData,
rewriteUri,
}, {
readFileStream: (resource, etag) => readFileStream(this.fileService, resource, etag),
}, this.requestService, this.logService, CancellationToken.None);
switch (result.type) {
case WebviewResourceResponse.Type.Success:
{
const { buffer } = await streamToBuffer(result.stream);
return this._send('did-load-resource', {
id,
status: 200,
path: requestPath,
mime: result.mimeType,
data: buffer,
etag: result.etag,
});
}
case WebviewResourceResponse.Type.NotModified:
{
return this._send('did-load-resource', {
id,
status: 304, // not modified
path: requestPath,
mime: result.mimeType,
});
}
case WebviewResourceResponse.Type.AccessDenied:
{
return this._send('did-load-resource', {
id,
status: 401, // unauthorized
path: requestPath,
});
}
}
} catch {
// noop
}
return this._send('did-load-resource', {
id,
status: 404,
path: requestPath
});
}
private async localLocalhost(id: string, origin: string) {
const authority = this.environmentService.remoteAuthority;
const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined;
const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined;
return this._send('did-load-localhost', {
id,
origin,
location: redirect
});
}
protected doPostMessage(channel: string, data?: any): void {
if (this.element) {
this.element.contentWindow!.postMessage({ channel, args: data }, '*');
......
......@@ -6,12 +6,10 @@
(function () {
'use strict';
const ipcRenderer = require('electron').ipcRenderer;
let isInDevelopmentMode = false;
const { ipcRenderer, contextBridge } = require('electron');
/**
* @type {import('../../browser/pre/main').WebviewHost}
* @type {import('../../browser/pre/main').WebviewHost & {isInDevelopmentMode: boolean}}
*/
const host = {
onElectron: true,
......@@ -23,44 +21,14 @@
ipcRenderer.on(channel, handler);
},
focusIframeOnCreate: true,
onIframeLoaded: (newFrame) => {
newFrame.contentWindow.onbeforeunload = () => {
if (isInDevelopmentMode) { // Allow reloads while developing a webview
host.postMessage('do-reload');
return false;
}
// Block navigation when not in development mode
console.log('prevented webview navigation');
return false;
};
// Electron 4 eats mouseup events from inside webviews
// https://github.com/microsoft/vscode/issues/75090
// Try to fix this by rebroadcasting mouse moves and mouseups so that we can
// emulate these on the main window
let isMouseDown = false;
newFrame.contentWindow.addEventListener('mousedown', () => {
isMouseDown = true;
});
const tryDispatchSyntheticMouseEvent = (e) => {
if (!isMouseDown) {
host.postMessage('synthetic-mouse-event', { type: e.type, screenX: e.screenX, screenY: e.screenY, clientX: e.clientX, clientY: e.clientY });
}
};
newFrame.contentWindow.addEventListener('mouseup', e => {
tryDispatchSyntheticMouseEvent(e);
isMouseDown = false;
});
newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent);
},
rewriteCSP: (csp) => {
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
},
isInDevelopmentMode: false
};
host.onMessage('devtools-opened', () => {
isInDevelopmentMode = true;
host.isInDevelopmentMode = true;
});
document.addEventListener('DOMContentLoaded', e => {
......@@ -70,5 +38,5 @@
};
});
require('../../browser/pre/main')(host);
contextBridge.exposeInMainWorld('vscodeHost', host);
}());
<!DOCTYPE html>
<html lang="en" style="width: 100%; height: 100%">
<head>
<title>Virtual Document</title>
<title>Virtual Document</title>
</head>
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%" role="document">
<script src="main.js"></script>
<script>
(function () {
(/** @type {any} */ (window)).createWebviewManager({
...window.vscodeHost,
onIframeLoaded: (newFrame) => {
newFrame.contentWindow.onbeforeunload = () => {
if (window.vscodeHost.isInDevelopmentMode) { // Allow reloads while developing a webview
window.vscodeHost.postMessage('do-reload');
return false;
}
// Block navigation when not in development mode
console.log('prevented webview navigation');
return false;
};
// Electron 4 eats mouseup events from inside webviews
// https://github.com/microsoft/vscode/issues/75090
// Try to fix this by rebroadcasting mouse moves and mouseups so that we can
// emulate these on the main window
let isMouseDown = false;
newFrame.contentWindow.addEventListener('mousedown', () => {
isMouseDown = true;
});
const tryDispatchSyntheticMouseEvent = (e) => {
if (!isMouseDown) {
window.vscodeHost.postMessage('synthetic-mouse-event', { type: e.type, screenX: e.screenX, screenY: e.screenY, clientX: e.clientX, clientY: e.clientY });
}
};
newFrame.contentWindow.addEventListener('mouseup', e => {
tryDispatchSyntheticMouseEvent(e);
isMouseDown = false;
});
newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent);
},
});
}());
</script>
</body>
</html>
......@@ -10,12 +10,15 @@ import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { IDisposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
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 { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
......@@ -23,7 +26,7 @@ import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/t
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget';
import { WebviewIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager';
import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading';
import { rewriteVsCodeResourceUrls } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> implements Webview, WebviewFindDelegate {
......@@ -43,14 +46,9 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
private _webviewFindWidget: WebviewFindWidget | undefined;
private _findStarted: boolean = false;
private readonly _resourceRequestManager: WebviewResourceRequestManager;
private readonly _focusDelayer = this._register(new ThrottledDelayer(10));
private _elementFocusImpl!: (options?: FocusOptions | undefined) => void;
private _isWebviewReadyForMessages = false;
private readonly _pendingMessages: Array<{ channel: string, data: any }> = [];
constructor(
id: string,
options: WebviewOptions,
......@@ -64,8 +62,21 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
@IConfigurationService configurationService: IConfigurationService,
@IMainProcessService mainProcessService: IMainProcessService,
@INotificationService notificationService: INotificationService,
@IFileService fileService: IFileService,
@IRequestService requestService: IRequestService,
@ITunnelService tunnelService: ITunnelService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
) {
super(id, options, contentOptions, extension, _webviewThemeDataProvider, notificationService, _myLogService, telemetryService, environmentService);
super(id, options, contentOptions, extension, _webviewThemeDataProvider, {
notificationService,
logService: _myLogService,
telemetryService,
environmentService,
fileService,
requestService,
tunnelService,
remoteAuthorityResolverService
});
/* __GDPR__
"webview.createWebview" : {
......@@ -82,18 +93,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
this._myLogService.debug(`Webview(${this.id}): init`);
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options));
this._resourceRequestManager.ensureReady()
.then(() => {
this._isWebviewReadyForMessages = true;
while (this._pendingMessages.length) {
const { channel, data } = this._pendingMessages.shift()!;
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
this.element?.send(channel, data);
}
});
this._register(addDisposableListener(this.element!, 'dom-ready', once(() => {
this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!));
})));
......@@ -162,7 +161,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
// and not the `vscode-file` URI because preload scripts are loaded
// via node.js from the main side and only allow `file:` protocol
this.element!.preload = FileAccess.asFileUri('./pre/electron-index.js', require).toString(true);
this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser/index.html?platform=electron`;
this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-origin=${encodeURIComponent(this.webviewResourceEndpoint)}`;
}
protected createElement(options: WebviewOptions) {
......@@ -189,16 +188,11 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
public set contentOptions(options: WebviewContentOptions) {
this._myLogService.debug(`Webview(${this.id}): will set content options`);
this._resourceRequestManager.update(options);
super.contentOptions = options;
}
public set localResourcesRoot(resources: URI[]) {
this._resourceRequestManager.update({
...this.contentOptions,
localResourceRoots: resources,
});
super.localResourcesRoot = resources;
private get webviewResourceEndpoint(): string {
return `https://${this.id}.vscode-webview-test.com`;
}
protected readonly extraContentOptions = {};
......@@ -221,12 +215,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
}
protected async doPostMessage(channel: string, data?: any): Promise<void> {
this._myLogService.debug(`Webview(${this.id}): will post message on '${channel}'`);
if (!this._isWebviewReadyForMessages) {
this._pendingMessages.push({ channel, data });
return;
}
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
this.element?.send(channel, data);
}
......
......@@ -3,10 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
......@@ -19,7 +17,7 @@ import { WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/bas
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-sandbox/resourceLoading';
import { rewriteVsCodeResourceUrls } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading';
import { WindowIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
......@@ -28,11 +26,6 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
*/
export class ElectronIframeWebview extends IFrameWebview {
private readonly _resourceRequestManager: WebviewResourceRequestManager;
private _isWebviewReadyForMessages = false;
private readonly _pendingMessages: Array<{ channel: string, data: any }> = [];
private readonly _webviewKeyboardHandler: WindowIgnoreMenuShortcutsManager;
constructor(
......@@ -48,25 +41,13 @@ export class ElectronIframeWebview extends IFrameWebview {
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@ILogService logService: ILogService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IMainProcessService mainProcessService: IMainProcessService,
@INotificationService noficationService: INotificationService,
@INativeHostService nativeHostService: INativeHostService,
) {
super(id, options, contentOptions, extension, webviewThemeDataProvider,
noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, configurationService, _remoteAuthorityResolverService, logService);
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options));
this._resourceRequestManager.ensureReady()
.then(() => {
this._isWebviewReadyForMessages = true;
while (this._pendingMessages.length) {
const { channel, data } = this._pendingMessages.shift()!;
this.element?.contentWindow!.postMessage({ channel, args: data }, '*');
}
});
configurationService, fileService, logService, noficationService, _remoteAuthorityResolverService, requestService, telemetryService, tunnelService, environmentService);
this._webviewKeyboardHandler = new WindowIgnoreMenuShortcutsManager(configurationService, mainProcessService, nativeHostService);
......@@ -80,32 +61,31 @@ export class ElectronIframeWebview extends IFrameWebview {
}
protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
super.initElement(extension, options, { platform: 'electron' });
super.initElement(extension, options, {
platform: 'electron',
'vscode-resource-origin': this.webviewResourceEndpoint,
});
}
public set contentOptions(options: WebviewContentOptions) {
this._resourceRequestManager.update(options);
super.contentOptions = options;
protected get webviewContentEndpoint(): string {
const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
if (endpoint[endpoint.length - 1] === '/') {
return endpoint.slice(0, endpoint.length - 1);
}
return endpoint;
}
public set localResourcesRoot(resources: URI[]) {
this._resourceRequestManager.update({
...this.contentOptions,
localResourceRoots: resources,
});
super.localResourcesRoot = resources;
protected get webviewResourceEndpoint(): string {
return `https://${this.id}.vscode-webview-test.com`;
}
protected get extraContentOptions() {
return {};
return {
endpoint: this.webviewContentEndpoint,
};
}
protected async doPostMessage(channel: string, data?: any): Promise<void> {
if (!this._isWebviewReadyForMessages) {
this._pendingMessages.push({ channel, data });
return;
}
this.element?.contentWindow!.postMessage({ channel, args: data }, '*');
}
......
......@@ -3,25 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals } from 'vs/base/common/arrays';
import { streamToBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { IFileService } from 'vs/platform/files/common/files';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRequestService } from 'vs/platform/request/common/request';
import { loadLocalResource, readFileStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping';
import { WebviewContentOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
/**
* Try to rewrite `vscode-resource:` urls in html
......@@ -43,136 +25,3 @@ export function rewriteVsCodeResourceUrls(
});
}
/**
* 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<IWebviewPortMapping>;
private _ready: Promise<void>;
constructor(
private readonly id: string,
private readonly extension: WebviewExtensionDescription | undefined,
initialContentOptions: WebviewContentOptions,
@ILogService private readonly _logService: ILogService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IMainProcessService mainProcessService: IMainProcessService,
@INativeHostService nativeHostService: INativeHostService,
@IFileService fileService: IFileService,
@IRequestService requestService: IRequestService,
) {
super();
this._logService.debug(`WebviewResourceRequestManager(${this.id}): init`);
this._webviewManagerService = ProxyChannel.toService<IWebviewManagerService>(mainProcessService.getChannel('webview'));
this._localResourceRoots = initialContentOptions.localResourceRoots || [];
this._portMappings = initialContentOptions.portMapping || [];
const remoteAuthority = environmentService.remoteAuthority;
const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
this._ready = this._webviewManagerService.registerWebview(this.id, nativeHostService.windowId, {
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`);
});
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)));
const loadResourceChannel = `vscode:loadWebviewResource-${id}`;
const loadResourceListener = async (_event: any, requestId: number, resource: UriComponents, ifNoneMatch: string | undefined) => {
const uri = URI.revive(resource);
try {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): starting resource load. uri: ${uri}`);
const response = await loadLocalResource(uri, ifNoneMatch, {
extensionLocation: this.extension?.location,
roots: this._localResourceRoots,
remoteConnectionData: remoteConnectionData,
}, {
readFileStream: (resource, etag) => readFileStream(fileService, resource, etag),
}, requestService, this._logService, CancellationToken.None);
this._logService.debug(`WebviewResourceRequestManager(${this.id}): finished resource load. uri: ${uri}, type=${response.type}`);
switch (response.type) {
case WebviewResourceResponse.Type.Success:
{
const buffer = await streamToBuffer(response.stream);
return this._webviewManagerService.didLoadResource(requestId, buffer, { etag: response.etag });
}
case WebviewResourceResponse.Type.NotModified:
return this._webviewManagerService.didLoadResource(requestId, 'not-modified');
case WebviewResourceResponse.Type.AccessDenied:
return this._webviewManagerService.didLoadResource(requestId, 'access-denied');
}
} catch {
// Noop
}
this._webviewManagerService.didLoadResource(requestId, 'not-found');
};
ipcRenderer.on(loadResourceChannel, loadResourceListener);
this._register(toDisposable(() => ipcRenderer.removeListener(loadResourceChannel, loadResourceListener)));
}
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 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;
}
}
......@@ -72,7 +72,13 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment
get webviewExternalEndpoint(): string { return `${Schemas.vscodeWebview}://{{uuid}}`; }
@memoize
get webviewResourceRoot(): string { return `${Schemas.vscodeWebviewResource}://{{uuid}}/{{resource}}`; }
get webviewResourceRoot(): string {
// On desktop, this endpoint is only used for the service worker to identify resouce loads and
// should never actually be requested.
//
// Required due to https://github.com/electron/electron/issues/28528
return 'https://{{uuid}}.vscode-webview-test.com/vscode-resource/{{resource}}';
}
@memoize
get webviewCspSource(): string { return `${Schemas.vscodeWebviewResource}:`; }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册