{ This file is part of the Free Pascal fcl-web package Copyright (c) 1999-2022 by the Free Pascal development team Web page object See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} {$IFNDEF FPC_DOTTEDUNITS} unit WebPage; {$ENDIF FPC_DOTTEDUNITS} {$mode objfpc}{$H+} interface {$IFDEF FPC_DOTTEDUNITS} uses System.Classes, System.SysUtils, FpWeb.Html, Xml.HtmlElements, Html.Writer, FpWeb.Http.Defs, FpWeb.Base, System.Contnrs, Xml.Dom; {$ELSE FPC_DOTTEDUNITS} uses Classes, SysUtils, fphtml, htmlelements, htmlwriter, HTTPDefs, fpweb, contnrs, dom; {$ENDIF FPC_DOTTEDUNITS} type TRequestResponseEvent = procedure(Sender: TObject; ARequest: TRequest; AResponse: TResponse) of object; TRequestEvent = procedure(Sender: TObject; ARequest: TRequest) of object; THandleAjaxRequest = procedure(Sender: TObject; ARequest: TRequest; AnAjaxResponse: TAjaxResponse; var handled: boolean) of object; TAjaxRequestResponseEvent = procedure(Sender: TObject; ARequest: TRequest; AResponse: TAjaxResponse) of object; type { IWebPageDesigner } IWebPageDesigner = interface(IUnknown) ['{25629DEA-79D5-4165-A0A3-BE6E2BA74442}'] procedure Invalidate; end; { IHTMLDesignable } IHTMLDesignable = interface(IUnknown) ['{C75546D6-9C93-49F0-809F-D29C52CD306D}'] function GetDesigner: IWebPageDesigner; procedure SetDesigner(const AValue: IWebPageDesigner); property Designer: IWebPageDesigner read GetDesigner write SetDesigner; end; IHTMLIterationGroup = interface(IUnknown) ['{95575CB6-7D96-4F72-AF72-D2EAF0BECE71}'] procedure SetIDSuffix(const AHTMLContentProducer: THTMLContentProducer); procedure SetAjaxIterationID(AValue: String); end; { TStandardWebController } TStandardWebController = class(TWebController) private FScriptFileReferences: TStringList; FScripts: TFPObjectList; FStyleSheetReferences: TContainerStylesheets; protected function GetScriptFileReferences: TStringList; override; function GetScripts: TFPObjectList; override; function GetStyleSheetReferences: TContainerStylesheets; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; function CreateNewJavascriptStack(AJavaType: TJavaType): TJavaScriptStack; override; function GetUrl(ParamNames, ParamValues, KeepParams: array of string; const Action: string = ''): string; override; procedure BindJavascriptCallstackToElement(AComponent: TComponent; AnElement: THtmlCustomElement; const AnEvent: string); override; procedure AddScriptFileReference(const AScriptFile: String); override; procedure AddStylesheetReference(const Ahref, Amedia: String); override; function DefaultMessageBoxHandler(Sender: TObject; const AText: String; Buttons: TWebButtons; const ALoaded: string = ''): string; override; function CreateNewScript: TStringList; override; procedure ShowRegisteredScript(ScriptID: integer); override; procedure FreeScript(var AScript: TStringList); override; published property OnGetURL; end; { TWebPage } TWebPage = class(TDataModule, IHTMLContentProducerContainer, IHTMLDesignable) private FAfterAjaxRequest: TAjaxRequestResponseEvent; FBaseURL: string; FBeforeRequest: TRequestEvent; FBeforeShowPage: TRequestEvent; FDesigner: IWebPageDesigner; FOnAjaxRequest: THandleAjaxRequest; FRequest: TRequest; FWebController: TWebController; FWebModule: TFPWebModule; FContentProducers: TFPList; // list of THTMLContentProducer function GetContentProducer(Index: integer): THTMLContentProducer; function GetContentProducerList: TFPList; function GetContentProducers(Index: integer): THTMLContentProducer; function GetDesigner: IWebPageDesigner; function GetHasWebController: boolean; function GetWebController: TWebController; procedure SetDesigner(const AValue: IWebPageDesigner); protected procedure DoAfterAjaxRequest(ARequest: TRequest; AnAjaxResponse: TAjaxResponse); virtual; procedure DoHandleAjaxRequest(ARequest: TRequest; AnAjaxResponse: TAjaxResponse; var Handled: boolean); virtual; procedure DoBeforeRequest(ARequest: TRequest); virtual; procedure DoBeforeShowPage(ARequest: TRequest); virtual; procedure DoCleanupAfterRequest(const AContentProducer: THTMLContentProducer); procedure SetRequest(ARequest: TRequest); virtual; procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override; property ContentProducerList: TFPList read GetContentProducerList; public destructor Destroy; override; function ContentProducerCount: integer; function ProduceContent : string; procedure AddContentProducer(AContentProducer: THTMLContentProducer); procedure RemoveContentProducer(AContentProducer: THTMLContentProducer); function ExchangeContentProducers(Child1, Child2: THTMLContentProducer) : boolean; function MoveContentProducer(MoveElement, MoveBeforeElement: THTMLContentProducer) : boolean; procedure ForeachContentProducer(AForeachChildsProc: TForeachContentProducerProc; Recursive: boolean); function IsAjaxCall: boolean; virtual; procedure HandlePage(ARequest: TRequest; AResponse: TResponse; AWriter: THTMLwriter; AWebModule: TFPWebModule = nil); virtual; procedure DoBeforeGenerateXML; virtual; procedure CleanupAfterRequest; virtual; property Designer: IWebPageDesigner read GetDesigner write SetDesigner; property Request: TRequest read FRequest; property ContentProducers[Index: integer]: THTMLContentProducer read GetContentProducer; property HasWebController: boolean read GetHasWebController; property WebController: TWebController read GetWebController write FWebController; property WebModule: TFPWebModule read FWebModule; published property BeforeRequest: TRequestEvent read FBeforeRequest write FBeforeRequest; property BeforeShowPage: TRequestEvent read FBeforeShowPage write FBeforeShowPage; property AfterAjaxRequest: TAjaxRequestResponseEvent read FAfterAjaxRequest write FAfterAjaxRequest; property OnAjaxRequest: THandleAjaxRequest read FOnAjaxRequest write FOnAjaxRequest; property BaseURL: string read FBaseURL write FBaseURL; end; function RegisterScript(const AScript: string) : integer; implementation {$IFDEF FPC_DOTTEDUNITS} uses System.TypInfo, System.StrUtils; {$ELSE FPC_DOTTEDUNITS} uses typinfo, strutils; {$ENDIF FPC_DOTTEDUNITS} var RegisteredScriptList : TStrings; function RegisterScript(const AScript: string) : integer; begin if not Assigned(RegisteredScriptList) then begin RegisteredScriptList := TStringList.Create; end; result := RegisteredScriptList.Add(AScript); end; { TWebPage } function TWebPage.ProduceContent: string; var i : integer; begin result := ''; for i := 0 to ContentProducerCount-1 do result := result + THTMLContentProducer(ContentProducers[i]).ProduceContent; end; procedure TWebPage.AddContentProducer(AContentProducer: THTMLContentProducer); begin ContentProducerList.Add(AContentProducer); end; procedure TWebPage.RemoveContentProducer(AContentProducer: THTMLContentProducer); begin ContentProducerList.Remove(AContentProducer); end; function TWebPage.ExchangeContentProducers(Child1, Child2: THTMLContentProducer): boolean; var ChildIndex1, ChildIndex2: integer; begin result := false; ChildIndex1:=GetContentProducerList.IndexOf(Child1); if (ChildIndex1=-1) then Exit; ChildIndex2:=GetContentProducerList.IndexOf(Child2); if (ChildIndex2=-1) then Exit; GetContentProducerList.Exchange(ChildIndex1,ChildIndex2); result := true; end; function TWebPage.MoveContentProducer(MoveElement, MoveBeforeElement: THTMLContentProducer): boolean; var ChildIndex1, ChildIndex2: integer; begin result := false; ChildIndex1:=GetContentProducerList.IndexOf(MoveElement); if (ChildIndex1=-1) then Exit; ChildIndex2:=GetContentProducerList.IndexOf(MoveBeforeElement); if (ChildIndex2=-1) then Exit; GetContentProducerList.Move(ChildIndex1,ChildIndex2); result := true; end; procedure TWebPage.ForeachContentProducer(AForeachChildsProc: TForeachContentProducerProc; Recursive: boolean); var i : integer; tmpChild: THTMLContentProducer; begin for i := 0 to ContentProducerCount -1 do begin tmpChild := ContentProducers[i]; AForeachChildsProc(tmpChild); if recursive then tmpChild.ForeachContentProducer(AForeachChildsProc,Recursive); end; end; procedure TWebPage.HandlePage(ARequest: TRequest; AResponse: TResponse; AWriter: THTMLwriter; AWebModule: TFPWebModule=nil); var Handled: boolean; CompName: string; AComponent: TComponent; AnAjaxResponse: TAjaxResponse; i: integer; ASuffixID: string; AIterationGroup: IHTMLIterationGroup; AIterComp: TComponent; wc: TWebController; Iterationlevel: integer; procedure SetIdSuffixes(AComp: THTMLContentProducer); var i: integer; s: string; begin if assigned(AComp.parent) and (acomp.parent is THTMLContentProducer) then SetIdSuffixes(THTMLContentProducer(AComp.parent)); if supports(AComp,IHTMLIterationGroup,AIterationGroup) then begin if assigned(FWebController) then begin iterationlevel := FWebController.IncrementIterationLevel; assert(length(ASuffixID)>0); i := PosEx('_',ASuffixID,2); if i > 0 then s := copy(ASuffixID,2,i-2) else s := copy(ASuffixID,2,length(ASuffixID)-1); acomp.IDSuffix := s; AIterationGroup.SetAjaxIterationID(s); FWebController.SetIterationIDSuffix(iterationlevel,s); acomp.ForeachContentProducer(@AIterationGroup.SetIDSuffix,true); ASuffixID := copy(ASuffixID,i,length(ASuffixID)-i+1); end; end; end; begin SetRequest(ARequest); FWebModule := AWebModule; try try DoBeforeRequest(ARequest); if IsAjaxCall then begin AnAjaxResponse := TAjaxResponse.Create(GetWebController, AResponse); try try if HasWebController then WebController.InitializeAjaxRequest; Handled := false; DoHandleAjaxRequest(ARequest, AnAjaxResponse, Handled); if not Handled then begin CompName := Request.QueryFields.Values['AjaxID']; if CompName='' then CompName := Request.GetNextPathInfo; i := pos('$',CompName); AComponent:=self; while (i > 0) and (assigned(AComponent)) do begin AComponent := AComponent.FindComponent(copy(CompName,1,i-1)); CompName := copy(compname,i+1,length(compname)-i); i := pos('$',CompName); end; if assigned(AComponent) then AComponent := AComponent.FindComponent(CompName); if assigned(AComponent) and (AComponent is THTMLContentProducer) then begin // Handle the SuffixID, search for iteration-groups and set their iteration-id-values ASuffixID := ARequest.QueryFields.Values['IterationID']; if ASuffixID<>'' then begin SetIdSuffixes(THTMLContentProducer(AComponent)); webcontroller.ResetIterationLevel; end; THTMLContentProducer(AComponent).HandleAjaxRequest(ARequest, AnAjaxResponse); end; end; DoAfterAjaxRequest(ARequest, AnAjaxResponse); except on E: Exception do AnAjaxResponse.SetError(e.HelpContext, e.Message); end; AnAjaxResponse.BindToResponse; finally AnAjaxResponse.Free; end; end else begin if HasWebController then WebController.InitializeShowRequest; DoBeforeShowPage(ARequest); AResponse.Content := ProduceContent; if HasWebController then WebController.CleanupShowRequest; end; finally CleanupAfterRequest; end; finally SetRequest(nil); AWebModule := nil; end; end; procedure TWebPage.DoBeforeGenerateXML; begin // Do Nothing end; procedure TWebPage.CleanupAfterRequest; begin ForeachContentProducer(@DoCleanupAfterRequest, True); if HasWebController then WebController.CleanupAfterRequest; end; procedure TWebPage.DoCleanupAfterRequest(const AContentProducer: THTMLContentProducer); begin AContentProducer.CleanupAfterRequest; end; procedure TWebPage.SetRequest(ARequest: TRequest); begin FRequest := ARequest; end; procedure TWebPage.GetChildren(Proc: TGetChildProc; Root: TComponent); var i : integer; begin inherited GetChildren(Proc, Root); if (Root=Self) then for I:=0 to ContentProducerCount-1 do Proc(ContentProducers[i]); end; destructor TWebPage.Destroy; begin inherited Destroy; if assigned(FContentProducers) then FreeAndNil(FContentProducers); end; function TWebPage.ContentProducerCount: integer; begin if assigned(FContentProducers) then result := FContentProducers.Count else result := 0; end; function TWebPage.GetContentProducers(Index: integer): THTMLContentProducer; begin Result:=THTMLContentProducer(ContentProducerList[Index]); end; function TWebPage.GetDesigner: IWebPageDesigner; begin result := FDesigner; end; function TWebPage.GetHasWebController: boolean; begin result := assigned(FWebController); end; function TWebPage.GetWebController: TWebController; begin if not assigned(FWebController) then raise EHTTP.create('No webcontroller available'); result := FWebController; end; procedure TWebPage.SetDesigner(const AValue: IWebPageDesigner); begin FDesigner := AValue; end; function TWebPage.GetContentProducerList: TFPList; begin if not assigned(FContentProducers) then FContentProducers := tfplist.Create; Result := FContentProducers; end; function TWebPage.GetContentProducer(Index: integer): THTMLContentProducer; begin Result := THTMLContentProducer(ContentProducerList[Index]); end; procedure TWebPage.DoAfterAjaxRequest(ARequest: TRequest; AnAjaxResponse: TAjaxResponse); begin if assigned(AfterAjaxRequest) then AfterAjaxRequest(Self,ARequest,AnAjaxResponse); end; procedure TWebPage.DoHandleAjaxRequest(ARequest: TRequest; AnAjaxResponse: TAjaxResponse; var Handled: boolean); begin if assigned(OnAjaxRequest) then OnAjaxRequest(Self,ARequest,AnAjaxResponse, Handled); end; procedure TWebPage.DoBeforeRequest(ARequest: TRequest); begin if assigned(BeforeRequest) then BeforeRequest(Self,ARequest); end; procedure TWebPage.DoBeforeShowPage(ARequest: TRequest); begin if assigned(BeforeShowPage) then BeforeShowPage(Self,ARequest); end; function TWebPage.IsAjaxCall: boolean; var s : string; begin if assigned(request) then begin s := Request.HTTPXRequestedWith; result := sametext(s,'XmlHttpRequest'); end else result := false; end; { TStandardWebController } function TStandardWebController.GetScriptFileReferences: TStringList; begin Result:=FScriptFileReferences; end; function TStandardWebController.GetScripts: TFPObjectList; begin if not assigned(FScripts) then begin FScripts:=TFPObjectList.Create; FScripts.OwnsObjects:=true; end; Result:=FScripts; end; function TStandardWebController.GetStyleSheetReferences: TContainerStylesheets; begin Result:=FStyleSheetReferences; end; function TStandardWebController.CreateNewScript: TStringList; begin Result:=TStringList.Create; GetScripts.Add(result); end; procedure TStandardWebController.ShowRegisteredScript(ScriptID: integer); var i: Integer; s: string; begin s := '// ' + inttostr(ScriptID); for i := 0 to GetScripts.Count -1 do if tstrings(GetScripts.Items[i]).Strings[0]=s then Exit; with CreateNewScript do begin Append(s); Append(RegisteredScriptList.Strings[ScriptID]); end; end; procedure TStandardWebController.FreeScript(var AScript: TStringList); begin with GetScripts do GetScripts.Delete(IndexOf(AScript)); AScript := nil; end; function TStandardWebController.DefaultMessageBoxHandler(Sender: TObject; const AText: String; Buttons: TWebButtons; const ALoaded: string = ''): string; var i : integer; HasCancel: boolean; OnOk: string; OnCancel: string; begin HasCancel:=false; OnOk:=''; OnCancel:=''; for i := low(Buttons) to High(Buttons) do begin if Buttons[i].ButtonType=btOk then OnOk := Buttons[i].OnClick; if Buttons[i].ButtonType=btCancel then begin HasCancel := True; OnCancel := Buttons[i].OnClick; end; end; if HasCancel then result := 'if (confirm('''+AText+''')==true) {'+OnOk+'} else {'+OnCancel+'}' else result := 'alert('''+AText+''');'+OnOk; end; constructor TStandardWebController.Create(AOwner: TComponent); begin inherited Create(AOwner); FStyleSheetReferences := TContainerStylesheets.Create(TContainerStylesheet); FScriptFileReferences := TStringList.Create; // For some reason the Duplicates property does not work when sorted is true, // But we don't want a sorted list so do a manual check in AddScriptFileReference //FScriptFileReferences.Sorted:=true; FScriptFileReferences.Duplicates:=dupIgnore; end; destructor TStandardWebController.Destroy; begin FScriptFileReferences.Free; FScripts.Free; FStyleSheetReferences.Free; inherited Destroy; end; function TStandardWebController.CreateNewJavascriptStack(AJavaType: TJavaType): TJavaScriptStack; begin Result:=TJavaScriptStack.Create(self, AJavaType); end; function TStandardWebController.GetUrl(ParamNames, ParamValues, KeepParams: array of string; const Action: string): string; var qs,p : String; i,j : integer; found: boolean; FancyTitle: boolean; ConnectChar: char; CGIScriptName: string; ActionVar: string; ARequest: TRequest; WebMod: TFPWebModule; begin FancyTitle:=false; qs := ''; result := Action; ARequest := GetRequest; ActionVar := ''; if assigned(owner) then begin if (owner is TWebPage) then WebMod := TWebPage(Owner).WebModule else if (owner is TFPWebModule) then WebMod := TFPWebModule(Owner); if assigned(WebMod) then begin ActionVar := WebMod.ActionVar; if (action = '') and assigned(WebMod.Actions) and assigned(WebMod.Actions.CurrentAction) then result := WebMod.Actions.CurrentAction.Name; end; end; if ActionVar='' then FancyTitle:=true; if Assigned(ARequest) then begin if (high(KeepParams)>=0) and (KeepParams[0]='*') then begin for i := 0 to ARequest.QueryFields.Count-1 do begin p := ARequest.QueryFields.Names[i]; found := False; for j := 0 to high(ParamNames) do if sametext(ParamNames[j],p) then begin found := True; break; end; if not FancyTitle and SameText(ActionVar,p) then found := true; if not found then qs := qs + p + '=' + ARequest.QueryFields.ValueFromIndex[i] + '&'; end; end else for i := 0 to high(KeepParams) do begin p := ARequest.QueryFields.Values[KeepParams[i]]; if p <> '' then qs := qs + KeepParams[i] + '=' + p + '&'; end; end; for i := 0 to high(ParamNames) do qs := qs + ParamNames[i] + '=' + ParamValues[i] + '&'; ConnectChar:='?'; if ScriptName='' then CGIScriptName:='.' else begin CGIScriptName:=ScriptName; if pos('?',ScriptName)>0 then ConnectChar := '&'; end; if FancyTitle then // use ? or / result := CGIScriptName + '/' + Result else begin result := CGIScriptName + ConnectChar +ActionVar+'=' + Result; ConnectChar:='&'; end; p := copy(qs,1,length(qs)-1); if p <> '' then result := result + ConnectChar + p; if assigned(OnGetURL) then OnGetURL(ParamNames, ParamValues, KeepParams, Action, Result); end; procedure TStandardWebController.BindJavascriptCallstackToElement(AComponent: TComponent; AnElement: THtmlCustomElement; const AnEvent: string); begin if AnEvent='onclick' then (AnElement as THTMLAttrsElement).onclick:=CurrentJavaScriptStack.GetScript else if AnEvent='onchange' then if AnElement is THTML_input then (AnElement as THTML_input).onchange:=CurrentJavaScriptStack.GetScript; end; procedure TStandardWebController.AddScriptFileReference(const AScriptFile: String); begin if FScriptFileReferences.IndexOf(AScriptFile)=-1 then FScriptFileReferences.Add(AScriptFile); end; procedure TStandardWebController.AddStylesheetReference(Const Ahref, Amedia: String); begin with FStyleSheetReferences.Add do begin href:=Ahref; media:=Amedia; end; end; initialization RegisteredScriptList := nil; finalization if assigned(RegisteredScriptList) then RegisteredScriptList.Free; end.