mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-30 17:23:29 -05:00
Revert "Revert "Add a command line interface for connecting to a SQL Server (#3047)""
This reverts commit a747b6a500.
This commit is contained in:
17
src/sql/parts/commandLine/common/commandLine.ts
Normal file
17
src/sql/parts/commandLine/common/commandLine.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
export interface ICommandLineProcessing {
|
||||
_serviceBrand: any;
|
||||
/**
|
||||
* Interprets the various Azure Data Studio-specific command line switches and
|
||||
* performs the requisite tasks such as connecting to a server
|
||||
*/
|
||||
processCommandLine() : void;
|
||||
}
|
||||
|
||||
export const ICommandLineProcessing = createDecorator<ICommandLineProcessing>('commandLineService');
|
||||
77
src/sql/parts/commandLine/common/commandLineService.ts
Normal file
77
src/sql/parts/commandLine/common/commandLineService.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
import { ICommandLineProcessing } from 'sql/parts/commandLine/common/commandLine';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import * as Constants from 'sql/parts/connection/common/constants';
|
||||
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
|
||||
import * as platform from 'vs/platform/registry/common/platform';
|
||||
import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export class CommandLineService implements ICommandLineProcessing {
|
||||
private _connectionProfile : ConnectionProfile;
|
||||
private _showConnectionDialog: boolean;
|
||||
|
||||
constructor(
|
||||
@IConnectionManagementService private _connectionManagementService : IConnectionManagementService,
|
||||
@ICapabilitiesService private _capabilitiesService : ICapabilitiesService,
|
||||
@IEnvironmentService private _environmentService : IEnvironmentService,
|
||||
@IQueryEditorService private _queryEditorService : IQueryEditorService,
|
||||
@IObjectExplorerService private _objectExplorerService : IObjectExplorerService,
|
||||
@IEditorService private _editorService : IEditorService,
|
||||
)
|
||||
{
|
||||
let profile = null;
|
||||
if (this._environmentService && this._environmentService.args.server) {
|
||||
profile = new ConnectionProfile(_capabilitiesService, null);
|
||||
// We want connection store to use any matching password it finds
|
||||
profile.savePassword = true;
|
||||
profile.providerName = Constants.mssqlProviderName;
|
||||
profile.serverName = _environmentService.args.server;
|
||||
profile.databaseName = _environmentService.args.database ? _environmentService.args.database : '';
|
||||
profile.userName = _environmentService.args.user ? _environmentService.args.user : '';
|
||||
profile.authenticationType = _environmentService.args.integrated ? 'Integrated' : 'SqlLogin';
|
||||
profile.connectionName = '';
|
||||
profile.setOptionValue('applicationName', Constants.applicationName);
|
||||
profile.setOptionValue('databaseDisplayName', profile.databaseName);
|
||||
profile.setOptionValue('groupId', profile.groupId);
|
||||
}
|
||||
this._connectionProfile = profile;
|
||||
const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);
|
||||
let sqlProvider = registry.getProperties( Constants.mssqlProviderName);
|
||||
// We can't connect to object explorer until the MSSQL connection provider is registered
|
||||
if (sqlProvider) {
|
||||
this.processCommandLine();
|
||||
} else {
|
||||
registry.onNewProvider(e => {
|
||||
if (e.id === Constants.mssqlProviderName)
|
||||
{
|
||||
this.processCommandLine();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
public _serviceBrand: any;
|
||||
public processCommandLine(): void {
|
||||
if (!this._connectionProfile && !this._connectionManagementService.hasRegisteredServers()) {
|
||||
// prompt the user for a new connection on startup if no profiles are registered
|
||||
this._connectionManagementService.showConnectionDialog();
|
||||
} else if (this._connectionProfile) {
|
||||
this._connectionManagementService.connectIfNotConnected(this._connectionProfile, 'connection')
|
||||
.then(result => TaskUtilities.newQuery(this._connectionProfile,
|
||||
this._connectionManagementService,
|
||||
this._queryEditorService,
|
||||
this._objectExplorerService,
|
||||
this._editorService))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
private _onConnectRequestSent = new Emitter<void>();
|
||||
private _onConnectionChanged = new Emitter<IConnectionParams>();
|
||||
private _onLanguageFlavorChanged = new Emitter<sqlops.DidChangeLanguageFlavorParams>();
|
||||
|
||||
private _connectionGlobalStatus = new ConnectionGlobalStatus(this._statusBarService);
|
||||
|
||||
private _configurationEditService: ConfigurationEditingService;
|
||||
@@ -124,16 +123,6 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
100 /* High Priority */
|
||||
));
|
||||
|
||||
if (_capabilitiesService && Object.keys(_capabilitiesService.providers).length > 0 && !this.hasRegisteredServers()) {
|
||||
// prompt the user for a new connection on startup if no profiles are registered
|
||||
this.showConnectionDialog();
|
||||
} else if (_capabilitiesService && !this.hasRegisteredServers()) {
|
||||
_capabilitiesService.onCapabilitiesRegistered(e => {
|
||||
// prompt the user for a new connection on startup if no profiles are registered
|
||||
this.showConnectionDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);
|
||||
|
||||
let providerRegistration = (p: { id: string, properties: ConnectionProviderProperties }) => {
|
||||
@@ -282,29 +271,30 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
* @param options to use after the connection is complete
|
||||
*/
|
||||
private tryConnect(connection: IConnectionProfile, owner: IConnectableInput, options?: IConnectionCompletionOptions): Promise<IConnectionResult> {
|
||||
let self = this;
|
||||
return new Promise<IConnectionResult>((resolve, reject) => {
|
||||
// Load the password if it's not already loaded
|
||||
this._connectionStore.addSavedPassword(connection).then(result => {
|
||||
self._connectionStore.addSavedPassword(connection).then(result => {
|
||||
let newConnection = result.profile;
|
||||
let foundPassword = result.savedCred;
|
||||
|
||||
// If there is no password, try to load it from an existing connection
|
||||
if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection)) {
|
||||
let existingConnection = this._connectionStatusManager.findConnectionProfile(connection);
|
||||
if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection)) {
|
||||
let existingConnection = self._connectionStatusManager.findConnectionProfile(connection);
|
||||
if (existingConnection && existingConnection.connectionProfile) {
|
||||
newConnection.password = existingConnection.connectionProfile.password;
|
||||
foundPassword = true;
|
||||
}
|
||||
}
|
||||
// If the password is required and still not loaded show the dialog
|
||||
if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
|
||||
resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
|
||||
if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
|
||||
resolve(self.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
|
||||
} else {
|
||||
// Try to connect
|
||||
this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
|
||||
self.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
|
||||
if (!connectionResult.connected && !connectionResult.errorHandled) {
|
||||
// If connection fails show the dialog
|
||||
resolve(this.showConnectionDialogOnError(connection, owner, connectionResult, options));
|
||||
resolve(self.showConnectionDialogOnError(connection, owner, connectionResult, options));
|
||||
} else {
|
||||
//Resolve with the connection result
|
||||
resolve(connectionResult);
|
||||
@@ -390,7 +380,15 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
if (this._connectionStatusManager.isConnected(ownerUri)) {
|
||||
resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri));
|
||||
} else {
|
||||
this.connect(connection, ownerUri).then(connectionResult => {
|
||||
const options: IConnectionCompletionOptions = {
|
||||
// Should saving the connection be a command line switch?
|
||||
saveTheConnection : true,
|
||||
showConnectionDialogOnError : true,
|
||||
showDashboard : purpose === 'dashboard',
|
||||
params : undefined,
|
||||
showFirewallRuleOnError : true,
|
||||
};
|
||||
this.connect(connection, ownerUri, options).then(connectionResult => {
|
||||
if (connectionResult && connectionResult.connected) {
|
||||
resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri));
|
||||
} else {
|
||||
|
||||
176
src/sqltest/parts/commandLine/commandLineService.test.ts
Normal file
176
src/sqltest/parts/commandLine/commandLineService.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import * as Constants from 'sql/parts/connection/common/constants';
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as assert from 'assert';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
|
||||
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
import { CommandLineService } from 'sql/parts/commandLine/common/commandLineService';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
import { CapabilitiesService, ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService';
|
||||
import { QueryEditorService } from 'sql/parts/query/services/queryEditorService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
|
||||
import {
|
||||
IConnectionManagementService, IConnectionDialogService, INewConnectionParams,
|
||||
ConnectionType, IConnectableInput, IConnectionCompletionOptions, IConnectionCallbacks,
|
||||
IConnectionParams, IConnectionResult, IServerGroupController, IServerGroupDialogCallbacks,
|
||||
RunQueryOnConnectionMode
|
||||
} from 'sql/parts/connection/common/connectionManagement';
|
||||
import { ConnectionStore } from 'sql/parts/connection/common/connectionStore';
|
||||
import { TestConnectionManagementService } from 'sqltest/stubs/connectionManagementService.test';
|
||||
|
||||
|
||||
class TestParsedArgs implements ParsedArgs{
|
||||
[arg: string]: any;
|
||||
_: string[];
|
||||
aad?: boolean;
|
||||
add?: boolean;
|
||||
database?:string;
|
||||
debugBrkPluginHost?: string;
|
||||
debugBrkSearch?: string;
|
||||
debugId?: string;
|
||||
debugPluginHost?: string;
|
||||
debugSearch?: string;
|
||||
diff?: boolean;
|
||||
'disable-crash-reporter'?: string;
|
||||
'disable-extension'?: string | string[];
|
||||
'disable-extensions'?: boolean;
|
||||
'disable-restore-windows'?: boolean;
|
||||
'disable-telemetry'?: boolean;
|
||||
'disable-updates'?: string;
|
||||
'driver'?: string;
|
||||
'enable-proposed-api'?: string | string[];
|
||||
'export-default-configuration'?: string;
|
||||
'extensions-dir'?: string;
|
||||
extensionDevelopmentPath?: string;
|
||||
extensionTestsPath?: string;
|
||||
'file-chmod'?: boolean;
|
||||
'file-write'?: boolean;
|
||||
'folder-uri'?: string | string[];
|
||||
goto?: boolean;
|
||||
help?: boolean;
|
||||
'install-extension'?: string | string[];
|
||||
'install-source'?: string;
|
||||
integrated?: boolean;
|
||||
'list-extensions'?: boolean;
|
||||
locale?: string;
|
||||
log?: string;
|
||||
logExtensionHostCommunication?: boolean;
|
||||
'max-memory'?: number;
|
||||
'new-window'?: boolean;
|
||||
'open-url'?: boolean;
|
||||
performance?: boolean;
|
||||
'prof-append-timers'?: string;
|
||||
'prof-startup'?: string;
|
||||
'prof-startup-prefix'?: string;
|
||||
'reuse-window'?: boolean;
|
||||
server?: string;
|
||||
'show-versions'?: boolean;
|
||||
'skip-add-to-recently-opened'?: boolean;
|
||||
'skip-getting-started'?: boolean;
|
||||
'skip-release-notes'?: boolean;
|
||||
status?: boolean;
|
||||
'sticky-quickopen'?: boolean;
|
||||
'uninstall-extension'?: string | string[];
|
||||
'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch.
|
||||
'upload-logs'?: string;
|
||||
user?: string;
|
||||
'user-data-dir'?: string;
|
||||
_urls?: string[];
|
||||
verbose?: boolean;
|
||||
version?: boolean;
|
||||
wait?: boolean;
|
||||
waitMarkerFilePath?: string;
|
||||
}
|
||||
suite('commandLineService tests', () => {
|
||||
|
||||
let capabilitiesService: CapabilitiesTestService;
|
||||
let commandLineService : CommandLineService;
|
||||
let environmentService : TypeMoq.Mock<EnvironmentService>;
|
||||
let queryEditorService : TypeMoq.Mock<QueryEditorService>;
|
||||
let editorService:TypeMoq.Mock<IEditorService>;
|
||||
let objectExplorerService : TypeMoq.Mock<ObjectExplorerService>;
|
||||
let connectionStore: TypeMoq.Mock<ConnectionStore>;
|
||||
|
||||
setup(() => {
|
||||
capabilitiesService = new CapabilitiesTestService();
|
||||
connectionStore = TypeMoq.Mock.ofType(ConnectionStore);
|
||||
});
|
||||
|
||||
function getCommandLineService(connectionManagementService : IConnectionManagementService,
|
||||
environmentService? : IEnvironmentService,
|
||||
capabilitiesService? : ICapabilitiesService
|
||||
) : CommandLineService
|
||||
{
|
||||
let service= new CommandLineService(
|
||||
connectionManagementService,
|
||||
capabilitiesService,
|
||||
environmentService,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
test('processCommandLine shows connection dialog by default', done => {
|
||||
const connectionManagementService : TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable();
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
let service = getCommandLineService(connectionManagementService.object);
|
||||
service.processCommandLine();
|
||||
connectionManagementService.verifyAll();
|
||||
done();
|
||||
});
|
||||
|
||||
test('processCommandLine does nothing if registered servers exist and no server name is provided', done => {
|
||||
const connectionManagementService : TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
let service = getCommandLineService(connectionManagementService.object);
|
||||
service.processCommandLine();
|
||||
connectionManagementService.verifyAll();
|
||||
done();
|
||||
});
|
||||
|
||||
test('processCommandLine opens a new connection if a server name is passed', done => {
|
||||
const connectionManagementService : TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
const environmentService : TypeMoq.Mock<IEnvironmentService> = TypeMoq.Mock.ofType<IEnvironmentService>(EnvironmentService);
|
||||
const args : TestParsedArgs = new TestParsedArgs();
|
||||
args.server = 'myserver';
|
||||
args.database = 'mydatabase';
|
||||
environmentService.setup(e => e.args).returns(() => args).verifiable(TypeMoq.Times.atLeastOnce());
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), 'connection'))
|
||||
.returns(() => new Promise<string>((resolve, reject) => { reject('unused');}))
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService);
|
||||
service.processCommandLine();
|
||||
environmentService.verifyAll();
|
||||
connectionManagementService.verifyAll();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,13 @@ export interface ParsedArgs {
|
||||
'upload-logs'?: string;
|
||||
'driver'?: string;
|
||||
'driver-verbose'?: boolean;
|
||||
// {{SQL CARBON EDIT}}
|
||||
aad?: boolean;
|
||||
database?: string;
|
||||
integrated?: boolean;
|
||||
server?: string;
|
||||
user?: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
}
|
||||
|
||||
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
|
||||
|
||||
@@ -33,7 +33,12 @@ const options: minimist.Opts = {
|
||||
'export-default-configuration',
|
||||
'install-source',
|
||||
'upload-logs',
|
||||
'driver'
|
||||
'driver',
|
||||
// {{SQL CARBON EDIT}}
|
||||
'database',
|
||||
'server',
|
||||
'user',
|
||||
// {{SQL CARBON EDIT}}
|
||||
],
|
||||
boolean: [
|
||||
'help',
|
||||
@@ -66,7 +71,11 @@ const options: minimist.Opts = {
|
||||
'status',
|
||||
'file-write',
|
||||
'file-chmod',
|
||||
'driver-verbose'
|
||||
'driver-verbose',
|
||||
// {{SQL CARBON EDIT}}
|
||||
'aad',
|
||||
'integrated',
|
||||
// {{SQL CARBON EDIT}}
|
||||
],
|
||||
alias: {
|
||||
add: 'a',
|
||||
@@ -85,6 +94,12 @@ const options: minimist.Opts = {
|
||||
'debugBrkPluginHost': 'inspect-brk-extensions',
|
||||
'debugSearch': 'inspect-search',
|
||||
'debugBrkSearch': 'inspect-brk-search',
|
||||
// {{SQL CARBON EDIT}}
|
||||
database: 'D',
|
||||
integrated: 'E',
|
||||
server: 'S',
|
||||
user: 'U',
|
||||
// {{SQL CARBON EDIT}}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -172,6 +172,10 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'
|
||||
import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
|
||||
// {{SQL CARBON EDIT}}
|
||||
import { ICommandLineProcessing } from 'sql/parts/commandLine/common/commandLine';
|
||||
import { CommandLineService } from 'sql/parts/commandLine/common/commandLineService';
|
||||
// {{SQL CARBON EDIT}}
|
||||
|
||||
interface WorkbenchParams {
|
||||
configuration: IWindowConfiguration;
|
||||
@@ -575,7 +579,9 @@ export class Workbench extends Disposable implements IPartService {
|
||||
serviceCollection.set(IAccountManagementService, accountManagementService);
|
||||
serviceCollection.set(IAccountPickerService, this.instantiationService.createInstance(AccountPickerService));
|
||||
serviceCollection.set(IProfilerService, this.instantiationService.createInstance(ProfilerService));
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
serviceCollection.set(ICommandLineProcessing, this.instantiationService.createInstance(CommandLineService));
|
||||
// {{SQL CARBON EDIT}}
|
||||
this._register(toDisposable(() => connectionManagementService.shutdown()));
|
||||
this._register(toDisposable(() => accountManagementService.shutdown()));
|
||||
this._register(toDisposable(() => capabilitiesService.shutdown()));
|
||||
|
||||
Reference in New Issue
Block a user