-
Notifications
You must be signed in to change notification settings - Fork 4
PBExpress FileSession Modul
PureBasic besitzt leider kein schlankes elegantes Format zur Speicherung von Session-Informationen. JSON ist in PureBasic noch sehr komplex zu händeln, XML leidet unter erheblichen Overhead und in beiden Fällen sind die Parser schlicht zu mächtig und verbrauchen nicht unerheblich viel Performance. Bleiben noch die Preferences. Nur die haben das Problem, das diese nicht die Objekt-Abstraktion von PureBasic verwenden, weshalb nur eine Preference-Datei zur Zeit verarbeitet werden kann.
Kurzer Hand wurde nach einer Speicherstruktur gesucht, die zum einen schnell verarbeitet werden kann, einfach zu verwenden ist und für Listen eine zweite Dimension ermöglicht. Nach Möglichkeit sollten dafür Prozeduren eingesetzt werden, die PureBasic bereits mitbringt und in Form von Maschinencode kompiliert vorliegen und optimiert arbeiten. Außerdem muss das Parsen relativ sicher sein. Dafür wurde in der Session-Bibliothek eine abgewandelte Form der URL-Kodierten Key-Value Liste vorbereitet, die allerdings zur Sicherheit Trennzeichen für die Abschnitte verwendet, die durch URLEncode() auch wirklich mitkodiert werden.
Die Session-Dateien enthalten folgende Trennzeichen:
key>value ; Der Schlüssel "key" verweist (>) auf den Wert "value"
key>value+key2>value2 ; Das zweite Paar wird an die Session angehängt (+) (Das + dient aber dem Parser als trenner)
§liste>entry1|entry2|entry3 ; Der Paragraph (§) leitet eine Liste unter dem Schlüssel "liste" ein. Der Seperator (|) trennt die Listeneinträge
Beim schreiben von Werten in die Datei werden alle Werte durch URLEncode() kodiert. Dadurch können in selbige keine dieser Zeichen mehr vorkommen. Denn alle 4 Sonderzeichen werden von URLEncode() strikt kodiert:
§ wird zu %C2%A7
> wird zu %3E
| wird zu %7C
+ wird zu %2B
Da die Routinen die Struktur selbst vorbereiten und nur die Werte durch den Encoder schicken, bleiben diese Zeichen zum schnellen parsen sicher erhalten. Eine Session-Datei könnte so aussehen:
username>MMustermann+userid>5896+§warenkorb>ID12|ID321|ID5587
Wie man sehen kann, bleiben alle Informationen erhalten. Man kann auch kleine oder auch größere Texte ablegen:
reim>Das%20ist%20das%20Haus%20vom%20Nikolaus.%20Und%20auch%20wenn%20es%20einfach%20ist,%20der%20Wind%20ihm%20nicht%20die%20W%C3%A4nde%20frisst
Hier wurde im Schlüssel "reim" der Text
Das ist das Haus vom Nikolaus. Und auch wenn es einfach ist, der Wind ihm nicht die Wände frisst
gespeichert.
Alle Module des Frameworks liegen im ordner "framework". Das Einbinden des Moduls erfolgt auf die gleiche Art wie das Hauptmodul mit folgendem Aufruf:
IncludeFile "framework\pbefilesession.pb"
WICHTIG: Alle Module haben den Prefix "pbe" gefolgt vom Namen des Moduls.
Die Konfiguration ist sehr einfach. PBEFileSession besitzt nur eine Prozedur, mit der man den Speicherpfad für Sessions setzen kann. Mit PBEFilseSession::SetSessionDirectory("PFAD") kann man Global den Pfad zum Ordner angeben. Will man den Server auf Linux und Windows einsetzen, empfiehlt es sich, die Kosntante #PATH_SEPERATOR anstatt "/" oder "" zu verwenden.
Define.s PfadTux = GetCurrentDirectory()+"/sessions" ; Kann auf Windows-Servern zu Fehlern führen
Define.s PfadWin = GetCurrentDirectory()+"\sessions" ; Kann auf Linux-Servern zu Fehlern führen
Define.s PfadTop = GetCurrentDirectory()+#PATH_SEPERATOR+"sessions" ; Funktioniert auf allen und der Code muss nicht angepasst werden
PBEFileSession::SetSessionDirectory(PfadTop) ; Und damit gibt es keine Probleme
So ist der Pfad für die Sessions gesetzt.
WICHTIG: Der Pfad muss existieren und der Benutzer, der den Server ausführt benötigt Lese- sowie Schreibrechte in dem Ordner. Existiert der Pfad nicht, liefert
SetSessionDirectory()#Falsezurück. Besitzt der Nutzer keine Leserechte, so liefertLoadSession()definitiv#Falsezurück und wenn keine Schreibrechte existieren, liefertCloseSession()hier#Falsezurück. Die Bibliothek erstellt den Ordner nicht selbst und passt auch nicht selbstständig die Rechte an!
Der Umgang mit Sessions ist etwas trickreich. Denn das Verständnis sagt einem ja bereits, das alles einen Anfang haben sollte. Aber dem ist leider nicht immer so. Denn gerade im Webbereich wird eine Session ja nicht nur genutzt, um Daten vorzuhalten, wenn man eingeloggt ist. Es wird auch genutzt, um zum Beispiel Aktivitäten nachzuvollziehen oder die Benutzererfahrung zu verbessern. Folglich ist der Startpunkt für eine Session nicht immer der Login. Oftmals existiert bei Aufruf einer Seite ja bereits schon eine Session und wenn dies nicht der Fall ist, muss eine angelegt werden. Der erste Schritt sollte also immer sein, zu prüfen ob eine Session für einen Nutzer existiert. Speichert man den Session-Schlüssel als Cookie (was immer vorzuziehen ist), so sollte zuerst über das Hauptmodul eine Abfrage des Session-Cookies erfolgen, die dort gespeicherte ID der Prozedur PBEFileSession::LoadSession() übergeben und die Rückgabe der Prozedur überprüft werden. Existiert kein Session-File für die Session, so liefert die Prozedur ein #False zurück. Andererseits liefert sie #True. Erst bei #False kann man mit PBEFileSession::StartSession() eine neue Session erzeugen. Bei erfolgreichem Start der Session liefert die Prozedur #True und bei misslingen #False zurück. So könnte der Intial-Block für eine Session aussehen:
Define.s SessionID
If Not PBEFileSession::LoadSession(PBExpress::GetCookie("SESSION"))
If PBEFileSession::StartSession()
SessionID = PBEFileSession::SessionID()
Else
ProcedureReturn #False
EndIf
Else
SessionID = PBEFileSession::SessionID()
EndIf
Sollte die Session bereits existieren, liefert PBEFileSession::SessionID() die selbe SessionID zurück, die bereits vom Cookie geliefert wird. Existiert sie nicht, dann wird eine neue gestartet, die eigenständig eine ID errechnet. Diese wird ebenfalls über PBEFileSession::SessionID() zurückgeliefert. Der Cookie-Schlüssel "SESSION" dient hier nur als Platzhalter. Im Gegensatz zu PHP sendet das Modul die SessionID nicht direkt an den Browser.
Um Werte aus der geöffneten Session abzufragen, gibt es zwei Möglichkeiten. Zum einen um direkt den Wert eines Schlüssels abzufragen und zum anderen sich eine Liste mit Werten geben lassen. Die erste Möglichkeit wird über PBEFileSession::GetSessionValue("SCHLÜSSEL") realisiert. Diese Prozedur liefert bei Existenz des Schlüssels den Wert als String zurück. Sollte der Schlüssel nicht exisztieren, bleibt der String leer.
Die zweite Möglichkeit, sich eine Liste zurückliefern zu lassen ist etwas anders. Die Prozedur PBEFileSession::GetSessionList("SCHLÜSSEL", List LISTE.s()) mutet den aus PHP kommenden Entwicklern vielleicht etwas seltsam an. Denn es ist in einer typsicheren Sprache mit starren Stapelkonstrukten äußerst schwer, mehrdimensionale assoziative Stapel zu realisieren, die auch direkt ansprechbar sind. Arrays haben eine feste Anzahl von Dimensionen und eine feste Anzahl an Elementen. Diese müssten ständig neu in der Größe dimensioniert werden. Listen und Maps haben nicht das Problem mit der festen Anzahl. Allerdings sind diese Grundsätzlich eindimensional. Um dies zu ändern, müssten mehrere solcher Stapel im Hintergrund gehalten und über einen Struktur-Datentyp mit einernander verbunden werden (ähnlich wie bei relationalen Datenbanken). Aus diesem Grund wurde es so entwickelt, das man selbst eine Liste des Datentyps String erzeugt und diese als zweiten Parameter der GetSessionList-Prozedur übergibt. Da diese Übergabe grundsätzlich By-Reference stattfindet, kann man nach Aufruf dieser Prozedur und Erfolg mit der eigenen List direkt die Daten verarbeiten.
NewList Liste.s()
PBEFileSession::GetSessionList("warenkorb",Liste())
Define.s SQLWhereString = ""
ForEach Liste()
If ListIndex(Liste()) = 0
SQLWhereString + "ID="+Liste()
EndIf
SQLWhereString + " AND ID="+Liste()
Next
WICHTIG: Die zu übergebene Liste sollte grundsätzlich nur für diesen Zweck bestimmt sein und folglich vor dem Aufruf leer sein. Diese Prozedur führt keinen Append durch. Die Liste wird gelöscht und dann mit den Daten aus der Liste neu beschrieben. WICHTIG: Man erhält alle Werte als String!
Dies läuft genau in die andere Richtung. Für das ablegen gibt es die SetSessionValue sowie SetSessionList Prozeduren. SetSessionValue erwartet allerdings zwei Parameter. Der erste ist wieder der Schlüssel. Der zweite ist der Wert, der in die Session geschrieben werden soll.
PBEFileSession::SetSessionValue("SCHLÜSSEL","WERT")
SetSessionList arbeitet simultan zu GetSessionList. In diesem Falle aber werden die Daten aus der Liste unter dem Schlüssel in die Session geschrieben.
WICHTIG: Alle Werte werden als String erwartet!
Das Modul arbeitet komplett unabhängig vom Hauptmodul von PBExpress. Um die Belastung der Massenspeicher so gering wie möglich zu halten sieht es dieses Modul vor, das die Datei nur einmal zum auslesen und nur einmal zum speichern verwendet wird. Das bedeutet, das zum speichern der Session ein Impuls innerhalb der Seiten-Prozedur erfolgen muss. Dieser Impuls wird durch das schließen der Session gegeben. Mit PBEFileSession::CloseSession() werden alle gesammelten Session-Daten auf einen Schlag in die dafür vorgesehene Datei geschrieben. Diese Prozedur schließt auch gleichzeitig das Sessionhandling vollständig ab und leert die Stapel. Ein Zugriff auf die Sessiondaten wirft nur einen leeren String bzw. im Falle von Listen #False zurück.
Bei einem Logout sollte nach Möglichkeit eine Session auch wieder gelöscht werden, um sicher zu gehen, das nicht mit der Zeit Probleme durch übernommene Sessions auftreten. Um die Session des Nutzers für den Logout zu löschen, reicht der Aufruf der Prozedur PBEFileSession::DestroySession() und die Sessiondatei sowie die Stapel werden gelöscht und die ID verworfen.
Diese kleine unscheinbare Prozedur kann zu einem mächtigen Werkzeug werden. Denn wenn konsequent CloseSession() in jeder Seiten-Prozedur verwendet wird, gibt diese im Grunde den letzten Besuch des Nutzers der angegebenen Session zurück. Diese Prozedur prüft, wann zuletzt Modifizierungen an der Session-Datei vorgenommen wurden. Dies ist Grundsätzlich bei CloseSession() der Fall. Aufrufen dieses Features erfolgt mit
PBEFileSession::LastSessionDate()
Zurück bekommt man einen Integer mit dem Unix-Timestamp der letzten Modifizierung. Zum einen kann dieser Wert statistisch verwendet werden. Es kann aber auch nützlich sein, um eine wirkungsvolle Methode gegen Session-Hijacking zu implementieren. Näheres dazu im Tipps- und Tricks-Kapitel.