Fpcsession
Fpcsession
Abstract
In this second article about Lazarus web programming, the support for sessions in
Lazarus web modules is investigated.
1 Introduction
Most websites these days have some kind of session support: Either implicitly (to see how
a user navigates through a site) or explicitly: requiring a user to log in to be able to navigate
through certain parts of the website. The FPC/Lazarus web components provide support
for simple session management using cookies which are sent to the browser: Values can
be registered in a session, and they will be loaded when an active session is detected. The
session variables are by default stored in ini files, but they can be stored in any desired way:
it will be shown how to create a custom session storage using SQLDB: The database for
session data will be a Firebird database, but this could be any other database.
In this article, it will be assumed that the weblaz package (introduced in the previous article)
is installed in the Lazarus IDE, and that SQLDB is installed as well (this should be so by
default as of Lazarus 0.9.14).
2 Sessions
Support for sessions is restricted to saving and loading a set of variables between client
requests. To keep the interface simple, these variables are stored and retrieved as strings,
and they are identified by strings. The sessions work through cookies, so the web-client (a
browser) should have cookie supporte enabled, or the sessions will not work.
Sessions are defined in httpdefs, in the TCustomSession class. The methods and prop-
erties to manage the session are the following:
TCustomSession = Class(TComponent)
Public
Procedure Terminate;
Property TimeOutMinutes : Integer;
Property SessionID : String;
Property Variables[VarName : String] : String;
end;
Terminate Terminates the session. For instance on websites where the user is reauired to
log in, this method can be called to invalidate the session when the user logs out.
1
TimeOutMinutes This property can be used to specify a timeout. The timeout can be
used to automatically invalidate a session.
SessionID This is the unique ID that is used to identify a session. This ID is generated
automatically by the session component. By default, this will be a GUID.
Variables This array property can be used to store and retrieve session variables. All
variables will be saved at the end of the client request, and the stored variables will
be retrieved when the session is re-opened.
TSessionHTTPModule = Class(TCustomHTTPModule)
Property CreateSession : Boolean;
Property Session : TCustomSession;
Property OnNewSession : TNotifyEvent;
Property OnSessionExpired : TNotifyEvent;
OnNewSession This event is triggered when a new session is started: it can be used to set
certain variables, modify the default actions, and so on.
OnSessionExpired This event is triggered when the timeout period of the session was
reached. It can be used to display custom pages.
Sessions are initialized before the actual request is handled. First the session is initialized
(which can trigger one of the 2 events described above) and only then the client request is
handled.
To demonstrate this, a small project can be created. It stores a single value. This is not
much, but shows how the session support works. The project is created using ’New’ and
then ’CGI Application’. A new project (name it cgisession) and a new module (name it
sessionmodule) are created. The CreateSession property of the webmodule should be
set to True
To the module, 3 actions are added:
HaveSession This action will be executed if a session is active. This is the default action
for the webmodule.
2
TSessionModule.SessionModuleNewSession(Sender: TObject);
begin
Actions[1].Default:=False;
Actions[0].Default:=True;
end;
This makes the SessionStarted action the default action if a new session was started.
This works because the session is started before the request is handled.
The following code is placed in the OnRequest handler of the SessionStarted ac-
tion.
Var
C : TCookie;
begin
With AResponse.Contents do
begin
Add(’<HTML><TITLE>Session started</TITLE><BODY>’);
Add(’<H1>Session started</H1>’);
Add(’A session was started.’);
If Session is TFPWebSession then
begin
C:=AResponse.Cookies.FindCookie((Session as TFPWebSession).SessionCookie);
If Assigned(C) then
begin
Add(’Session Cookie is ’);
Add(’called <B>’+C.Name+’</B> ’);
Add(’and has value <B>’+C.Value+’</B>.’);
end
else
begin
Add(’Session cookie was not yet set.’);
end;
end;
Add(’</BODY></HTML>’);
end;
AResponse.SendContent;
Handled:=True;
end;
The above code simply shows the session cookie value if the session is of type TFPWebSession.
By entering the URL
http://localhost/~michael/cgisession.cgi
In the browser, the page as shown in figure 1 on page 4 is sent to the browser.
Secondly, in the OnRequest handler of the HaveSession action, the following code is
placed:
3
Figure 1: Start of a session
Var
C : TCookie;
V : String;
WS : TFPWebSession;
begin
With AResponse.Contents do
begin
Add(’<HTML><TITLE>In Session</TITLE><BODY>’);
Add(’<H1>In Session</H1>’);
Add(’The session is still active’);
if Session is TFPWebSession then
begin
WS:=Session as TFPWebSession
C:=AResponse.Cookies.FindCookie(WS.SessionCookie);
If Assigned(C) then
begin
Add(’Sending session cookie. Session Cookie is ’);
Add(’called <B>’+C.Name+’</B> ’);
Add(’and has value <B>’+C.Value+’</B>.’);
end;
end;
V:=Session.Variables[’Value’];
If (V<>’’) then
Add(’<P>Stored session value: <B>’+V+’</B>.’)
else
Add(’<P>No values stored in session.’);
V:=ARequest.QueryFields.Values[’Value’];
If V<>’’ then
begin
Add(’<P>Storing new session value: <B>’+V+’</B>.’);
Session.Variables[’Value’]:=V;
4
Figure 2: Active session, no variables in session
end;
end;
Add(’</BODY></HTML>’);
end;
end;
The above again shows the session cookie, and displays the value of the session variable
Value, if it exists.
If a variable named Value is passed on to the CGI application by the browser, then it’s
value is stored in the session.
If the previous URL is invoked a second time, the result of the above code is page as shown
in figure 2 on page 5.
By entering the URL
http://localhost/~michael/cgisession.cgi?Value=ABC
The variable Value is set to the value ABC and the page depicted in figure 3 on page 6 is
sent to the browser.
Changing the URL to
http://localhost/~michael/cgisession.cgi?Value=ABC2
will change the value to ABC2 and will send the page as shown in figure 4 on page 6 to the
browser.
Finally, entering the following code in the OnRequest handler of the Terminate action
will terminat the session:
5
Figure 3: Active session, setting variable Value
6
Figure 5: Terminating the session
Add(’<HTML><TITLE>Session terminated</TITLE><BODY>’);
Add(’<H1>Session Terminated</H1>’);
Add(’The session was terminated’);
Add(’</BODY></HTML>’);
end;
end;
http://localhost/~michael/cgisession.cgi/terminate
Cached if False, then after a variable is set, the whole .ini file is written to disk. This
hurts performance, but avoids data loss.
SessionCookie The name of the cookie to be used when managing the session. By default,
this is FPWebSession.
SessionDir The name of the directory where the .ini files are written. By default this is
the temporary directory (actually, the value of the GlobalSessionDir variable).
Obviously, this directory should be writable by the CGI process.
Now, many CGI applications will want to use a database. Since the database is present
anyway, it is probably a good idea to store the session data in the database as well.
7
For this a TSQLDBSession component will be developed. This component can be used
to store the data in a database. For the demo application, a Firebird database was used. In
the database, 2 tables are used:
SESSIONS This table keeps the list of active sessions. It also keeps the session parame-
ters, the start time, last seen time and the timeout. It can be created with the following
SQL Statement:
The SE_SESSION field is used to store the session ID (a GUID). The SE_STARTED
field can be used for determining when the session started. The SE_LASTSEEN field
keeps the time of the last client request. SE_TIMEOUT field keeps the timeout in
minutes. The SE_ID field is an autonumber field, which will be filled by a trigger.
Using an integer field as the primary key keeps the indexes smaller and faster than
when the SE_SESSION field was used for the primary kay.
SESSIONDATA This table keeps the actual data. It can be created with the following
SQL statement:
The meaning of the fields should be obvious. The reference to the SESSIONS table
is cascading, meaning that if the session is deleted from the SESSIONS table, all
data associated with it is deleted too.
Now that the structure of the database is clear, the component can be coded.
The TCustomSession class has 5 abstract methods that must be overridden, they are
shown below (there are other methods, but these are not shown. The reader is referred to
the source files on the CD accompagnying this issue):
TSQLDBSession = Class(TCustomSession)
Public
Function GetSessionVariable(VarName : String) : String;
procedure SetSessionVariable(VarName : String;
const AValue: String);
Procedure UpdateResponse(AResponse : TResponse);
Procedure InitSession(ARequest : TRequest;
8
OnNewSession, OnExpired: TNotifyEvent);
Procedure InitResponse(AResponse : TResponse);
Procedure Terminate;
Published
Property Connection : TSQLConnection Read FConnection Write SetConnection;
Property SessionCookie : String Read FSessionCookie Write FSessionCookie;
end;
The 2 properties are the SQLDB connection component reference, and the SessionCookie
wich is the name of the cookie to be used when managing the session.
The methods to be overridden are the following:
For faster operation, the session variables will be stored in a stringlist, as name=value pairs.
When the session is initialized, the variables are loaded from the database. When the list is
updated, then the added variables will be stored in the database as well.
The first method that will be called is the InitSession method. It’s coded as follows:
Var
L : TDateTime;
T : Integer;
S : String;
begin
CheckSession;
S:=ARequest.CookieFields.Values[SessionCookie];
If (S<>’’) and GetSessionData(S,L,T) then
begin
If ((Now-L)>(T/(24*60))) then
9
begin
If Assigned(OnExpired) then
OnExpired(Self);
DeleteSession(S);
S:=’’;
end
else
SID:=S;
end;
If (S=’’) then
begin
If Assigned(OnNewSession) then
OnNewSession(Self);
GetSessionID;
CreateSession(SID);
FVariables.Clear;
end
else
begin
UpdateSessionTime(SID);
LoadSessionData(SID,FVariables);
end;
FSessionStarted:=True;
end;
The CheckSession method will see if a database connection is present, and will initial-
ize the SessionCookie property, if it was not set. After this the session data is retrieved from
the database: the GetSessionData will return True if data for the session was found.
If so, the data is checked: the variable L will contain the last seen time. The variable T
contains the timeout of the session. If the session timed out, then the OnExpired handler
is called, and the session is deleted from the database (in DeleteSession)
If at this point, there is no active session, a session is started: a session ID is created (using
GetSessionID), and the session is created in the databas (using CreateSession).
The list of session variables (in FVariables, a TStrings instance) is cleared.
If there is an active session, the last session time is set in UpdateSessionTime. The call
to LoadSessionData will load the list of variables with values found in the database.
The InitResponse call will be called after the session is started. It’s a very simple
method, all it does is send a cookie with the session ID:
10
C.Value:=’’;
end;
end;
Since this is called after the request was handled, all variables will be in the database at this
point.
When a variable is requested, it is simply retrieved from the stringlist:
function TSQLDBSession.GetSessionVariable(
VarName: String): String;
begin
CheckSession;
Result:=FVariables.Values[VarName];
end;
When a variable needs to be set, it is either inserted in the database or it’s value is updated,
using the InsertVariable or UpdateVariable calls.
Var
I : Integer;
begin
CheckSession;
I:=FVariables.IndexOfName(VarName);
If (I=-1) then
begin
if (AValue<>’’) then
InsertVariable(SID,VarName,AValue)
end
else
UpdateVariable(SID,VarName,AValue);
FVariables.Values[VarName]:=AValue;
end;
This mechanism ensures that a minimal number of queries is run on the database.
When the session is terminated, the Terminate method is called:
procedure TSQLDBSession.Terminate;
begin
FTerminated:=True;
11
FSessionStarted:=False;
DeleteSession(SID);
SID:=’’;
FVariables.Clear;
end;
The DeleteSession method deletes all session data from the database.
The session data is loaded from the database in the GetSessionData method, which
simply runs a query (the exact SQL statement can be found in the SGetSessionData
constant) on the database:
begin
With FQuery do
begin
SQL.Text:=Format(SGetSessionData,[ASID]);
Open;
Try
Result:=Not (EOF AND BOF);
if Result then
begin
LastSeen:=FieldByName(’SE_LASTSEEN’).AsDateTime;
ATimeOut:=FieldByName(’SE_TIMEOUT’).AsInteger;
end;
Finally
Close;
end;
end;
end;
The FQuery object (of class TSQLQuery) is created when the session object is created. It
is configured in the CheckSession method:
procedure TSQLDBSession.CheckSession;
begin
If Not Assigned(FConnection) then
raise ESession.CreateFmt(SErrNoConnection,[Name]);
If (SessionCookie=’’) then
SessionCookie:=SFPWebSession;
FConnection.Connected:=True;
If FQuery.Database<>FConnection then
begin
FQuery.Database:=FConnection;
FTransaction.Database:=FConnection;
end;
end;
This method is called when a session is started. It simply sets up the query and transaction
component for use with the connection.
The session variables are loaded in the LoadSessionData method:
12
Procedure TSQLDBSession.LoadSessionData(Const ASID : String;
List : TStrings);
Var
FN,FV : TField;
begin
List.Clear;
With FQuery do
begin
SQL.Text:=Format(SGetVariables,[ASID]);
Open;
Try
FN:=Fields.FieldByName(’SD_NAME’);
FV:=Fields.FieldByName(’SD_VALUE’);
While not EOF do
begin
List.Add(FN.AsString+’=’+FV.AsString);
Next;
end;
Finally
Close;
end;
end;
end;
Var
S :TSQLDBSession;
begin
S:=TSQLDBSession.Create(Self);
S.Connection:=FPCDB;
Session:=S;
end;
13
4 Conclusion
In this second article about web programming, a non-visual aspect of web-programming
was examined: Session management, or keeping the state of the client who is browsing a
website. It was shown that the web support of FPC/Lazarus supports sessions by default,
and that with a little effort, sessions can be managed using a database as the back-end.
No fancy output was created in this article: this is the subject of the next articles about
web-programming, where the support for creating HTML will be examined.
14