restemplate/indy/Core/IdCommandHandlers.pas

683 lines
22 KiB
Plaintext

{
$Project$
$Workfile$
$Revision$
$DateUTC$
$Id$
This file is part of the Indy (Internet Direct) project, and is offered
under the dual-licensing agreement described on the Indy website.
(http://www.indyproject.org/)
Copyright:
(c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
$Log$
}
{
Rev 1.36 2/1/05 12:37:48 AM RLebeau
Removed IdCommandHandlersEnabledDefault variable, no longer used.
Rev 1.35 1/3/05 4:43:20 PM RLebeau
Changed use of AnsiSameText() to use TextIsSame() instead
Rev 1.34 12/17/04 12:54:04 PM RLebeau
Updated TIdCommandHandler.Check() to not match misspelled commands when a
CmdDelimiter is specified.
Rev 1.33 12/10/04 1:48:04 PM RLebeau
Bug fix for TIdCommandHandler.DoCommand()
Rev 1.32 10/26/2004 8:42:58 PM JPMugaas
Should be more portable with new references to TIdStrings and TIdStringList.
Rev 1.31 6/17/2004 2:19:50 AM JPMugaas
Problem with unparsed parameters. The initial deliniator between the command
and reply was being added to Unparsed Params leading some strange results and
command failures.
Rev 1.30 6/6/2004 11:44:34 AM JPMugaas
Removed a temporary workaround for a Telnet Sequences issue in the
TIdFTPServer. That workaround is no longer needed as we fixed the issue
another way.
Rev 1.29 5/16/04 5:20:22 PM RLebeau
Removed local variable from TIdCommandHandler constructor, no longer used
Rev 1.28 2004.03.03 3:19:52 PM czhower
sorted
Rev 1.27 3/3/2004 4:59:40 AM JPMugaas
Updated for new properties.
Rev 1.26 3/2/2004 8:10:36 AM JPMugaas
HelpHide renamed to HelpVisable.
Rev 1.25 3/2/2004 6:37:36 AM JPMugaas
Updated with properties for more comprehensive help systems.
Rev 1.24 2004.03.01 7:13:40 PM czhower
Comaptibilty fix.
Rev 1.23 2004.03.01 5:12:26 PM czhower
-Bug fix for shutdown of servers when connections still existed (AV)
-Implicit HELP support in CMDserver
-Several command handler bugs
-Additional command handler functionality.
Rev 1.22 2004.02.29 9:49:06 PM czhower
Bug fix, and now responses are also write buffered.
Rev 1.21 2004.02.03 4:17:10 PM czhower
For unit name changes.
Rev 1.20 1/29/04 10:00:40 PM RLebeau
Added setter methods to various TIdReply properties
Rev 1.19 2003.12.31 7:31:58 PM czhower
AnsiSameText --> TextIsSame
Rev 1.18 10/19/2003 11:36:52 AM DSiders
Added localization comments where setting response codes.
Rev 1.17 2003.10.18 9:33:26 PM czhower
Boatload of bug fixes to command handlers.
Rev 1.16 2003.10.18 8:07:12 PM czhower
Fixed bug with defaults.
Rev 1.15 2003.10.18 8:03:58 PM czhower
Defaults for codes
Rev 1.14 10/5/2003 03:06:18 AM JPMugaas
Should compile.
Rev 1.13 8/9/2003 3:52:44 PM BGooijen
TIdCommandHandlers can now create any TIdCommandHandler descendant. this
makes it possible to override TIdCommandHandler.check and check for the
command a different way ( binary commands, protocols where the string doesn't
start with the command )
Rev 1.12 8/2/2003 2:22:54 PM SPerry
Fixed OnCommandHandlersException problem
Rev 1.11 8/2/2003 1:43:08 PM SPerry
Modifications to get command handlers to work
Rev 1.9 7/30/2003 10:18:30 PM SPerry
Fixed AV when creating commandhandler (again) -- for some reason the bug
fixed in Rev. 1.7 was still there.
Rev 1.8 7/30/2003 8:31:58 PM SPerry
Fixed AV with LFReplyClass.
Rev 1.4 7/9/2003 10:55:26 PM BGooijen
Restored all features
Rev 1.3 7/9/2003 04:36:10 PM JPMugaas
You now can override the TIdReply with your own type. This should illiminate
some warnings about some serious issues. TIdReply is ONLY a base class with
virtual methods.
Rev 1.2 7/9/2003 01:43:22 PM JPMugaas
Should now compile.
Rev 1.1 7/9/2003 2:56:44 PM SPerry
Added OnException event
Rev 1.0 7/6/2003 4:47:38 PM SPerry
Units that use Command handlers
}
unit IdCommandHandlers;
{
Original author: Chad Z. Hower
Separate Unit : Sergio Perry
}
interface
{$I IdCompilerDefines.inc}
//Put FPC into Delphi mode
uses
Classes,
IdBaseComponent, IdComponent, IdReply, IdGlobal,
IdContext, IdReplyRFC;
const
IdEnabledDefault = True;
// DO NOT change this default (ParseParams). Many servers rely on this
IdParseParamsDefault = True;
IdHelpVisibleDef = True;
type
TIdCommandHandlers = class;
TIdCommandHandler = class;
TIdCommand = class;
{ Events }
TIdCommandEvent = procedure(ASender: TIdCommand) of object;
TIdAfterCommandHandlerEvent = procedure(ASender: TIdCommandHandlers;
AContext: TIdContext) of object;
TIdBeforeCommandHandlerEvent = procedure(ASender: TIdCommandHandlers;
var AData: string; AContext: TIdContext) of object;
TIdCommandHandlersExceptionEvent = procedure(ACommand: String; AContext: TIdContext) of object;
{ TIdCommandHandler }
TIdCommandHandler = class(TCollectionItem)
protected
FCmdDelimiter: Char;
FCommand: string;
{$IFDEF USE_OBJECT_ARC}
// When ARC is enabled, object references MUST be valid objects.
// It is common for users to store non-object values, though, so
// we will provide separate properties for those purposes
//
// TODO; use TValue instead of separating them
//
FDataObject: TObject;
FDataValue: PtrInt;
{$ELSE}
FData: TObject;
{$ENDIF}
FDescription: TStrings;
FDisconnect: boolean;
FEnabled: boolean;
FExceptionReply: TIdReply;
FHelpSuperScript : String; //may be something like * or + which should appear in help
FHelpVisible : Boolean;
FName: string;
FNormalReply: TIdReply;
FOnCommand: TIdCommandEvent;
FParamDelimiter: Char;
FParseParams: Boolean;
FReplyClass : TIdReplyClass;
FResponse: TStrings;
FTag: integer;
//
function GetDisplayName: string; override;
procedure SetDescription(AValue: TStrings);
procedure SetExceptionReply(AValue: TIdReply);
procedure SetNormalReply(AValue: TIdReply);
procedure SetResponse(AValue: TStrings);
public
function Check(const AData: string; AContext: TIdContext): boolean; virtual;
procedure DoCommand(const AData: string; AContext: TIdContext; AUnparsedParams: string); virtual;
procedure DoParseParams(AUnparsedParams: string; AParams: TStrings); virtual;
constructor Create(ACollection: TCollection); override;
destructor Destroy; override;
// function GetNamePath: string; override;
function NameIs(const ACommand: string): Boolean;
//
{$IFDEF USE_OBJECT_ARC}
property DataObject: TObject read FDataObject write FDataObject;
property DataValue: PtrInt read FDataValue write FDataValue;
{$ELSE}
property Data: TObject read FData write FData;
{$ENDIF}
published
property CmdDelimiter: Char read FCmdDelimiter write FCmdDelimiter;
property Command: string read FCommand write FCommand;
property Description: TStrings read FDescription write SetDescription;
property Disconnect: boolean read FDisconnect write FDisconnect;
property Enabled: boolean read FEnabled write FEnabled default IdEnabledDefault;
property ExceptionReply: TIdReply read FExceptionReply write SetExceptionReply;
property Name: string read FName write FName;
property NormalReply: TIdReply read FNormalReply write SetNormalReply;
property ParamDelimiter: Char read FParamDelimiter write FParamDelimiter;
property ParseParams: Boolean read FParseParams write FParseParams;
property Response: TStrings read FResponse write SetResponse;
property Tag: Integer read FTag write FTag;
//
property HelpSuperScript : String read FHelpSuperScript write FHelpSuperScript; //may be something like * or + which should appear in help
property HelpVisible : Boolean read FHelpVisible write FHelpVisible default IdHelpVisibleDef;
property OnCommand: TIdCommandEvent read FOnCommand write FOnCommand;
end;
TIdCommandHandlerClass = class of TIdCommandHandler;
{ TIdCommandHandlers }
TIdCommandHandlers = class(TOwnedCollection)
protected
FBase: TIdComponent;
FExceptionReply: TIdReply;
FOnAfterCommandHandler: TIdAfterCommandHandlerEvent;
FOnBeforeCommandHandler: TIdBeforeCommandHandlerEvent;
FOnCommandHandlersException: TIdCommandHandlersExceptionEvent;
FParseParamsDef: Boolean;
FPerformReplies: Boolean;
FReplyClass: TIdReplyClass;
FReplyTexts: TIdReplies;
//
procedure DoAfterCommandHandler(AContext: TIdContext);
procedure DoBeforeCommandHandler(AContext: TIdContext; var VLine: string);
procedure DoOnCommandHandlersException(const ACommand: String; AContext: TIdContext);
function GetItem(AIndex: Integer): TIdCommandHandler;
// This is used instead of the OwnedBy property directly calling GetOwner because
// D5 dies with internal errors and crashes
// function GetOwnedBy: TIdPersistent;
procedure SetItem(AIndex: Integer; const AValue: TIdCommandHandler);
public
function Add: TIdCommandHandler;
constructor Create(
ABase: TIdComponent;
AReplyClass: TIdReplyClass;
AReplyTexts: TIdReplies;
AExceptionReply: TIdReply = nil;
ACommandHandlerClass: TIdCommandHandlerClass = nil
); reintroduce;
function HandleCommand(AContext: TIdContext; var VCommand: string): Boolean; virtual;
//
property Base: TIdComponent read FBase;
property Items[AIndex: Integer]: TIdCommandHandler read GetItem write SetItem;
// OwnedBy is used so as not to conflict with Owner in D6
//property OwnedBy: TIdPersistent read GetOwnedBy;
property ParseParamsDefault: Boolean read FParseParamsDef write FParseParamsDef;
property PerformReplies: Boolean read FPerformReplies write FPerformReplies;
property ReplyClass: TIdReplyClass read FReplyClass;
property ReplyTexts: TIdReplies read FReplyTexts;
//
property OnAfterCommandHandler: TIdAfterCommandHandlerEvent read FOnAfterCommandHandler
write FOnAfterCommandHandler;
// Occurs in the context of the peer thread
property OnBeforeCommandHandler: TIdBeforeCommandHandlerEvent read FOnBeforeCommandHandler
write FOnBeforeCommandHandler;
property OnCommandHandlersException: TIdCommandHandlersExceptionEvent read FOnCommandHandlersException
write FOnCommandHandlersException;
end;
{ TIdCommand }
TIdCommand = class(TObject)
protected
FCommandHandler: TIdCommandHandler;
FDisconnect: Boolean;
FParams: TStrings;
FPerformReply: Boolean;
FRawLine: string;
FReply: TIdReply;
FResponse: TStrings;
FContext: TIdContext;
FUnparsedParams: string;
FSendEmptyResponse: Boolean;
//
procedure DoCommand; virtual;
procedure SetReply(AValue: TIdReply);
procedure SetResponse(AValue: TStrings);
public
constructor Create(AOwner: TIdCommandHandler); virtual;
destructor Destroy; override;
procedure SendReply;
//
property CommandHandler: TIdCommandHandler read FCommandHandler;
property Disconnect: Boolean read FDisconnect write FDisconnect;
property PerformReply: Boolean read FPerformReply write FPerformReply;
property Params: TStrings read FParams;
property RawLine: string read FRawLine;
property Reply: TIdReply read FReply write SetReply;
property Response: TStrings read FResponse write SetResponse;
property Context: TIdContext read FContext;
property UnparsedParams: string read FUnparsedParams;
property SendEmptyResponse: Boolean read FSendEmptyResponse write FSendEmptyResponse;
end;//TIdCommand
implementation
uses
SysUtils;
{ TIdCommandHandlers }
constructor TIdCommandHandlers.Create(
ABase: TIdComponent;
AReplyClass: TIdReplyClass;
AReplyTexts: TIdReplies;
AExceptionReply: TIdReply = nil;
ACommandHandlerClass: TIdCommandHandlerClass = nil
);
begin
if ACommandHandlerClass = nil then begin
ACommandHandlerClass := TIdCommandHandler;
end;
inherited Create(ABase, ACommandHandlerClass);
FBase := ABase;
FExceptionReply := AExceptionReply;
FParseParamsDef := IdParseParamsDefault;
FPerformReplies := True; // RLebeau: default to legacy behavior
FReplyClass := AReplyClass;
FReplyTexts := AReplyTexts;
end;
function TIdCommandHandlers.Add: TIdCommandHandler;
begin
Result := TIdCommandHandler(inherited Add);
end;
function TIdCommandHandlers.HandleCommand(AContext: TIdContext;
var VCommand: string): Boolean;
var
i, j: Integer;
begin
j := Count - 1;
Result := False;
DoBeforeCommandHandler(AContext, VCommand); try
i := 0;
while i <= j do begin
if Items[i].Enabled then begin
Result := Items[i].Check(VCommand, AContext);
if Result then begin
Break;
end;
end;
Inc(i);
end;
finally DoAfterCommandHandler(AContext); end;
end;
procedure TIdCommandHandlers.DoAfterCommandHandler(AContext: TIdContext);
begin
if Assigned(OnAfterCommandHandler) then begin
OnAfterCommandHandler(Self, AContext);
end;
end;
procedure TIdCommandHandlers.DoBeforeCommandHandler(AContext: TIdContext;
var VLine: string);
begin
if Assigned(OnBeforeCommandHandler) then begin
OnBeforeCommandHandler(Self, VLine, AContext);
end;
end;
procedure TIdCommandHandlers.DoOnCommandHandlersException(const ACommand: String;
AContext: TIdContext);
begin
if Assigned(FOnCommandHandlersException) then begin
OnCommandHandlersException(ACommand, AContext);
end;
end;
function TIdCommandHandlers.GetItem(AIndex: Integer): TIdCommandHandler;
begin
Result := TIdCommandHandler(inherited Items[AIndex]);
end;
{
function TIdCommandHandlers.GetOwnedBy: TIdPersistent;
begin
Result := GetOwner;
end;
}
procedure TIdCommandHandlers.SetItem(AIndex: Integer; const AValue: TIdCommandHandler);
begin
inherited SetItem(AIndex, AValue);
end;
{ TIdCommandHandler }
procedure TIdCommandHandler.DoCommand(const AData: string; AContext: TIdContext; AUnparsedParams: string);
var
LCommand: TIdCommand;
begin
LCommand := TIdCommand.Create(Self);
try
LCommand.FRawLine := AData;
LCommand.FContext := AContext;
LCommand.FUnparsedParams := AUnparsedParams;
if ParseParams then begin
DoParseParams(AUnparsedParams, LCommand.Params);
end;
// RLebeau 2/21/08: for the IRC protocol, RFC 2812 section 2.4 says that
// clients are not allowed to issue numeric replies for server-issued
// commands. Added the PerformReplies property so TIdIRC can specify
// that behavior.
if Collection is TIdCommandHandlers then begin
LCommand.PerformReply := TIdCommandHandlers(Collection).PerformReplies;
end;
try
if (LCommand.Reply.Code = '') and (NormalReply.Code <> '') then begin
LCommand.Reply.Assign(NormalReply);
end;
//if code<>'' before DoCommand, then it breaks exception handling
Assert(LCommand.Reply.Code <> '');
LCommand.DoCommand;
if LCommand.Reply.Code = '' then begin
LCommand.Reply.Assign(NormalReply);
end;
// UpdateText here in case user wants to add to it. SendReply also gets it in case
// a different reply is sent (ie exception, etc), or the user changes the code in the event
LCommand.Reply.UpdateText;
except
on E: Exception do begin
// If there is an unhandled exception, we override all replies
// If nothing specified to override with, we throw the exception again.
// If the user wants a custom value on exception other, its their responsibility
// to catch it before it reaches us
LCommand.Reply.Clear;
if LCommand.PerformReply then begin
// Try from command handler first
if ExceptionReply.Code <> '' then begin
LCommand.Reply.Assign(ExceptionReply);
// If still no go, from server
// Can be nil though. Typically only servers pass it in
end else if (Collection is TIdCommandHandlers) and (TIdCommandHandlers(Collection).FExceptionReply <> nil) then begin
LCommand.Reply.Assign(TIdCommandHandlers(Collection).FExceptionReply);
end;
if LCommand.Reply.Code <> '' then begin
//done this way in case an exception message has more than one line.
//otherwise you could get something like this:
//
// 550 System Error. Code: 2
// The system cannot find the file specified
//
//and the second line would throw off some clients.
LCommand.Reply.Text.Text := E.Message;
//Reply.Text.Add(E.Message);
LCommand.SendReply;
end else begin
raise;
end;
end else begin
raise;
end;
end else begin
raise;
end;
end;
if LCommand.PerformReply then begin
LCommand.SendReply;
end;
if (LCommand.Response.Count > 0) or LCommand.SendEmptyResponse then begin
AContext.Connection.WriteRFCStrings(LCommand.Response);
end else if Response.Count > 0 then begin
AContext.Connection.WriteRFCStrings(Response);
end;
finally
try
if LCommand.Disconnect then begin
AContext.Connection.Disconnect;
end;
finally
LCommand.Free;
end;
end;
end;
procedure TIdCommandHandler.DoParseParams(AUnparsedParams: string; AParams: TStrings);
// AUnparsedParams is not preparsed and is completely left up to the command handler. This will
// allow for future expansion such as multiple delimiters etc, and allow the logic to properly
// remain in each of the command handler implementations. In the future there may be a base type
// and multiple descendants
begin
AParams.Clear;
SplitDelimitedString(AUnparsedParams, AParams, FParamDelimiter <> #32, FParamDelimiter);
end;
function TIdCommandHandler.Check(const AData: string; AContext: TIdContext): boolean;
// AData is not preparsed and is completely left up to the command handler. This will allow for
// future expansion such as wild cards etc, and allow the logic to properly remain in each of the
// command handler implementations. In the future there may be a base type and multiple descendants
var
LUnparsedParams: string;
begin
LUnparsedParams := '';
Result := TextIsSame(AData, Command); // Command by itself
if not Result then begin
if CmdDelimiter <> #0 then begin
Result := TextStartsWith(AData, Command + CmdDelimiter);
if Result then begin
LUnparsedParams := Copy(AData, Length(Command) + 2, MaxInt);
end;
end else begin
// Dont strip any part of the params out.. - just remove the command purely on length and
// no delim
Result := TextStartsWith(AData, Command);
if Result then begin
LUnparsedParams := Copy(AData, Length(Command) + 1, MaxInt);
end;
end;
end;
if Result then begin
DoCommand(AData, AContext, LUnparsedParams);
end;
end;
constructor TIdCommandHandler.Create(ACollection: TCollection);
begin
inherited Create(ACollection);
FReplyClass := TIdCommandHandlers(ACollection).ReplyClass;
if FReplyClass = nil then begin
FReplyClass := TIdReplyRFC;
end;
FCmdDelimiter := #32;
FEnabled := IdEnabledDefault;
FName := ClassName + IntToStr(ID);
FParamDelimiter := #32;
FParseParams := TIdCommandHandlers(ACollection).ParseParamsDefault;
FResponse := TStringList.Create;
FDescription := TStringList.Create;
FNormalReply := FReplyClass.CreateWithReplyTexts(nil, TIdCommandHandlers(ACollection).ReplyTexts);
if FNormalReply is TIdReplyRFC then begin
FNormalReply.Code := '200'; {do not localize}
end;
FHelpVisible := IdHelpVisibleDef;
// Dont initialize, pulls from CmdTCPServer for defaults
FExceptionReply := FReplyClass.CreateWithReplyTexts(nil, TIdCommandHandlers(ACollection).ReplyTexts);
end;
destructor TIdCommandHandler.Destroy;
begin
FreeAndNil(FResponse);
FreeAndNil(FNormalReply);
FreeAndNil(FDescription);
FreeAndNil(FExceptionReply);
inherited Destroy;
end;
function TIdCommandHandler.GetDisplayName: string;
begin
if Command = '' then begin
Result := Name;
end else begin
Result := Command;
end;
end;
{
function TIdCommandHandler.GetNamePath: string;
begin
if Collection <> nil then begin
// OwnedBy is used because D4/D5 dont expose Owner on TOwnedCollection but D6 does
Result := TIdCommandHandlers(Collection).OwnedBy.GetNamePath + '.' + Name;
end else begin
Result := inherited GetNamePath;
end;
end;
}
function TIdCommandHandler.NameIs(const ACommand: string): Boolean;
begin
Result := TextIsSame(ACommand, FName);
end;
procedure TIdCommandHandler.SetExceptionReply(AValue: TIdReply);
begin
FExceptionReply.Assign(AValue);
end;
procedure TIdCommandHandler.SetNormalReply(AValue: TIdReply);
begin
FNormalReply.Assign(AValue);
end;
procedure TIdCommandHandler.SetResponse(AValue: TStrings);
begin
FResponse.Assign(AValue);
end;
procedure TIdCommandHandler.SetDescription(AValue: TStrings);
begin
FDescription.Assign(AValue);
end;
{ TIdCommand }
constructor TIdCommand.Create(AOwner: TIdCommandHandler);
begin
inherited Create;
FParams := TStringList.Create;
FReply := AOwner.FReplyClass.CreateWithReplyTexts(nil, TIdCommandHandlers(AOwner.Collection).ReplyTexts);
FResponse := TStringList.Create;
FCommandHandler := AOwner;
FDisconnect := AOwner.Disconnect;
end;
destructor TIdCommand.Destroy;
begin
FreeAndNil(FReply);
FreeAndNil(FResponse);
FreeAndNil(FParams);
inherited Destroy;
end;
procedure TIdCommand.DoCommand;
begin
if Assigned(CommandHandler.OnCommand) then begin
CommandHandler.OnCommand(Self);
end;
end;
procedure TIdCommand.SendReply;
begin
PerformReply := False;
Reply.UpdateText;
Context.Connection.IOHandler.Write(Reply.FormattedReply);
end;
procedure TIdCommand.SetReply(AValue: TIdReply);
begin
FReply.Assign(AValue);
end;
procedure TIdCommand.SetResponse(AValue: TStrings);
begin
FResponse.Assign(AValue);
end;
end.