From 5cd1432690619326a5f437406737fad648bfbc68 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 11 Nov 2019 20:45:54 +0000 Subject: [PATCH] [webview] encode plugin id into webview origin in order to: - shorten webview section - allow resource caching per a plugin service worker Signed-off-by: Anton Kosyakov --- .../plugin-ext/src/common/plugin-api-rpc.ts | 18 +++++---- .../browser/plugin-ext-frontend-module.ts | 8 +--- .../src/main/browser/webview/pre/host.js | 4 +- .../browser/webview/webview-environment.ts | 17 ++++++++ .../src/main/browser/webview/webview.ts | 1 + .../src/main/browser/webviews-main.ts | 40 ++++++++++--------- .../src/main/common/webview-protocol.ts | 3 +- .../src/main/node/plugin-service.ts | 8 ++-- packages/plugin-ext/src/plugin/webviews.ts | 26 ++++++------ 9 files changed, 74 insertions(+), 51 deletions(-) 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/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 {