diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 6fccf4bbc16e1..a24c409fe6011 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1238,18 +1238,20 @@ export interface WebviewsExt { } export interface WebviewsMain { - $createWebviewPanel(handle: string, + $createWebviewPanel( + pluginId: string, + handle: string, viewType: string, title: string, showOptions: theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions): void; - $disposeWebview(handle: string): void; - $reveal(handle: string, showOptions: theia.WebviewPanelShowOptions): void; - $setTitle(handle: string, value: string): void; - $setIconPath(handle: string, value: IconUrl | undefined): void; - $setHtml(handle: string, value: string): void; - $setOptions(handle: string, options: theia.WebviewOptions): void; - $postMessage(handle: string, value: any): Thenable; + $disposeWebview(pluginId: string, handle: string): void; + $reveal(pluginId: string, handle: string, showOptions: theia.WebviewPanelShowOptions): void; + $setTitle(pluginId: string, handle: string, value: string): void; + $setIconPath(pluginId: string, handle: string, value: IconUrl | undefined): void; + $setHtml(pluginId: string, handle: string, value: string): void; + $setOptions(pluginId: string, handle: string, options: theia.WebviewOptions): void; + $postMessage(pluginId: string, handle: string, value: any): Thenable; $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 6b763f62b29c3..6b4061c27fa1d 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -161,15 +161,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: WebviewWidget.FACTORY_ID, createWidget: async (identifier: WebviewWidgetIdentifier) => { - const externalEndpoint = await container.get(WebviewEnvironment).externalEndpoint(); - let endpoint = externalEndpoint.replace('{{uuid}}', identifier.id); - if (endpoint[endpoint.length - 1] === '/') { - endpoint = endpoint.slice(0, endpoint.length - 1); - } + const endpoint = await container.get(WebviewEnvironment).pluginEndpointUrl(identifier.pluginId); const child = container.createChild(); child.bind(WebviewWidgetIdentifier).toConstantValue(identifier); - child.bind(WebviewWidgetExternalEndpoint).toConstantValue(endpoint); + child.bind(WebviewWidgetExternalEndpoint).toConstantValue(endpoint + '/webview'); return child.get(WebviewWidget); } })).inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/webview/pre/host.js b/packages/plugin-ext/src/main/browser/webview/pre/host.js index e8f9323adaf2d..b5fa2e9cd01b3 100644 --- a/packages/plugin-ext/src/main/browser/webview/pre/host.js +++ b/packages/plugin-ext/src/main/browser/webview/pre/host.js @@ -37,7 +37,7 @@ if (handler) { handler(e, e.data.args); } else { - console.log('no handler for ', e); + console.error('no handler for ', e); } }); } @@ -53,7 +53,7 @@ const workerReady = new Promise(async (resolveWorkerReady) => { if (!areServiceWorkersEnabled()) { - console.log('Service Workers are not enabled. Webviews will not work properly'); + console.error('Service Workers are not enabled. Webviews will not work properly'); return resolveWorkerReady(); } diff --git a/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js b/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js index 3b2acb9baefe2..a6de65764b6d0 100644 --- a/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js +++ b/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js @@ -13,10 +13,10 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ - /*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ // copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/browser/pre/service-worker.js const VERSION = 1; @@ -148,7 +148,7 @@ self.addEventListener('message', async (event) => { : undefined; if (!resourceRequestStore.resolve(webviewId, data.path, response)) { - console.log('Could not resolve unknown resource', data.path); + console.error('Could not resolve unknown resource', data.path); } return; } @@ -158,13 +158,13 @@ self.addEventListener('message', async (event) => { const webviewId = getWebviewIdForClient(event.source); const data = event.data.data; if (!localhostRequestStore.resolve(webviewId, data.origin, data.location)) { - console.log('Could not resolve unknown localhost', data.origin); + console.error('Could not resolve unknown localhost', data.origin); } return; } } - console.log('Unknown message'); + console.error('Unknown message'); }); self.addEventListener('fetch', (event) => { @@ -192,7 +192,7 @@ self.addEventListener('activate', (event) => { async function processResourceRequest(event, requestUrl) { const client = await self.clients.get(event.clientId); if (!client) { - console.log('Could not find inner client for request'); + console.error('Could not find inner client for request'); return notFound(); } @@ -211,7 +211,7 @@ async function processResourceRequest(event, requestUrl) { const parentClient = await getOuterIframeClient(webviewId); if (!parentClient) { - console.log('Could not find parent client for request'); + console.error('Could not find parent client for request'); return notFound(); } @@ -259,7 +259,7 @@ async function processLocalhostRequest(event, requestUrl) { const parentClient = await getOuterIframeClient(webviewId); if (!parentClient) { - console.log('Could not find parent client for request'); + console.error('Could not find parent client for request'); return notFound(); } diff --git a/packages/plugin-ext/src/main/browser/webview/webview-environment.ts b/packages/plugin-ext/src/main/browser/webview/webview-environment.ts index 232ec8f481c7b..adee7086f0674 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview-environment.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview-environment.ts @@ -57,4 +57,21 @@ export class WebviewEnvironment { return (await this.externalEndpoint()).replace('{{uuid}}', '*'); } + protected encodedPluginIdSequence = 0; + protected readonly encodedPluginIds = new Map(); + + async pluginEndpointUrl(pluginId: string): Promise { + const externalEndpoint = await this.externalEndpoint(); + let encodedPluginId = this.encodedPluginIds.get(pluginId); + if (typeof encodedPluginId !== 'string') { + encodedPluginId = String(this.encodedPluginIdSequence++); + this.encodedPluginIds.set(pluginId, encodedPluginId); + } + let endpoint = externalEndpoint.replace('{{uuid}}', encodedPluginId); + if (endpoint[endpoint.length - 1] === '/') { + endpoint = endpoint.slice(0, endpoint.length - 1); + } + return endpoint; + } + } diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 20cbc85fc92eb..67fbc035e9580 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -72,6 +72,7 @@ export interface WebviewContentOptions { @injectable() export class WebviewWidgetIdentifier { id: string; + pluginId: string; } export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoint'); diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index 58fba5af9a259..d7c39495f42c4 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -55,13 +55,14 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { } async $createWebviewPanel( + pluginId: string, panelId: string, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: WebviewPanelOptions & WebviewOptions ): Promise { - const view = await this.widgets.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: panelId }); + const view = await this.widgets.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: panelId, pluginId }); this.hookWebview(view); view.viewType = viewType; view.title.label = title; @@ -121,15 +122,15 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { } } - async $disposeWebview(handle: string): Promise { - const view = await this.tryGetWebview(handle); + async $disposeWebview(pluginId: string, handle: string): Promise { + const view = await this.tryGetWebview(handle, pluginId); if (view) { view.dispose(); } } - async $reveal(handle: string, showOptions: WebviewPanelShowOptions): Promise { - const widget = await this.getWebview(handle); + async $reveal(pluginId: string, handle: string, showOptions: WebviewPanelShowOptions): Promise { + const widget = await this.getWebview(handle, pluginId); if (widget.isDisposed) { return; } @@ -149,23 +150,23 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { } } - async $setTitle(handle: string, value: string): Promise { - const webview = await this.getWebview(handle); + async $setTitle(pluginId: string, handle: string, value: string): Promise { + const webview = await this.getWebview(handle, pluginId); webview.title.label = value; } - async $setIconPath(handle: string, iconUrl: IconUrl | undefined): Promise { - const webview = await this.getWebview(handle); + async $setIconPath(pluginId: string, handle: string, iconUrl: IconUrl | undefined): Promise { + const webview = await this.getWebview(handle, pluginId); webview.setIconUrl(iconUrl); } - async $setHtml(handle: string, value: string): Promise { - const webview = await this.getWebview(handle); + async $setHtml(pluginId: string, handle: string, value: string): Promise { + const webview = await this.getWebview(handle, pluginId); webview.setHTML(value); } - async $setOptions(handle: string, options: WebviewOptions): Promise { - const webview = await this.getWebview(handle); + async $setOptions(pluginId: string, handle: string, options: WebviewOptions): Promise { + const webview = await this.getWebview(handle, pluginId); const { enableScripts, localResourceRoots, ...contentOptions } = options; webview.setContentOptions({ allowScripts: enableScripts, @@ -175,8 +176,8 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { } // tslint:disable-next-line:no-any - async $postMessage(handle: string, value: any): Promise { - const webview = await this.getWebview(handle); + async $postMessage(pluginId: string, handle: string, value: any): Promise { + const webview = await this.getWebview(handle, pluginId); webview.sendMessage(value); return true; } @@ -233,16 +234,17 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { this.proxy.$onDidChangeWebviewPanelViewState(widget.identifier.id, widget.viewState); } - private async getWebview(viewId: string): Promise { - const webview = await this.tryGetWebview(viewId); + private async getWebview(viewId: string, pluginId: string): Promise { + const webview = await this.tryGetWebview(viewId, pluginId); if (!webview) { throw new Error(`Unknown Webview: ${viewId}`); } return webview; } - private async tryGetWebview(id: string): Promise { - return this.widgets.getWidget(WebviewWidget.FACTORY_ID, { id }); + private async tryGetWebview(id: string, pluginId: string): Promise { + const identifier: WebviewWidgetIdentifier = { id, pluginId }; + return this.widgets.getWidget(WebviewWidget.FACTORY_ID, identifier); } } diff --git a/packages/plugin-ext/src/main/common/webview-protocol.ts b/packages/plugin-ext/src/main/common/webview-protocol.ts index ee1d194132685..1bed125320bee 100644 --- a/packages/plugin-ext/src/main/common/webview-protocol.ts +++ b/packages/plugin-ext/src/main/common/webview-protocol.ts @@ -19,7 +19,8 @@ * to ensure isolation from browser shared state as cookies, local storage and so on. * * Use `THEIA_WEBVIEW_EXTERNAL_ENDPOINT` to customize the hostname pattern of a origin. - * By default is `{{uuid}}.webview.{{hostname}}`. Where `{{uuid}}` is a placeholder for a webview global id. + * By default is `{{uuid}}.webview.{{hostname}}`. + * There `{{uuid}}` is a placeholder for a plugin id serving webviews. */ export namespace WebviewExternalEndpoint { export const pattern = 'THEIA_WEBVIEW_EXTERNAL_ENDPOINT'; diff --git a/packages/plugin-ext/src/main/node/plugin-service.ts b/packages/plugin-ext/src/main/node/plugin-service.ts index 05d05b4c79b13..1f2572f5672bb 100644 --- a/packages/plugin-ext/src/main/node/plugin-service.ts +++ b/packages/plugin-ext/src/main/node/plugin-service.ts @@ -36,12 +36,14 @@ export class PluginApiContribution implements BackendApplicationContribution { const webviewApp = connect(); webviewApp.use('/webview', serveStatic(path.join(__dirname, '../../../src/main/browser/webview/pre'))); - app.use(vhost(this.webviewExternalEndpoint(), webviewApp)); + const webviewExternalEndpoint = this.webviewExternalEndpoint(); + console.log(`Configuring to accept webviews on '${webviewExternalEndpoint}' hostname.`); + app.use(vhost(new RegExp(webviewExternalEndpoint, 'i'), webviewApp)); } protected webviewExternalEndpoint(): string { return (process.env[WebviewExternalEndpoint.pattern] || WebviewExternalEndpoint.defaultPattern) - .replace('{{uuid}}', '*') - .replace('{{hostname}}', '*'); + .replace('{{uuid}}', '.+') + .replace('{{hostname}}', '.+'); } } diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index ee7175c5d6a69..d1ec526779a6e 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -90,7 +90,7 @@ export class WebviewsExtImpl implements WebviewsExt { const { serializer, plugin } = entry; const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); - const revivedPanel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, toViewColumn(position)!, options, webview); + const revivedPanel = new WebviewPanelImpl(plugin, viewId, this.proxy, viewType, title, toViewColumn(position)!, options, webview); this.webviewPanels.set(viewId, revivedPanel); return serializer.deserializeWebviewPanel(revivedPanel, state); } @@ -107,10 +107,10 @@ export class WebviewsExtImpl implements WebviewsExt { } const webviewShowOptions = toWebviewPanelShowOptions(showOptions); const viewId = v4(); - this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, WebviewImpl.toWebviewOptions(options, this.workspace, plugin)); + this.proxy.$createWebviewPanel(plugin.model.id, viewId, viewType, title, webviewShowOptions, WebviewImpl.toWebviewOptions(options, this.workspace, plugin)); const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin); - const panel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, webviewShowOptions, options, webview); + const panel = new WebviewPanelImpl(plugin, viewId, this.proxy, viewType, title, webviewShowOptions, options, webview); this.webviewPanels.set(viewId, panel); return panel; } @@ -193,7 +193,7 @@ export class WebviewImpl implements theia.Webview { this.checkIsDisposed(); if (this._html !== value) { this._html = value; - this.proxy.$setHtml(this.viewId, value); + this.proxy.$setHtml(this.plugin.model.id, this.viewId, value); } } @@ -204,14 +204,14 @@ export class WebviewImpl implements theia.Webview { set options(newOptions: theia.WebviewOptions) { this.checkIsDisposed(); - this.proxy.$setOptions(this.viewId, WebviewImpl.toWebviewOptions(newOptions, this.workspace, this.plugin)); + this.proxy.$setOptions(this.plugin.model.id, this.viewId, WebviewImpl.toWebviewOptions(newOptions, this.workspace, this.plugin)); this._options = newOptions; } // tslint:disable-next-line:no-any postMessage(message: any): PromiseLike { this.checkIsDisposed(); - return this.proxy.$postMessage(this.viewId, message); + return this.proxy.$postMessage(this.plugin.model.id, this.viewId, message); } private checkIsDisposed(): void { @@ -245,7 +245,9 @@ export class WebviewPanelImpl implements theia.WebviewPanel { readonly onDidChangeViewStateEmitter = new Emitter(); public readonly onDidChangeViewState: Event = this.onDidChangeViewStateEmitter.event; - constructor(private readonly viewId: string, + constructor( + private readonly plugin: Plugin, + private readonly viewId: string, private readonly proxy: WebviewsMain, private readonly _viewType: string, private _title: string, @@ -265,7 +267,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { this.isDisposed = true; this.onDisposeEmitter.fire(void 0); - this.proxy.$disposeWebview(this.viewId); + this.proxy.$disposeWebview(this.plugin.model.id, this.viewId); this._webview.dispose(); this.onDisposeEmitter.dispose(); @@ -286,7 +288,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { this.checkIsDisposed(); if (this._title !== newTitle) { this._title = newTitle; - this.proxy.$setTitle(this.viewId, newTitle); + this.proxy.$setTitle(this.plugin.model.id, this.viewId, newTitle); } } @@ -298,7 +300,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { this.checkIsDisposed(); if (this._iconPath !== iconPath) { this._iconPath = iconPath; - this.proxy.$setIconPath(this.viewId, PluginIconPath.toUrl(iconPath, this._webview.plugin)); + this.proxy.$setIconPath(this.plugin.model.id, this.viewId, PluginIconPath.toUrl(iconPath, this._webview.plugin)); } } @@ -370,7 +372,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { preserveFocus = arg2; } this.checkIsDisposed(); - this.proxy.$reveal(this.viewId, { + this.proxy.$reveal(this.plugin.model.id, this.viewId, { area, viewColumn: viewColumn ? fromViewColumn(viewColumn) : undefined, preserveFocus @@ -380,7 +382,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { // tslint:disable-next-line:no-any postMessage(message: any): PromiseLike { this.checkIsDisposed(); - return this.proxy.$postMessage(this.viewId, message); + return this.proxy.$postMessage(this.plugin.model.id, this.viewId, message); } private checkIsDisposed(): void {