![]() |
CGI-Programmierung - The hard way :-)
|
Bei der CGI-Programmierung gibt es eine Vielzahl von Tätigkeiten die eigentlich in jedem CGI-Skript durchgeführt werden müssen. Dies betrifft z.B. die folgenden Punkte:
Die dafür notwendigen Perl-Anweisungen sind zwar normalerweise nicht gerade kompliziert, trotzdem ist es aber eine lästige Pflichtübung diese jedesmal neu zu programmieren. Dieses Kapitel trägt die Zusatzbezeichnung "The hard way", weil es eigentlich für genau diese Pflichtübungen sehr komfortable Perl-Module gibt. Trotzdem will ich Ihnen hier die einzelnen Schritte aufzeigen die notwendig sind um ohne diese Module zum Erfolg zu gelangen. Sie können dadurch besser erkennen welche Einzelschritte das HTTP-Protokoll vorsieht und sind damit für evtl. Fehlersuchen besser gerüstet. Außerdem kann es sein, dass auf dem von ihnen verwendeten WWW-Server die notwendigen Perl-Module nicht installiert sind.
Wie bei allen Programmierübungen soll auch hier das berühmte "Hello world" am Anfang stehen. Das nachfolgende CGI-Skript macht nichts anderes als diesen kurzen Text als HTML-Datei auszugeben. Es dient auch zum Test ob die CGI-Konfiguration ihres WWW-Servers richtig ist. Wenn dieses Skript nicht funktioniert, überprüfen Sie bitte zuerst die Konfiguration ihres Servers.
#!/usr/bin/perl -w print "Content-type: text/html\n\n"; print " <html> <head> <title>Hello world!</title> </head> <body> <h1>Hello world!</h1> </body> </html>"; |
Beispiel 1: Hello world!
Aus Perl-Sicht besteht dieses Skript nur aus zwei print-Anweisung. Die Ausgabe selbst stellt eine einfachste HTML-Datei dar.
Einzige Unterschied zu einem ganz normalen HTML-Dokument ist die erste Zeile der Ausgabe. Diese wird bei normalen HTML-Dateien vom WWW-Server erzeugt. Selbiger erkennt an der Dateiendung "html"; (bzw. "htm") den MIME-Typ der Daten und fügt die "Content-type:"-Zeile ein. Bitte beachten Sie aber noch den Zeilenumbruch am Ende der Zeile sowie die nachfolgende Leerzeile. Diese beiden Merkmale sind unbedingt erforderlich da sie das Ende des http-Headers an den Browser übermitteln! Das Fehlen dieser beiden Merkmale ist eine der häufigsten Fehlerursachen von Anfägern in CGI-Skripten.
Die "Content-type:"-Zeile ist Bestandteil des http-Protokolls und muß als letzte Zeile des http-Headers an den Client übermittelt werden. Da unsere Perl-Skripte verschiedene Ergebnisse haben können (HTML-Code, normaler Text, dynamisch erstellte Grafiken, ...), kann der Server hierbei den MIME-Typ nicht selbst bestimmen und überläßt dies dem Skript. Die vorangegangenen Zeilen des http-Headers (z.B. Expires: oder Date:) werden hier nach wie vor vom WWW-Server erzeugt. Diese Aufgabe kann das Perl-Skript jedoch auch übernehmen.
Um die Ergebnisse des vorigen Beispiels (statische HTML-Seite) zu erhalten muß natürlich kein CGI-Skript programmiert werden. Deren Einsatzgebiet ist vielmehr die dynamische Erstellung von HTML-Seiten. Lassen Sie uns deshalb das "Hello-world";-Skript umschreiben, so daß eine persönliche Begrüßung erfolgt. Hierzu müssen wir jedoch erst denn Namen des Client-Bedieners in Erfahrung bringen. Hierzu verwenden wir ein HTML-Formular mit einem Eingabefeld.
<html> <head> <title>Wie heist du denn?</title> </head> <body> <form action="bsp2.pl" method=get> <input type=text name="vorname" size=30> <input type=submit value="Ich haben fertig!"> </form> </body> </html> |
Das Formular verwendet die Methode "get"; um die Formulardaten an das
Perl-Skript "bsp2.pl"; zu übermitteln. Dabei werden die Formularwerte durch
ein Fragezeichen getrennt an die URL angehängt. Wird das Formularfeld z.B. mit
"Michael"; ausgefüllt, so wird anschließend vom Browser die URL
bsp2.pl?vorname=Michael aufgerufen. Der URL-Teil nach dem
? wird mit Hilfe der Umgebungsvariablen QUERY_STRING
an das CGI-Skript übergeben. Damit hat diese Variable folgenden Wert:
QUERY_STRING => vorname=Michael
Sie finden in diesem Übergabewert die Bezeichnung des Formularfelds und den
vom Anwender eingegebenen Wert wieder. Man könnte auch sagen: die Bezeichnung
des Formularfelds ist der Variablenname, der eingegebene Wert ist der
Variableninhalt. Allerdings stehen diese beiden Angaben einem Perl-Skript nun
nicht einzeln zur Verfügung. Sie befinden sich vielmehr innerhalb einer
einzelnen Zeichenkette - der Environment-Variablen QUERY_STRING.
Uns ist aber schon bekannt, dass das "="-Zeichen die Grenze
zwischen den beiden Angaben bildet. Somit können wir die Perl-Funktion
split() verwenden um die beiden Teile zu extrahieren.
$qrystrg = $ENV{'QUERY_STRING'};
($varname, $varwert) = split(/=/, $qrystrg);
Die Funktion split() trennt eine Zeichenkette (das
2. Argument) anhand eines Suchmusters (1. Argument) auf. Das Suchmuster darf
auch mehrfach auftreten. Allerdings liefert split() dann auch
weitere Werte als Ergebnis. Der Rückgabewert von split() ist ein
Array. Im obigen Beispiel wissen wir ja aber schon wieviele Rückgabewerte von
split() zu erwarten sind. Deshalb wird gleich ein anonymisiertes
Array (die zwei runden Klammern) verwendet. Es besteht fest aus den beiden
Variablen $varname und $varwert und kann somit nur
zwei Elemente aufnehmen. Unmittelbar nachdem das Array durch
split() gefüllt wurde, existiert es mangels eines Namens auch
schon nicht mehr. Die beiden Einzelvariablen überleben dies aber und stehen
auch im weiteren Skriptverlauf zur Verfügung.
Damit könnte das personalisierte "Hello world"-Skript folgendermaßen aussehen:
#!/usr/bin/perl -w
$qrystrg = $ENV{'QUERY_STRING'};
($varname, $varwert) = split(/=/, $qrystrg);
print "Content-type: text/html\n\n";
print "
<html>
<head>
<title>Hallo $varwert</title>
</head>
<body>
<h1>Hallo $varwert</h1>
</body>
</html>";
|
Beispiel 2: Persönliche Begrüßung
Die einzelnen Schritte:
%ENV zur Verfügung. Damit kann auf die übergebenen
Formulardaten zugegriffen werden.Das Formular aus dem vorigen Beispiel hatte nur ein einziges Eingabefeld
und stellt somit eher einen Sonderfall dar. Bei Formularen mit mehreren
Eingabefeldern wird der Übergabewert für das CGI-Skript ähnlich wie im vorigen
Beispiel gebildet. Auch in so einem Fall wird für jedes Formularelement die
obige Kombination aus Elementname und Elementwert gebildet (z.B.
vorname=Michael und nachname=Schuhmacher). Diese
Kombinationen werden anschließend mit Hilfe des &-Zeichens verkettet. Das
sieht dann folgendermaßen aus:
vorname=Michael&nachname=Schuhmacher
Unser Skript muß nun also noch einen zusätzlichen split()-Lauf
einlegen um die &-Verkettung wieder aufzulösen.
#!/usr/bin/perl -w
# -w => ausfuehrlichere Warnungen/Fehlermeldungen
# Perl-Pragma um u.a. Variablendeklaration zu erzwingen
# Insbesondere in Hinblick auf Apache-httpd mit mod_perl
# zu empfehlen.
use strict;
# Variablendeklaration
my ($daten, @formulardaten, $formfeld, $name, $wert, %feld);
$daten = $ENV{'QUERY_STRING'};
# Aufspaltung der Formulardaten in die einzelnen Felder
@formulardaten = split(/&/, $daten);
|
Das Array @formulardaten enthält nun die einzelnen
Formularfelder wieder in getrennter Form.
$formulardaten[0] enthält vorname=Michael
$formulardaten[1] enthält nachname=Schuhmacher
Nun muß jedes einzelne Arrayelement wieder wie bereits weiter oben anhand
des "="-Zeichens aufgesplittet werden. Die erhaltenen Werte werden im Hash
%feld abgelegt um in weiteren Skriptverlauf jederzeit auf die
Übergabewerte zugreifen zu können.
# Jedes Formularfeld einzeln bearbeiten und Ergebnis
# im Hash $feld ablegen.
foreach $formfeld (@formulardaten){
# Aufspaltung eines Felds in Feldname und -wert
($name, $wert) = split(/=/, $formfeld);
$feld{$name}=$wert;
}
|
Der Hash-Inhalt sieht damit wie folgt aus
| Key | Inhalt |
|---|---|
vorname |
Michael |
nachname |
Schuhmacher |
und auf die einzelnen Werte kann z.B. via $feld{"vorname"}
oder $feld{"nachname"} zugegriffen werden. Anschließend noch der
Rest des Skripts um die Formulardaten als Feedback in einer HTML-Seite
zurückzuliefern.
# Content-Type und Headerende des http-Headers erstellen
print "Content-Type: text/html\n\n";
# Eroeffnende HTML-Anweisungen ausgeben
print "<html><head><title>CGI-Test</title></head><body>\n";
print "<h1>Übergebene Formulardaten</h1>\n";
# Die Daten jedes Formularfelds in eigener Zeile ausgeben
print "<table border=1><tr><th>Feldname</th><th>Feldwert</th>\n";
foreach $name (keys(%feld)){
print "<tr valign='top'><td><pre>$name</pre></td>
<td><pre>$feld{$name} </pre></td></tr>\n";
}
print "</table>\n";
# HTML-Ausgabe abschliessen
print "</body></html>\n";
|
Beispiel 3: Erste Form des Feedback-Skripts
Soweit sieht das dann ja schon recht gut aus. Versuchen sie aber mal die
Eingabe von Wörtern mit nationalen Sonderzeichen (ä, ß, ...). Das Skript
liefert für die Sonderzeichen den Hexadezimalcode des Zeichens mit einem
vorangestellten "%"-Zeichen. Ein großes "Ö" wird somit als %D6
dargestellt. Grund dafür ist die ursprügliche Begrenzung des Internets auf den
7-Bit-ASCII-Code. Alle Sonderzeichen benötigen noch das achte Bit und können
somit nicht übertragen werden. Deshalb werden sie vor der Übertragung in
Hex-Code konvertiert. Ähnliche Probleme gibt es bei Leerzeichen. Da die
Formulardaten bei der GET-Methode an die URL angehängt werden, können diese
auch nicht unkonvertiert bleiben. Leerzeichen in einer URL sind nämlich nicht
zulässig. Ein Leerzeichen wird vor der Übertragung in ein "+"-Zeichen
umgewandelt. Wird in unser obiges Formular also der Wert Karl
Österle eingetragen, so wird bei der Übertragung daraus
Karl+%D6sterle.
Damit unser Skript auch ordentliche Ergebnisse bringt, müssen diese Konvertierungen natürlich alle rückgängig gemacht werden. Die nachfolgenden Zeilen erledigen dies und filtern dabei auch noch unerlaubte Eingaben (in diesem Fall eXtended Server Side Includes) aus den Übergabewerten heraus.
# MIME-Kodierung der Leerzeichen aufheben
$wert =~ tr/+/ /;
# MIME-Kodierung von Sonderzeichen aufheben
$wert =~ s/%([a-fA-F0-9].)/pack("C", hex($1))/eg;
# XSSI-Anweisungen entfernen! Sicherheit!!!
$wert =~ s/<!--(.|\n)*-->//g;
|
Tja, und dann gibt es da nochmal ein klitzekleines Problem :-). Im Formular
gibt es nämlich eine Auswahlliste die eine Mehrfachauswahl erlaubt. Im
QUERY_STRING taucht in so einem Fall der Name des Formularelements mehrfach
auf. Im obigen Beispiel handelt es sich um das Formularelement
opt_learn_time. Werden hier die beiden Punkte "Am Morgen" und "Am
Abend" ausgewählt, dann sieht das in der URL so aus:
opt_learn_time=Am+Morgen&opt_learn_time=Am+Abend
Auch das wird bislang von unserem Skript nicht berücksichtigt. Der
jeweils letzte Parameter überschreibt im Hash %feld den
Vorgängerwert. Die nachfolgenden Zeilen verhindern dies und sorgen dafür, daß
alle ausgewählten Zeilen in unserer Ausgabe erscheinen.
if(exists($feld{$name})){
$feld{$name} .= " / " . $wert;
}
else{
$feld{$name}=$wert;
}
|
Nachdem unser Skript um diese beiden Codeblöcke ergänzt wurde, verhält es sich wie im Beispiel 4.
Beispiel 4: Zweite Form des Feedback-Skripts
Nun verbleibt eigentlich nur noch ein einziger Punkt zu Klären. Bislang
haben wir die Formulardaten immer mit Hilfe der GET-Methode an das CGI-Skript
übermittelt. Dabei werden ja wie erläutert die Daten in einem speziellen
Format an die URL angehängt. Die Formuladaten können aber auch mit Hilfe der
POST-Methode übertragen werden. In diesem Fall erhält das Skript die
Formulardaten über die Standardeingabe vom Server byteweise übermittelt. Der
Anwender sieht die Daten dann nicht mehr. Die Anzahl der Bytes erfährt das
Skript über die Environment-Variable CONTENT_LENGTH. Damit wird
in diesem Fall folgender Code verwendet um an die Daten zu gelangen:
read(STDIN, $daten, $ENV{'CONTENT_LENGTH'} );
Das Skript kann mit Hilfe der Umgebungsvariablen
REQUEST_METHOD sogar selbst in Erfahrung bringen mit welcher
Methode es aufgerufen wurde. Soll das Skript also wie gewünscht ein ganz
universelles Verhalten aufweisen, so wird der nachfolgende Codeblock
verwendet.
# Je nach Formular-Method (GET/POST/PUT) werden die Formulardaten
# in die skalare Variable $daten eingelesen.
if($ENV{'REQUEST_METHOD'} eq 'GET'){
# Formular hat GET-Methode verwendet
$daten = $ENV{'QUERY_STRING'};
}
else{
# Formular hat POST-Methode verwendet
read(STDIN, $daten, $ENV{'CONTENT_LENGTH'} );
}
|
Beispiel 5: Endgültiges Feedback-Skript (mit POST-Methode)
Und hier das ganze Skript in der Übersicht
Lokal bzw. Remote-Zugriffe
seit dem 8. Juni 1999