webviewElement.ts 15.4 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

M
Matt Bierner 已提交
6
import { addClass, addDisposableListener } from 'vs/base/browser/dom';
M
Matt Bierner 已提交
7 8
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
9
import { getMediaMime, guessMimeTypes } from 'vs/base/common/mime';
M
Matt Bierner 已提交
10
import { extname, nativeSep } from 'vs/base/common/paths';
11
import { startsWith } from 'vs/base/common/strings';
M
Matt Bierner 已提交
12
import URI from 'vs/base/common/uri';
13
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
M
Matt Bierner 已提交
14
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
15
import { IFileService } from 'vs/platform/files/common/files';
M
Matt Bierner 已提交
16
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
17
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
M
Matt Bierner 已提交
18 19
import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
import { WebviewFindWidget } from './webviewFindWidget';
20

M
Matt Bierner 已提交
21
export interface WebviewOptions {
M
Matt Bierner 已提交
22 23 24 25
	readonly allowScripts?: boolean;
	readonly allowSvgs?: boolean;
	readonly svgWhiteList?: string[];
	readonly enableWrappedPostMessage?: boolean;
26
	readonly useSameOriginForRoot?: boolean;
27
	readonly localResourceRoots?: ReadonlyArray<URI>;
M
Matt Bierner 已提交
28 29
}

30 31 32 33 34
const CORE_RESOURCE_PROTOCOL = 'vscode-core-resource';
const VSCODE_RESOURCE_PROTOCOL = 'vscode-resource';

