From 40bc003e245af57ca7e9ad26ec0e8dc6d8d48071 Mon Sep 17 00:00:00 2001 From: Pine Wu Date: Wed, 11 Sep 2019 10:39:42 -0700 Subject: [PATCH] Fix #80595 --- src/vs/nls.d.ts | 10 +- .../contrib/url/common/trustedDomains.ts | 193 ++++++++++++++ .../url/common/trustedDomainsValidator.ts | 155 +++++++++++ .../contrib/url/common/url.contribution.ts | 249 ++---------------- .../test/contrib/linkProtection.test.ts | 2 +- 5 files changed, 373 insertions(+), 236 deletions(-) create mode 100644 src/vs/workbench/contrib/url/common/trustedDomains.ts create mode 100644 src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts diff --git a/src/vs/nls.d.ts b/src/vs/nls.d.ts index 3942ff08669..ce3d8e72305 100644 --- a/src/vs/nls.d.ts +++ b/src/vs/nls.d.ts @@ -9,11 +9,17 @@ export interface ILocalizeInfo { } /** - * Localize a message. `message` can contain `{n}` notation where it is replaced by the nth value in `...args`. + * Localize a message. + * + * `message` can contain `{n}` notation where it is replaced by the nth value in `...args` + * For example, `localize('hello {0}', name)` */ export declare function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string; /** - * Localize a message. `message` can contain `{n}` notation where it is replaced by the nth value in `...args`. + * Localize a message. + * + * `message` can contain `{n}` notation where it is replaced by the nth value in `...args` + * For example, `localize('hello {0}', name)` */ export declare function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string; 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..bf0c498fcdb --- /dev/null +++ b/src/vs/workbench/contrib/url/common/trustedDomains.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IProductService } from 'vs/platform/product/common/product'; + +export const enum ConfigureTrustedDomainActionType { + ToggleTrustAll = 'toggleTrustAll', + Add = 'add', + Configure = 'configure', + Reset = 'reset' +} + +export const configureTrustedDomainSettingsCommand = { + id: 'workbench.action.configureTrustedDomain', + description: { + description: localize('configureTrustedDomain', 'Configure Trusted Domains for Link Protection'), + args: [] + }, + handler: async (accessor: ServicesAccessor) => { + const quickInputService = accessor.get(IQuickInputService); + const storageService = accessor.get(IStorageService); + const productService = accessor.get(IProductService); + + let trustedDomains: string[] = productService.linkProtectionTrustedDomains + ? [...productService.linkProtectionTrustedDomains] + : []; + + try { + const trustedDomainsSrc = storageService.get('http.linkProtectionTrustedDomains', StorageScope.GLOBAL); + if (trustedDomainsSrc) { + trustedDomains = JSON.parse(trustedDomainsSrc); + } + } catch (err) { } + + const trustOrUntrustAllLabel = + trustedDomains.indexOf('*') === -1 + ? localize('trustedDomain.trustAll', 'Disable Link Protection') + : localize('trustedDomain.untrustAll', 'Enable Link Protection'); + + const trustOrUntrustAll: IQuickPickItem = { + id: ConfigureTrustedDomainActionType.ToggleTrustAll, + label: trustOrUntrustAllLabel + }; + + const result = await quickInputService.pick( + [ + trustOrUntrustAll, + { id: ConfigureTrustedDomainActionType.Add, label: localize('trustedDomain.add', 'Add Trusted Domain') }, + { + id: ConfigureTrustedDomainActionType.Configure, + label: localize('trustedDomain.edit', 'View and configure Trusted Domains') + }, + { id: ConfigureTrustedDomainActionType.Reset, label: localize('trustedDomain.reset', 'Reset Trusted Domains') } + ], + {} + ); + + if (result) { + switch (result.id) { + case ConfigureTrustedDomainActionType.ToggleTrustAll: + toggleAll(trustedDomains, storageService); + break; + case ConfigureTrustedDomainActionType.Add: + addDomain(trustedDomains, storageService, quickInputService); + break; + case ConfigureTrustedDomainActionType.Configure: + configureDomains(trustedDomains, storageService, quickInputService); + break; + case ConfigureTrustedDomainActionType.Reset: + resetDomains(storageService, productService); + break; + } + } + } +}; + +function toggleAll(trustedDomains: string[], storageService: IStorageService) { + if (trustedDomains.indexOf('*') === -1) { + storageService.store( + 'http.linkProtectionTrustedDomains', + JSON.stringify(trustedDomains.concat(['*'])), + StorageScope.GLOBAL + ); + } else { + storageService.store( + 'http.linkProtectionTrustedDomains', + JSON.stringify(trustedDomains.filter(x => x !== '*')), + StorageScope.GLOBAL + ); + } +} + +function addDomain(trustedDomains: string[], storageService: IStorageService, quickInputService: IQuickInputService) { + quickInputService + .input({ + placeHolder: 'Domain to trust', + validateInput: i => { + if (!i.match(/^https?:\/\//)) { + return Promise.resolve(undefined); + } + + return Promise.resolve(i); + } + }) + .then(result => { + console.log(result); + if (result) { + storageService.store( + 'http.linkProtectionTrustedDomains', + JSON.stringify(trustedDomains.concat([result])), + StorageScope.GLOBAL + ); + } + }); +} + +function configureDomains( + trustedDomains: string[], + storageService: IStorageService, + quickInputService: IQuickInputService +) { + const domainQuickPickItems: IQuickPickItem[] = trustedDomains + .filter(d => d !== '*') + .map(d => { + return { + type: 'item', + label: d, + id: d, + picked: true + }; + }); + + quickInputService.pick(domainQuickPickItems, { canPickMany: true }).then(result => { + const pickedDomains: string[] = result.map(r => r.id!); + storageService.store('http.linkProtectionTrustedDomains', JSON.stringify(pickedDomains), StorageScope.GLOBAL); + }); +} + +function resetDomains(storageService: IStorageService, productService: IProductService) { + if (productService.linkProtectionTrustedDomains) { + storageService.store( + 'http.linkProtectionTrustedDomains', + JSON.stringify(productService.linkProtectionTrustedDomains), + StorageScope.GLOBAL + ); + } else { + storageService.store('http.linkProtectionTrustedDomains', JSON.stringify([]), StorageScope.GLOBAL); + } +} + +export async function configureOpenerTrustedDomainsHandler( + trustedDomains: string[], + domainToConfigure: string, + quickInputService: IQuickInputService, + storageService: IStorageService +) { + const openAllLinksItem: IQuickPickItem = { + type: 'item', + label: localize('trustedDomain.trustAllAndOpenLink', 'Disable Link Protection and open link'), + id: '*', + picked: trustedDomains.indexOf('*') !== -1 + }; + const trustDomainItem: IQuickPickItem = { + type: 'item', + label: localize('trustedDomain.trustDomainAndOpenLink', 'Trust {0} and open link', domainToConfigure), + id: domainToConfigure, + picked: true + }; + + const pickedResult = await quickInputService.pick([openAllLinksItem, trustDomainItem], { + activeItem: trustDomainItem + }); + + if (pickedResult) { + if (pickedResult.id && trustedDomains.indexOf(pickedResult.id) === -1) { + storageService.store( + 'http.linkProtectionTrustedDomains', + JSON.stringify([...trustedDomains, pickedResult.id]), + StorageScope.GLOBAL + ); + + return [...trustedDomains, pickedResult.id]; + } + } + + return []; +} diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts new file mode 100644 index 00000000000..aa93ea0e980 --- /dev/null +++ b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import Severity from 'vs/base/common/severity'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/product'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { configureOpenerTrustedDomainsHandler } from 'vs/workbench/contrib/url/common/trustedDomains'; + +export class OpenerValidatorContributions implements IWorkbenchContribution { + constructor( + @IOpenerService private readonly _openerService: IOpenerService, + @IStorageService private readonly _storageService: IStorageService, + @IDialogService private readonly _dialogService: IDialogService, + @IProductService private readonly _productService: IProductService, + @IQuickInputService private readonly _quickInputService: IQuickInputService + ) { + this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); + } + + async validateLink(resource: URI): Promise { + const { scheme, authority } = resource; + + if (!equalsIgnoreCase(scheme, Schemas.http) && !equalsIgnoreCase(scheme, Schemas.https)) { + return true; + } + + const domainToOpen = `${scheme}://${authority}`; + const trustedDomains = readTrustedDomains(this._storageService, this._productService); + + if (isURLDomainTrusted(resource, trustedDomains)) { + return true; + } else { + const { choice } = await this._dialogService.show( + Severity.Info, + localize( + 'openExternalLinkAt', + 'Do you want {0} to open the external website?\n{1}', + this._productService.nameShort, + resource.toString(true) + ), + [ + localize('openLink', 'Open Link'), + localize('cancel', 'Cancel'), + localize('configureTrustedDomains', 'Configure Trusted Domains') + ], + { + cancelId: 1 + } + ); + + // Open Link + if (choice === 0) { + return true; + } + // Configure Trusted Domains + else if (choice === 2) { + const pickedDomains = await configureOpenerTrustedDomainsHandler( + trustedDomains, + domainToOpen, + this._quickInputService, + this._storageService + ); + // Trust all domains + if (pickedDomains.indexOf('*') !== -1) { + return true; + } + // Trust current domain + if (pickedDomains.indexOf(domainToOpen) !== -1) { + return true; + } + return false; + } + + return false; + } + } +} + +function readTrustedDomains(storageService: IStorageService, productService: IProductService) { + let trustedDomains: string[] = productService.linkProtectionTrustedDomains + ? [...productService.linkProtectionTrustedDomains] + : []; + + try { + const trustedDomainsSrc = storageService.get('http.linkProtectionTrustedDomains', StorageScope.GLOBAL); + if (trustedDomainsSrc) { + trustedDomains = JSON.parse(trustedDomainsSrc); + } + } catch (err) { } + + return trustedDomains; +} + +const rLocalhost = /^localhost(:\d+)?$/i; +const r127 = /^127.0.0.1(:\d+)?$/; + +function isLocalhostAuthority(authority: string) { + return rLocalhost.test(authority) || r127.test(authority); +} + +/** + * 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. For example https://*.microsoft.com matches https://www.microsoft.com + */ +export function isURLDomainTrusted(url: URI, trustedDomains: string[]) { + if (isLocalhostAuthority(url.authority)) { + return true; + } + + const domain = `${url.scheme}://${url.authority}`; + + for (let i = 0; i < trustedDomains.length; i++) { + if (trustedDomains[i] === '*') { + return true; + } + + if (trustedDomains[i] === domain) { + return true; + } + + if (trustedDomains[i].indexOf('*') !== -1) { + const parsedTrustedDomain = URI.parse(trustedDomains[i]); + if (url.scheme === parsedTrustedDomain.scheme) { + const authoritySegments = url.authority.split('.'); + const trustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.'); + + if (authoritySegments.length === trustedDomainAuthoritySegments.length) { + if ( + authoritySegments.every( + (val, i) => trustedDomainAuthoritySegments[i] === '*' || val === trustedDomainAuthoritySegments[i] + ) + ) { + return true; + } + } + } + } + } + + return false; +} diff --git a/src/vs/workbench/contrib/url/common/url.contribution.ts b/src/vs/workbench/contrib/url/common/url.contribution.ts index 396ec1c3a43..23130d51316 100644 --- a/src/vs/workbench/contrib/url/common/url.contribution.ts +++ b/src/vs/workbench/contrib/url/common/url.contribution.ts @@ -3,29 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { IURLService } from 'vs/platform/url/common/url'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { URI } from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { - IWorkbenchContribution, - IWorkbenchContributionsRegistry, - Extensions as WorkbenchExtensions -} from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IProductService } from 'vs/platform/product/common/product'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; -import { Schemas } from 'vs/base/common/network'; -import Severity from 'vs/base/common/severity'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IURLService } from 'vs/platform/url/common/url'; +import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { configureTrustedDomainSettingsCommand } from 'vs/workbench/contrib/url/common/trustedDomains'; +import { OpenerValidatorContributions } from 'vs/workbench/contrib/url/common/trustedDomainsValidator'; export class OpenUrlAction extends Action { static readonly ID = 'workbench.action.url.openUrl'; @@ -54,227 +44,20 @@ Registry.as(ActionExtensions.WorkbenchActions).registe localize('developer', 'Developer') ); -const configureTrustedDomainsHandler = async ( - quickInputService: IQuickInputService, - storageService: IStorageService, - linkProtectionTrustedDomains: string[], - domainToConfigure?: string -) => { - try { - const trustedDomainsSrc = storageService.get('http.linkProtectionTrustedDomains', StorageScope.GLOBAL); - if (trustedDomainsSrc) { - linkProtectionTrustedDomains = JSON.parse(trustedDomainsSrc); - } - } catch (err) { } - - const domainQuickPickItems: IQuickPickItem[] = linkProtectionTrustedDomains - .filter(d => d !== '*') - .map(d => { - return { - type: 'item', - label: d, - id: d, - picked: true - }; - }); - - const specialQuickPickItems: IQuickPickItem[] = [ - { - type: 'item', - label: localize('openAllLinksWithoutPrompt', 'Open all links without prompt'), - id: '*', - picked: linkProtectionTrustedDomains.indexOf('*') !== -1 - } - ]; - - let domainToConfigureItem: IQuickPickItem | undefined = undefined; - if (domainToConfigure && linkProtectionTrustedDomains.indexOf(domainToConfigure) === -1) { - domainToConfigureItem = { - type: 'item', - label: domainToConfigure, - id: domainToConfigure, - picked: true, - description: localize('trustDomainAndOpenLink', 'Trust domain and open link') - }; - specialQuickPickItems.push(domainToConfigureItem); - } - - const quickPickItems: (IQuickPickItem | IQuickPickSeparator)[] = - domainQuickPickItems.length === 0 - ? specialQuickPickItems - : [...specialQuickPickItems, { type: 'separator' }, ...domainQuickPickItems]; - - const pickedResult = await quickInputService.pick(quickPickItems, { - canPickMany: true, - activeItem: domainToConfigureItem - }); - - if (pickedResult) { - const pickedDomains: string[] = pickedResult.map(r => r.id!); - storageService.store('http.linkProtectionTrustedDomains', JSON.stringify(pickedDomains), StorageScope.GLOBAL); - - return pickedDomains; - } - - return []; -}; - -const configureTrustedDomainCommand = { - id: 'workbench.action.configureLinkProtectionTrustedDomains', - description: { - description: localize('configureLinkProtectionTrustedDomains', 'Configure Trusted Domains for Link Protection'), - args: [{ name: 'domainToConfigure', schema: { type: 'string' } }] - }, - handler: (accessor: ServicesAccessor, domainToConfigure?: string) => { - const quickInputService = accessor.get(IQuickInputService); - const storageService = accessor.get(IStorageService); - const productService = accessor.get(IProductService); - - const trustedDomains = productService.linkProtectionTrustedDomains - ? [...productService.linkProtectionTrustedDomains] - : []; - - return configureTrustedDomainsHandler(quickInputService, storageService, trustedDomains, domainToConfigure); - } -}; +/** + * Trusted Domains Contribution + */ -CommandsRegistry.registerCommand(configureTrustedDomainCommand); +CommandsRegistry.registerCommand(configureTrustedDomainSettingsCommand); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { - id: configureTrustedDomainCommand.id, - title: configureTrustedDomainCommand.description.description + id: configureTrustedDomainSettingsCommand.id, + title: configureTrustedDomainSettingsCommand.description.description } }); -class OpenerValidatorContributions implements IWorkbenchContribution { - constructor( - @IOpenerService private readonly _openerService: IOpenerService, - @IStorageService private readonly _storageService: IStorageService, - @IDialogService private readonly _dialogService: IDialogService, - @IProductService private readonly _productService: IProductService, - @IQuickInputService private readonly _quickInputService: IQuickInputService - ) { - this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); - } - - async validateLink(resource: URI): Promise { - const { scheme, authority } = resource; - - if (!equalsIgnoreCase(scheme, Schemas.http) && !equalsIgnoreCase(scheme, Schemas.https)) { - return true; - } - - let trustedDomains: string[] = this._productService.linkProtectionTrustedDomains - ? [...this._productService.linkProtectionTrustedDomains] - : []; - - try { - const trustedDomainsSrc = this._storageService.get('http.linkProtectionTrustedDomains', StorageScope.GLOBAL); - if (trustedDomainsSrc) { - trustedDomains = JSON.parse(trustedDomainsSrc); - } - } catch (err) { } - - const domainToOpen = `${scheme}://${authority}`; - - if (isURLDomainTrusted(resource, trustedDomains)) { - return true; - } else { - const { choice } = await this._dialogService.show( - Severity.Info, - localize( - 'openExternalLinkAt', - 'Do you want {0} to open the external website?\n{1}', - this._productService.nameShort, - resource.toString(true) - ), - [ - localize('openLink', 'Open Link'), - localize('cancel', 'Cancel'), - localize('configureTrustedDomains', 'Configure Trusted Domains') - ], - { - cancelId: 1 - } - ); - - // Open Link - if (choice === 0) { - return true; - } - // Configure Trusted Domains - else if (choice === 2) { - const pickedDomains = await configureTrustedDomainsHandler( - this._quickInputService, - this._storageService, - trustedDomains, - domainToOpen - ); - if (pickedDomains.indexOf(domainToOpen) !== -1) { - return true; - } - return false; - } - - return false; - } - } -} - Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( OpenerValidatorContributions, LifecyclePhase.Restored ); -const rLocalhost = /^localhost(:\d+)?$/i; -const r127 = /^127.0.0.1(:\d+)?$/; - -function isLocalhostAuthority(authority: string) { - return rLocalhost.test(authority) || r127.test(authority); -} - -/** - * 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. For example https://*.microsoft.com matches https://www.microsoft.com - */ -export function isURLDomainTrusted(url: URI, trustedDomains: string[]) { - if (isLocalhostAuthority(url.authority)) { - return true; - } - - const domain = `${url.scheme}://${url.authority}`; - - for (let i = 0; i < trustedDomains.length; i++) { - if (trustedDomains[i] === '*') { - return true; - } - - if (trustedDomains[i] === domain) { - return true; - } - - if (trustedDomains[i].indexOf('*') !== -1) { - const parsedTrustedDomain = URI.parse(trustedDomains[i]); - if (url.scheme === parsedTrustedDomain.scheme) { - const authoritySegments = url.authority.split('.'); - const trustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.'); - - if (authoritySegments.length === trustedDomainAuthoritySegments.length) { - if ( - authoritySegments.every( - (val, i) => trustedDomainAuthoritySegments[i] === '*' || val === trustedDomainAuthoritySegments[i] - ) - ) { - return true; - } - } - } - } - } - - return false; -} diff --git a/src/vs/workbench/test/contrib/linkProtection.test.ts b/src/vs/workbench/test/contrib/linkProtection.test.ts index ac4e9f58627..a31e6283c06 100644 --- a/src/vs/workbench/test/contrib/linkProtection.test.ts +++ b/src/vs/workbench/test/contrib/linkProtection.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; -import { isURLDomainTrusted } from 'vs/workbench/contrib/url/common/url.contribution'; +import { isURLDomainTrusted } from 'vs/workbench/contrib/url/common/trustedDomainsValidator'; import { URI } from 'vs/base/common/uri'; suite('Link protection domain matching', () => { -- GitLab