提交 9a2001bc 编写于 作者: P Pine Wu

Improvements

上级 4fa08902
......@@ -14,6 +14,9 @@ import { IOpenerService, IOpener } from 'vs/platform/opener/common/opener';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { IDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { localize } from 'vs/nls';
export class OpenerService implements IOpenerService {
......@@ -24,6 +27,8 @@ export class OpenerService implements IOpenerService {
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
@ICommandService private readonly _commandService: ICommandService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IDialogService private readonly _dialogService: IDialogService
) {
//
}
......@@ -51,12 +56,42 @@ export class OpenerService implements IOpenerService {
private _doOpen(resource: URI, options?: { openToSide?: boolean }): Promise<boolean> {
const { scheme, path, query, fragment } = resource;
const { scheme, authority, path, query, fragment } = resource;
if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https) || equalsIgnoreCase(scheme, Schemas.mailto)) {
// open http or default mail application
if (equalsIgnoreCase(scheme, Schemas.mailto)) {
// open default mail application
return this.openExternal(resource);
}
if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) {
const trustedDomains = this._configurationService.getValue<string[]>('http.trustedDomains');
const domainToOpen = `${scheme}://${authority}`;
if (isDomainTrusted(domainToOpen, trustedDomains)) {
return this.openExternal(resource);
} else {
return this._dialogService.confirm({
title: localize('openExternalLink', 'Open External Link'),
type: 'question',
message: localize('openExternalLinkAt', 'Do you want to leave VS Code and open the external website at') + ` ${resource.toString()}?`,
detail: resource.toString(),
primaryButton: localize('openLink', 'Open Link'),
secondaryButton: localize('cance', 'Cancel'),
checkbox: {
label: localize('trustAllLinksOn', 'Trust all links on') + ` ${domainToOpen}`,
checked: false
}
}).then(({ confirmed, checkboxChecked }) => {
if (checkboxChecked) {
this._configurationService.updateValue('http.trustedDomains', [...trustedDomains, domainToOpen]);
}
if (confirmed) {
return this.openExternal(resource);
}
return Promise.resolve(false);
});
}
} else if (equalsIgnoreCase(scheme, Schemas.command)) {
// run command or bail out if command isn't known
if (!CommandsRegistry.getCommand(path)) {
......@@ -106,3 +141,22 @@ export class OpenerService implements IOpenerService {
return Promise.resolve(true);
}
}
/**
* Check whether a domain like https://www.microsoft.com matches
* the list of trusted domains.
*
*/
function isDomainTrusted(domain: string, trustedDomains: string[]) {
for (let i = 0; i < trustedDomains.length; i++) {
if (trustedDomains[i] === '*') {
return true;
}
if (trustedDomains[i] === domain) {
return true;
}
}
return false;
}
......@@ -38,6 +38,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { clearAllFontInfos } from 'vs/editor/browser/config/configuration';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
......@@ -51,7 +52,12 @@ function withAllStandaloneServices<T extends editorCommon.IEditor>(domElement: H
}
if (!services.has(IOpenerService)) {
services.set(IOpenerService, new OpenerService(services.get(ICodeEditorService), services.get(ICommandService)));
services.set(IOpenerService, new OpenerService(
services.get(ICodeEditorService),
services.get(ICommandService),
services.get(IConfigurationService),
services.get(IDialogService),
));
}
let result = callback(services);
......
......@@ -7,6 +7,9 @@ import { URI } from 'vs/base/common/uri';
import { OpenerService } from 'vs/editor/browser/services/openerService';
import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices';
import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { deepClone } from 'vs/base/common/objects';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
suite('OpenerService', function () {
......@@ -24,19 +27,56 @@ suite('OpenerService', function () {
}
};
function getConfigurationService(trustedDomainsSetting: string[]) {
let _settings = deepClone(trustedDomainsSetting);
return new class implements IConfigurationService {
getValue = () => _settings;
updateValue = (key: string, val: string[]) => {
_settings = val;
return Promise.resolve();
}
// Don't care
_serviceBrand: any;
onDidChangeConfiguration = () => ({ dispose: () => { } });
getConfigurationData = () => null;
reloadConfiguration = () => Promise.resolve();
inspect = () => null as any;
keys = () => null as any;
};
}
function getDialogService() {
return new class implements IDialogService {
_confirmInvoked = 0;
confirm = () => {
this._confirmInvoked++;
return Promise.resolve({} as any);
}
get confirmInvoked() { return this._confirmInvoked; }
// Don't care
_serviceBrand: any;
show = () => {
return Promise.resolve({} as any);
}
};
}
setup(function () {
lastCommand = undefined;
});
test('delegate to editorService, scheme:///fff', function () {
const openerService = new OpenerService(editorService, NullCommandService);
const openerService = new OpenerService(editorService, NullCommandService, getConfigurationService([]), getDialogService());
openerService.open(URI.parse('another:///somepath'));
assert.equal(editorService.lastInput!.options!.selection, undefined);
});
test('delegate to editorService, scheme:///fff#L123', function () {
const openerService = new OpenerService(editorService, NullCommandService);
const openerService = new OpenerService(editorService, NullCommandService, getConfigurationService([]), getDialogService());
openerService.open(URI.parse('file:///somepath#L23'));
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
......@@ -59,7 +99,7 @@ suite('OpenerService', function () {
test('delegate to editorService, scheme:///fff#123,123', function () {
const openerService = new OpenerService(editorService, NullCommandService);
const openerService = new OpenerService(editorService, NullCommandService, getConfigurationService([]), getDialogService());
openerService.open(URI.parse('file:///somepath#23'));
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
......@@ -78,7 +118,7 @@ suite('OpenerService', function () {
test('delegate to commandsService, command:someid', function () {
const openerService = new OpenerService(editorService, commandService);
const openerService = new OpenerService(editorService, commandService, getConfigurationService([]), getDialogService());
const id = `aCommand${Math.random()}`;
CommandsRegistry.registerCommand(id, function () { });
......@@ -98,4 +138,46 @@ suite('OpenerService', function () {
assert.equal(lastCommand!.args[0], 12);
assert.equal(lastCommand!.args[1], true);
});
test('links are protected by dialog confirmation', function () {
const dialogService = getDialogService();
const openerService = new OpenerService(editorService, commandService, getConfigurationService([]), dialogService);
openerService.open(URI.parse('https://www.microsoft.com'));
assert.equal(dialogService.confirmInvoked, 1);
});
test('links on the whitelisted domains can be opened without dialog confirmation', function () {
const dialogService = getDialogService();
const openerService = new OpenerService(editorService, commandService, getConfigurationService(['https://microsoft.com']), dialogService);
openerService.open(URI.parse('https://microsoft.com'));
openerService.open(URI.parse('https://microsoft.com/'));
openerService.open(URI.parse('https://microsoft.com/en-us/'));
openerService.open(URI.parse('https://microsoft.com/en-us/?foo=bar'));
openerService.open(URI.parse('https://microsoft.com/en-us/?foo=bar#baz'));
assert.equal(dialogService.confirmInvoked, 0);
});
test('variations of links are protected by dialog confirmation', function () {
const dialogService = getDialogService();
const openerService = new OpenerService(editorService, commandService, getConfigurationService(['https://microsoft.com']), dialogService);
openerService.open(URI.parse('http://microsoft.com'));
openerService.open(URI.parse('https://www.microsoft.com'));
assert.equal(dialogService.confirmInvoked, 2);
});
test('* removes all link protection', function () {
const dialogService = getDialogService();
const openerService = new OpenerService(editorService, commandService, getConfigurationService(['*']), dialogService);
openerService.open(URI.parse('https://code.visualstudio.com/'));
openerService.open(URI.parse('https://www.microsoft.com'));
openerService.open(URI.parse('https://www.github.com'));
assert.equal(dialogService.confirmInvoked, 0);
});
});
......@@ -117,6 +117,14 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration)
type: 'boolean',
default: true,
description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS a reload of the window is required after turning this off.)")
},
'http.trustedDomains': {
type: 'array',
default: ['https://code.visualstudio.com'],
description: localize('trustedDomains', "Controls whether a http/https link can be opened directly in browser.\n\nAdd `*` to the list to whitelist all domains."),
items: {
type: 'string'
}
}
}
});
......@@ -11,6 +11,7 @@ import { IURLService } from 'vs/platform/url/common/url';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { URI } from 'vs/base/common/uri';
import { Action } from 'vs/base/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class OpenUrlAction extends Action {
......@@ -34,5 +35,50 @@ export class OpenUrlAction extends Action {
}
}
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions)
.registerWorkbenchAction(new SyncActionDescriptor(OpenUrlAction, OpenUrlAction.ID, OpenUrlAction.LABEL), 'Open URL', localize('developer', "Developer"));
\ No newline at end of file
export class ConfigureTrustedDomainsAction extends Action {
static readonly ID = 'workbench.action.configureTrustedDomains';
static readonly LABEL = localize('configureTrustedDomains', "Configure Trusted Domains");
constructor(
id: string,
label: string,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super(id, label);
}
run(): Promise<any> {
const trustedDomains = this.configurationService.getValue<string[]>('http.trustedDomains');
return this.quickInputService.pick(trustedDomains.map(d => {
return {
type: 'item',
label: d,
picked: true,
};
}), {
canPickMany: true
}).then(result => {
if (result) {
this.configurationService.updateValue('http.trustedDomains', result.map(r => r.label));
}
});
}
}
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions).registerWorkbenchAction(
new SyncActionDescriptor(OpenUrlAction, OpenUrlAction.ID, OpenUrlAction.LABEL),
'Open URL',
localize('developer', 'Developer')
);
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions).registerWorkbenchAction(
new SyncActionDescriptor(
ConfigureTrustedDomainsAction,
ConfigureTrustedDomainsAction.ID,
ConfigureTrustedDomainsAction.LABEL
),
'Configure Trusted Domains'
);
......@@ -12,6 +12,8 @@ import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
export class OpenerService extends BaseOpenerService {
......@@ -20,9 +22,11 @@ export class OpenerService extends BaseOpenerService {
constructor(
@ICodeEditorService codeEditorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@IWindowsService private readonly windowsService: IWindowsService
@IWindowsService private readonly windowsService: IWindowsService,
@IConfigurationService readonly configurationService: IConfigurationService,
@IDialogService readonly dialogService: IDialogService
) {
super(codeEditorService, commandService);
super(codeEditorService, commandService, configurationService, dialogService);
}
async openExternal(resource: URI): Promise<boolean> {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册