Skip to content
This repository has been archived by the owner on Jan 22, 2023. It is now read-only.

PBExpress Haupt Modul

Dennis Rönnau edited this page Dec 29, 2016 · 5 revisions

Das Hauptmodul von PBExpress hat die Aufgaben, zum einen das Routing zu managen und zum anderen die Ein- und Ausgabe von Informationen zu vereinfachen.

Einbinden des Moduls

Nach dem herunterladen des Zip-Archis findet ihr in selbigen den Ordner 'framework'. Kopiert diesen samt Inhalt in euer Projektverzeichnis. Liegt der Ordner im selben wie eure erste Projektdatei, bindet ihr das Hauptmodul mit IncludeFile "framework\pbexpress.pb" in euren Code ein. Danach steht euch das Modul zur Verfügung. Ihr könnt auf die Prozeduren direkt mit PBExpress::PROZEDURNAME() zugreifen oder das Hauptprogramm in den Namensraum des Moduls mit UseModule PBExpress bringen.

Schreiben der Seiten-Prozeduren

Seitenprozeduren sind die Programme, die einzelne Seiten darstellen. Es handelt sich dabei um den operativen Code einer Seite. Die Seitenprozeduren benötigen genau 4 Maps als Parameter, um die Benutzereingaben in nahezu jeder Form zu erfassen. Die Request-Map enthält alle in FastCGI verfügbaren Query-Header des Requests. Die Get-Map enthält alle Variablen aus dem Query-String. Auch die für das Routing wichtigen Schlüssel werden mitgegeben, um diese Informationen in der Prozedur zu verwenden. Die Post-Map enthält alle durch Formulare übergebenen Werte sowie bei Ajax-Requests im Schlüssel JSON den JSON-String, der sich mit der JSON-Bibliothek von PureBasic weiter verarbeiten lässt (WICHTIG: Der Content-Type für solche Requests muss immer application/json sein). Die letzte Map ist Files. Diese hat eine kleine Besonderheit. Die Dateien werden gemäß ihren Formular-Namen in diese Map übernommen. Allerdings erhält man dabei keinen einzelnen String, sondern basiert auf einer Struktur (PBEFiles), die 3 weitere Schlüssel besitzt. Size gibt die Dateigröße in Bytes an. Buffer beinhaltet die Adresse des ersten Bytes der Datei aus dem Arbeitsspeicher. Mit beiden Werten zusammen kann man den Inhalt der Datei in eine Datei auf dem Server schreiben. Der Schlüssel Filename enthält den Dateinamen der Datei.

WICHTIG: Alle Werte (bis auf File()\Size und File()\Buffer) liegen als String vor. Damit zieht in der Verarbeitung PBExpress mit PHP konform. Vergesst also nicht, die Daten in den jeweiligen Datentyp zu casten!

Eine Seitenprozedur hat im grunde folgenden Aufbau:

Procedure PROZEDURENNAME(Map Request.s(), Map Get.s(), Map Post.s(), Map Files.PBEFiles())
  ; Prozeduren-Code hier!
EndProcedure

Am besten nimmt man sich dies als Vorlage und ändert nur den Prozedurennamen in etwas, das sich an die Route anlehnt.

Die Eingaben

Alle Informationen, die über den Request zur Verfügung stehen, befinden sich in den 4 Maps. Allerdings gibt es noch zwei Prozeduren, mit denen man auch noch auf die Cookies des jeweiligen Reuests einfach zugreifen kann.

Request-Daten

Mit diesen Daten kann man näheres über den Nutzer in Erfahrung bringen. Von welcher IP kommt der Request? Welcher Browser wird genutzt? Welcher Domain wurde genau angesprochen? Und so weiter. Eine Liste mit diesen möglichen Parametern findet man im PureBasic Online-Handbuch. Die Liste hier zeigt die PureBasic-Konstanten. Die Zeichenkette ist bis auf den Anfang (#PB_CGI_) mit den Schlüsseln identisch. Zum Beispiel nutzt man bei der Konstante #PB_CGI_ContentLength den String ContentLength als Schlüssel für Request().

GET-Daten

Innerhalb der Seitenprozeduren kann man auf diese mit der Map "Get" zugreifen. Um zum Beispiel an weitere Informationen zur angeforderten Ressource zu kommen, kann man diese mit Get() abfragen. Nehmen wir einmal folgendes Beispiel: Wir implementieren eine Seiten-Routine, die dem Benutzer eine Liste mit den letzten 20 Blogeinträgen anzeigen soll. Nun will der Benutzer aber die nächsten 20 Einträge sehen. Dazu kann dann ein Link auf die URL http://www.example.de/?mod=blog&action=list&page=2 auf der ersten Seite gesetzt werden, die über die URL http://www.example.de/?mod=blog&action=list aufgerufen wurde. Die Parameter "mod" und "action" sind vom Router interpretiert worden (näheres dazu weiter unten). Wir als Programmierer haben jetzt die Aufgabe, den Parameter "page" zu nutzen. Den Wert dieses Parameters erhalten wir mit Get("page"). Um jetzt den Wert weiter in der Prozedur zu verwenden, erfasst man ihn am besten direkt mit einem Typecasting:
Define.i SelectedPage = Str(Get("Page"))
Der Vorteil dabei ist, das der Wert direkt für die Verwendung mit der Datenbank abgesichert wird. Denn bei SQL-Injections sind bestimmte Zeichen bzw. Strings von Bedeutung, während die Angriffswahrscheinlichkeit mit reinen Zahlen auf 0 fällt. Außerdem lässt sich so das Paging dynamisch regeln. Will man 20 Beiträge anzeigen, multipliziert man den Wert aus Page mit 20 und nimmt dies als Start-Index. Solche Berechnungen lassen sich mit Zeichenketten nicht machen.

POST-Daten

Dies ist eine Map, die alle Daten aus einem Formular enthält. Der Zugriff dieser Daten erfolgt ebenfalls über den Namen des Elements aus dem DOM der Seite als Schlüssel und über die Map Post(). Auch hier verhält sich PBExpress nicht anders als PHP. Hat man zum Beispiel ein Formular zum Login mit den Feldern "username", "password" und "submit" mit vordefinierten Wert "Login", so kann man die Daten in Variablen übernehmen und weiterverarbeiten:

Define.s UserName,PassWord,Submit
Submit = Post("submit")
If Submit = "Login"
    UserName = Post("username")
    PassWord = Post("password")
    ; ... Weiter im Text
Else
    ; ... Hinweis an den Browser mit Fehlercode!
EndIf

WICHTIG: Der Content-Type für den Request muss entweder application/x-www-form-urlencoded oder multipart/form-data sein. Diesen kann man im HTML über das Attribut "enctype" setzen. Der Typ application/json sollte entweder nur von einer Client-Software (Webservice) oder einem Ajax-Request verwendet werden. Der Json-String befindet sich dann in Post("JSON").

FILES-Daten

Die Map Files() arbeitet in Verbindung mit dem Content-Type multipart/form-data und liefert alle hochgeladenen Dateien mit. Anders als bei PHP wird hier kein mehrdimensionales assoziatives Array zur Verfügung gestellt, sondern eine Map vom Typ PBEFiles vorbereitet. Der Typ PBEFiles ist eine Struktur mit den Variablen Size.i, Buffer.i und Filename.s. Mit diese Informationen lässt sich eine Datei problemlos auf den Server ablegen. Den Datentyp ermittelt man zur Zeit am besten mit GetExtensionPart(Files("bild")\Filename). So könnte man die Behandlung einer Datei durchführen:

Define.i Size,Address
Define.s Filename,Extension
Size = Files("bild")\Size
Adress = Files("bild")\Buffer
Filename = Files("Bild")\Filename
Extension = GetExtensionPart(Filename)

If Extension = "jpg" Or Extension = "jpeg"
    If CreateFile(0,Filename)
        WriteData(0,Adress,Size)
        CloseFile(0)
    EndIf
EndIf

COOKIE-Daten

FastCGI hat nur eine recht rudimentäre Art, mit Cookies umzugehen. Besonders beim setzen des Cookies ist es eine fummelige Sache. Denn der Browser erwartet dabei einen korrekt formatierten Zeitpunkt für das Ende seiner Gültigkeit. Die ins Framework eingebaute Prozedur PBExpress::SetCookie erleichtert das ganze ungemein. Hier einmal ein paar Beispiele für das setzen eines Cookies.
Gültig so lange der Browser geöffnet ist:
PBExpress::SetCookie("Key","Value")
Gültig für eine Stunde:
PBExpress::SetCookie("Key","Value",Date()+3600)
Cookie wird nur bei HTTPS übertragen und darf nicht über Javascript verwendbar sein:
PBExpress::SetCookie("Key","Value",Date()+3600,#True,#True)
Genauere Information erhält man in der Referenz.
Um den Wert des Cookies zu erhalten, verwedet man einfach PBExpress::GetCookie("Key") und erhält bei Existenz den entsprechenden Wert, ansonsten einen leeren String.

Die Ausgabe

Es muss ja auch noch etwas an den Browser gesendet werden. Dazu bietet das Haupt-Modul eine Prozedur, die das ganze etwas erleichtern. Die Prozedur PBExpress::Header() ist im Grunde nur ein Alias für die PureBasic-Prozedur WriteCGIHeader(). Diesen Vorgang zu abstrahieren macht nicht allzuviel Sinn. Eine genaue Beschreibung dazu liefert hier wieder das Handbuch zu PureBasic auf der Seite zu zu WriteCGIHeader(). Die abstrahierte Prozedur ist hingegen PBExpress::Output(). Sie erledigt zwar die gleiche Aufgabe wie WriteCGIString(), kommt aber mit einer Besonderheit um die Ecke. Ist die Option #PBExpress_ContentLength auf #True gesetzt (siehe nächsten Abschnitt: Der Router), werden alle Strings, die über PBExpress::Output() gesendet werden, im Speicher gecashed und und erst nach Verlassen der Seiten-Prozedur an den Browser ausgeliefert. Das hat den Vorteil, das Header und Cookies problemlos noch an den Browser gesendet werden können, obwohl bereits eine "Ausgabe" erfolgte. PBExpress::Output() erwartet als Parameter nur den String, der ausgegeben werden soll.

Zum senden von Dateien an den Browser gibt es 2 Möglichkeiten. Zum einen kann man eine Datei direkt vom Massenspeicher des Server an den Nutzer schicken. Man kann aber auch einen Speicherpuffer an den Browser senden. Die Prozeduren dazu sind PBExpress::SendFile() und PBExpress::SendBufferAsFile(). Beide Prozeduren haben die Deckungsgleichen Parameter Dateiname, Inline bzw. Show und Cache. Während die letzten beiden Optional sind, muss der Dateiname immer angegeben werden. Anhand diesem kann das Dateiformat ermittelt werden und dem Benutzer wird ein Dateiname zum Speichern vorgeschlagen.

PBExpress::SendFile(FileName.s,Show.b = #False,Cache = #True)
PBExpress::SendBufferAsFile(FileName.s,Buffer.i,Length.i,Show.b = #False,Cache = #True)

FileName ist der Dateiname. Während bei SendFile der Dateiname den kompletten Pfad auf dem Server enthält, reicht bei SendBufferAsFile nur der reine Dateiname. SendBufferAsFile erwartet als zweiten Parameter die Adresse des Puffers im Arbeitsspeicher und als dritten Parameter die größe der Daten in Bytes. Show bedeutet, ob Medien-Dateien angezeigt werden oder nur als Download zur Verfügung stehen. Wird dieser auf #False gesetzt bzw. belassen, dann wird die Datei als Attachement gekennzeichnet und es findet immer ein Download statt. Cache soll bei #False dem Browser mitteilen, das die Datei nicht gecached werden soll. Das hat allerdings nur bei Inlinefähigen (z.B. Grafiken oder Audio) Dateien Sinn.

WICHTIG: Der Content-Type wird immer an Hand der Dateiendung erkannt. Im Sourcecode befindet sich eine Map mit allen Elementen, die das Modul erfassen kann. Das Senden von Dateien sollte in der Pageprozedur die einzige Ausgabe an den Browser sein. Es sollte davon abgesehen werden, weitere Inhalte im selben Request an den Browser zu senden.

Der Router

Nun wird es für Entwickler, die von Sprachen wie z.B. PHP kommen wieder etwas seltsam. PureBasic ist eine Programmiersprache, dessen Compiler Maschinencode (Bitcode oder auch Microcode) erzeugt. Es handelt sich also um einen nativen Compiler. Im Gegensatz zu PHP lässt sich mit PureBasic nicht mit z.B. IncludeFile je nach Route ein anderes Skript laden. Die einzelnen Skripts müssen in Prozeduren (in anderen Sprachen auch als Funktionen bekannt) gekapselt werden. Der Router verwendet zum Routen sogenannte Callbacks und ist darauf angewiesen, das die Routen einzeln in die Callbackliste eingetragen werden. Ebanfalls setzt der Router feste Routingschlüssel vorraus. Ohne diese Wird der Router die Routen nicht interpretieren können.

Aber keine Panik. Hier wird erklärt, wie dies funktioniert.

Schritt 1

Passt die Optionen an. Dies ist ein schnelles Vergnügen. Denn es stehen nur 2 Optionen zur Verfügung. Die Konstante #PBExpress_ContentLength ermöglicht einem, die maximale Größe des Request-Body auf eine bestimmte KB-Größe zu beschränken. Dies schließt auch den Upload von Dateien mit ein. Voreingestellt sind 10 MB. Es ist aber ratsam, das wenn ihr nur kleine Dateien haben wollt, diese entsprechend anzupassen. Wollt ihr nur den Upload von Bildern erlauben, könnt ihr die Option auch auf 2048 (2MB) setzen.
Die zweite Option ist #PBExpress_OutputBuffer. Diese gibt an, ob Ausgaben an den Browser gepuffert werden sollen oder direkt gesendet werden. Wichtig dafür ist, das die Output-Prozedur dieses Moduls verwendet wird. Per default steht diese Option auf #False.
So setzt man eine Option: PBExpress::SetOption(#PBExpress_OutputBuffer,#True)
Ab jetzt werden alle Ausgaben über Output in einen Pufferstring gespeichert. Nach Beenden des Callbacks wird dieser Puffer an den Browser gesendet.

Schritt 2

Setzt die Routing-Schlüssel. Man kann theoretisch so viele Schlüssel setzen, wie man will. Allerdings sollte man sich für maximal 2 Schlüssel entscheiden. Einen Anwednungsschlüssel und einen Funktionsschlüssel. Diese Namen sind natürlich im Framework nicht vorgegeben. Sie dienen lediglich dem Verständnis zum Umgang mit Routen. Inspiriert ist diese Idee vom CMS ClanSphere, das im Grunde genauso arbeitet. Der Schlüssel "mod" zeigt auf eine Anwendung und der Schlüssel "action" zeigt auf eine Funktion. Und So setzt man 2 Routingschlüssel:
PBExpress::AddRouteKey("mod")
PBExpress::AddRouteKey("action")
Diese Schlüssel sorgen jetzt dafür, das der Router die in der URL verborgene Route extrahieren kann.

WICHTIG: Ohne diesen Schritt wird das Framework die Route nicht auflösen. Der Grund für diesen Schritt ist zum einen das eindeutige ablegen der Prozeduradresse in der Routingtable und zum anderen ein gezielteres und schnelleres Erfassen der Daten aus dem Query-String.

Schritt 3

Nun fügt eine Route der Liste hinzu. Dazu sollte man jetzt natürlich wissen, wie ein HTTP-Querystring aufgebaut ist. Aber auch das ist ganz leicht. Man nehme einen Schlüssel, den wir vorher mit AddRouteKey gesetzt haben und verpassen ihn einen Wert. Für mehrere Schlüssel hängen wir diese Schlüssel-Wert-Paare einfach mit einem Ampersand (kaufmännisches Und "&") aneinander. Zum Beispiel wollen wir die Funktion "view" (also ansehen) für die Anwendung "profiles" (Benutzerprofile) in die Tabelle eintragen. Der Querystring dazu sieht so aus:
mod=profiles&action=view
Die Reihenfolge der Paare spielen dabei keine Rolle. Die Prozedur AddRoute bringt sie sowieso immer in die gleiche Reihenfolge. Wenn wir jetzt davon ausgehen, das wir bereits eine Prozedur für die Funktion geschrieben haben, dann können wir jetzt die Route mit dieser Prozedur als Callback verbinden. Dies funktioniert so:
PBExpress::AddRoute("mod=profiles&action=view",@ProfilesView())
Damit haben wir unsere erste Route definiert.

WICHTIG: Die Route muss mindestens einen Schlüssel verwenden, der mit AddRouteKey definiert wurde. Nach Möglichkeit sollte man aber beide nutzen. Denn es kann passieren, das man sich bei der Route vertippt oder versucht, einen dritten Schlüssel einzubauen, ohne ihn mit AddRouteKey vorab zu definieren. Dadurch könnten ärgerlicherweise vorher gesetzte Routen überschrieben werden. Beispiel:

PBExpress::AddRouteKey("mod")
PBExpress::AddRouteKey("action")
PBExpress::AddRoute("mod=test1",@test1())
PBExpress::AddRoute("mod=test1&page=test2",@test2())

In Zeile 3 wird dem "mod" "test1" die Default-Page test1() zugewiesen. In Zeile 4 wird diese allerdings mit test2() überschrieben, da der Schlüssel "page" nicht als Routing-Schlüssel definiert war. Page wird folglich ignoriert!

Schritt 4

Die Default-Anwendung definieren. Dies bedeutet im Grunde nur, das wenn kein Routingschlüssel passt und somit keine theoretische Route erkannt wird, diese Seite ausgeführt wird. Man kann es im Grunde auch als Startseite ansehen. Setzen tut man diese so: PBExpress::SetDefaultPage(@NewsList())

Starten der Anwendung

Um jetzt die Webanwendung zu starten, muss nur noch die Prozedur zum Ausführen der Software angesprochen werden. Dies tut man mit PBExpress::RunServer().

WICHTIG: Alle für die Laufzeit relavanten Einstellungen und Prozeduren sollten vor dem Aufruf dieser Prozedur implementiert, registriert oder ausgeführt werden. Alles, was hinter dieser Prozedur implementiert wird, kommt unter Umständen garnicht zur Ausführung. Zur Zeit ist die Rückgabe eines boolischen Wertes für diese Prozedur vorgesehen. Es ist aber zum jetzigen Zeitpunkt noch nicht klar, ob mit einem Callback eine Ereignisprozedur gesetzt werden soll oder ob im Fehlerfall die Schleife unterbricht und #False zurückwirft. Aber zum jetzigen Zeitpunkt muss der Prozess/Task gekillt werden, da ein Sauberer Exit zur Zeit noch nicht implementiert ist.