export class WebviewElement extends Disposable {
	private _webview: Electron.WebviewTag;
35
	private _ready: Promise<this>;
36

37 38
	private _webviewFindWidget: WebviewFindWidget;
	private _findStarted: boolean = false;
M
Matt Bierner 已提交
39
	private _contents: string = '';
40
	private _state: string | undefined = undefined;
41

42
	constructor(
43 44 45
		private readonly _styleElement: Element,
		private readonly _contextKey: IContextKey<boolean>,
		private readonly _findInputContextKey: IContextKey<boolean>,
46
		private _options: WebviewOptions,
47
		@IInstantiationService instantiationService: IInstantiationService,
48 49 50
		@IThemeService private readonly _themeService: IThemeService,
		@IEnvironmentService private readonly _environmentService: IEnvironmentService,
		@IFileService private readonly _fileService: IFileService,
51
	) {
52
		super();
53
		this._webview = document.createElement('webview');
54
		this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Date.now()}`);
55

56 57 58
		// disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick)
		this._webview.setAttribute('disableblinkfeatures', 'Auxclick');

59
		this._webview.setAttribute('disableguestresize', '');
60
		this._webview.setAttribute('webpreferences', 'contextIsolation=yes');
61

62 63 64
		this._webview.style.flex = '0 1';
		this._webview.style.width = '0';
		this._webview.style.height = '0';
65 66
		this._webview.style.outline = '0';

M
Matt Bierner 已提交
67
		this._webview.preload = require.toUrl('./webview-pre.js');
68
		this._webview.src = this._options.useSameOriginForRoot ? require.toUrl('./webview.html') : 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E';
69

70
		this._ready = new Promise<this>(resolve => {
M
Matt Bierner 已提交
71
			const subscription = this._register(addDisposableListener(this._webview, 'ipc-message', (event) => {
72
				if (event.channel === 'webview-ready') {
73
					// console.info('[PID Webview] ' event.args[0]);
74 75 76 77 78
					addClass(this._webview, 'ready'); // can be found by debug command

					subscription.dispose();
					resolve(this);
				}
M
Matt Bierner 已提交
79
			}));
80 81
		});

82
		if (!this._options.useSameOriginForRoot) {
83
			let loaded = false;
84
			this._register(addDisposableListener(this._webview, 'did-start-loading', () => {
85 86 87 88 89 90
				if (loaded) {
					return;
				}
				loaded = true;

				const contents = this._webview.getWebContents();
M
Matt Bierner 已提交
91
				this.registerFileProtocols(contents);
92 93 94
			}));
		}

95 96
		if (!this._options.allowSvgs) {
			let loaded = false;
97
			this._register(addDisposableListener(this._webview, 'did-start-loading', () => {
98 99
				if (loaded) {
					return;
100
				}
101 102 103 104 105 106 107
				loaded = true;

				const contents = this._webview.getWebContents();
				if (!contents) {
					return;
				}

108
				(contents.session.webRequest as any).onBeforeRequest((details, callback) => {
109 110 111
					if (details.url.indexOf('.svg') > 0) {
						const uri = URI.parse(details.url);
						if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg') && !this.isAllowedSvg(uri)) {
112
							this.onDidBlockSvg();
113 114 115 116 117
							return callback({ cancel: true });
						}
					}
					return callback({});
				});
M
Matt Bierner 已提交
118

119
				(contents.session.webRequest as any).onHeadersReceived((details, callback) => {
M
Matt Bierner 已提交
120 121 122 123
					const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']) as any;
					if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) {
						const uri = URI.parse(details.url);
						if (uri && !this.isAllowedSvg(uri)) {
124
							this.onDidBlockSvg();
M
Matt Bierner 已提交
125 126 127
							return callback({ cancel: true });
						}
					}
128 129
					return callback({ cancel: false, responseHeaders: details.responseHeaders });
				});
130
			}));
131
		}
M
Matt Bierner 已提交
132

133
		this._toDispose.push(
134 135 136
			addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) {
				console.log(`[Embedded Page] ${e.message}`);
			}),
137 138 139
			addDisposableListener(this._webview, 'dom-ready', () => {
				this.layout();
			}),
M
Matt Bierner 已提交
140
			addDisposableListener(this._webview, 'crashed', () => {
141 142 143
				console.error('embedded page crashed');
			}),
			addDisposableListener(this._webview, 'ipc-message', (event) => {
M
Matt Bierner 已提交
144
				switch (event.channel) {
M
Matt Bierner 已提交
145 146 147 148 149 150
					case 'onmessage':
						if (this._options.enableWrappedPostMessage && event.args && event.args.length) {
							this._onMessage.fire(event.args[0]);
						}
						return;

M
Matt Bierner 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
					case 'did-click-link':
						let [uri] = event.args;
						this._onDidClickLink.fire(URI.parse(uri));
						return;

					case 'did-set-content':
						this._webview.style.flex = '';
						this._webview.style.width = '100%';
						this._webview.style.height = '100%';
						this.layout();
						return;

					case 'did-scroll':
						if (event.args && typeof event.args[0] === 'number') {
							this._onDidScroll.fire({ scrollYPercentage: event.args[0] });
						}
						return;
168 169 170 171

					case 'do-reload':
						this.reload();
						return;
172 173 174 175 176

					case 'do-update-state':
						this._state = event.args[0];
						this._onDidUpdateState.fire(this._state);
						return;
177
				}
178 179 180 181 182 183 184 185 186 187
			}),
			addDisposableListener(this._webview, 'focus', () => {
				if (this._contextKey) {
					this._contextKey.set(true);
				}
			}),
			addDisposableListener(this._webview, 'blur', () => {
				if (this._contextKey) {
					this._contextKey.reset();
				}
188 189 190 191
			}),
			addDisposableListener(this._webview, 'devtools-opened', () => {
				this._send('devtools-opened');
			}),
192 193
		);

194
		this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this));
195

196
		this.style(this._themeService.getTheme());
197
		this._themeService.onThemeChange(this.style, this, this._toDispose);
M
Matt Bierner 已提交
198
	}
199

M
Matt Bierner 已提交
200 201 202
	public mountTo(parent: HTMLElement) {
		parent.appendChild(this._webviewFindWidget.getDomNode());
		parent.appendChild(this._webview);
203 204 205 206
	}

	public notifyFindWidgetFocusChanged(isFocused: boolean) {
		this._contextKey.set(isFocused || document.activeElement === this._webview);
207 208
	}

209 210 211 212
	public notifyFindWidgetInputFocusChanged(isFocused: boolean) {
		this._findInputContextKey.set(isFocused);
	}

213
	dispose(): void {
214 215 216 217
		if (this._contextKey) {
			this._contextKey.reset();
		}

218 219 220 221 222 223
		if (this._webview) {
			this._webview.guestinstance = 'none';

			if (this._webview.parentElement) {
				this._webview.parentElement.removeChild(this._webview);
			}
J
Joao Moreno 已提交
224
		}
225

226 227 228
		this._webview = undefined;
		this._webviewFindWidget = undefined;
		super.dispose();
229 230
	}

231 232
	private readonly _onDidClickLink = this._register(new Emitter<URI>());
	public readonly onDidClickLink = this._onDidClickLink.event;
233

234 235
	private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number }>());
	public readonly onDidScroll = this._onDidScroll.event;
236

237 238
	private readonly _onDidUpdateState = this._register(new Emitter<string | undefined>());
	public readonly onDidUpdateState = this._onDidUpdateState.event;
239

240 241
	private readonly _onMessage = this._register(new Emitter<any>());
	public readonly onMessage = this._onMessage.event;
M
Matt Bierner 已提交
242

243 244 245
	private _send(channel: string, ...args: any[]): void {
		this._ready
			.then(() => this._webview.send(channel, ...args))
246
			.catch(err => console.error(err));
247 248
	}

249
	public set initialScrollProgress(value: number) {
250 251 252
		this._send('initial-scroll-position', value);
	}

253 254 255 256
	public set state(value: string | undefined) {
		this._state = value;
	}

257
	public set options(value: WebviewOptions) {
258
		this._options = value;
259 260 261 262 263
		this._send('content', {
			contents: this._contents,
			options: this._options,
			state: this._state
		});
264 265
	}

266
	public set contents(value: string) {
M
Matt Bierner 已提交
267
		this._contents = value;
M
Matt Bierner 已提交
268 269
		this._send('content', {
			contents: value,
270 271
			options: this._options,
			state: this._state
M
Matt Bierner 已提交
272
		});
273 274
	}

275
	public set baseUrl(value: string) {
276 277 278
		this._send('baseUrl', value);
	}

279
	public focus(): void {
280
		this._webview.focus();
281 282 283
		this._send('focus');
	}

284 285 286 287
	public sendMessage(data: any): void {
		this._send('message', data);
	}

288 289 290 291 292 293
	private onDidBlockSvg() {
		this.sendMessage({
			name: 'vscode-did-block-svg'
		});
	}

294
	private style(theme: ITheme): void {
295
		const { fontFamily, fontWeight, fontSize } = window.getComputedStyle(this._styleElement); // TODO@theme avoid styleElement
296

M
Matt Bierner 已提交
297 298 299 300 301 302 303 304 305
		const exportedColors = colorRegistry.getColorRegistry().getColors().reduce((colors, entry) => {
			const color = theme.getColor(entry.id);
			if (color) {
				colors['vscode-' + entry.id.replace('.', '-')] = color.toString();
			}
			return colors;
		}, {});


306
		const styles = {
M
Matt Bierner 已提交
307
			// Old vars
308 309 310
			'font-family': fontFamily,
			'font-weight': fontWeight,
			'font-size': fontSize,
M
Matt Bierner 已提交
311 312
			'background-color': theme.getColor(colorRegistry.editorBackground).toString(),
			'color': theme.getColor(colorRegistry.editorForeground).toString(),
313
			'link-color': theme.getColor(colorRegistry.textLinkForeground).toString(),
M
Matt Bierner 已提交
314 315 316 317 318 319 320
			'link-active-color': theme.getColor(colorRegistry.textLinkActiveForeground).toString(),

			// Offical API
			'vscode-editor-font-family': fontFamily,
			'vscode-editor-font-weight': fontWeight,
			'vscode-editor-font-size': fontSize,
			...exportedColors
321
		};
322

M
Matt Bierner 已提交
323
		const activeTheme = ApiThemeClassName.fromTheme(theme);
324
		this._send('styles', styles, activeTheme);
325 326

		this._webviewFindWidget.updateTheme(theme);
327
	}
328 329 330

	public layout(): void {
		const contents = (this._webview as any).getWebContents();
331
		if (!contents || contents.isDestroyed()) {
332 333
			return;
		}
334
		const window = contents.getOwnerBrowserWindow();
M
Matt Bierner 已提交
335
		if (!window || !window.webContents || window.webContents.isDestroyed()) {
336 337 338
			return;
		}
		window.webContents.getZoomFactor(factor => {
339 340 341 342
			if (contents.isDestroyed()) {
				return;
			}

343
			contents.setZoomFactor(factor);
M
Matt Bierner 已提交
344 345 346
			if (!this._webview || !this._webview.parentElement) {
				return;
			}
347

M
Matt Bierner 已提交
348 349
			const width = this._webview.parentElement.clientWidth;
			const height = this._webview.parentElement.clientHeight;
350 351 352 353 354 355 356 357
			contents.setSize({
				normal: {
					width: Math.floor(width * factor),
					height: Math.floor(height * factor)
				}
			});
		});
	}
M
Matt Bierner 已提交
358 359

	private isAllowedSvg(uri: URI): boolean {
360 361
		if (this._options.allowSvgs) {
			return true;
M
Matt Bierner 已提交
362
		}
363
		if (this._options.svgWhiteList) {
K
kieferrm 已提交
364
			return this._options.svgWhiteList.indexOf(uri.authority.toLowerCase()) >= 0;
365 366
		}
		return false;
M
Matt Bierner 已提交
367
	}
368

M
Matt Bierner 已提交
369 370 371 372 373
	private registerFileProtocols(contents: Electron.WebContents) {
		if (!contents || contents.isDestroyed()) {
			return;
		}

374 375
		const appRootUri = URI.file(this._environmentService.appRoot);

376
		registerFileProtocol(contents, CORE_RESOURCE_PROTOCOL, this._fileService, () => [
377
			appRootUri
M
Matt Bierner 已提交
378
		]);
379

380
		registerFileProtocol(contents, VSCODE_RESOURCE_PROTOCOL, this._fileService, () =>
381
			(this._options.localResourceRoots || [])
M
Matt Bierner 已提交
382 383 384 385
		);
	}

	public startFind(value: string, options?: Electron.FindInPageOptions) {
386 387 388 389 390 391 392 393
		if (!value) {
			return;
		}

		// ensure options is defined without modifying the original
		options = options || {};

		// FindNext must be false for a first request
M
Matt Bierner 已提交
394
		const findOptions: Electron.FindInPageOptions = {
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
			forward: options.forward,
			findNext: false,
			matchCase: options.matchCase,
			medialCapitalAsWordStart: options.medialCapitalAsWordStart
		};

		this._findStarted = true;
		this._webview.findInPage(value, findOptions);
	}

	/**
	 * Webviews expose a stateful find API.
	 * Successive calls to find will move forward or backward through onFindResults
	 * depending on the supplied options.
	 *
M
Matt Bierner 已提交
410 411
	 * @param value The string to search for. Empty strings are ignored.
	 * @param options
412
	 */
M
Matt Bierner 已提交
413
	public find(value: string, options?: Electron.FindInPageOptions): void {
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
		// Searching with an empty value will throw an exception
		if (!value) {
			return;
		}

		if (!this._findStarted) {
			this.startFind(value, options);
			return;
		}

		this._webview.findInPage(value, options);
	}

	public stopFind(keepSelection?: boolean): void {
		this._findStarted = false;
429
		this._webview.stopFindInPage(keepSelection ? 'keepSelection' : 'clearSelection');
430
	}
431 432 433 434 435 436 437 438

	public showFind() {
		this._webviewFindWidget.reveal();
	}

	public hideFind() {
		this._webviewFindWidget.hide();
	}
439

M
Matt Bierner 已提交
440 441 442
	public reload() {
		this.contents = this._contents;
	}
443
}
444

M
Matt Bierner 已提交
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463

enum ApiThemeClassName {
	light = 'vscode-light',
	dark = 'vscode-dark',
	highContrast = 'vscode-high-contrast'
}

namespace ApiThemeClassName {
	export function fromTheme(theme: ITheme): ApiThemeClassName {
		if (theme.type === LIGHT) {
			return ApiThemeClassName.light;
		} else if (theme.type === DARK) {
			return ApiThemeClassName.dark;
		} else {
			return ApiThemeClassName.highContrast;
		}
	}
}

464 465 466
function registerFileProtocol(
	contents: Electron.WebContents,
	protocol: string,
467
	fileService: IFileService,
468
	getRoots: () => ReadonlyArray<URI>
469
) {
470
	contents.session.protocol.registerBufferProtocol(protocol, (request, callback: any) => {
471
		const requestPath = URI.parse(request.url).path;
472
		const normalizedPath = URI.file(requestPath);
473
		for (const root of getRoots()) {
474 475
			if (startsWith(normalizedPath.fsPath, root.fsPath + nativeSep)) {
				fileService.resolveContent(normalizedPath, { encoding: 'binary' }).then(contents => {
476
					const mime = getMimeType(normalizedPath);
477 478 479 480 481
					callback({
						data: Buffer.from(contents.value, contents.encoding),
						mimeType: mime
					});
				}, () => {
482
					callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
483
				});
484 485
				return;
			}
486
		}
487 488
		console.error('Webview: Cannot load resource outside of protocol root');
		callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
489 490 491 492 493
	}, (error) => {
		if (error) {
			console.error('Failed to register protocol ' + protocol);
		}
	});
494 495 496 497 498 499 500 501 502 503
}

const webviewMimeTypes = {
	'.svg': 'image/svg+xml'
};

function getMimeType(normalizedPath: URI) {
	const ext = extname(normalizedPath.fsPath).toLowerCase();
	return webviewMimeTypes[ext] || getMediaMime(normalizedPath.fsPath) || guessMimeTypes(normalizedPath.fsPath)[0];
}