Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize, localize2 } from '../../../../nls.js';
7
import { IDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
10
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
11
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
12
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
13
import { IURLHandler, IURLService, IOpenURLOptions } from '../../../../platform/url/common/url.js';
14
import { IHostService } from '../../host/browser/host.js';
15
import { ActivationKind, IExtensionService } from '../common/extensions.js';
16
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
17
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
18
import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
19
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
20
import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
21
import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';
22
import { IProductService } from '../../../../platform/product/common/productService.js';
23
import { disposableWindowInterval } from '../../../../base/browser/dom.js';
24
import { mainWindow } from '../../../../base/browser/window.js';
25
import { ICommandService } from '../../../../platform/commands/common/commands.js';
26
import { isCancellationError } from '../../../../base/common/errors.js';
27
import { INotificationService } from '../../../../platform/notification/common/notification.js';
28
import { MarkdownString } from '../../../../base/common/htmlContent.js';
29
import { equalsIgnoreCase } from '../../../../base/common/strings.js';
30
31
const FIVE_MINUTES = 5 * 60 * 1000;
32
const THIRTY_SECONDS = 30 * 1000;
33
const URL_TO_HANDLE = 'extensionUrlHandler.urlToHandle';
34
const USER_TRUSTED_EXTENSIONS_CONFIGURATION_KEY = 'extensions.confirmedUriHandlerExtensionIds';
35
const USER_TRUSTED_EXTENSIONS_STORAGE_KEY = 'extensionUrlHandler.confirmedExtensions';
36
37
function isExtensionId(value: string): boolean {
38
return /^[a-z0-9][a-z0-9\-]*\.[a-z0-9][a-z0-9\-]*$/i.test(value);
39
}
40
41
class UserTrustedExtensionIdStorage {
42
43
get extensions(): string[] {
44
const userTrustedExtensionIdsJson = this.storageService.get(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, StorageScope.PROFILE, '[]');
45
46
try {
47
return JSON.parse(userTrustedExtensionIdsJson);
48
} catch {
49
return [];
50
}
51
}
52
53
constructor(private storageService: IStorageService) { }
54
55
has(id: string): boolean {
56
return this.extensions.indexOf(id) > -1;
57
}
58
59
add(id: string): void {
60
this.set([...this.extensions, id]);
61
}
62
63
set(ids: string[]): void {
64
this.storageService.store(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, JSON.stringify(ids), StorageScope.PROFILE, StorageTarget.MACHINE);
65
}
66
}
67
68
export const IExtensionUrlHandler = createDecorator<IExtensionUrlHandler>('extensionUrlHandler');
69
70
export interface IExtensionContributedURLHandler extends IURLHandler {
71
extensionDisplayName: string;
72
}
73
74
export interface IExtensionUrlHandler {
75
readonly _serviceBrand: undefined;
76
registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IExtensionContributedURLHandler): void;
77
unregisterExtensionHandler(extensionId: ExtensionIdentifier): void;
78
}
79
80
export interface IExtensionUrlHandlerOverride {
81
canHandleURL(uri: URI): boolean;
82
handleURL(uri: URI): Promise<boolean>;
83
}
84
85
export class ExtensionUrlHandlerOverrideRegistry {
86
87
private static readonly handlers = new Set<IExtensionUrlHandlerOverride>();
88
89
static registerHandler(handler: IExtensionUrlHandlerOverride): IDisposable {
90
this.handlers.add(handler);
91
92
return toDisposable(() => this.handlers.delete(handler));
93
}
94
95
static getHandler(uri: URI): IExtensionUrlHandlerOverride | undefined {
96
for (const handler of this.handlers) {
97
if (handler.canHandleURL(uri)) {
98
return handler;
99
}
100
}
101
102
return undefined;
103
}
104
}
105
106
/**
107
* This class handles URLs which are directed towards extensions.
108
* If a URL is directed towards an inactive extension, it buffers it,
109
* activates the extension and re-opens the URL once the extension registers
110
* a URL handler. If the extension never registers a URL handler, the urls
111
* will eventually be garbage collected.
112
*
113
* It also makes sure the user confirms opening URLs directed towards extensions.
114
*/
115
class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
116
117
readonly _serviceBrand: undefined;
118
119
private extensionHandlers = new Map<string, IExtensionContributedURLHandler>();
120
private uriBuffer = new Map<string, { timestamp: number; uri: URI }[]>();
121
private userTrustedExtensionsStorage: UserTrustedExtensionIdStorage;
122
private disposable: IDisposable;
123
124
constructor(
125
@IURLService urlService: IURLService,
126
@IExtensionService private readonly extensionService: IExtensionService,
127
@IDialogService private readonly dialogService: IDialogService,
128
@ICommandService private readonly commandService: ICommandService,
129
@IHostService private readonly hostService: IHostService,
130
@IStorageService private readonly storageService: IStorageService,
131
@IConfigurationService private readonly configurationService: IConfigurationService,
132
@INotificationService private readonly notificationService: INotificationService,
133
@IProductService private readonly productService: IProductService,
134
) {
135
this.userTrustedExtensionsStorage = new UserTrustedExtensionIdStorage(storageService);
136
137
const interval = disposableWindowInterval(mainWindow, () => this.garbageCollect(), THIRTY_SECONDS);
138
const urlToHandleValue = this.storageService.get(URL_TO_HANDLE, StorageScope.WORKSPACE);
139
if (urlToHandleValue) {
140
this.storageService.remove(URL_TO_HANDLE, StorageScope.WORKSPACE);
141
this.handleURL(URI.revive(JSON.parse(urlToHandleValue)), { trusted: true });
142
}
143
144
this.disposable = combinedDisposable(
145
urlService.registerHandler(this),
146
interval
147
);
148
149
const cache = ExtensionUrlBootstrapHandler.cache;
150
setTimeout(() => cache.forEach(([uri, option]) => this.handleURL(uri, option)));
151
}
152
153
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
154
if (!isExtensionId(uri.authority)) {
155
return false;
156
}
157
158
const overrideHandler = ExtensionUrlHandlerOverrideRegistry.getHandler(uri);
159
if (overrideHandler) {
160
const handled = await overrideHandler.handleURL(uri);
161
if (handled) {
162
return handled;
163
}
164
}
165
166
const extensionId = uri.authority;
167
168
const initialHandler = this.extensionHandlers.get(ExtensionIdentifier.toKey(extensionId));
169
let extensionDisplayName: string;
170
171
if (!initialHandler) {
172
// The extension is not yet activated, so let's check if it is installed and enabled
173
const extension = await this.extensionService.getExtension(extensionId);
174
if (!extension) {
175
await this.handleUnhandledURL(uri, extensionId, options);
176
return true;
177
} else {
178
extensionDisplayName = extension.displayName ?? '';
179
}
180
} else {
181
extensionDisplayName = initialHandler.extensionDisplayName;
182
}
183
184
const trusted = options?.trusted
185
|| this.productService.trustedExtensionProtocolHandlers?.some(value => equalsIgnoreCase(value, extensionId))
186
|| this.didUserTrustExtension(ExtensionIdentifier.toKey(extensionId));
187
188
if (!trusted) {
189
const uriString = uri.toString(false);
190
let uriLabel = uriString;
191
192
if (uriLabel.length > 40) {
193
uriLabel = `${uriLabel.substring(0, 30)}...${uriLabel.substring(uriLabel.length - 5)}`;
194
}
195
196
const result = await this.dialogService.confirm({
197
message: localize('confirmUrl', "Allow '{0}' extension to open this URI?", extensionDisplayName),
198
checkbox: {
199
label: localize('rememberConfirmUrl', "Do not ask me again for this extension"),
200
},
201
primaryButton: localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Open"),
202
custom: {
203
markdownDetails: [{
204
markdown: new MarkdownString(`<div title="${uriString}" aria-label='${uriString}'>${uriLabel}</div>`, { supportHtml: true }),
205
}]
206
}
207
});
208
209
if (!result.confirmed) {
210
return true;
211
}
212
213
if (result.checkboxChecked) {
214
this.userTrustedExtensionsStorage.add(ExtensionIdentifier.toKey(extensionId));
215
}
216
}
217
218
const handler = this.extensionHandlers.get(ExtensionIdentifier.toKey(extensionId));
219
220
if (handler) {
221
if (!initialHandler) {
222
// forward it directly
223
return await this.handleURLByExtension(extensionId, handler, uri, options);
224
}
225
226
// let the ExtensionUrlHandler instance handle this
227
return false;
228
}
229
230
// collect URI for eventual extension activation
231
const timestamp = new Date().getTime();
232
let uris = this.uriBuffer.get(ExtensionIdentifier.toKey(extensionId));
233
234
if (!uris) {
235
uris = [];
236
this.uriBuffer.set(ExtensionIdentifier.toKey(extensionId), uris);
237
}
238
239
uris.push({ timestamp, uri });
240
241
// activate the extension using ActivationKind.Immediate because URI handling might be part
242
// of resolving authorities (via authentication extensions)
243
await this.extensionService.activateByEvent(`onUri:${ExtensionIdentifier.toKey(extensionId)}`, ActivationKind.Immediate);
244
return true;
245
}
246
247
registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IExtensionContributedURLHandler): void {
248
this.extensionHandlers.set(ExtensionIdentifier.toKey(extensionId), handler);
249
250
const uris = this.uriBuffer.get(ExtensionIdentifier.toKey(extensionId)) || [];
251
252
for (const { uri } of uris) {
253
this.handleURLByExtension(extensionId, handler, uri);
254
}
255
256
this.uriBuffer.delete(ExtensionIdentifier.toKey(extensionId));
257
}
258
259
unregisterExtensionHandler(extensionId: ExtensionIdentifier): void {
260
this.extensionHandlers.delete(ExtensionIdentifier.toKey(extensionId));
261
}
262
263
private async handleURLByExtension(extensionId: ExtensionIdentifier | string, handler: IURLHandler, uri: URI, options?: IOpenURLOptions): Promise<boolean> {
264
return await handler.handleURL(uri, options);
265
}
266
267
private async handleUnhandledURL(uri: URI, extensionId: string, options?: IOpenURLOptions): Promise<void> {
268
try {
269
await this.commandService.executeCommand('workbench.extensions.installExtension', extensionId, {
270
justification: {
271
reason: `${localize('installDetail', "This extension wants to open a URI:")}\n${uri.toString()}`,
272
action: localize('openUri', "Open URI")
273
},
274
enable: true
275
});
276
} catch (error) {
277
if (!isCancellationError(error)) {
278
this.notificationService.error(error);
279
}
280
return;
281
}
282
283
const extension = await this.extensionService.getExtension(extensionId);
284
285
if (extension) {
286
await this.handleURL(uri, { ...options, trusted: true });
287
}
288
289
/* Extension cannot be added and require window reload */
290
else {
291
const result = await this.dialogService.confirm({
292
message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extensionId),
293
primaryButton: localize({ key: 'reloadAndOpen', comment: ['&& denotes a mnemonic'] }, "&&Reload Window and Open")
294
});
295
296
if (!result.confirmed) {
297
return;
298
}
299
300
this.storageService.store(URL_TO_HANDLE, JSON.stringify(uri.toJSON()), StorageScope.WORKSPACE, StorageTarget.MACHINE);
301
await this.hostService.reload();
302
}
303
}
304
305
// forget about all uris buffered more than 5 minutes ago
306
private garbageCollect(): void {
307
const now = new Date().getTime();
308
const uriBuffer = new Map<string, { timestamp: number; uri: URI }[]>();
309
310
this.uriBuffer.forEach((uris, extensionId) => {
311
uris = uris.filter(({ timestamp }) => now - timestamp < FIVE_MINUTES);
312
313
if (uris.length > 0) {
314
uriBuffer.set(extensionId, uris);
315
}
316
});
317
318
this.uriBuffer = uriBuffer;
319
}
320
321
private didUserTrustExtension(id: string): boolean {
322
if (this.userTrustedExtensionsStorage.has(id)) {
323
return true;
324
}
325
326
return this.getConfirmedTrustedExtensionIdsFromConfiguration().indexOf(id) > -1;
327
}
328
329
private getConfirmedTrustedExtensionIdsFromConfiguration(): Array<string> {
330
const trustedExtensionIds = this.configurationService.getValue(USER_TRUSTED_EXTENSIONS_CONFIGURATION_KEY);
331
332
if (!Array.isArray(trustedExtensionIds)) {
333
return [];
334
}
335
336
return trustedExtensionIds;
337
}
338
339
dispose(): void {
340
this.disposable.dispose();
341
this.extensionHandlers.clear();
342
this.uriBuffer.clear();
343
}
344
}
345
346
registerSingleton(IExtensionUrlHandler, ExtensionUrlHandler, InstantiationType.Eager);
347
348
/**
349
* This class handles URLs before `ExtensionUrlHandler` is instantiated.
350
* More info: https://github.com/microsoft/vscode/issues/73101
351
*/
352
class ExtensionUrlBootstrapHandler implements IWorkbenchContribution, IURLHandler {
353
354
static readonly ID = 'workbench.contrib.extensionUrlBootstrapHandler';
355
356
private static _cache: [URI, IOpenURLOptions | undefined][] = [];
357
private static disposable: IDisposable;
358
359
static get cache(): [URI, IOpenURLOptions | undefined][] {
360
ExtensionUrlBootstrapHandler.disposable.dispose();
361
362
const result = ExtensionUrlBootstrapHandler._cache;
363
ExtensionUrlBootstrapHandler._cache = [];
364
return result;
365
}
366
367
constructor(@IURLService urlService: IURLService) {
368
ExtensionUrlBootstrapHandler.disposable = urlService.registerHandler(this);
369
}
370
371
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
372
if (!isExtensionId(uri.authority)) {
373
return false;
374
}
375
376
ExtensionUrlBootstrapHandler._cache.push([uri, options]);
377
return true;
378
}
379
}
380
381
registerWorkbenchContribution2(ExtensionUrlBootstrapHandler.ID, ExtensionUrlBootstrapHandler, WorkbenchPhase.BlockRestore /* registration only */);
382
383
class ManageAuthorizedExtensionURIsAction extends Action2 {
384
385
constructor() {
386
super({
387
id: 'workbench.extensions.action.manageAuthorizedExtensionURIs',
388
title: localize2('manage', 'Manage Authorized Extension URIs...'),
389
category: localize2('extensions', 'Extensions'),
390
menu: {
391
id: MenuId.CommandPalette,
392
when: IsWebContext.toNegated()
393
}
394
});
395
}
396
397
async run(accessor: ServicesAccessor): Promise<void> {
398
const storageService = accessor.get(IStorageService);
399
const quickInputService = accessor.get(IQuickInputService);
400
const storage = new UserTrustedExtensionIdStorage(storageService);
401
const items = storage.extensions.map((label): IQuickPickItem => ({ label, picked: true }));
402
403
if (items.length === 0) {
404
await quickInputService.pick([{ label: localize('no', 'There are currently no authorized extension URIs.') }]);
405
return;
406
}
407
408
const result = await quickInputService.pick(items, { canPickMany: true });
409
410
if (!result) {
411
return;
412
}
413
414
storage.set(result.map(item => item.label));
415
}
416
}
417
418
registerAction2(ManageAuthorizedExtensionURIsAction);
419
420