pas2js/demo/webwidget/nativedesign/webideintf.pp
2021-12-21 13:24:23 +00:00

818 lines
19 KiB
ObjectPascal

unit webideintf;
{$mode objfpc}{$H+}
interface
uses
fpMimeTypes, Classes, SysUtils, StrUtils, httpdefs, fphttpclient,custhttpapp, fpjson, jsonparser, httproute;
Const
SFilesURL = '/Project/';
SIDEURL = '/IDE/';
Type
TClientObject = Class(TObject)
Private
FID: Int64;
public
Procedure FromJSON(aJSON : TJSONObject); virtual; abstract;
Procedure ToJSON(aJSON : TJSONObject); virtual; abstract;
Property ID : Int64 Read FID Write FID;
end;
{ TIDEClient }
TIDEClient = Class(TClientObject)
private
FURL: String;
Public
Procedure FromJSON(aJSON : TJSONObject); override;
Procedure ToJSON(aJSON : TJSONObject); override;
Property URL : String Read FURL Write FURL;
end;
{ TIDEExchange }
TIDEExchange = Class(TClientObject)
private
FClientID: Int64;
FName: String;
FPayLoad: TJSONData;
Public
Destructor Destroy; override;
Procedure FromJSON(aJSON : TJSONObject); override;
Procedure ToJSON(aJSON : TJSONObject); override;
Property ClientID : Int64 Read FClientID Write FClientID;
Property Name : String Read FName Write FName;
Property PayLoad : TJSONData Read FPayLoad Write FPayLoad;
end;
TIDEAction = Class(TIDEExchange)
end;
{ TClientObjectList }
TClientObjectList = Class(TThreadList)
Public
Function FindID(aID : int64) : TClientObject;
end;
{ TIDECommand }
TIDECommand = Class(TIDEExchange)
private
FConfirmed: Boolean;
FNeedsConfirmation: Boolean;
FSent: Boolean;
Public
Property NeedsConfirmation : Boolean Read FNeedsConfirmation Write FNeedsConfirmation;
Property Sent : Boolean Read FSent Write FSent;
Property Confirmed : Boolean Read FConfirmed Write FConfirmed;
end;
{ TIDEThread }
TIDEThread = Class(TThread)
Private
FHandler : TFPHTTPServerHandler;
FExceptionClass : String;
FExceptionMessage : String;
Public
Constructor Create(aHandler : TFPHTTPServerHandler);
Procedure Execute; override;
end;
TIDENotification = Procedure(Sender : TObject; aExchange : TIDEExchange) of object;
TIDEClientNotification = Procedure(Sender : TObject; aClient : TIDEClient) of object;
TIDERequestNotification = Procedure(Sender : TObject; aURL : String) of object;
{ TIDEServer }
TIDEServer = Class(TComponent)
private
FOnRequest: TIDERequestNotification;
FQuitting : Boolean;
FClients,
FCommands,
FActions : TClientObjectList;
FIDCounter: Int64;
FOnAction: TIDENotification;
FOnClient: TIDEClientNotification;
FOnClientRemoved: TIDEClientNotification;
FOnConfirmCommand: TIDENotification;
FProjectDir: String;
FWebHandler : TFPHTTPServerHandler;
FThread : TIDEThread;
FLastAction : TIDEAction;
FLastCommand : TIDECommand;
FLastClient : TIDEClient;
function CheckClient(aRequest: TRequest): INt64;
procedure DeActivatedThread(Sender: TObject);
function Do404(is404: boolean; aResponse: TResponse): Boolean;
procedure DoEvent(aProc: TThreadMethod);
procedure DoQuit(ARequest: TRequest; AResponse: TResponse);
procedure DoRouteRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
function GetAction(Index : Integer): TIDEAction;
function GetActionCount: Integer;
function GetPort: Integer;
function GetActive: Boolean;
procedure SetActive(AValue: Boolean);
procedure SetPort(AValue: Integer);
procedure SetProjectDir(AValue: String);
Protected
procedure RegisterRoutes; virtual;
// HTTP request extraction
procedure GetClientObjectFromRequest(ARequest: TRequest; AObject: TClientObject);
function GetActionFromRequest(ARequest: TRequest): TIDEAction;
function GetCommandFromRequest(ARequest: TRequest): TIDECommand;
function GetClientFromRequest(ARequest: TRequest): TIDEClient;
function GetJSONFromRequest(ARequest: TRequest): TJSONObject;
// Sending responses
procedure SendClientObjectResponse(AObject: TClientObject; AResponse: TResponse);
Procedure SendJSONResponse(aJSON : TJSONObject; aResponse : TResponse);
// HTTP route handlers
procedure DoDeleteAction(ARequest: TRequest; AResponse: TResponse); virtual;
procedure DoDeleteClient(ARequest: TRequest; AResponse: TResponse); virtual;
procedure DoGetCommand(ARequest: TRequest; AResponse: TResponse);virtual;
procedure DoGetFile(ARequest: TRequest; AResponse: TResponse);virtual;
procedure DoPostAction(ARequest: TRequest; AResponse: TResponse);virtual;
procedure DoPostClient(ARequest: TRequest; AResponse: TResponse);virtual;
procedure DoPutCommand(ARequest: TRequest; AResponse: TResponse);virtual;
// Event handler synchronisation. Rework this to objects
Procedure DoOnAction;
Procedure DoOnConfirmCommand;
Procedure DoOnClientAdded;
Procedure DoOnClientRemoved;
Public
Constructor Create(aOwner : TComponent); override;
Destructor Destroy; override;
Function GetNextCounter : Int64;
// Public API to communicate with browser
Function SendCommand(aCommand : TIDECommand) : Int64;
Procedure GetClientActions(aClientID : Int64; aList : TFPList);
Function DeleteAction(aID: Int64; Const aClientID : Int64 = -1): Boolean;
// Public properties
Property ProjectDir : String Read FProjectDir Write SetProjectDir;
Property Port : Integer Read GetPort Write SetPort;
Property Active : Boolean read GetActive write SetActive;
Property ActionCount : Integer Read GetActionCount;
Property Action[Index : Integer] : TIDEAction Read GetAction;
// Events
Property OnRequest : TIDERequestNotification Read FOnRequest Write FOnRequest;
Property OnConfirmCommand : TIDENotification Read FOnConfirmCommand Write FOnConfirmCommand;
Property OnAction : TIDENotification Read FOnAction Write FOnAction;
Property OnClientAdded : TIDEClientNotification Read FOnClient Write FOnClient;
Property OnClientRemoved : TIDEClientNotification Read FOnClientRemoved Write FOnClientRemoved;
end;
implementation
{ TClientObjectList }
function TClientObjectList.FindID(aID: int64): TClientObject;
Var
L : TList;
I : integer;
begin
Result:=Nil;
L:=LockList;
try
I:=L.Count-1;
While (Result=Nil) and (I>=0) do
begin
Result:=TClientObject(L[i]);
if Result.ID<>aID then
Result:=nil;
Dec(I);
end;
finally
UnlockList;
end;
end;
{ TIDEClient }
procedure TIDEClient.FromJSON(aJSON: TJSONObject);
begin
FID:=aJSON.Get('id',Int64(-1));
FURL:=aJSON.Get('url','');
end;
procedure TIDEClient.ToJSON(aJSON: TJSONObject);
begin
aJSON.Add('id',ID);
aJSON.Add('url',url);
end;
{ TIDEExchange }
destructor TIDEExchange.Destroy;
begin
FreeAndNil(FPayload);
Inherited;
end;
procedure TIDEExchange.FromJSON(aJSON: TJSONObject);
Var
P : TJSONObject;
begin
ID:=aJSON.Get('id',Int64(0));
Name:=aJSON.Get('name','');
P:=aJSON.Get('payload',TJSONObject(Nil));
if Assigned(P) then
Payload:=aJSON.Extract('payload');
end;
procedure TIDEExchange.ToJSON(aJSON: TJSONObject);
begin
aJSON.Add('id',ID);
aJSON.Add('name',name);
if Assigned(Payload) then
aJSON.Add('payload',Payload.Clone);
end;
{ TIDEThread }
constructor TIDEThread.Create(aHandler: TFPHTTPServerHandler);
begin
FHandler:=AHandler;
FreeOnTerminate:=True;
Inherited Create(False);
end;
procedure TIDEThread.Execute;
begin
try
FHandler.Run;
FHandler:=nil;
except
On E : Exception do
begin
FExceptionClass:=E.ClassName;
FExceptionMessage:=E.Message;
end;
end;
end;
{ TIDEServer }
function TIDEServer.GetAction(Index : Integer): TIDEAction;
Var
L : TList;
begin
L:=FActions.LockList;
try
Result:=TIDEAction(L.Items[Index]);
finally
FActions.UnlockList;
end;
end;
procedure TIDEServer.DeActivatedThread(Sender: TObject);
begin
FThread:=Nil;
end;
function TIDEServer.GetActionCount: Integer;
Var
L : TList;
begin
L:=FActions.LockList;
try
Result:=L.Count;
finally
FActions.UnlockList;
end;
end;
function TIDEServer.GetActive: Boolean;
begin
Result:=Assigned(FThread);
end;
function TIDEServer.GetPort: Integer;
begin
Result:=FWebHandler.Port;
end;
procedure TIDEServer.SetActive(AValue: Boolean);
begin
if Active=AValue then Exit;
if AValue then
begin
FThread:=TIDEThread.Create(FWebHandler);
FThread.OnTerminate:=@DeActivatedThread;
end
else
begin
FWebHandler.Terminate; // will cause thread to stop.
try
// Send a Quit request just in case. Normally this should fail.
FQuitting:=True;
TFPHTTPClient.SimpleGet(Format('http://localhost:%d/Quit',[Port]));
except
FQuitting:=False;
end;
end;
end;
procedure TIDEServer.SetPort(AValue: Integer);
begin
FWebHandler.Port:=aValue;
end;
procedure TIDEServer.SetProjectDir(AValue: String);
begin
if FProjectDir=AValue then Exit;
FProjectDir:=IncludeTrailingPathDelimiter(AValue);
end;
procedure TIDEServer.DoOnAction;
begin
If Assigned(FOnAction) then
FonAction(Self,FLastAction);
FLastAction:=Nil;
end;
procedure TIDEServer.DoOnConfirmCommand;
begin
If Assigned(FOnAction) then
FonAction(Self,FLastCommand);
FLastCommand:=Nil;
end;
procedure TIDEServer.DoOnClientAdded;
begin
if Assigned(FOnClient) then
FOnClient(Self,FLastClient);
FLastClient:=Nil;
end;
procedure TIDEServer.DoOnClientRemoved;
begin
if Assigned(FOnClientRemoved) then
FOnClientRemoved(Self,FLastClient);
FLastClient:=Nil;
end;
procedure TIDEServer.DoGetCommand(ARequest: TRequest; AResponse: TResponse);
Var
L : TList;
I : integer;
J,C : TJSONObject;
A :TJSONArray;
Cmd : TIDECommand;
L2 : TFPList;
aClient : Int64;
begin
aClient:=CheckClient(aRequest);
J:=nil;
A:=nil;
L:=FCommands.LockList;
try
L2:=TFPList.Create;
J:=TJSONObject.Create;
A:=TJSONArray.Create;
J.Add('commands',A);
For I:=0 to L.Count-1 do
begin
CMD:=TIDECommand(L[i]);
if Not Cmd.Sent and (Cmd.ClientID=aClient) then
begin
C:=TJSONObject.Create;
Cmd.ToJSON(C);
A.Add(C);
L2.Add(CMD);
end;
end;
SendJSONResponse(J,aResponse);
// Remove sent from list
for I:=0 to L2.Count-1 do
begin
Cmd:=TIDECommand(L2[i]);
if Cmd.NeedsConfirmation then
Cmd.Sent:=True
else
begin
Cmd.Free;
L.Remove(Cmd);
end;
end;
finally
J.Free;
FCommands.UnLockList;
l2.Free;
end;
end;
procedure TIDEServer.DoPutCommand(ARequest: TRequest; AResponse: TResponse);
Var
cmd,oCmd : TIDECommand;
aID,aClient : Int64;
begin
aClient:=CheckClient(aRequest);
aID:=StrToIntDef(aRequest.RouteParams['ID'],-1);
cmd:=TIDECommand.Create;
try
GetClientObjectFromRequest(aRequest,Cmd);
cmd.ClientID:=aClient;
oCmd:=TIDECommand(FCommands.FindID(aID));
if Do404((oCmd=Nil) or (oCmd.ClientID<>aClient),aResponse) then
exit;
// Later on we can add more modifications
oCmd.Confirmed:=True;
aResponse.Code:=204;
aResponse.CodeText:='OK';
aResponse.SendResponse;
FLastCommand:=oCmd;
DoEvent(@DoOnConfirmCommand);
FCommands.Remove(oCmd);
Finally
cmd.Free;
end;
end;
procedure TIDEServer.DoQuit(ARequest: TRequest; AResponse: TResponse);
begin
if FQuitting then
aResponse.Code:=200
else
aResponse.Code:=401;
aResponse.SendResponse;
end;
procedure TIDEServer.DoRouteRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
begin
If Assigned(FonRequest) then
FOnRequest(Self,aRequest.URI);
end;
function TIDEServer.GetJSONFromRequest(ARequest: TRequest): TJSONObject;
var
D : TJSONData;
begin
if ARequest.ContentType<>'application/json' then
Raise Exception.Create('Not valid JSON payload: content type must be application/json');
D:=GetJSON(ARequest.Content);
if Not (D is TJSONObject) then
begin
FreeAndNil(D);
Raise EJSON.Create('Payload is valid JSON but not a JSON object');
end;
Result:=D as TJSONObject;
end;
procedure TIDEServer.SendJSONResponse(aJSON: TJSONObject; aResponse: TResponse);
Var
JS : TJSONStringType;
begin
JS:=aJSON.AsJSON;
aResponse.FreeContentStream:=True;
aResponse.ContentStream:=TMemoryStream.Create;
aResponse.ContentStream.WriteBuffer(JS[1],Length(JS));
aResponse.ContentLength:=Length(JS);
aResponse.ContentType:='application/json';
aResponse.SendResponse;
end;
procedure TIDEServer.GetClientObjectFromRequest(ARequest: TRequest; AObject: TClientObject);
Var
J : TJSONObject;
begin
J:=GetJSONFromRequest(aRequest);
try
AObject.FromJSON(J);
finally
J.Free;
end;
end;
procedure TIDEServer.SendClientObjectResponse(AObject: TClientObject; AResponse: TResponse);
Var
J : TJSONObject;
begin
J:=TJSONObject.Create;
try
aObject.ToJSON(J);
SendJSONResponse(J,aResponse);
finally
J.Free;
end;
end;
function TIDEServer.GetActionFromRequest(ARequest: TRequest): TIDEAction;
begin
Result:=TIDEAction.Create;
try
GetClientObjectFromRequest(aRequest,Result);
except
Result.Free;
raise;
end;
end;
function TIDEServer.GetCommandFromRequest(ARequest: TRequest): TIDECommand;
begin
Result:=TIDECommand.Create;
try
GetClientObjectFromRequest(aRequest,Result);
except
Result.Free;
Raise;
end;
end;
function TIDEServer.GetClientFromRequest(ARequest: TRequest): TIDEClient;
begin
Result:=TIDEClient.Create;
try
GetClientObjectFromRequest(aRequest,Result);
except
Result.Free;
Raise;
end;
end;
procedure TIDEServer.DoPostAction(ARequest: TRequest; AResponse: TResponse);
var
A : TIDEAction;
aId,aClient : Int64;
begin
aClient:=CheckClient(aRequest);
aID:=StrToInt64Def(aRequest.RouteParams['ID'],-1);
Try
A:=GetACtionFromRequest(aRequest);
A.ClientID:=aClient;
if A.ID=0 then
a.ID:=aID;
FActions.Add(A);
FLastAction:=A;
DoEvent(@DoOnAction);
AResponse.Code:=201;
AResponse.Codetext:='Created';
except
On E: Exception do
begin
AResponse.Code:=400;
AResponse.Codetext:='Invalid Param';
AResponse.Content:='Invalid data ('+E.ClassName+'): '+E.Message;
end;
end;
aResponse.SendResponse;
end;
function TIDEServer.CheckClient(aRequest: TRequest): INt64;
Var
S : String;
begin
S:=ARequest.RouteParams['Client'];
if (S='') then
Raise EJSON.Create('Missing client ID in request');
if Not TryStrToInt64(S,Result) then
Raise EJSON.CreateFmt('Invalid client ID: %s',[S]);
end;
procedure TIDEServer.DoDeleteAction(ARequest: TRequest; AResponse: TResponse);
var
SID : String;
ID,aClient : Int64;
begin
Try
aClient:=CheckClient(ARequest);
SID:=ARequest.RouteParams['ID'];
ID:=StrtoInt64Def(SID,-1);
if Do404((ID=-1) or not (DeleteAction(ID,aClient)),aResponse) then
exit;
AResponse.Code:=204;
AResponse.Codetext:='No content';
aResponse.SendResponse;
except
On E: Exception do
begin
AResponse.Code:=400;
AResponse.Codetext:='Invalid Param';
AResponse.Content:='Invalid data ('+E.ClassName+'): '+E.Message;
end;
end;
end;
procedure TIDEServer.DoGetFile(ARequest: TRequest; AResponse: TResponse);
Var
FN : String;
begin
FN:=ARequest.URL;
if AnsiStartsText(SFilesURL,FN) then
Delete(FN,1,Length(SFilesURL));
FN:=ExpandFileName(FProjectDir+FN);
if Pos('..',ExtractRelativepath(FProjectDir,FN))<>0 then
begin
aResponse.Code:=401;
aResponse.CodeText:='Forbidden';
aResponse.Content:='<H1>Forbidden</H1>';
end
else if Do404(Not FileExists(FN),aResponse) then
exit;
aResponse.FreeContentStream:=True;
aResponse.ContentStream:=TFileStream.Create(FN,fmOpenRead or fmShareDenyWrite);
aResponse.ContentLength:=aResponse.ContentStream.Size;
aResponse.ContentType:=MimeTypes.GetMimeType(ExtractFileExt(FN));
if aResponse.ContentType='' then
aResponse.ContentType:='text/html';
aResponse.SendResponse;
end;
constructor TIDEServer.Create(aOwner: TComponent);
begin
Inherited;
FProjectDir:=ExtractFilePath(Paramstr(0));
FActions:=TClientObjectList.Create;
FCommands:=TClientObjectList.Create;
FClients:=TClientObjectList.Create;
FWebHandler:=TFPHTTPServerHandler.Create(Self);
FWebHandler.Port:=8080;
RegisterRoutes;
end;
procedure TIDEServer.DoEvent(aProc : TThreadMethod);
begin
if Assigned(FThread) then
FThread.Synchronize(aProc)
else
aProc;
end;
procedure TIDEServer.DoPostClient(ARequest: TRequest; AResponse: TResponse);
Var
aClient : TIDEClient;
begin
aClient:=GetClientFromRequest(aRequest);
aClient.FID:=GetNextCounter;
FClients.Add(aClient);
SendClientObjectResponse(aClient,aResponse);
FLastClient:=aClient;
DoEvent(@DoOnClientAdded);
end;
function TIDEServer.Do404(is404: boolean; aResponse: TResponse): Boolean;
begin
Result:=is404;
if Result then
begin
aResponse.Code:=404;
aResponse.Codetext:='Not found';
aResponse.SendResponse;
end;
end;
procedure TIDEServer.DoDeleteClient(ARequest: TRequest; AResponse: TResponse);
Var
aClientID : Int64;
aClient : TIDEClient;
begin
aClientID:=CheckClient(aRequest);
aClient:=TIDEClient(FClients.FindID(aClientID));
if Do404(not Assigned(aClient),aResponse) then
exit;
FLastClient:=aClient;
DoEvent(@DoOnClientRemoved);
FClients.Remove(aClient);
end;
procedure TIDEServer.RegisterRoutes;
begin
// get command
HTTPRouter.RegisterRoute(SIDEURL+'Quit',rmGet,@DoQuit);
HTTPRouter.RegisterRoute(SIDEURL+'Client/',rmPost,@DoPostClient);
HTTPRouter.RegisterRoute(SIDEURL+'Client/:Client',rmDelete,@DoDeleteClient);
HTTPRouter.RegisterRoute(SIDEURL+'Command/:Client/',rmGet,@DoGetCommand);
// PUT command for confirm.
HTTPRouter.RegisterRoute(SIDEURL+'Command/:Client/:ID',rmPut,@DoPutCommand);
// POST action
HTTPRouter.RegisterRoute(SIDEURL+'Action/:Client/:ID',rmPost,@DoPostAction);
HTTPRouter.RegisterRoute(SIDEURL+'Action/:Client/:ID',rmDelete,@DoDeleteAction);
// GET file
HTTPRouter.RegisterRoute(SFilesURL+'*',rmGet,@DoGetFile,true);
HTTPRouter.BeforeRequest:=@DoRouteRequest;
end;
destructor TIDEServer.Destroy;
begin
Active:=False;
While Active do
Sleep(20);
FreeAndNil(FActions);
FreeAndNil(FCommands);
FreeAndNil(FClients);
inherited Destroy;
end;
function TIDEServer.GetNextCounter: Int64;
begin
Inc(FIDCounter);
Result:=FIDCounter;
end;
function TIDEServer.SendCommand(aCommand: TIDECommand): Int64;
begin
Result:=GetNextCounter;
aCommand.ID:=Result;
FCommands.Add(aCommand);
end;
function TIDEServer.DeleteAction(aID: Int64; const aClientID: Int64): Boolean;
Var
P : TIDEAction;
L : TList;
I : Integer;
begin
P:=nil;
L:=FActions.LockList;
try
I:=L.Count-1;
While (I>=0) and (P=Nil) do
begin
P:=TIDEAction(L[i]);
if P.ID<>AID then P:=Nil;
Dec(i)
end;
finally
L.Free;
end;
Result:=(P<>Nil) and ((aClientID=-1) or (P.ClientID=aClientID));
if Result then
FActions.Remove(P);
end;
procedure TIDEServer.GetClientActions(aClientID: Int64; aList: TFPList);
Var
P : TIDEAction;
L : TList;
I : Integer;
begin
P:=nil;
L:=FActions.LockList;
try
I:=L.Count-1;
While (I>=0) and (P=Nil) do
begin
P:=TIDEAction(L[i]);
if P.ClientID=aClientID then
begin
aList.Add(P);
L.Delete(I);
end;
Dec(i);
end;
finally
L.Free;
end;
end;
end.