restemplate/indy/Protocols/IdGopher.pas

725 lines
24 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.17 3/4/2005 10:34:34 PM JPMugaas
Fix for compiler warnings and removed duplicate code.
Rev 1.16 2004.10.27 9:17:52 AM czhower
For TIdStrings
Rev 1.15 10/26/2004 10:10:58 PM JPMugaas
Updated refs.
Rev 1.14 2004.05.20 11:37:06 AM czhower
IdStreamVCL
Rev 1.13 2004.02.03 5:44:46 PM czhower
Name changes
Rev 1.12 1/21/2004 3:26:42 PM JPMugaas
InitComponent
Rev 1.11 10/24/2003 03:26:18 PM JPMugaas
Attempted to restore functionality after Kudzu's "surgery"
Rev 1.10 2003.10.24 10:43:06 AM czhower
TIdSTream to dos
Rev 1.9 10/21/2003 8:47:44 PM BGooijen
Fixed WriteLn and ReadLn namespaces
Rev 1.7 10/19/2003 6:00:04 PM BGooijen
Did Todo
Rev 1.6 2003.10.12 3:50:42 PM czhower
Compile todos
Rev 1.5 6/5/2003 04:54:12 AM JPMugaas
Reworkings and minor changes for new Reply exception framework.
Rev 1.4 2/24/2003 08:50:58 PM JPMugaas
Rev 1.3 12/8/2002 07:26:22 PM JPMugaas
Added published host and port properties.
Rev 1.2 12/6/2002 05:29:46 PM JPMugaas
Now decend from TIdTCPClientCustom instead of TIdTCPClient.
Rev 1.1 12/6/2002 04:35:04 PM JPMugaas
Now compiles with new code.
Rev 1.0 11/13/2002 08:29:48 AM JPMugaas
Initial import from FTP VC.
2000-June- 9 J. Peter Mugaas
-adjusted the Gopher+ support so that line-unfolding is disabled in
FGopherBlock. Many headers we use start with spaces
-made the ASK block into a TIdHeaderList to facilitate use better. This does
unfold lines
2000-May -24 J. Peter Mugaas
-changed interface of file retrieval routines to so DestStream property does
not have to even exist now.
2000-May -17 J. Peter Mugaas
-Optimized the DoneSettingInfoBlock method in the TIdGopherMenuItem object
-Added Ask property to the TIdGopherMenuItem
2000-May -13 J. Peter Mugaas
-Chanded the event types and classes to be prefixed with Id.
2000-Apr.-28 J. Peter Mugaas
-Added built in Gopher+ support
2000-Apr.-21 J. Peter Mugaas
-Added the ability to receive a file
-Restructured this component to make the code more reabible,
facilitate processing, and improve object orientation
2000-Apr.-20 J. Peter Mugaas
-Started this unit
}
unit IdGopher;
{*******************************************************}
{ }
{ Indy Gopher Client TIdGopher }
{ }
{ Copyright (C) 2000 Winshoes Working Group }
{ Started by J. Peter Mugaas }
{ April 20, 2000 }
{ }
{*******************************************************}
interface
{$i IdCompilerDefines.inc}
uses
Classes,
IdAssignedNumbers,
IdEMailAddress,
IdGlobal,
IdHeaderList, IdTCPClient, IdBaseComponent;
type
TIdGopherMenuItem = class(TCollectionItem)
protected
FTitle : String;
FItemType : Char;
FSelector : String;
FServer : String;
FPort : TIdPort;
FGopherPlusItem : Boolean;
FGopherBlock : TIdHeaderList;
FViews : TStrings;
FURL : String;
FAbstract : TStrings;
FAsk : TIdHeaderList;
fAdminEmail : TIdEMailAddressItem;
function GetLastModified : String;
function GetOrganization : String;
function GetLocation : String;
function GetGeog : String;
public
constructor Create(ACollection: TCollection); override;
destructor Destroy; override;
{This procedure updates several internal variables and should be done when
all data has been added}
procedure DoneSettingInfoBlock; virtual;
{This is the title for the gopher Menu item and should be displayed to the
user}
property Title : String read FTitle write FTitle;
{This charactor indicates the type of Item that this is.
Use this to determine what methods to call to get the item}
property ItemType : Char read FItemType write FItemType;
{This is the Selector you use to retreive the item}
property Selector : String read FSelector write FSelector;
{This is the server you connect to and request the item from. Set the host
property to this when retrieving it}
property Server : String read FServer write FServer;
{This indicates the port you connect to in order to request the item. Set
the port property to this value to get an item.}
property Port : TIdPort read FPort write FPort;
{This indicates if the item is on a Gopher+ server - you can use
GetExtended Menues for menus}
property GopherPlusItem : Boolean read FGopherPlusItem
write FGopherPlusItem;
{These items are only available if you use the GetExtendedMenu method}
{This is the complete information block for this gopher+ item}
property GopherBlock : TIdHeaderList read FGopherBlock;
{URL listed at +URL: Section }
property URL : String read FURL;
{This is the Gopher Views available for the item. You can include this
when requesting it}
property Views : TStrings read FViews;
{abstract of Gopher item - had to be AAbstract due to Pascal reserved word}
{this is a summery of a particular item - e.g. "Read about our greate
products"}
property AAbstract : TStrings read FAbstract;
{This is the date that the item was last modified}
property LastModified : String read GetLastModified;
{This is contact information for the adminst}
property AdminEMail : TIdEMailAddressItem read fAdminEmail;
{This is the organization running the server and
is usually only found in the Root item}
property Organization : String read GetOrganization;
{This is the location where the Gopher is
and is usually only found in the Root item}
property Location : String read GetLocation;
{This is the latitude longitude and longitude of the Gopher server
and is usually only found in the Root item}
property Geog : String read GetGeog;
{This Gopher+ information is used for prmoting users for Query data}
property Ask : TIdHeaderList read FAsk;
end;
TIdGopherMenu = class ( TCollection )
protected
function GetItem ( Index: Integer ) : TIdGopherMenuItem;
procedure SetItem ( Index: Integer; const Value: TIdGopherMenuItem );
public
constructor Create; reintroduce;
function Add: TIdGopherMenuItem;
property Items [ Index: Integer ] : TIdGopherMenuItem read GetItem
write SetItem; default;
end;
TIdGopherMenuEvent = procedure ( Sender : TObject;
MenuItem : TIdGopherMenuItem ) of object;
TIdGopher = class ( TIdTCPClientCustom )
private
{ Private declarations }
protected
{ Protected declarations }
FOnMenuItem : TIdGopherMenuEvent;
{This triggers the menu item event}
Procedure DoMenu ( MenuItem : TIdGopherMenuItem );
{This fires an exception for Gopher+ errors}
Procedure ProcessGopherError;
{This takes parses a string and makes a Menu Item for it}
Function MenuItemFromString ( stLine : String; Menu : TIdGopherMenu)
: TIdGopherMenuItem;
{Process the menu while we retreive it}
Function ProcessDirectory ( PreviousData : String = ''; {Do not Localize}
const ExpectedLength: Integer = 0) : TIdGopherMenu;
{This processes extended Gopher Menues}
Function LoadExtendedDirectory ( PreviousData : String = ''; {Do not Localize}
const ExpectedLength: Integer = 0) : TIdGopherMenu;
{This processes the file when we retreive it and puts it in ADestStream. }
procedure ProcessFile ( ADestStream : TStream; APreviousData : String = ''; {Do not Localize}
const ExpectedLength : Integer = 0);
{For Gopher +, we call this routine when we get a -2 length which means,
read until you see EOL+.+EOL}
Procedure ProcessTextFile ( ADestStream : TStream;
APreviousData: String = ''; const ExpectedLength: Integer = 0); {Do not Localize}
procedure InitComponent; override;
public
{ Public declarations }
Function GetMenu (ASelector : String; IsGopherPlus : Boolean = False; AView : String = '' ) : {Do not Localize}
TIdGopherMenu;
Function Search(ASelector, AQuery : String) : TIdGopherMenu;
procedure GetFile (ASelector : String; ADestStream : TStream; IsGopherPlus : Boolean = False; AView: String = ''); {Do not Localize}
procedure GetTextFile(ASelector : String; ADestStream : TStream; IsGopherPlus : Boolean = False; AView: String = ''); {Do not Localize}
Function GetExtendedMenu (ASelector : String; AView: String = '' ) : TIdGopherMenu; {Do not Localize}
published
{ Published declarations }
property OnMenuItem : TIdGopherMenuEvent read FOnMenuItem write FOnMenuItem;
property Port default IdPORT_Gopher;
property Host;
end;
implementation
uses
IdComponent, IdException,
IdGlobalProtocols, IdGopherConsts, IdReplyRFC,
IdTCPConnection, SysUtils;
{ TIdGopher }
procedure TIdGopher.InitComponent;
begin
inherited InitComponent;
Port := IdPORT_GOPHER;
end;
procedure TIdGopher.DoMenu(MenuItem: TIdGopherMenuItem);
begin
if Assigned( FOnMenuItem ) then
FOnMenuItem( Self, MenuItem );
end;
procedure TIdGopher.ProcessGopherError;
var ErrorNo : Integer;
ErrMsg : String;
begin
ErrMsg := IOHandler.AllData;
{Get the error number from the error reply line}
ErrorNo := IndyStrToInt ( Fetch ( ErrMsg ) );
{we want to drop the CRLF+'.'+CRLF} {Do not Localize}
LastCmdResult.SetReply(ErrorNo,ErrMsg);
LastCmdResult.RaiseReplyError;
end;
function TIdGopher.MenuItemFromString(stLine: String;
Menu: TIdGopherMenu): TIdGopherMenuItem;
begin
{just in case a space thows things off}
stLine := Trim(stLine);
if Assigned ( Menu ) then
begin
Result := Menu.Add;
end // if Assigned ( Menu ) then
else
begin
Result := TIdGopherMenuItem.Create( nil );
end; // else .. if Assigned ( Menu ) then
{title and Item Type}
Result.Title := Fetch ( stLine, TAB );
if Length ( Result.Title ) > 0 then
begin
Result.ItemType := Result.Title [ 1 ];
end //if Length.Result.Title > 0 then
else
begin
Result.ItemType := IdGopherItem_Error;
end; //else..if Length.Result.Title > 0 then
{drop first charactor because that was the item type indicator}
Result.Title := Copy ( Result.Title, 2, Length ( Result.Title ) );
{selector string}
Result.Selector := Fetch ( stLine, TAB );
{server}
Result.Server := Fetch ( stLine, TAB );
{port}
Result.Port := IndyStrToInt ( Fetch ( stLine, TAB ) );
{is Gopher + Item}
stLine := Fetch ( stLine, TAB );
Result.GopherPlusItem := ( (Length ( stLine) > 0 ) and
( stLine [ 1 ] = '+' ) ); {Do not Localize}
end;
Function TIdGopher.LoadExtendedDirectory ( PreviousData : String = ''; {Do not Localize}
const ExpectedLength: Integer = 0) : TIdGopherMenu;
var
stLine : String;
gmnu : TIdGopherMenuItem;
begin
BeginWork(wmRead, ExpectedLength); try
Result := TIdGopherMenu.Create;
gmnu := nil;
repeat
stLine := PreviousData + IOHandler.ReadLn;
{we use the Previous data only ONCE}
PreviousData := ''; {Do not Localize}
{we process each line only if it is not the last and the
OnMenuItem is assigned}
if ( stLine <> '.' ) then {Do not Localize}
begin
{This is a new Extended Gopher menu so lets start it}
if ( Copy (stLine, 1, Length ( IdGopherPlusInfo ) ) = IdGopherPlusInfo ) then
begin
{fire event for previous item}
if (gmnu <> nil) then
begin
gmnu.DoneSettingInfoBlock;
DoMenu ( gmnu );
end; //if (gmnu <> nil) then
gmnu := MenuItemFromString ( RightStr( stLine,
Length ( stLine ) - Length ( IdGopherPlusInfo ) ) , Result );
gmnu.GopherBlock.Add ( stLine);
end //if (Pos(IdGopherGPlusInfo, stLine) = 0) then
else
begin
if Assigned( gmnu ) and (stLine <> '') then {Do not Localize}
begin
gmnu.GopherBlock.Add ( stLine );
end;
end; //else...if (Pos(IdGopherGPlusInfo, stLine) = 0) then
end //if not stLine = '.' then {Do not Localize}
else
begin
{fire event for the last line}
if (gmnu <> nil) then
begin
DoMenu ( gmnu );
end; //if (gmnu <> nil) then
end; //if ( stLine <> '.' ) then {Do not Localize}
until (stLine = '.') or not Connected; {Do not Localize}
finally EndWork(wmRead); end;
end;
Function TIdGopher.ProcessDirectory ( PreviousData : String = ''; {Do not Localize}
const ExpectedLength: Integer = 0) : TIdGopherMenu;
var stLine : String;
begin
BeginWork(wmRead,ExpectedLength); try
Result := TIdGopherMenu.Create;
repeat
stLine := PreviousData + IOHandler.ReadLn;
{we use the Previous data only ONCE}
PreviousData := ''; {Do not Localize}
{we process each line only if it is not the last and the OnMenuItem
is assigned}
if ( stLine <> '.' ) then {Do not Localize}
begin
//add Gopher Menu item and fire event
DoMenu ( MenuItemFromString ( stLine, Result ) );
end; //if not stLine = '.' then {Do not Localize}
until (stLine = '.') or not Connected; {Do not Localize}
finally
EndWork(wmRead);
end; //try..finally
end;
procedure TIdGopher.ProcessTextFile(ADestStream : TStream; APreviousData: String = ''; {Do not Localize}
const ExpectedLength: Integer = 0);
var
LEnc: IIdTextEncoding;
begin
LEnc := IndyTextEncoding_8Bit;
WriteStringToStream(ADestStream, APreviousData, LEnc{$IFDEF STRING_IS_ANSI}, LEnc{$ENDIF});
BeginWork(wmRead,ExpectedLength);
try
IOHandler.Capture(ADestStream, '.', True); {Do not Localize}
finally
EndWork(wmRead);
end; //try..finally
end;
procedure TIdGopher.ProcessFile ( ADestStream : TStream; APreviousData : String = ''; {Do not Localize}
const ExpectedLength : Integer = 0);
var
LEnc: IIdTextEncoding;
begin
BeginWork(wmRead,ExpectedLength);
try
LEnc := IndyTextEncoding_8Bit;
WriteStringToStream(ADestStream, APreviousData, LEnc{$IFDEF STRING_IS_ANSI}, LEnc{$ENDIF});
IOHandler.ReadStream(ADestStream, -1, True);
ADestStream.Position := 0;
finally
EndWork(wmRead);
end;
end;
Function TIdGopher.Search(ASelector, AQuery : String) : TIdGopherMenu;
begin
Connect;
try
{Gopher does not give a greating}
IOHandler.WriteLn ( ASelector + TAB + AQuery );
Result := ProcessDirectory;
finally
Disconnect;
end; {try .. finally .. end }
end;
procedure TIdGopher.GetFile (ASelector : String; ADestStream : TStream;
IsGopherPlus : Boolean = False;
AView: String = ''); {Do not Localize}
var
Reply : Char;
LengthBytes : Integer; {legnth of the gopher items}
begin
Connect;
try
if not IsGopherPlus then
begin
IOHandler.WriteLn ( ASelector );
ProcessFile ( ADestStream );
end // if not IsGopherPlus then
else
begin
{I hope that this drops the size attribute and that this will cause the
Views to work, I'm not sure} {Do not Localize}
AView := Trim ( Fetch ( AView, ':' ) ); {Do not Localize}
IOHandler.WriteLn ( ASelector + TAB +'+'+ AView ); {Do not Localize}
{We read only one byte from the peer}
Reply := Char(IOHandler.ReadByte);
{Get the additonal reply code for error or success}
case Reply of
'-' : begin {Do not Localize}
{Get the length byte}
IOHandler.ReadLn;
ProcessGopherError;
end; {-}
{success - read file}
'+' : begin {Do not Localize}
{Get the length byte}
LengthBytes := IndyStrToInt ( IOHandler.ReadLn );
case LengthBytes of
{dot terminated - probably a text file}
-1 : ProcessTextFile ( ADestStream );
{just read until I disconnect you}
-2 : ProcessFile ( ADestStream );
else
ProcessFile ( ADestStream, '', LengthBytes); {Do not Localize}
end; //case LengthBytes of
end; {+}
else
begin
ProcessFile ( ADestStream, Reply );
end; //else ..case Reply of
end; //case Reply of
end; //else..if IsGopherPlus then
finally
Disconnect;
end; {try .. finally .. end }
end;
function TIdGopher.GetMenu ( ASelector : String; IsGopherPlus : Boolean = False; AView : String = '' ) : {Do not Localize}
TIdGopherMenu;
var
Reply : Char;
LengthBytes : Integer; {legnth of the gopher items}
begin
Result := nil;
Connect;
try
if not IsGopherPlus then
begin
IOHandler.WriteLn ( ASelector );
Result := ProcessDirectory;
end // if not IsGopherPlus then
else
begin
{Gopher does not give a greating}
IOHandler.WriteLn ( ASelector + TAB+'+' + AView ); {Do not Localize}
{We read only one byte from the peer}
Reply := Char(IOHandler.ReadByte);
{Get the additonal reply code for error or success}
case Reply of
'-' : begin {Do not Localize}
IOHandler.ReadLn;
ProcessGopherError;
end; {-}
'+' : begin {Do not Localize}
{Get the length byte}
LengthBytes := IndyStrToInt ( IOHandler.ReadLn );
Result := ProcessDirectory ('', LengthBytes ); {Do not Localize}
end; {+}
else
begin
Result := ProcessDirectory ( Reply );
end; //else..case Reply of
end; //case Reply of
end; //if not IsGopherPlus then
finally
Disconnect;
end; {try .. finally .. end }
end;
Function TIdGopher.GetExtendedMenu(ASelector, AView: String) : TIdGopherMenu;
var
Reply : Char;
LengthBytes : Integer; {legnth of the gopher items}
begin
Result := nil;
Connect; try
{Gopher does not give a greating}
IOHandler.WriteLn(ASelector + TAB + '$' + AView); {Do not Localize}
{We read only one byte from the peer}
Reply := Char(IOHandler.ReadByte);
{Get the additonal reply code for error or success}
case Reply of
'-' : begin {Do not Localize}
IOHandler.ReadLn;
ProcessGopherError;
end; {-}
'+' : begin {Do not Localize}
{Get the length byte}
LengthBytes := IndyStrToInt ( IOHandler.ReadLn );
Result := LoadExtendedDirectory( '', LengthBytes); {Do not Localize}
end; {+}
else
Result := ProcessDirectory ( Reply );
end; //case Reply of
finally
Disconnect;
end; {try .. finally .. end }
end;
procedure TIdGopher.GetTextFile(ASelector: String; ADestStream: TStream;
IsGopherPlus: Boolean; AView: String);
var
Reply : Char;
LengthBytes : Integer; {length of the gopher items}
begin
Connect;
try
if not IsGopherPlus then
begin
IOHandler.WriteLn ( ASelector );
ProcessTextFile ( ADestStream );
end // if not IsGopherPlus then
else
begin
{I hope that this drops the size attribute and that this will cause the
Views to work, I'm not sure} {Do not Localize}
AView := Trim ( Fetch ( AView, ':' ) ); {Do not Localize}
IOHandler.WriteLn ( ASelector + TAB +'+'+ AView ); {Do not Localize}
{We read only one byte from the peer}
Reply := Char(IOHandler.ReadByte);
{Get the additonal reply code for error or success}
case Reply of
'-' : begin {Do not Localize}
{Get the length byte}
IOHandler.ReadLn;
ProcessGopherError;
end; {-}
{success - read file}
'+' : begin {Do not Localize}
{Get the length byte}
LengthBytes := IndyStrToInt ( IOHandler.ReadLn );
case LengthBytes of
{dot terminated - probably a text file}
-1 : ProcessTextFile ( ADestStream );
{just read until I disconnect you}
-2 : ProcessFile ( ADestStream );
else
ProcessTextFile ( ADestStream, '', LengthBytes); {Do not Localize}
end; //case LengthBytes of
end; {+}
else
begin
ProcessTextFile ( ADestStream, Reply );
end; //else ..case Reply of
end; //case Reply of
end; //else..if IsGopherPlus then
finally
Disconnect;
end; {try .. finally .. end }
end;
{ TIdGopherMenu }
function TIdGopherMenu.Add: TIdGopherMenuItem;
begin
Result := TIdGopherMenuItem ( inherited Add );
end;
constructor TIdGopherMenu.Create;
begin
inherited Create ( TIdGopherMenuItem );
end;
function TIdGopherMenu.GetItem(Index: Integer): TIdGopherMenuItem;
begin
result := TIdGopherMenuItem( inherited Items [ index ] );
end;
procedure TIdGopherMenu.SetItem( Index: Integer;
const Value: TIdGopherMenuItem );
begin
inherited SetItem ( Index, Value );
end;
{ TIdGopherMenuItem }
constructor TIdGopherMenuItem.Create(ACollection: TCollection);
begin
inherited Create(ACollection);
FGopherBlock := TIdHeaderList.Create(QuotePlain);
{we don't unfold or fold lines as headers in that block start with a space} {Do not Localize}
FGopherBlock.UnfoldLines := False;
FGopherBlock.FoldLines := False;
FViews := TStringList.Create;
FAbstract := TStringList.Create;
FAsk := TIdHeaderList.Create(QuotePlain);
fAdminEmail := TIdEMailAddressItem.Create ( nil );
end;
destructor TIdGopherMenuItem.Destroy;
begin
FreeAndNil ( fAdminEmail );
FreeAndNil ( FAsk );
FreeAndNil ( FAbstract );
FreeAndNil ( FGopherBlock );
FreeAndNil ( FViews );
inherited Destroy;
end;
procedure TIdGopherMenuItem.DoneSettingInfoBlock;
{These constants are for blocks we wish to obtain - don't change as they are
part of Gopher+ protocol}
const
BlockTypes : Array [1..3] of String = ('+VIEWS', '+ABSTRACT', '+ASK'); {Do not Localize}
var
idx : Integer;
line : String;
Procedure ParseBlock ( Block : TStrings);
{Put our the sublock in the Block TIdStrings and increment
the pointer appropriatriately}
begin
Inc ( idx );
while ( idx < FGopherBlock.Count ) and
( FGopherBlock [ idx ] [ 1 ] = ' ' ) do {Do not Localize}
begin
Block.Add ( TrimLeft ( FGopherBlock [ idx ] ) );
Inc ( idx );
end; //while
{correct for incrementation in the main while loop}
Dec ( idx );
end;
begin
idx := 0;
while ( idx < FGopherBlock.Count ) do
begin
Line := FGopherBlock [ idx ];
Line := Fetch( Line, ':' ); {Do not Localize}
case PosInStrArray ( Line, BlockTypes, False ) of
{+VIEWS:}
0 : ParseBlock ( FViews );
{+ABSTRACT:}
1 : ParseBlock ( FAbstract );
{+ASK:}
2 : ParseBlock ( FAsk );
end;
Inc ( idx );
end;
fAdminEmail.Text := FGopherBlock.Values [ ' Admin' ]; {Do not Localize}
end;
function TIdGopherMenuItem.GetGeog: String;
begin
Result := FGopherBlock.Values [ ' Geog' ]; {Do not Localize}
end;
function TIdGopherMenuItem.GetLastModified: String;
begin
Result := FGopherBlock.Values [ ' Mod-Date' ]; {Do not Localize}
end;
function TIdGopherMenuItem.GetLocation: String;
begin
Result := FGopherBlock.Values [ ' Loc' ]; {Do not Localize}
end;
function TIdGopherMenuItem.GetOrganization: String;
begin
Result := FGopherBlock.Values [ ' Org' ]; {Do not Localize}
end;
end.