-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbrowserView.ts
291 lines (249 loc) · 10.9 KB
/
browserView.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import * as vscode from "vscode";
import { EXTENSION_ID } from "./extension";
import * as nodeCrypto from "crypto";
/**
* Options for how to display the browser window e.g. which column to place it in
*/
export interface ShowOptions {
readonly viewColumn?: vscode.ViewColumn;
}
/**
* Messages received from the owned WebView
*/
enum FromWebViewMessageType {
/**
* Request to open a URL in the system browser
*/
OpenInSystemBrowser = "open-in-system-browser",
/**
* The user changed the automatic browser cache bypass setting
*/
AutomaticBrowserCacheBybassStateChanged = "automatic-browser-cache-bypass-setting-changed"
}
/**
* Messages sent to the owned WebView
*/
enum ToWebViewMessageType {
/**
* Focus lock indicator setting has changed
*/
FocusIndicatorLockEnabledStateChanged = "focus-lock-indicator-setting-changed",
/**
* Automatic browser cache bypass setting has changed
*/
AutomaticBrowserCacheBybassStateChanged = "automatic-browser-cache-bypass-setting-changed",
/**
* Force an refresh of the focus lock state
*/
RefreshFocusLockState = "refresh-focus-lock-state",
/**
* Request the WebView to open a specific URL
*/
NavigateToUrl = "navigate-to-url"
}
const BROWSER_TITLE: string = "Inner Loop Buddy Browser";
const FOCUS_LOCK_SETTING_SECTION = "focusLockIndicator";
const AUTOMATIC_BROWSER_CACHE_BYPASS_SETTING_SECTION = "automaticBrowserCacheBypass";
export const BROWSER_VIEW_TYPE = `${EXTENSION_ID}.browser.view`;
function escapeAttribute(value: string | vscode.Uri): string {
return value.toString().replace(/"/g, """);
}
/**
* Generate a nonce for the content security policy attributes
*/
function getNonce(): string {
// Favour the browser crypto, if not use nodes (compatible) API
const actualCrypto = global.crypto ?? <Crypto>nodeCrypto.webcrypto;
const values = new Uint8Array(64);
actualCrypto.getRandomValues(values);
return values.reduce<string>((p, v) => p += v.toString(16), "");
}
/**
* A Browser view that can navigate to URLs and allow forward/back of navigation
* that happens within that WebView
*/
export class BrowserView {
private disposables: vscode.Disposable[] = [];
private readonly _onDidDispose = new vscode.EventEmitter<void>();
public readonly onDispose = this._onDidDispose.event;
/**
* Creates a browser view & editor
*
* @param extensionUri The base URI for resources to be loaded from
* @param targetUrl URL to display
* @param showOptions How the pane should be displayed
* @param targetWebView If supplied, editor will be created in that pane. If
* omitted, a new pane will be created
* @returns
*/
public static create(
extensionUri: vscode.Uri,
targetUrl: string,
showOptions?: ShowOptions,
targetWebView?: vscode.WebviewPanel
): BrowserView {
// Restore scenarios provide an existing Web View to attach to. If it's
// not supplied, we assume we want to create a new one.
if (!targetWebView) {
targetWebView = vscode.window.createWebviewPanel(
BROWSER_VIEW_TYPE,
BROWSER_TITLE, {
viewColumn: showOptions?.viewColumn ?? vscode.ViewColumn.Active,
preserveFocus: true // Don't automatically switch focus to the pane
}, {
enableScripts: true, // We execute scripts
enableForms: true, // We need form submissions
retainContextWhenHidden: true, // Don't purge the page when it's no longer the active tab
localResourceRoots: [
vscode.Uri.joinPath(extensionUri, "out/browser")
]
}
);
}
return new BrowserView(extensionUri, targetUrl, targetWebView);
}
private constructor(
private readonly extensionUri: vscode.Uri,
url: string,
private webViewPanel: vscode.WebviewPanel,
) {
this.disposables.push(this._onDidDispose);
this.disposables.push(webViewPanel);
vscode.workspace.onDidChangeConfiguration(this.handleConfigurationChanged, this, this.disposables);
this.webViewPanel.webview.onDidReceiveMessage(this.handleWebViewMessage, this, this.disposables);
this.webViewPanel.onDidDispose(this.dispose, this, this.disposables);
// When we're not longer the active editor, we need to re-evaluate the
// display of the focus captured indicator.
this.webViewPanel.onDidChangeViewState((e) => {
this.webViewPanel.webview.postMessage({
type: ToWebViewMessageType.RefreshFocusLockState
});
}, null, this.disposables);
this.show(url);
}
private handleWebViewMessage(payload: { type: FromWebViewMessageType, url?: string, automaticBrowserCacheBypass?: boolean }) {
switch (payload.type) {
case FromWebViewMessageType.OpenInSystemBrowser:
try {
const url = vscode.Uri.parse(payload.url!);
vscode.env.openExternal(url);
} catch { /* Noop */ }
break;
case FromWebViewMessageType.AutomaticBrowserCacheBybassStateChanged:
vscode.workspace.getConfiguration(EXTENSION_ID).update(AUTOMATIC_BROWSER_CACHE_BYPASS_SETTING_SECTION, payload.automaticBrowserCacheBypass);
break;
default:
debugger;
break;
}
}
private handleConfigurationChanged(e: vscode.ConfigurationChangeEvent) {
const configuration = vscode.workspace.getConfiguration(EXTENSION_ID);
if (e.affectsConfiguration(`${EXTENSION_ID}.${FOCUS_LOCK_SETTING_SECTION}`)) {
this.webViewPanel.webview.postMessage({
type: ToWebViewMessageType.FocusIndicatorLockEnabledStateChanged,
focusLockIndicator: configuration.get<boolean>(FOCUS_LOCK_SETTING_SECTION, true)
});
}
if (e.affectsConfiguration(`${EXTENSION_ID}.${AUTOMATIC_BROWSER_CACHE_BYPASS_SETTING_SECTION}`)) {
this.webViewPanel.webview.postMessage({
type: ToWebViewMessageType.AutomaticBrowserCacheBybassStateChanged,
automaticBrowserCacheBypass: configuration.get<boolean>(AUTOMATIC_BROWSER_CACHE_BYPASS_SETTING_SECTION, true)
});
}
}
/**
* Generates the HTML for the webview -- this is the actual editor HTML that
* includes our interactive controls etc. Of note, it includes the URL that
* will be navigated to when the editor renders.
*
* Important: This does nonce / Content Security Policy calculations.
* @param url URL to navigate to
* @returns HTML as a string to pass to a web view
*/
private getHtml(url: string): string {
const configuration = vscode.workspace.getConfiguration(EXTENSION_ID);
const nonce = getNonce();
const mainJs = this.extensionResourceUrl("out", "browser", "browserUi.js");
const mainCss = this.extensionResourceUrl("out", "browser", "styles.css");
const automaticBrowserBypass = configuration.get<boolean>(AUTOMATIC_BROWSER_CACHE_BYPASS_SETTING_SECTION, true);
const settingsData = escapeAttribute(JSON.stringify({
url: url,
focusLockIndiciatorEnabled: configuration.get<boolean>(FOCUS_LOCK_SETTING_SECTION, true),
automaticBrowserCacheBypass: automaticBrowserBypass
}));
return /* html */ `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
font-src 'nonce-${nonce}';
style-src-elem 'nonce-${nonce}';
script-src-elem 'nonce-${nonce}';
frame-src *;
">
<meta id="browser-settings" data-settings="${settingsData}">
<link rel="stylesheet" type="text/css" href="${mainCss}" nonce="${nonce}">
</head>
<body>
<header class="header">
<nav class="controls">
<button title="Back"
class="back-button icon">back</button>
<button title="Forward"
class="forward-button icon">forward</button>
</nav>
<input class="url-input" type="text">
<nav class="controls">
<input id="bypassCacheCheckbox"
type="checkbox" ${automaticBrowserBypass ? "checked" : ""}>
<label for="bypassCacheCheckbox">Bypass cache</label>
<button title="Reload"
class="reload-button icon">reload</button>
<button title="Open in system browser"
class="open-external-button icon">external</button>
</nav>
</header>
<div class="content">
<div class="iframe-focused-alert">Focus captured</div>
<iframe sandbox="allow-scripts allow-forms allow-same-origin"></iframe>
</div>
<script src="${mainJs}" nonce="${nonce}"></script>
</body>
</html>`;
}
/**
* Paths inside the webview need to reference unique & opaque URLs to access
* local resources. This is a conveniance function to make those conversions
* clearer
* @param pathComponents Directory paths to combine to get final relative path
* @returns the opaque url for the resource
*/
private extensionResourceUrl(...pathComponents: string[]): vscode.Uri {
return this.webViewPanel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, ...pathComponents));
}
public dispose() {
this._onDidDispose.fire();
vscode.Disposable.from(...this.disposables).dispose();
this.disposables = [];
}
/**
* Show a specific URL in this instance of the browser. This will also bring
* the editor pane to the front in the requested column
* @param url URL to navigate to
* @param options What display options to use
*/
public show(url: string, options?: ShowOptions) {
if (!this.webViewPanel.webview.html) {
this.webViewPanel.webview.html = this.getHtml(url);
} else {
this.webViewPanel.webview.postMessage({
type: ToWebViewMessageType.NavigateToUrl,
url: url
});
options = undefined;
}
this.webViewPanel.reveal(options?.viewColumn, true);
}
}