/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IConnectionManagementService, ConnectionType, INewConnectionParams, IConnectionCompletionOptions, IConnectionResult } from 'sql/platform/connection/common/connectionManagement'; import { ConnectionDialogWidget, OnShowUIResponse } from 'sql/workbench/services/connection/browser/connectionDialogWidget'; import { ConnectionController } from 'sql/workbench/services/connection/browser/connectionController'; import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils'; import * as Constants from 'sql/platform/connection/common/constants'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { entries } from 'sql/base/common/objects'; import { Deferred } from 'sql/base/common/promise'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as platform from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { Action, IAction } from 'vs/base/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as types from 'vs/base/common/types'; import { trim } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { CmsConnectionController } from 'sql/workbench/services/connection/browser/cmsConnectionController'; export interface IConnectionValidateResult { isValid: boolean; connection: IConnectionProfile; } export interface IConnectionComponentCallbacks { onSetConnectButton: (enable: boolean) => void; onCreateNewServerGroup?: () => void; onAdvancedProperties?: () => void; onSetAzureTimeOut?: () => void; onFetchDatabases?: (serverName: string, authenticationType: string, userName?: string, password?: string) => Promise; } export interface IConnectionComponentController { showUiComponent(container: HTMLElement, didChange?: boolean): void; initDialog(providers: string[], model: IConnectionProfile): void; validateConnection(): IConnectionValidateResult; fillInConnectionInputs(connectionInfo: IConnectionProfile): void; handleOnConnecting(): void; handleResetConnection(): void; focusOnOpen(): void; closeDatabaseDropdown(): void; databaseDropdownExpanded: boolean; } export class ConnectionDialogService implements IConnectionDialogService { _serviceBrand: any; private _connectionDialog: ConnectionDialogWidget; private _connectionControllerMap: { [providerName: string]: IConnectionComponentController } = {}; private _model: ConnectionProfile; private _params: INewConnectionParams; private _options: IConnectionCompletionOptions; private _inputModel: IConnectionProfile; private _providerNameToDisplayNameMap: { [providerDisplayName: string]: string } = {}; private _providerTypes: string[] = []; private _currentProviderType: string = Constants.mssqlProviderName; private _previousProviderType: string = undefined; private _connecting: boolean = false; private _connectionErrorTitle = localize('connectionError', 'Connection error'); private _dialogDeferredPromise: Deferred; private _toDispose = []; /** * This is used to work around the interconnectedness of this code */ private ignoreNextConnect = false; private _connectionManagementService: IConnectionManagementService; constructor( @IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService, @IInstantiationService private _instantiationService: IInstantiationService, @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, @IErrorMessageService private _errorMessageService: IErrorMessageService, @IConfigurationService private _configurationService: IConfigurationService, @IClipboardService private _clipboardService: IClipboardService, @ICommandService private _commandService: ICommandService ) { this.initializeConnectionProviders(); } /** * Set the initial value for the connection provider and listen to the provider change event */ private initializeConnectionProviders() { this.setConnectionProviders(); if (this._capabilitiesService) { this._capabilitiesService.onCapabilitiesRegistered(() => { this.setConnectionProviders(); if (this._connectionDialog) { this._connectionDialog.updateConnectionProviders(this._providerTypes, this._providerNameToDisplayNameMap); } }); } } /** * Update the available provider types using the values from capabilities service */ private setConnectionProviders() { if (this._capabilitiesService) { this._providerTypes = []; this._providerNameToDisplayNameMap = {}; entries(this._capabilitiesService.providers).forEach(p => { this._providerTypes.push(p[1].connection.displayName); this._providerNameToDisplayNameMap[p[0]] = p[1].connection.displayName; }); } } /** * Gets the default provider with the following actions * 1. Checks if master provider(map) has data * 2. If so, filters provider parameter against master map * 3. Fetches the result array and extracts the first element * 4. If none of the above data exists, returns 'MSSQL' * @returns: Default provider as string */ private getDefaultProviderName(): string { let defaultProvider: string; if (this._providerNameToDisplayNameMap) { let keys = Object.keys(this._providerNameToDisplayNameMap); let filteredKeys: string[]; if (keys && keys.length > 0) { if (this._params && this._params.providers && this._params.providers.length > 0) { //Filter providers from master keys. filteredKeys = keys.filter(key => this._params.providers.includes(key)); } if (filteredKeys && filteredKeys.length > 0) { defaultProvider = filteredKeys[0]; } else { defaultProvider = keys[0]; } } } if (!defaultProvider && this._configurationService) { defaultProvider = WorkbenchUtils.getSqlConfigValue(this._configurationService, Constants.defaultEngine); } // as a fallback, default to MSSQL if the value from settings is not available return defaultProvider || Constants.mssqlProviderName; } private handleOnConnect(params: INewConnectionParams, profile?: IConnectionProfile): void { if (!this._connecting) { this._connecting = true; this.handleProviderOnConnecting(); if (!profile) { let result = this.uiController.validateConnection(); if (!result.isValid) { this._connecting = false; this._connectionDialog.resetConnection(); return; } profile = result.connection; profile.serverName = trim(profile.serverName); // append the port to the server name for SQL Server connections if (this._currentProviderType === Constants.mssqlProviderName || this._currentProviderType === Constants.cmsProviderName) { let portPropertyName: string = 'port'; let portOption: string = profile.options[portPropertyName]; if (portOption && portOption.indexOf(',') === -1) { profile.serverName = profile.serverName + ',' + portOption; } profile.options[portPropertyName] = undefined; profile.providerName = Constants.mssqlProviderName; } // Disable password prompt during reconnect if connected with an empty password if (profile.password === '' && profile.savePassword === false) { profile.savePassword = true; } this.handleDefaultOnConnect(params, profile); } else { profile.serverName = trim(profile.serverName); this._connectionManagementService.addSavedPassword(profile).then(connectionWithPassword => { this.handleDefaultOnConnect(params, connectionWithPassword); }); } } } private handleOnCancel(params: INewConnectionParams): void { if (this.ignoreNextConnect) { this._connectionDialog.resetConnection(); this._connectionDialog.close(); this.ignoreNextConnect = false; this._dialogDeferredPromise.resolve(undefined); return; } if (this.uiController.databaseDropdownExpanded) { this.uiController.closeDatabaseDropdown(); } else { if (params && params.input && params.connectionType === ConnectionType.editor) { this._connectionManagementService.cancelEditorConnection(params.input); } else { this._connectionManagementService.cancelConnection(this._model); } if (params && params.input && params.input.onConnectCanceled) { params.input.onConnectCanceled(); } this._connectionDialog.resetConnection(); this._connecting = false; } this.uiController.databaseDropdownExpanded = false; if (this._dialogDeferredPromise) { this._dialogDeferredPromise.resolve(undefined); } } private handleDefaultOnConnect(params: INewConnectionParams, connection: IConnectionProfile): Thenable { if (this.ignoreNextConnect) { this._connectionDialog.resetConnection(); this._connectionDialog.close(); this.ignoreNextConnect = false; this._connecting = false; this._dialogDeferredPromise.resolve(connection); return Promise.resolve(); } let fromEditor = params && params.connectionType === ConnectionType.editor; let isTemporaryConnection = params && params.connectionType === ConnectionType.temporary; let uri: string = undefined; if (fromEditor && params && params.input) { uri = params.input.uri; } let options: IConnectionCompletionOptions = this._options || { params: params, saveTheConnection: !isTemporaryConnection, showDashboard: params && params.showDashboard !== undefined ? params.showDashboard : !fromEditor && !isTemporaryConnection, showConnectionDialogOnError: false, showFirewallRuleOnError: true }; return this._connectionManagementService.connectAndSaveProfile(connection, uri, options, params && params.input).then(connectionResult => { this._connecting = false; if (connectionResult && connectionResult.connected) { this._connectionDialog.close(); if (this._dialogDeferredPromise) { this._dialogDeferredPromise.resolve(connectionResult.connectionProfile); } } else if (connectionResult && connectionResult.errorHandled) { this._connectionDialog.resetConnection(); } else { this._connectionDialog.resetConnection(); this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack); } }).catch(err => { this._connecting = false; this._connectionDialog.resetConnection(); this.showErrorDialog(Severity.Error, this._connectionErrorTitle, err); }); } private get uiController(): IConnectionComponentController { // Find the provider name from the selected provider type, or throw an error if it does not correspond to a known provider let providerName = this._currentProviderType; if (!providerName) { throw Error('Invalid provider type'); } // Set the model name, initialize the controller if needed, and return the controller this._model.providerName = providerName; if (!this._connectionControllerMap[providerName]) { if (providerName === Constants.cmsProviderName) { this._connectionControllerMap[providerName] = this._instantiationService.createInstance(CmsConnectionController, this._connectionManagementService, this._capabilitiesService.getCapabilities(providerName).connection, { onSetConnectButton: (enable: boolean) => this.handleSetConnectButtonEnable(enable) }, providerName); } else { this._connectionControllerMap[providerName] = this._instantiationService.createInstance(ConnectionController, this._connectionManagementService, this._capabilitiesService.getCapabilities(providerName).connection, { onSetConnectButton: (enable: boolean) => this.handleSetConnectButtonEnable(enable) }, providerName); } } return this._connectionControllerMap[providerName]; } private handleSetConnectButtonEnable(enable: boolean): void { this._connectionDialog.connectButtonState = enable; } private handleShowUiComponent(input: OnShowUIResponse) { if (input.selectedProviderType) { // If the call is for specific providers let isParamProvider: boolean = false; if (this._params && this._params.providers) { this._params.providers.forEach((provider) => { if (input.selectedProviderType === this._providerNameToDisplayNameMap[provider]) { isParamProvider = true; this._currentProviderType = provider; } }); } if (!isParamProvider) { this._currentProviderType = Object.keys(this._providerNameToDisplayNameMap).find((key) => this._providerNameToDisplayNameMap[key] === input.selectedProviderType ); } } this._model.providerName = this._currentProviderType; this._model = new ConnectionProfile(this._capabilitiesService, this._model); this.uiController.showUiComponent(input.container); } private handleInitDialog() { this.uiController.initDialog(this._params && this._params.providers, this._model); } private handleFillInConnectionInputs(connectionInfo: IConnectionProfile): void { this._connectionManagementService.addSavedPassword(connectionInfo).then(connectionWithPassword => { let model = this.createModel(connectionWithPassword); this._model = model; this.uiController.fillInConnectionInputs(model); }); this._connectionDialog.updateProvider(this._providerNameToDisplayNameMap[connectionInfo.providerName]); } private handleProviderOnResetConnection(): void { this.uiController.handleResetConnection(); } private handleProviderOnConnecting(): void { this.uiController.handleOnConnecting(); } private updateModelServerCapabilities(model: IConnectionProfile) { this._model = this.createModel(model); if (this._model.providerName) { this._currentProviderType = this._model.providerName; if (this._connectionDialog) { this._connectionDialog.updateProvider(this._providerNameToDisplayNameMap[this._currentProviderType]); } } } private createModel(model: IConnectionProfile): ConnectionProfile { let defaultProvider = this.getDefaultProviderName(); let providerName = model ? model.providerName : defaultProvider; providerName = providerName ? providerName : defaultProvider; let newProfile = new ConnectionProfile(this._capabilitiesService, model || providerName); newProfile.saveProfile = true; newProfile.generateNewId(); // If connecting from a query editor set "save connection" to false if (this._params && this._params.input && this._params.connectionType === ConnectionType.editor) { newProfile.saveProfile = false; } return newProfile; } private showDialogWithModel(): Promise { return new Promise((resolve, reject) => { this.updateModelServerCapabilities(this._inputModel); this.doShowDialog(this._params); resolve(null); }); } public openDialogAndWait( connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult, doConnect: boolean = true): Thenable { if (!doConnect) { this.ignoreNextConnect = true; } this._dialogDeferredPromise = new Deferred(); this.showDialog(connectionManagementService, params, model, connectionResult).then(() => { }, error => { this._dialogDeferredPromise.reject(error); }); return this._dialogDeferredPromise; } public showDialog( connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult, connectionOptions?: IConnectionCompletionOptions): Thenable { this._connectionManagementService = connectionManagementService; this._options = connectionOptions; this._params = params; this._inputModel = model; return new Promise((resolve, reject) => { this.updateModelServerCapabilities(model); // If connecting from a query editor set "save connection" to false if (params && (params.input && params.connectionType === ConnectionType.editor || params.connectionType === ConnectionType.temporary)) { this._model.saveProfile = false; } resolve(this.showDialogWithModel().then(() => { if (connectionResult && connectionResult.errorMessage) { this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack); } })); }); } private doShowDialog(params: INewConnectionParams): Promise { if (!this._connectionDialog) { this._connectionDialog = this._instantiationService.createInstance(ConnectionDialogWidget, this._providerTypes, this._providerNameToDisplayNameMap[this._model.providerName], this._providerNameToDisplayNameMap); this._connectionDialog.onCancel(() => { this._connectionDialog.databaseDropdownExpanded = this.uiController.databaseDropdownExpanded; this.handleOnCancel(this._connectionDialog.newConnectionParams); }); this._connectionDialog.onConnect((profile) => { this.handleOnConnect(this._connectionDialog.newConnectionParams, profile); }); this._connectionDialog.onShowUiComponent((input) => this.handleShowUiComponent(input)); this._connectionDialog.onInitDialog(() => this.handleInitDialog()); this._connectionDialog.onFillinConnectionInputs((input) => this.handleFillInConnectionInputs(input)); this._connectionDialog.onResetConnection(() => this.handleProviderOnResetConnection()); this._connectionDialog.render(); this._previousProviderType = this._currentProviderType; } this._connectionDialog.newConnectionParams = params; // if provider changed if ((this._previousProviderType !== this._currentProviderType) || // or if currentProvider not set correctly yet !(this._currentProviderType === Constants.cmsProviderName && params.providers && params.providers.length > 1)) { this._previousProviderType = undefined; this._connectionDialog.updateProvider(this._providerNameToDisplayNameMap[this.getDefaultProviderName()]); } else { this._connectionDialog.newConnectionParams = params; } return new Promise(() => { this._connectionDialog.open(this._connectionManagementService.getRecentConnections(params.providers).length > 0); this.uiController.focusOnOpen(); }); } private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): void { // Kerberos errors are currently very hard to understand, so adding handling of these to solve the common scenario // note that ideally we would have an extensible service to handle errors by error code and provider, but for now // this solves the most common "hard error" that we've noticed const helpLink = 'https://aka.ms/sqlopskerberos'; let actions: IAction[] = []; if (!platform.isWindows && types.isString(message) && message.toLowerCase().includes('kerberos') && message.toLowerCase().includes('kinit')) { message = [ localize('kerberosErrorStart', "Connection failed due to Kerberos error."), localize('kerberosHelpLink', "Help configuring Kerberos is available at {0}", helpLink), localize('kerberosKinit', "If you have previously connected you may need to re-run kinit.") ].join('\r\n'); actions.push(new Action('Kinit', 'Run kinit', null, true, () => { this._connectionDialog.close(); this._clipboardService.writeText('kinit\r'); this._commandService.executeCommand('workbench.action.terminal.focus').then(resolve => { // setTimeout to allow for terminal Instance to load. setTimeout(() => { return this._commandService.executeCommand('workbench.action.terminal.paste'); }, 10); }).then(resolve => null, reject => null); return null; })); } this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions); } }