mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00
Code cosmetic in initialization code:
* Move TMainForm.ParseCommandLineParameters to helpers:ParseCommandLine * Modify ParseCommandLine to work without private statics FCmdlineFilenames and FCmdlineConnectionParams * Move code from helpers:setLocales to dpr file * Rename TMainForm.Startup to TMainForm.AfterFormCreate * Destroy AppSettings explicitly in single instance mode
This commit is contained in:
@ -47,18 +47,25 @@ uses
|
|||||||
{$R ..\..\res\updater.RES}
|
{$R ..\..\res\updater.RES}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
SetLocales;
|
// Use MySQL standard format for date/time variables: YYYY-MM-DD HH:MM:SS
|
||||||
|
// Be aware that Delphi internally converts the slashes in ShortDateFormat to the DateSeparator
|
||||||
|
FormatSettings.DateSeparator := '-';
|
||||||
|
FormatSettings.TimeSeparator := ':';
|
||||||
|
FormatSettings.ShortDateFormat := 'yyyy/mm/dd';
|
||||||
|
FormatSettings.LongTimeFormat := 'hh:nn:ss';
|
||||||
|
|
||||||
AppSettings := TAppSettings.Create;
|
AppSettings := TAppSettings.Create;
|
||||||
SecondInstMsgId := RegisterWindowMessage(APPNAME);
|
SecondInstMsgId := RegisterWindowMessage(APPNAME);
|
||||||
if (not AppSettings.ReadBool(asAllowMultipleInstances)) and CheckForSecondInstance then
|
if (not AppSettings.ReadBool(asAllowMultipleInstances)) and CheckForSecondInstance then begin
|
||||||
Application.Terminate
|
AppSettings.Free;
|
||||||
else begin
|
Application.Terminate;
|
||||||
|
end else begin
|
||||||
Application.Initialize;
|
Application.Initialize;
|
||||||
Application.Title := APPNAME;
|
Application.Title := APPNAME;
|
||||||
Application.UpdateFormatSettings := False;
|
Application.UpdateFormatSettings := False;
|
||||||
Application.CreateForm(TMainForm, MainForm);
|
Application.CreateForm(TMainForm, MainForm);
|
||||||
Application.OnMessage := Mainform.OnMessageHandler;
|
Application.OnMessage := Mainform.OnMessageHandler;
|
||||||
MainForm.Startup;
|
MainForm.AfterFormCreate;
|
||||||
Application.MainFormOnTaskBar := True;
|
Application.MainFormOnTaskBar := True;
|
||||||
Application.Run;
|
Application.Run;
|
||||||
end;
|
end;
|
||||||
|
@ -267,7 +267,6 @@ type
|
|||||||
function UnformatNumber(Val: String): String;
|
function UnformatNumber(Val: String): String;
|
||||||
function FormatNumber( int: Int64; Thousands: Boolean=True): String; Overload;
|
function FormatNumber( int: Int64; Thousands: Boolean=True): String; Overload;
|
||||||
function FormatNumber( flt: Double; decimals: Integer = 0; Thousands: Boolean=True): String; Overload;
|
function FormatNumber( flt: Double; decimals: Integer = 0; Thousands: Boolean=True): String; Overload;
|
||||||
procedure setLocales;
|
|
||||||
procedure ShellExec(cmd: String; path: String=''; params: String='');
|
procedure ShellExec(cmd: String; path: String=''; params: String='');
|
||||||
function getFirstWord( text: String ): String;
|
function getFirstWord( text: String ): String;
|
||||||
function FormatByteNumber( Bytes: Int64; Decimals: Byte = 1 ): String; Overload;
|
function FormatByteNumber( Bytes: Int64; Decimals: Byte = 1 ): String; Overload;
|
||||||
@ -322,11 +321,12 @@ type
|
|||||||
function ErrorDialog(Msg: string): Integer; overload;
|
function ErrorDialog(Msg: string): Integer; overload;
|
||||||
function ErrorDialog(const Title, Msg: string): Integer; overload;
|
function ErrorDialog(const Title, Msg: string): Integer; overload;
|
||||||
function GetHTMLCharsetByEncoding(Encoding: TEncoding): String;
|
function GetHTMLCharsetByEncoding(Encoding: TEncoding): String;
|
||||||
|
procedure ParseCommandLine(Parameters: TStringlist;
|
||||||
|
var ConnectionParams: TConnectionParameters; var FileNames: TStringList);
|
||||||
|
|
||||||
var
|
var
|
||||||
AppSettings: TAppSettings;
|
AppSettings: TAppSettings;
|
||||||
MutexHandle: THandle = 0;
|
MutexHandle: THandle = 0;
|
||||||
DecimalSeparatorSystemdefault: Char;
|
|
||||||
|
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@ -933,27 +933,6 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{***
|
|
||||||
Set global variables containing the standard local format for date and time
|
|
||||||
values. Standard means the MySQL-standard format, which is YYYY-MM-DD HH:MM:SS
|
|
||||||
|
|
||||||
@note Be aware that Delphi internally converts the slashes in ShortDateFormat
|
|
||||||
to the DateSeparator
|
|
||||||
}
|
|
||||||
procedure setLocales;
|
|
||||||
begin
|
|
||||||
FormatSettings.DateSeparator := '-';
|
|
||||||
FormatSettings.TimeSeparator := ':';
|
|
||||||
FormatSettings.ShortDateFormat := 'yyyy/mm/dd';
|
|
||||||
FormatSettings.LongTimeFormat := 'hh:nn:ss';
|
|
||||||
if DecimalSeparatorSystemdefault = '' then
|
|
||||||
DecimalSeparatorSystemdefault := FormatSettings.DecimalSeparator;
|
|
||||||
FormatSettings.DecimalSeparator := DecimalSeparatorSystemdefault;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{***
|
{***
|
||||||
Open URL or execute system command
|
Open URL or execute system command
|
||||||
|
|
||||||
@ -2492,6 +2471,77 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure ParseCommandLine(Parameters: TStringlist;
|
||||||
|
var ConnectionParams: TConnectionParameters; var FileNames: TStringList);
|
||||||
|
var
|
||||||
|
rx: TRegExpr;
|
||||||
|
AllParams, SessName, Host, User, Pass, Socket: String;
|
||||||
|
i, Port: Integer;
|
||||||
|
|
||||||
|
function GetParamValue(ShortName, LongName: String): String;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
rx.Expression := '\s(\-'+ShortName+'|\-\-'+LongName+')\s*\=?\s*([^\-]\S*)';
|
||||||
|
if rx.Exec(AllParams) then
|
||||||
|
Result := rx.Match[2];
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
SessName := '';
|
||||||
|
FileNames := TStringList.Create;
|
||||||
|
|
||||||
|
// Prepend a space, so the regular expression can request a mandantory space
|
||||||
|
// before each param name including the first one
|
||||||
|
AllParams := ' ' + ImplodeStr(' ', Parameters);
|
||||||
|
rx := TRegExpr.Create;
|
||||||
|
SessName := GetParamValue('d', 'description');
|
||||||
|
if SessName <> '' then begin
|
||||||
|
try
|
||||||
|
ConnectionParams := TConnectionParameters.Create(SessName);
|
||||||
|
except
|
||||||
|
on E:Exception do begin
|
||||||
|
// Session params not found in registry
|
||||||
|
MainForm.LogSQL(E.Message);
|
||||||
|
SessName := '';
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Test if params were passed. If given, override previous values loaded from registry.
|
||||||
|
// Enables the user to log into a session with a different, non-stored user: -dSession -uSomeOther
|
||||||
|
Host := GetParamValue('h', 'host');
|
||||||
|
User := GetParamValue('u', 'user');
|
||||||
|
Pass := GetParamValue('p', 'password');
|
||||||
|
Socket := GetParamValue('S', 'socket');
|
||||||
|
Port := StrToIntDef(GetParamValue('P', 'port'), 0);
|
||||||
|
// Leave out support for startup script, seems reasonable for command line connecting
|
||||||
|
|
||||||
|
if (Host <> '') or (User <> '') or (Pass <> '') or (Port <> 0) or (Socket <> '') then begin
|
||||||
|
if not Assigned(ConnectionParams) then begin
|
||||||
|
ConnectionParams := TConnectionParameters.Create;
|
||||||
|
ConnectionParams.SessionPath := SessName;
|
||||||
|
end;
|
||||||
|
if Host <> '' then ConnectionParams.Hostname := Host;
|
||||||
|
if User <> '' then ConnectionParams.Username := User;
|
||||||
|
if Pass <> '' then ConnectionParams.Password := Pass;
|
||||||
|
if Port <> 0 then ConnectionParams.Port := Port;
|
||||||
|
if Socket <> '' then begin
|
||||||
|
ConnectionParams.Hostname := Socket;
|
||||||
|
ConnectionParams.NetType := ntMySQL_NamedPipe;
|
||||||
|
end;
|
||||||
|
// Ensure we have a session name to pass to InitConnection
|
||||||
|
if (ConnectionParams.SessionPath = '') and (ConnectionParams.Hostname <> '') then
|
||||||
|
ConnectionParams.SessionPath := ConnectionParams.Hostname;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Check for valid filename(s) in parameters
|
||||||
|
for i:=0 to Parameters.Count-1 do begin
|
||||||
|
if FileExists(Parameters[i]) then
|
||||||
|
FileNames.Add(Parameters[i]);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
{ Threading stuff }
|
{ Threading stuff }
|
||||||
|
|
||||||
|
122
source/main.pas
122
source/main.pas
@ -568,7 +568,7 @@ type
|
|||||||
procedure WMCopyData(var Msg: TWMCopyData); message WM_COPYDATA;
|
procedure WMCopyData(var Msg: TWMCopyData); message WM_COPYDATA;
|
||||||
procedure FormDestroy(Sender: TObject);
|
procedure FormDestroy(Sender: TObject);
|
||||||
procedure FormCreate(Sender: TObject);
|
procedure FormCreate(Sender: TObject);
|
||||||
procedure Startup;
|
procedure AfterFormCreate;
|
||||||
procedure FormResize(Sender: TObject);
|
procedure FormResize(Sender: TObject);
|
||||||
procedure actUserManagerExecute(Sender: TObject);
|
procedure actUserManagerExecute(Sender: TObject);
|
||||||
procedure actAboutBoxExecute(Sender: TObject);
|
procedure actAboutBoxExecute(Sender: TObject);
|
||||||
@ -908,8 +908,6 @@ type
|
|||||||
FFilterTextDatabase,
|
FFilterTextDatabase,
|
||||||
FFilterTextData: String;
|
FFilterTextData: String;
|
||||||
FTreeRefreshInProgress: Boolean;
|
FTreeRefreshInProgress: Boolean;
|
||||||
FCmdlineFilenames: TStringlist;
|
|
||||||
FCmdlineConnectionParams: TConnectionParameters;
|
|
||||||
FSearchReplaceExecuted: Boolean;
|
FSearchReplaceExecuted: Boolean;
|
||||||
FDataGridColumnWidthsCustomized: Boolean;
|
FDataGridColumnWidthsCustomized: Boolean;
|
||||||
FSnippetFilenames: TStringList;
|
FSnippetFilenames: TStringList;
|
||||||
@ -935,7 +933,6 @@ type
|
|||||||
FCommandStatsQueryCount: Int64;
|
FCommandStatsQueryCount: Int64;
|
||||||
FCommandStatsServerUptime: Integer;
|
FCommandStatsServerUptime: Integer;
|
||||||
|
|
||||||
procedure ParseCommandLineParameters(Parameters: TStringlist);
|
|
||||||
procedure SetDelimiter(Value: String);
|
procedure SetDelimiter(Value: String);
|
||||||
procedure DisplayRowCountStats(Sender: TBaseVirtualTree);
|
procedure DisplayRowCountStats(Sender: TBaseVirtualTree);
|
||||||
procedure insertFunction(Sender: TObject);
|
procedure insertFunction(Sender: TObject);
|
||||||
@ -1595,16 +1592,15 @@ end;
|
|||||||
{**
|
{**
|
||||||
Check for connection parameters on commandline or show connections form.
|
Check for connection parameters on commandline or show connections form.
|
||||||
}
|
}
|
||||||
procedure TMainForm.Startup;
|
procedure TMainForm.AfterFormCreate;
|
||||||
var
|
var
|
||||||
CmdlineParameters, LastSessions: TStringlist;
|
CmdlineParameters, LastSessions, FileNames: TStringlist;
|
||||||
Connection: TDBConnection;
|
Connection: TDBConnection;
|
||||||
LoadedParams: TConnectionParameters;
|
LoadedParams, ConnectionParams: TConnectionParameters;
|
||||||
LastUpdatecheck, LastStatsCall, LastConnect: TDateTime;
|
LastUpdatecheck, LastStatsCall, LastConnect: TDateTime;
|
||||||
UpdatecheckInterval, i: Integer;
|
UpdatecheckInterval, i: Integer;
|
||||||
DefaultLastrunDate, LastActiveSession: String;
|
DefaultLastrunDate, LastActiveSession: String;
|
||||||
frm : TfrmUpdateCheck;
|
frm : TfrmUpdateCheck;
|
||||||
Connected: Boolean;
|
|
||||||
StatsCall: THttpDownload;
|
StatsCall: THttpDownload;
|
||||||
SessionPaths: TStringlist;
|
SessionPaths: TStringlist;
|
||||||
DlgResult: TModalResult;
|
DlgResult: TModalResult;
|
||||||
@ -1668,15 +1664,13 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
Connected := False;
|
|
||||||
|
|
||||||
CmdlineParameters := TStringList.Create;
|
CmdlineParameters := TStringList.Create;
|
||||||
for i:=1 to ParamCount do
|
for i:=1 to ParamCount do
|
||||||
CmdlineParameters.Add(ParamStr(i));
|
CmdlineParameters.Add(ParamStr(i));
|
||||||
ParseCommandLineParameters(CmdlineParameters);
|
ParseCommandLine(CmdlineParameters, ConnectionParams, FileNames);
|
||||||
if Assigned(FCmdlineConnectionParams) then begin
|
if Assigned(ConnectionParams) then begin
|
||||||
// Minimal parameter for command line mode is hostname
|
// Minimal parameter for command line mode is hostname
|
||||||
Connected := InitConnection(FCmdlineConnectionParams, True, Connection);
|
InitConnection(ConnectionParams, True, Connection);
|
||||||
end else if AppSettings.ReadBool(asAutoReconnect) then begin
|
end else if AppSettings.ReadBool(asAutoReconnect) then begin
|
||||||
// Auto connection via preference setting
|
// Auto connection via preference setting
|
||||||
// Do not autoconnect if we're in commandline mode and the connection was not successful
|
// Do not autoconnect if we're in commandline mode and the connection was not successful
|
||||||
@ -1692,8 +1686,7 @@ begin
|
|||||||
for i:=0 to LastSessions.Count-1 do begin
|
for i:=0 to LastSessions.Count-1 do begin
|
||||||
try
|
try
|
||||||
LoadedParams := TConnectionParameters.Create(LastSessions[i]);
|
LoadedParams := TConnectionParameters.Create(LastSessions[i]);
|
||||||
if InitConnection(LoadedParams, LastActiveSession=LastSessions[i], Connection) then
|
InitConnection(LoadedParams, LastActiveSession=LastSessions[i], Connection);
|
||||||
Connected := True;
|
|
||||||
except on E:Exception do
|
except on E:Exception do
|
||||||
ErrorDialog(E.Message);
|
ErrorDialog(E.Message);
|
||||||
end;
|
end;
|
||||||
@ -1702,7 +1695,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// Display session manager
|
// Display session manager
|
||||||
if not Connected then begin
|
if Connections.Count = 0 then begin
|
||||||
// Cannot be done in OnCreate because we need ready forms here:
|
// Cannot be done in OnCreate because we need ready forms here:
|
||||||
SessionManager := TConnForm.Create(Self);
|
SessionManager := TConnForm.Create(Self);
|
||||||
DlgResult := mrCancel;
|
DlgResult := mrCancel;
|
||||||
@ -1720,92 +1713,17 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// Load SQL file(s) by command line
|
// Load SQL file(s) by command line
|
||||||
if not RunQueryFiles(FCmdlineFilenames, nil) then begin
|
if not RunQueryFiles(FileNames, nil) then begin
|
||||||
for i:=0 to FCmdlineFilenames.Count-1 do begin
|
for i:=0 to FileNames.Count-1 do begin
|
||||||
Tab := ActiveOrEmptyQueryTab(False);
|
Tab := ActiveOrEmptyQueryTab(False);
|
||||||
Tab.LoadContents(FCmdlineFilenames[i], True, nil);
|
Tab.LoadContents(FileNames[i], True, nil);
|
||||||
if i = FCmdlineFilenames.Count-1 then
|
if i = FileNames.Count-1 then
|
||||||
SetMainTab(Tab.TabSheet);
|
SetMainTab(Tab.TabSheet);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
procedure TMainForm.ParseCommandLineParameters(Parameters: TStringlist);
|
|
||||||
var
|
|
||||||
rx: TRegExpr;
|
|
||||||
AllParams, SessName, Host, User, Pass, Socket: String;
|
|
||||||
i, Port: Integer;
|
|
||||||
|
|
||||||
function GetParamValue(ShortName, LongName: String): String;
|
|
||||||
begin
|
|
||||||
Result := '';
|
|
||||||
rx.Expression := '\s(\-'+ShortName+'|\-\-'+LongName+')\s*\=?\s*([^\-]\S*)';
|
|
||||||
if rx.Exec(AllParams) then
|
|
||||||
Result := rx.Match[2];
|
|
||||||
end;
|
|
||||||
|
|
||||||
begin
|
|
||||||
// Initialize and clear variables
|
|
||||||
if not Assigned(FCmdlineFilenames) then
|
|
||||||
FCmdlineFilenames := TStringlist.Create;
|
|
||||||
FCmdlineFilenames.Clear;
|
|
||||||
SessName := '';
|
|
||||||
FreeAndNil(FCmdlineConnectionParams);
|
|
||||||
|
|
||||||
// Prepend a space, so the regular expression can request a mandantory space
|
|
||||||
// before each param name including the first one
|
|
||||||
AllParams := ' ' + ImplodeStr(' ', Parameters);
|
|
||||||
rx := TRegExpr.Create;
|
|
||||||
SessName := GetParamValue('d', 'description');
|
|
||||||
if SessName <> '' then begin
|
|
||||||
try
|
|
||||||
FCmdlineConnectionParams := TConnectionParameters.Create(SessName);
|
|
||||||
except
|
|
||||||
on E:Exception do begin
|
|
||||||
// Session params not found in registry
|
|
||||||
LogSQL(E.Message);
|
|
||||||
SessName := '';
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Test if params were passed. If given, override previous values loaded from registry.
|
|
||||||
// Enables the user to log into a session with a different, non-stored user: -dSession -uSomeOther
|
|
||||||
Host := GetParamValue('h', 'host');
|
|
||||||
User := GetParamValue('u', 'user');
|
|
||||||
Pass := GetParamValue('p', 'password');
|
|
||||||
Socket := GetParamValue('S', 'socket');
|
|
||||||
Port := StrToIntDef(GetParamValue('P', 'port'), 0);
|
|
||||||
// Leave out support for startup script, seems reasonable for command line connecting
|
|
||||||
|
|
||||||
if (Host <> '') or (User <> '') or (Pass <> '') or (Port <> 0) or (Socket <> '') then begin
|
|
||||||
if not Assigned(FCmdlineConnectionParams) then begin
|
|
||||||
FCmdlineConnectionParams := TConnectionParameters.Create;
|
|
||||||
FCmdlineConnectionParams.SessionPath := SessName;
|
|
||||||
end;
|
|
||||||
if Host <> '' then FCmdlineConnectionParams.Hostname := Host;
|
|
||||||
if User <> '' then FCmdlineConnectionParams.Username := User;
|
|
||||||
if Pass <> '' then FCmdlineConnectionParams.Password := Pass;
|
|
||||||
if Port <> 0 then FCmdlineConnectionParams.Port := Port;
|
|
||||||
if Socket <> '' then begin
|
|
||||||
FCmdlineConnectionParams.Hostname := Socket;
|
|
||||||
FCmdlineConnectionParams.NetType := ntMySQL_NamedPipe;
|
|
||||||
end;
|
|
||||||
// Ensure we have a session name to pass to InitConnection
|
|
||||||
if (FCmdlineConnectionParams.SessionPath = '') and (FCmdlineConnectionParams.Hostname <> '') then
|
|
||||||
FCmdlineConnectionParams.SessionPath := FCmdlineConnectionParams.Hostname;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Check for valid filename(s) in parameters
|
|
||||||
for i:=0 to Parameters.Count-1 do begin
|
|
||||||
if FileExists(Parameters[i]) then
|
|
||||||
FCmdlineFilenames.Add(Parameters[i]);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TMainForm.actSessionManagerExecute(Sender: TObject);
|
procedure TMainForm.actSessionManagerExecute(Sender: TObject);
|
||||||
var
|
var
|
||||||
Dialog: TConnForm;
|
Dialog: TConnForm;
|
||||||
@ -10018,18 +9936,20 @@ var
|
|||||||
i: Integer;
|
i: Integer;
|
||||||
Connection: TDBConnection;
|
Connection: TDBConnection;
|
||||||
Tab: TQueryTab;
|
Tab: TQueryTab;
|
||||||
|
ConnectionParams: TConnectionParameters;
|
||||||
|
FileNames: TStringList;
|
||||||
begin
|
begin
|
||||||
// Probably a second instance is posting its command line parameters here
|
// Probably a second instance is posting its command line parameters here
|
||||||
if (Msg.CopyDataStruct.dwData = SecondInstMsgId) and (SecondInstMsgId <> 0) then begin
|
if (Msg.CopyDataStruct.dwData = SecondInstMsgId) and (SecondInstMsgId <> 0) then begin
|
||||||
ParseCommandLineParameters(ParamBlobToStr(Msg.CopyDataStruct.lpData));
|
ParseCommandLine(ParamBlobToStr(Msg.CopyDataStruct.lpData), ConnectionParams, FileNames);
|
||||||
if not RunQueryFiles(FCmdlineFilenames, nil) then begin
|
if not RunQueryFiles(FileNames, nil) then begin
|
||||||
for i:=0 to FCmdlineFilenames.Count-1 do begin
|
for i:=0 to FileNames.Count-1 do begin
|
||||||
Tab := ActiveOrEmptyQueryTab(False);
|
Tab := ActiveOrEmptyQueryTab(False);
|
||||||
Tab.LoadContents(FCmdlineFilenames[i], True, nil);
|
Tab.LoadContents(FileNames[i], True, nil);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
if Assigned(FCmdlineConnectionParams) then
|
if Assigned(ConnectionParams) then
|
||||||
InitConnection(FCmdlineConnectionParams, True, Connection);
|
InitConnection(ConnectionParams, True, Connection);
|
||||||
end else
|
end else
|
||||||
// Not the right message id
|
// Not the right message id
|
||||||
inherited;
|
inherited;
|
||||||
|
Reference in New Issue
Block a user