Client und Server verständigen sich über das CGI-Protokoll normalerweise ohne daß einer der beiden Partner sich den Zustand des anderen merkt. War derselbe Kunde schon mal da? Keine Ahnung! Aber spätestens falls jemand eine Bestellung in einem Ordersystem abgibt, wäre es vielleicht sinnvoll, den Auftrag einer vorher eingegebenen Kundennummer zuzuordnen - es sei denn, man möchte nichts verkaufen.
Um diese Problematik anzugehen, braucht das CGI-Protokoll ein wenig Unterstützung, ein Zustand muß her: Der Kunde gibt die Kundennummer ein - schnipp! - bestellt etwas - schnipp! - bestellt noch etwas - schnipp, schnipp, schnipp! - alles Zustände, die gespeichert und später wieder abholt werden wollen! Bloß wo?
Das Skript cookie.pl
identifiziert Kunden anhand einer ID,
die es beim ersten Besuch eindeutig aus der aktuellen Uhrzeit
und der Nummer des ausführenden Prozesses - modulo 256 - generiert.
Vor der Ausgabe des 'Willkommen!'-Textes setzt es den Set-Cookie
-Header des
übermittelten Dokuments, jubelt so dem Browser
das Cookie unter, der es bei allen folgenden Besuchen wieder
herausrückt und dem Skript so gedächtnismäßig auf die Sprünge hilft.
cookie.pl
#!/usr/bin/perl -w ###################################################################### # Michael Schilli, 1998 (mschilli@perlmeister.com) ###################################################################### use CGI qw/:standard/; if(defined ($id=cookie(-name => 'ID'))) { # Cookie gesetzt! print header(); print b("Nummer $id! Was für eine Freude!"); } else { # Neuer Kunde $id = unpack ('H*', pack('Nc', time, $$ % 0xff)); $cookie = cookie('-name' => 'ID', '-value' => $id, '-expires' => '+1h', '-domain' => '.gauner.com'); print header('-cookie' => $cookie); print b("Willkommen bei der Cookie-Mafia, Nummer $id!"); }
Cookies enthalten Name/Value-Paare, im vorgestellten Beispiel
steht unter dem Schlüssel ID
eine eindeutige Hex-Zahl.
Die expire
-Option bestimmt ein Verfallsdatum, nach dessen Ablauf
der Browser die Information wieder vergißt.
In cookie.pl
passiert dies mit "+1h"
nach einer Stunde, andere
mögliche Werte wären z. B. "+1d"
für einen Tag oder "+10y"
für zehn Jahre.
Ist noch eine Domain angegeben, wie .gauner.com
im vorliegenden Beispiel,
liefert der Browser das Cookie nicht (nur) an die URL aus, hinter der das
Skript hängt, sondern fügt es allen Aufrufen aus der gleichen Domain bei.
So kommen unter Umständen mehrere Skripts in den Genuß des
Wiedererkennungs-Effekts.
Stimmt die Domain hingegen nicht, gibt's auch kein Cookie. Ähnlich
funktioniert die in cookie.pl
nicht verwendete -path
-Option, die
den Browser das Cookie nur an Skripts unter dem eingestellten Pfad
(z. B. cgi-bin/mydir
) übermitteln läßt.
Abbildung 1 zeigt den Browser beim ersten Aufruf von cookie.pl
, Abbildung 2
bei allen folgenden. Bei gesetzter -expire
-Option bleibt das Cookie
auch über das Programmende des Browsers hinaus in dessen ``Magen'' und ist
bei einem Neustart sofort wieder präsent.
Abb.1: Heute als Fremder ... |
Abb.2: ... nächstens als Freund! |
Erinnert sich noch jeder an die erste Folge dieser CGI-Reihe? Dort war davon
die Rede, daß das CGI-Modul normalerweise mit CGI-Objekten arbeitet und
man sich mit dem :standard
-Tag darum drücken kann - aber
jetzt trifft's uns: Die nachfolgende save
-Methode braucht
tatsächlich das doofe Objekt.
<include file=eg/savetest.pl filter=eg/cut2.sed>
Alles klar? Die save
-Methode schreibt alle hereinkommenden CGI-Parameter
im Format key=value
in Richtung des angegebenen File-Handles, im
Beispiel in die Standard-Ausgabe. Ruft man das Skript oben von der
Kommandozeile auf und tippt nach dem Prompt
(offline mode: enter name=value pairs on standard input)
die Werte (CTRL-D ist Control und 'D')
a='Hallo Test' b=(Klammer) CTRL-D
ein, erscheint
a=Hallo%20Test b=%28Klammer%29 =
save
schreibt also alle Eingabeparameter weg,
wobei es Sonderzeichen sauber nach dem URL-Encoding-Verfahren umsetzt
und schließt die Folge mit einer Zeile, die nur =
enthält, abschließt.
Andersherum erzeugt die Konstruktion
$q = new CGI(FILE);
ein neues CGI-Objekt, dessen Eingabedaten keineswegs über die CGI-Schnittstelle
eintrudeln, sondern aus einer Datei stammen, die mit dem File-Handle FILE
verbandelt ist. So darf ein Skript durchaus mit
mehreren CGI-Objekten jonglieren, die entweder aus dem CGI-Umfeld oder
aus der Konserve stammen.
Listing cart.pl
zeigt die ultimative Anwendung: Ein simples Shopping-Cart,
einen virtuellen Einkaufswagen. Erst trägt der potentielle Kunde seinen Namen
mit Adresse in ein Formular ein (Abb. 3) und dann bekommt er aus einem
Katalog von (für das Beispiel) durchnumerierten Produkten pro Seite jeweils
zehn zur Auswahl dargestellt. Er kann einzelne Artikel auswählen,
vor- und zurückblättern (Abb. 4), bis er sich schließlich dazu
durchringt, zur Kasse zu fahren und die Bestellung abzuschicken (Abb. 5).
Zeile 5 definiert das Verzeichnis für Dateien,
in denen er sich jeweils den Zustand der einzelnen Kunden merkt.
Im Beispiel muß das Verzeichnis customers
unterhalb von cgi-bin
bereits existieren und für den Eigentümer des Web-Servers beschreibbar sein,
sonst kracht's.
$items_total
ist die Gesamtzahl der zur Verfügung stehenden Artikel,
eine Anzahl $items_perpage
von Produkten stellt das Skript jeweils auf
einer Seite zur Auswahl dar.
Für das Testbeispiel generieren die Zeilen 10 bis 12 den Hash %merchandise
,
der jeder Artikel-Nummer 1 bis 100 den beschreibenden Text
Artikel Nummer X
zuordnet.
Zeile 14 liest ein eventuell schon übermitteltes Cookie ein, für Neukunden
ist $id
demnach nicht gesetzt.
In diesem Fall erzeugt cart.pl
in Zeile
18 eine eindeutige Identifikationsnummer, indem es die aktuelle Uhrzeit in
Sekunden und das letzte Byte (% 256
) der Nummer des laufenden
Prozesses ($$
) als Hex-Zahlen hintereinanderhängt.
Diese ID verpackt Zeile 21 in ein Cookie und sendet es dem Browser, der es
mangels
gesetztem -expire
-Datum nur im Rahmen seiner Laufzeit im Gedächtnis behält.
Bei einem Neustart ginge der Reigen von vorne los.
Die anschließend aufgerufene
Funktion print_address_form
gibt das Eingangsformular (Abb. 3)
aus - Ende der ersten Runde.
Füllt der Kunde das Formular aus und klickt auf den "Submit Query"
-Button,
kommt die Logik ab Zeile 28 zum Zug, die, falls noch keine Kundendatei existiert,
ein CGI-Objekt erzeugt und eine neue Datei, die den Namen der Kunden-ID trägt,
anlegt.
War der Kunde schon mal da, existiert die Datei schon und Zeile 31 liest den
aktuellen Zustand in das CGI-Objekt $q
ein.
Die Funktionen save_cgi
und restore_cgi
sind ab den Zeilen 96 bzw. 104
definiert und stützen sich auf die save
-Methode und den Konstruktor von
CGI.pm
. Falls in dem übermittelten Cookie nicht die erwartete Hex-Zahl,
sondern irgendwelche Schweinereien stehen, bricht Zeile 106 das Skript ab.
Zeile 34 prüft, ob der Kunde auch alle Felder des
Adressformulars ausgefüllt hat. Ist dies nicht der Fall, zeigt chart.pl
wieder das Adressformular, diesmal mit einer roten Fehlermeldung.
Der in Zeile 41 ausgelesene CGI-Parameter $offset
gibt an, an welcher Stelle
des Katalogs der Kunde beim letzten Aufruf schmökerte. Die Zeilen 43 bis 54
korrigieren den Wert des CGI-Parameters items
, der eine Liste ausgewählter
Artikelnummern enthält. Hierzu legt der grep
-Befehl aus Zeile 43 einen
Array @selected
an, der die Nummern bisher selektierter Artikel kopiert,
aber die im vorher dargestellten Fenster ausläßt. Warum? Das Formular aus
Abbildung 4 übermittelt im CGI-Parameter items
nur gesetzte Artikel.
Hat sich der Kunde umentschieden und einen vorselektierten Eintrag wieder
deselektiert, kommt für den entsprechenden Knopf aus dem Formular kein
CGI-Parameter herein, folglich entfernt cart.pl
vorsorglich
Werte im @select
-Array, die gerade in der Darstellung waren, um
anschließend ab Zeile 47 die neuselektierten wieder reinzupumpen.
Die Zeilen 51 bis 53 besetzen den CGI-Parameter items
, der die Liste
der selektierten Einträge enthält und halten den Zustand der Session
in der Server-Datei fest.
Drückte der Kunde den Button Bestellen
, verzweigt Zeile 55 zum Code
ab Zeile 56, der die Bestellungs-Bestätigung ausdruckt. Hier müsste
ein reales Order-System dann den Auftrag in die Wege leiten, z.B. eine
Email an einen Bearbeiter schicken und nach getaner Arbeit
die Zustandsdatei auf dem Server löschen.
Falls der Kunde Vorblättern
oder Zurückblättern
wählte,
verschrauben die Zeilen 68 und 71 den Wert der $offset
-Variablen
um eine Seitenlänge,
und 74-75 speichern den neuen Wert in der Zustandsdatei.
Die Zeilen 77 und 78 legen in den Array @subset
die Nummern der
im aktuellen Durchgang zur Auswahl gestellten Artikel ab, indem sie den
%merchandise
-Hash nach numerischen Schlüsseln sortieren und mit
splice
ein Segment herausschneiden.
Die Zeilen 81 bis 92 zeichnen für die Ausgabe des Auswahlformulars verantwortlich.
Der zentrale checkbox_group
-Befehl erzeugt den HTML-Code für die
aufgereihten Knöppe, die default
-Option selektiert die
irgendwann vorher ausgewählten praktischerweise gleich vor.
Zu beachten ist die unterschiedliche Notation der Funktionen aus dem CGI-Modul:
Da $q
vorher mit dem eingefrorenen Zustand aus einer Dateien-Konserve
initialisiert wurde, holt $q->param()
Werte aus dem in der Serverdatei
gesicherten CGI-Objekt, während param()
(ohne Objekt) sich auf die aktuell
hereinkommenden Parameter bezieht.
Nächstes Mal gibt's als Abschluß Non-parsed-Header-Skripts -- ohne Netz und doppelten Boden. Bleibt am Ball!
cart.pl
001 #!/usr/bin/perl -w 002 ###################################################################### 003 # Michael Schilli, 1998 (mschilli@perlmeister.com) 004 ###################################################################### 005 006 use CGI qw/:standard :html3/; # Standard und Tabellen 007 008 $cudir = "customers"; # Verzeichnis für temporäre Dateien 009 $items_total = 100; # Gesamtzahl aller Artikel 010 $items_perpage = 10; # Dargestellt pro Seite 011 012 for($i=1; $i<=$items_total; $i++) { # Pseudo-Artikel-Hash 013 $merchandise{$i} = "Artikel Nummer $i"; 014 } 015 016 $id = cookie(-name => 'ID'); # Cookie entgegennehmen 017 018 if(!defined $id) { # Kein Cookie - neuer Kunde! 019 # Neue ID: Zeit und Prozeßnummer 020 $id = unpack ('H*', pack('Nc', time, $$ % 0xff)); 021 022 print header('-cookie' => cookie('ID' => $id)); 023 print_address_form(); # Cookie/Adreßformular schicken 024 exit 0; 025 } 026 027 print header, start_html; 028 029 if(! -f "$cudir/$id") { 030 save_cgi($q = new CGI); # Neue Kundendatei anlegen 031 } else { 032 $q = restore_cgi(); # Kundendatei einlesen 033 } 034 # Adressinformation komplett? 035 if(!$q->param('name') || !$q->param('vorname') || 036 !$q->param('strasse') || !$q->param('plz') || 037 !$q->param('wohnort')) { 038 print_address_form("Bitte füllen Sie alle Felder aus."); 039 exit 0; 040 } 041 042 $offset = ($q->param('offset') || 0); 043 044 @selected = grep { $_ <= $offset || 045 $_ > $offset+$items_perpage } 046 ($q->param('items')); 047 048 foreach $item (param('newitems')) { 049 push(@selected, $item); 050 } 051 052 $q->delete('items'); # CGI-Parameter zurücksetzen 053 $q->param('items', @selected); 054 save_cgi($q); # Neu gesetzt und gesichert 055 056 if(param('Bestellen')) { # Ausgang 057 print $q->param('vorname'), " ", $q->param('name'), br, 058 $q->param('strasse'), br, 059 $q->param('plz'), " ", $q->param('wohnort'), 060 br, br, h1("Bestellung"); 061 foreach $item ($q->param('items')) { 062 print "1 Stueck $merchandise{$item}", br; 063 } 064 print p, b("Ihre Bestellung ist schon unterwegs!"), end_html; 065 066 } else { # Warenliste anzeigen 067 068 if($offset >= $items_perpage) { 069 $offset -= $items_perpage if param("Zurückblättern"); 070 } 071 if($offset < $items_total - $items_perpage) { 072 $offset += $items_perpage if param("Vorblättern"); 073 } 074 075 $q->param('offset', $offset); 076 save_cgi($q); 077 078 @subset = sort {$a <=> $b} keys %merchandise; 079 @subset = splice(@subset, $offset, $items_perpage); 080 081 # Neue Warenliste 082 print h1("Guten Tag, ", $q->param('vorname'), "!"), 083 p, "Zur Auswahl stehen heute:", 084 start_form(), 085 $q->checkbox_group( 086 '-name' => 'newitems', 087 '-values' => [@subset], 088 '-default' => [$q->param('items')], 089 '-linebreak' => 'true', 090 '-labels' => \%merchandise), 091 submit('Zurückblättern'), submit('Vorblättern'), 092 submit('Bestellen'), 093 end_form, end_html; 094 } 095 096 ############################################################# 097 sub save_cgi { 098 ############################################################# 099 open(FILE, ">$cudir/$id") || die "Can't open $cudir/$id"; 100 $_[0]->save(FILE); 101 close(FILE); 102 } 103 104 ############################################################# 105 sub restore_cgi { 106 ############################################################# 107 die "Get off, hacker!" if $id !~ /^[0-9a-f]+$/; 108 open(FILE, "<$cudir/$id") || die "Can't open $cudir/$id"; 109 my $q = new CGI(FILE); 110 close(FILE); 111 return $q; 112 } 113 114 ############################################################# 115 sub print_address_form { 116 ############################################################# 117 my $msg = (shift || ""); 118 print start_html, 119 tt(CGI::font({color => 'red'}, $msg)), 120 start_form, 121 table( 122 TR(td("Name:"), td(textfield('name'))), 123 TR(td("Vorname:"), td(textfield('vorname'))), 124 TR(td("Straße:"), td(textfield('strasse'))), 125 TR(td("Postleitzahl:"), td(textfield('plz'))), 126 TR(td("Wohnort:"), td(textfield('wohnort')))), 127 submit, end_form, end_html; 128 }
Abb.3: Adresse eintragen ... |
Abb.4: ... Produkte auswählen ... |
Abb.5: ... und zur Kasse! |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |