Skip to content

Commit

Permalink
[webview] encode plugin id into webview origin
Browse files Browse the repository at this point in the history
in order to:
- shorten webview section
- allow resource caching per a plugin service worker

Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Nov 12, 2019
1 parent f7508ff commit 730158f
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 61 deletions.
18 changes: 10 additions & 8 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
$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<boolean>;

$registerSerializer(viewType: string): void;
$unregisterSerializer(viewType: string): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-ext/src/main/browser/webview/pre/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
if (handler) {
handler(e, e.data.args);
} else {
console.log('no handler for ', e);
console.error('no handler for ', e);
}
});
}
Expand All @@ -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();
}

Expand Down
20 changes: 10 additions & 10 deletions packages/plugin-ext/src/main/browser/webview/pre/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand All @@ -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) => {
Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,21 @@ export class WebviewEnvironment {
return (await this.externalEndpoint()).replace('{{uuid}}', '*');
}

protected encodedPluginIdSequence = 0;
protected readonly encodedPluginIds = new Map<string, string>();

async pluginEndpointUrl(pluginId: string): Promise<string> {
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;
}

}
1 change: 1 addition & 0 deletions packages/plugin-ext/src/main/browser/webview/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface WebviewContentOptions {
@injectable()
export class WebviewWidgetIdentifier {
id: string;
pluginId: string;
}

export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoint');
Expand Down
40 changes: 21 additions & 19 deletions packages/plugin-ext/src/main/browser/webviews-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const view = await this.widgets.getOrCreateWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: panelId });
const view = await this.widgets.getOrCreateWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: panelId, pluginId });
this.hookWebview(view);
view.viewType = viewType;
view.title.label = title;
Expand Down Expand Up @@ -121,15 +122,15 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
}
}

async $disposeWebview(handle: string): Promise<void> {
const view = await this.tryGetWebview(handle);
async $disposeWebview(pluginId: string, handle: string): Promise<void> {
const view = await this.tryGetWebview(handle, pluginId);
if (view) {
view.dispose();
}
}

async $reveal(handle: string, showOptions: WebviewPanelShowOptions): Promise<void> {
const widget = await this.getWebview(handle);
async $reveal(pluginId: string, handle: string, showOptions: WebviewPanelShowOptions): Promise<void> {
const widget = await this.getWebview(handle, pluginId);
if (widget.isDisposed) {
return;
}
Expand All @@ -149,23 +150,23 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
}
}

async $setTitle(handle: string, value: string): Promise<void> {
const webview = await this.getWebview(handle);
async $setTitle(pluginId: string, handle: string, value: string): Promise<void> {
const webview = await this.getWebview(handle, pluginId);
webview.title.label = value;
}

async $setIconPath(handle: string, iconUrl: IconUrl | undefined): Promise<void> {
const webview = await this.getWebview(handle);
async $setIconPath(pluginId: string, handle: string, iconUrl: IconUrl | undefined): Promise<void> {
const webview = await this.getWebview(handle, pluginId);
webview.setIconUrl(iconUrl);
}

async $setHtml(handle: string, value: string): Promise<void> {
const webview = await this.getWebview(handle);
async $setHtml(pluginId: string, handle: string, value: string): Promise<void> {
const webview = await this.getWebview(handle, pluginId);
webview.setHTML(value);
}

async $setOptions(handle: string, options: WebviewOptions): Promise<void> {
const webview = await this.getWebview(handle);
async $setOptions(pluginId: string, handle: string, options: WebviewOptions): Promise<void> {
const webview = await this.getWebview(handle, pluginId);
const { enableScripts, localResourceRoots, ...contentOptions } = options;
webview.setContentOptions({
allowScripts: enableScripts,
Expand All @@ -175,8 +176,8 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
}

// tslint:disable-next-line:no-any
async $postMessage(handle: string, value: any): Promise<boolean> {
const webview = await this.getWebview(handle);
async $postMessage(pluginId: string, handle: string, value: any): Promise<boolean> {
const webview = await this.getWebview(handle, pluginId);
webview.sendMessage(value);
return true;
}
Expand Down Expand Up @@ -233,16 +234,17 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
this.proxy.$onDidChangeWebviewPanelViewState(widget.identifier.id, widget.viewState);
}

private async getWebview(viewId: string): Promise<WebviewWidget> {
const webview = await this.tryGetWebview(viewId);
private async getWebview(viewId: string, pluginId: string): Promise<WebviewWidget> {
const webview = await this.tryGetWebview(viewId, pluginId);
if (!webview) {
throw new Error(`Unknown Webview: ${viewId}`);
}
return webview;
}

private async tryGetWebview(id: string): Promise<WebviewWidget | undefined> {
return this.widgets.getWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id });
private async tryGetWebview(id: string, pluginId: string): Promise<WebviewWidget | undefined> {
const identifier: WebviewWidgetIdentifier = { id, pluginId };
return this.widgets.getWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, identifier);
}

}
3 changes: 2 additions & 1 deletion packages/plugin-ext/src/main/common/webview-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 5 additions & 3 deletions packages/plugin-ext/src/main/node/plugin-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}', '.+');
}
}
26 changes: 14 additions & 12 deletions packages/plugin-ext/src/plugin/webviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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<boolean> {
this.checkIsDisposed();
return this.proxy.$postMessage(this.viewId, message);
return this.proxy.$postMessage(this.plugin.model.id, this.viewId, message);
}

private checkIsDisposed(): void {
Expand Down Expand Up @@ -245,7 +245,9 @@ export class WebviewPanelImpl implements theia.WebviewPanel {
readonly onDidChangeViewStateEmitter = new Emitter<theia.WebviewPanelOnDidChangeViewStateEvent>();
public readonly onDidChangeViewState: Event<theia.WebviewPanelOnDidChangeViewStateEvent> = 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,
Expand All @@ -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();
Expand All @@ -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);
}
}

Expand All @@ -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));
}
}

Expand Down Expand Up @@ -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
Expand All @@ -380,7 +382,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel {
// tslint:disable-next-line:no-any
postMessage(message: any): PromiseLike<boolean> {
this.checkIsDisposed();
return this.proxy.$postMessage(this.viewId, message);
return this.proxy.$postMessage(this.plugin.model.id, this.viewId, message);
}

private checkIsDisposed(): void {
Expand Down

0 comments on commit 730158f

Please sign in to comment.