diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 66a80b8867f..0998d2fff63 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -17,6 +17,7 @@ "documentFiltersExclusive", "editorInsets", "embeddings", + "envExtractUri", "extensionRuntime", "extensionsAny", "externalUriOpener", diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 49804022e0a..6f7a0ff8516 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -174,6 +174,9 @@ const _allApiProposals = { embeddings: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', }, + envExtractUri: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.envExtractUri.d.ts', + }, extensionRuntime: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadUrls.ts b/src/vs/workbench/api/browser/mainThreadUrls.ts index bb0fa287aeb..d0bf2f4e746 100644 --- a/src/vs/workbench/api/browser/mainThreadUrls.ts +++ b/src/vs/workbench/api/browser/mainThreadUrls.ts @@ -10,6 +10,10 @@ import { URI, UriComponents } from '../../../base/common/uri.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import { IExtensionContributedURLHandler, IExtensionUrlHandler } from '../../services/extensions/browser/extensionUrlHandler.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; +import { ITrustedDomainService } from '../../contrib/url/browser/trustedDomainService.js'; +import { readStaticTrustedDomains } from '../../contrib/url/browser/trustedDomains.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { IWebContentExtractorService } from '../../../platform/webContentExtractor/common/webContentExtractor.js'; class ExtensionUrlHandler implements IExtensionContributedURLHandler { @@ -37,10 +41,15 @@ export class MainThreadUrls implements MainThreadUrlsShape { constructor( context: IExtHostContext, + @ITrustedDomainService trustedDomainService: ITrustedDomainService, @IURLService private readonly urlService: IURLService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWebContentExtractorService private readonly webContentExtractorService: IWebContentExtractorService, @IExtensionUrlHandler private readonly extensionUrlHandler: IExtensionUrlHandler ) { this.proxy = context.getProxy(ExtHostContext.ExtHostUrls); + trustedDomainService.onDidChangeTrustedDomains(() => this.handleTrustedDomainsChange()); + void this.handleTrustedDomainsChange(); } $registerUriHandler(handle: number, extensionId: ExtensionIdentifier, extensionDisplayName: string): Promise { @@ -73,6 +82,16 @@ export class MainThreadUrls implements MainThreadUrlsShape { return this.urlService.create(uri); } + async handleTrustedDomainsChange() { + const { defaultTrustedDomains, trustedDomains, } = this.instantiationService.invokeFunction(readStaticTrustedDomains); + await this.proxy.$updateTrustedDomains([...defaultTrustedDomains, ...trustedDomains]); + } + + async $extractExternalUris(uris: UriComponents[]): Promise { + const extractedUris = await this.webContentExtractorService.extract(uris.map(uri => URI.revive(uri))); + return extractedUris; + } + dispose(): void { this.handlers.forEach(({ disposable }) => disposable.dispose()); this.handlers.clear(); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 389260be966..49a830b0f45 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -416,6 +416,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I throw err; } }, + isTrustedExternalUris(uris: URI[]): boolean[] { + checkProposedApiEnabled(extension, 'envExtractUri'); + return extHostUrls.isTrustedExternalUris(uris); + }, + extractExternalUris(uris: URI[]): Promise { + checkProposedApiEnabled(extension, 'envExtractUri'); + return extHostUrls.extractExternalUris(uris); + }, get remoteName() { return getRemoteName(initData.remote.authority); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6acf90ca268..22bc07bfae9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1399,6 +1399,7 @@ export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier, extensionDisplayName: string): Promise; $unregisterUriHandler(handle: number): Promise; $createAppUri(uri: UriComponents): Promise; + $extractExternalUris(uris: UriComponents[]): Promise; } export interface IChatDto { @@ -1436,6 +1437,7 @@ export type IChatProgressDto = export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; + $updateTrustedDomains(trustedDomains: string[]): Promise; } export interface MainThreadUriOpenersShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostUrls.ts b/src/vs/workbench/api/common/extHostUrls.ts index c75ce2d4ca1..d2fce5fdfcf 100644 --- a/src/vs/workbench/api/common/extHostUrls.ts +++ b/src/vs/workbench/api/common/extHostUrls.ts @@ -9,6 +9,7 @@ import { URI, UriComponents } from '../../../base/common/uri.js'; import { toDisposable } from '../../../base/common/lifecycle.js'; import { onUnexpectedError } from '../../../base/common/errors.js'; import { ExtensionIdentifierSet, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { isURLDomainTrusted } from '../../contrib/url/common/trustedDomains.js'; export class ExtHostUrls implements ExtHostUrlsShape { @@ -18,6 +19,8 @@ export class ExtHostUrls implements ExtHostUrlsShape { private handles = new ExtensionIdentifierSet(); private handlers = new Map(); + private _trustedDomains: string[] = []; + constructor( mainContext: IMainContext ) { @@ -60,4 +63,16 @@ export class ExtHostUrls implements ExtHostUrlsShape { async createAppUri(uri: URI): Promise { return URI.revive(await this._proxy.$createAppUri(uri)); } + + async $updateTrustedDomains(trustedDomains: string[]): Promise { + this._trustedDomains = trustedDomains; + } + + isTrustedExternalUris(uris: URI[]): boolean[] { + return uris.map(uri => isURLDomainTrusted(uri, this._trustedDomains)); + } + + extractExternalUris(uris: URI[]): Promise { + return this._proxy.$extractExternalUris(uris); + } } diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainService.ts b/src/vs/workbench/contrib/url/browser/trustedDomainService.ts index 99cddd1842e..a5f3100c137 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainService.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainService.ts @@ -10,13 +10,14 @@ import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; import { TRUSTED_DOMAINS_STORAGE_KEY, readStaticTrustedDomains } from './trustedDomains.js'; -import { testUrlMatchesGlob } from '../common/urlGlob.js'; +import { isURLDomainTrusted } from '../common/trustedDomains.js'; +import { Event, Emitter } from '../../../../base/common/event.js'; export const ITrustedDomainService = createDecorator('ITrustedDomainService'); export interface ITrustedDomainService { _serviceBrand: undefined; - + onDidChangeTrustedDomains: Event; isValid(resource: URI): boolean; } @@ -25,6 +26,9 @@ export class TrustedDomainService extends Disposable implements ITrustedDomainSe private _staticTrustedDomainsResult!: WindowIdleValue; + private _onDidChangeTrustedDomains: Emitter = this._register(new Emitter()); + onDidChangeTrustedDomains: Event = this._onDidChangeTrustedDomains.event; + constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IStorageService private readonly _storageService: IStorageService, @@ -44,6 +48,7 @@ export class TrustedDomainService extends Disposable implements ITrustedDomainSe this._register(this._storageService.onDidChangeValue(StorageScope.APPLICATION, TRUSTED_DOMAINS_STORAGE_KEY, this._store)(() => { this._staticTrustedDomainsResult?.dispose(); this._staticTrustedDomainsResult = initStaticDomainsResult(); + this._onDidChangeTrustedDomains.fire(); })); } @@ -54,54 +59,3 @@ export class TrustedDomainService extends Disposable implements ITrustedDomainSe return isURLDomainTrusted(resource, allTrustedDomains); } } - -const rLocalhost = /^localhost(:\d+)?$/i; -const r127 = /^127.0.0.1(:\d+)?$/; - -function isLocalhostAuthority(authority: string) { - return rLocalhost.test(authority) || r127.test(authority); -} - -/** - * Case-normalize some case-insensitive URLs, such as github. - */ -function normalizeURL(url: string | URI): string { - const caseInsensitiveAuthorities = ['github.com']; - try { - const parsed = typeof url === 'string' ? URI.parse(url, true) : url; - if (caseInsensitiveAuthorities.includes(parsed.authority)) { - return parsed.with({ path: parsed.path.toLowerCase() }).toString(true); - } else { - return parsed.toString(true); - } - } catch { return url.toString(); } -} - -/** - * Check whether a domain like https://www.microsoft.com matches - * the list of trusted domains. - * - * - Schemes must match - * - There's no subdomain matching. For example https://microsoft.com doesn't match https://www.microsoft.com - * - Star matches all subdomains. For example https://*.microsoft.com matches https://www.microsoft.com and https://foo.bar.microsoft.com - */ -export function isURLDomainTrusted(url: URI, trustedDomains: string[]): boolean { - url = URI.parse(normalizeURL(url)); - trustedDomains = trustedDomains.map(normalizeURL); - - if (isLocalhostAuthority(url.authority)) { - return true; - } - - for (let i = 0; i < trustedDomains.length; i++) { - if (trustedDomains[i] === '*') { - return true; - } - - if (testUrlMatchesGlob(url, trustedDomains[i])) { - return true; - } - } - - return false; -} diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index 3a4df27b118..23f27810cae 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -18,7 +18,8 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ITrustedDomainService, isURLDomainTrusted } from './trustedDomainService.js'; +import { ITrustedDomainService } from './trustedDomainService.js'; +import { isURLDomainTrusted } from '../common/trustedDomains.js'; import { configureOpenerTrustedDomainsHandler, readStaticTrustedDomains } from './trustedDomains.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; diff --git a/src/vs/workbench/contrib/url/common/trustedDomains.ts b/src/vs/workbench/contrib/url/common/trustedDomains.ts new file mode 100644 index 00000000000..850a835769a --- /dev/null +++ b/src/vs/workbench/contrib/url/common/trustedDomains.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { URI } from '../../../../base/common/uri.js'; +import { testUrlMatchesGlob } from './urlGlob.js'; + + +/** + * Check whether a domain like https://www.microsoft.com matches + * the list of trusted domains. + * + * - Schemes must match + * - There's no subdomain matching. For example https://microsoft.com doesn't match https://www.microsoft.com + * - Star matches all subdomains. For example https://*.microsoft.com matches https://www.microsoft.com and https://foo.bar.microsoft.com + */ + +export function isURLDomainTrusted(url: URI, trustedDomains: string[]): boolean { + url = URI.parse(normalizeURL(url)); + trustedDomains = trustedDomains.map(normalizeURL); + + if (isLocalhostAuthority(url.authority)) { + return true; + } + + for (let i = 0; i < trustedDomains.length; i++) { + if (trustedDomains[i] === '*') { + return true; + } + + if (testUrlMatchesGlob(url, trustedDomains[i])) { + return true; + } + } + + return false; +} +/** + * Case-normalize some case-insensitive URLs, such as github. + */ + +export function normalizeURL(url: string | URI): string { + const caseInsensitiveAuthorities = ['github.com']; + try { + const parsed = typeof url === 'string' ? URI.parse(url, true) : url; + if (caseInsensitiveAuthorities.includes(parsed.authority)) { + return parsed.with({ path: parsed.path.toLowerCase() }).toString(true); + } else { + return parsed.toString(true); + } + } catch { return url.toString(); } +} +const rLocalhost = /^localhost(:\d+)?$/i; +const r127 = /^127.0.0.1(:\d+)?$/; + +export function isLocalhostAuthority(authority: string) { + return rLocalhost.test(authority) || r127.test(authority); +} + diff --git a/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts index 020ac3a9331..d36e21289d1 100644 --- a/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts +++ b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../../base/common/event.js'; import { URI } from '../../../../../base/common/uri.js'; -import { isURLDomainTrusted, ITrustedDomainService } from '../../browser/trustedDomainService.js'; +import { ITrustedDomainService } from '../../browser/trustedDomainService.js'; +import { isURLDomainTrusted } from '../../common/trustedDomains.js'; export class MockTrustedDomainService implements ITrustedDomainService { _serviceBrand: undefined; @@ -12,6 +14,8 @@ export class MockTrustedDomainService implements ITrustedDomainService { constructor(private readonly _trustedDomains: string[] = []) { } + onDidChangeTrustedDomains: Event = Event.None; + isValid(resource: URI): boolean { return isURLDomainTrusted(resource, this._trustedDomains); } diff --git a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index aef79543d56..50b2af6a09d 100644 --- a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { isURLDomainTrusted } from '../../browser/trustedDomainService.js'; +import { isURLDomainTrusted } from '../../common/trustedDomains.js'; function linkAllowedByRules(link: string, rules: string[]) { assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should be allowed by rules\n${JSON.stringify(rules)}`); diff --git a/src/vscode-dts/vscode.proposed.envExtractUri.d.ts b/src/vscode-dts/vscode.proposed.envExtractUri.d.ts new file mode 100644 index 00000000000..73d116bcc96 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.envExtractUri.d.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/243615 + + export namespace env { + export function isTrustedExternalUris(uri: Uri[]): boolean[]; + export function extractExternalUris(uris: Uri[]): Thenable; + } +}