Aus der CGI-Trickkiste (Teil 2) (Linux-Magazin, April 98)

Heute, im zweiten Teil, geht's um Formulare und ein simples Online-Order-System. Ein CGI-Skript kann mit CGI.pm dem Client nicht nur den notwendigen HTML-Code senden, sondern sogar die eingegebenen Parameter in Empfang nehmen und wiederum HTML zurückschicken.

Abbildung 1 zeigt, was mit reinem HTML heutzutage alles an Tipp- und Klick-Schnickschnack möglich ist: Popup-Menüs, die auf Knopfdruck hervorschnellen und eine Auswahl zulassen, drückbare Radio- und Checkbuttons, ein- oder mehrzeilige Textfelder, scrollbare Listen und schließlich Knöpfe zum Senden bzw. Zurücksetzen der Formularinformation.

Das CGI-Skript aus Listing form.pl zeichnet für diese Ausgabe verantwortlich. Die Tags :standard und :html3 exportieren aus CGI.pm reguläre HTML- und Tabellen-Funktionen.

Zeile 7 legt den HTML-Code für ein Popup-Menü in der Variablen $popup_menu ab. Das Teil trägt den Namen farbe1. Und genauso wird auch die Variable heißen, die der Browser nach dem Absenden des Formulars zurück an den Server sendet, gesetzt auf den vom Benutzer gewählten Wert. 'r', 'g' und 'b' stehen intern zur Auswahl, der Benutzer freilich bekommt nur die über den Hash %labels gemappten Wörter Rot, Grün und Blau zu sehen. 'r', also 'Rot' selektiert der Browser vor.

Eine radio_group wie die in Zeile 13 besteht aus einer Gruppe von Radio-Buttons, von denen genau einer selektiert ist und so den Wert der Ausgangsvariablen bestimmt.

Die textfield- bzw. textarea-Elemente aus den Zeilen 19 bzw. 23 unterscheiden sich nur durch die Anzahl der Zeilen des Eingabefensters - eines für textfield, beliebig viele für die textarea.

scrolling_list aus Zeile 29 funktioniert ähnlich wie das popup_menü weiter oben, nur daß die Option -size die Anzahl der sichtbaren Einträge bestimmt (der Rest ist über einen Scrollbar erreichbar) und mehrere Einträge selektiert werden können, wenn -multiple auf 'true' steht.

Die checkbox_group aus Zeile 37 ähnelt der radio_group aus Zeile 13, nur daß sie auch mehrere Optionen gleichzeitig zur Auswahl zuläßt. Gibt's statt freier Auswahl nur Ja oder Nein, tut's auch die Einzel-Checkbox aus Zeile 44.

Der Submit-Button aus Zeile 50 dient zum Absenden des Formulars. Die -value-Option bestimmt seine Beschriftung. Der Browser übermittelt diesen Wert in der Variable, die über den -name-Eintrag definiert ist. So kann man serverseitig feststellen, welcher von eventuell mehreren Submit-Buttons gedrückt wurde.

Der Reset-Button kommt ohne Parameter aus, da er lediglich die Formularparameter auf die ursprünglich gesetzten Werte zurücksetzt, nachdem der Benutzer daran herumgespielt hat.

Ab Zeile 55 macht sich form.pl daran, den ganzen Sermon auszugeben, angefangen vom Header und der start_html-Sequenz. Die start_form-Routine beginnt die HTML-Formular-Defininition und setzt die Übertragungsmethode auf GET (Standard ist POST) und die -action, das aufzurufende CGI-Skript auf /cgi-bin/dump.pl - unser letztens vorgestelltes CGI-Debug-Skript.

Ab Zeile 62 verpackt form.pl die Formular-Bestandteile in eine zweispaltige Tabelle mit Rahmen und setzt end_form bzw. end_html dahinter, um Formular und HTML-Code sauber abzuschließen.

Ins cgi-bin-Verzeichnis des Web-Servers verfrachtet, liefert form.pl einem mit http://server/cgi-bin/form.pl darauf deutenden Browser das in Abbildung 1 dargestellte Bild. Drückt der Benutzer den Submit-Knopf mit der Aufschrift Absenden, kontaktiert der Browser das in der start_form-Routine gesetzte Skript cgi-bin/dump.pl nach der GET-Methode. Dieses gibt vor lauter Schreck die Werte nach Abbildung 3 aus.

Listing form.pl

    01 #!/usr/bin/perl -w
    02 ######################################################################
    03 # Michael Schilli, 1998 (mschilli@perlmeister.com)
    04 ######################################################################
    05 
    06 use CGI qw/:standard :html3/;
    07 
    08 %labels = ('r' => 'Rot', 'b' => 'Blau', 'g' => 'Grün');
    09 
    10 $popup_menu = popup_menu(            ### Popup-Menü
    11    '-name'    =>  'farbe1',          # Name des Feldes
    12    '-values'  =>  ['r', 'g', 'b'],   # Einzelwerte
    13    '-default' =>  'r',               # Voreingestellt
    14    '-labels'  =>  \%labels);         # Wert -> angezeigter Name
    15 
    16 $radio_group = radio_group(          ### Gruppe von Radio
    17    '-name'    => 'farbe2',           # Name des Feldes
    18    '-values'  => ['r', 'g', 'b'],    # Einzelwerte
    19    '-default' => 'r',                # Vorausgewählt
    20    '-labels'  => \%labels);          # Name -> angezeigter Name
    21 
    22 $textfield = textfield(              ### Einzeiliger Text
    23    '-name'    => 'farbe3',           # Name des Feldes
    24    '-default' => '');                # Ist anfangs leer
    25 
    26 $textarea = textarea(                ### Mehrzeiliger Text
    27    '-name'    => 'farbe4',           # Name des Feldes
    28    '-default' => '',                 # Ist anfangs leer
    29    '-rows'    => 2,                  # Zwei Zeilen
    30    '-columns' => 20);                # 20 Zeichen breit
    31 
    32 $scrolling_list = scrolling_list(    ### Scrollbare Liste
    33    '-name'     =>  'farbe5',         # Name des Feldes
    34    '-values'   =>  ['r', 'g', 'b'],  # Wählbare Werte
    35    '-default'  =>  ['r', 'g'],       # Vorselektiert
    36    '-size'     =>  3,                # Höhe der Box
    37    '-multiple' => 'true',            # Multiple Auswahl OK
    38    '-labels'   => \%labels);         # Name -> angezeigter Name
    39 
    40 $checkbox_group = checkbox_group(    ### Gruppe von Schaltern
    41    '-name'      => 'farbe6',         # Name des Feldes
    42    '-values'    =>  ['r', 'g', 'b'], # Einzelwerte der Schalter
    43    '-default'   =>  'r',             # 1. Schalter gedrückt
    44    '-linebreak' => 'true',           # Untereinander aufreihen
    45    '-labels'    => \%labels);        # Name -> angezeigter Name
    46 
    47 $checkbox = checkbox(                ### Einzelknopf
    48    '-name'    => 'farbe7',           # Name des Feldes
    49    '-checked' => 'checked',          # Vorgewählt
    50    '-value'   => 'ja',               # Wert falls gedrückt
    51    '-label'   => 'Ja?');             # Dargestellter Text
    52 
    53 $submit = submit(                    ### Sende-Knopf
    54    '-name'  => 'submit_knopf',       # Name des Feldes
    55    '-value' => 'Absenden');          # Beschriftungstext und gelie-
    56                                      # ferter Wert falls ausgelöst
    57 
    58 $reset = reset(                      ### Reset-Knopf
    59    '-value' => 'Zurücksetzen');     # Beschriftungstext
    60 
    61 print header,                        # Alles als HTML ausgeben
    62       start_html('-title'   => 'Form Example',
    63                  '-BGCOLOR' => '#e0e0e6'),
    64 
    65       start_form('-method' => 'GET', # Formular-Anfang und Aktions-URL
    66                  '-action' => '/cgi-bin/dump.pl'),
    67       
    68       table({'border' => 1},         # Tabelle mit Formularelementen
    69             TR(td(tt("popup_menu")), td($popup_menu)),
    70             TR(td(tt("radio_group")), td($radio_group)),
    71             TR(td(tt("textfield")), td($textfield)),
    72             TR(td(tt("textarea")), td($textarea)),
    73             TR(td(tt("scrolling_list")), td($scrolling_list)),
    74             TR(td(tt("checkbox_group")), td($checkbox_group)),
    75             TR(td(tt("checkbox")), td($checkbox)),
    76             TR(td(tt("submit")), td($submit)),
    77             TR(td(tt("reset")), td($reset)),
    78            ),
    79       end_form,                      # Formularende
    80       end_html;                      # HTML-Ende

Abb.1: Ausgabe von form.pl Abb.2: ... nach dem Absenden

Parameter auslesen

Ungeachtet ob ein Parameter varname via die GET oder die POST-Methode zum CGI-Skript gelangt, liest

    $val = param('varname')

den übermittelten Wert aus. Dabei erledigt CGI.pm automatisch die Dekodierung maskierter Spezialzeichen. Im Falle von Listen mit multipler Auswahl liefert

    @list = param('multvarname');

eine Liste gesetzter Werte.

Lassen Sie uns einkaufen geh'n!

Das sagte immer der Showmaster von ``Hopp oder Top!'', eine einst auf Tele 5 ausgestrahlte Quizsendung, in der ich eines Tages die Ehre hatte, mitzuspielen. Leider verlor ich unglücklich gegen eine Hausfrau aus dem Münchner Umland und zog grummelnd mit dem Trostpreis, einem halben Dutzend mit Simpsons-Zeichentrick-Motiven bedruckter Socken ab. Egal!

Mangels Showkarriere stelle ich heute also das vereinfachte Order-System aus Listing shop.pl vor, dessen erste Seite zur Eingabe einer Kundennummer auffordert (siehe Abb. 3). Ein Klick auf den Auf geht's-Knopf übermittelt diese an den Server, der wiederum eine Seite mit einer Auswahl von drei Produkten zurückliefert (siehe Abb. 4). Entscheidet sich der Benutzer für eines, indem er den entsprechenden Radio-Button selektiert und den Bestellen!-Knopf drückt, schreibt shop.pl

    Kundennummer: 12345 Bestellung: Prodigy, The Fat of the Land, DM 19,90

in die Datei orders.txt im cgi-bin-Verzeichnis und zeigt dem Besteller die dritte Seite mit ein paar Dankesworten an (siehe Abb. 5).

Woher weiß shop.pl die Nummer des Kunden, der die Bestellen!-Taste auf der zweiten Seite drückte? Die erste Seite ist lange weg, und auch der Server weiß von nichts. Die Lösung: Kaum nimmt das Skript die Nummer aus dem ersten Formular entgegen, schmuggelt Zeile 31 sie mittels eines Hidden Fields in das Bestellformular, sodaß der Kunde seine Nummer unbewußt mitschickt, wenn er den Bestellen!-Knopf betätigt.

Das Hidden Field schleppt also die Kundennummer unsichtbar von der ersten Seite zur dritten.

Der flock-Befehl aus Zeile 35 sichert sich das Exklusiv-Recht auf die Datei orders.txt. Kein zweiter Prozeß oder Thread darf das zur gleichen Zeit. Da CGI-Skripts oft parallel ablaufen, ist diese Sicherung notwendig, gleichzeitige Schreiber könnten sonst das Dateiformat zerstören.

Hier muß ich noch erzählen, daß CGI.pm tatsächlich so etwas wie Status-Informationen behält: Muß es ein HTML-Formular malen, und stellt fest, daß der Name eines Feldes bereits im Eingabe-Bereich vorliegt, setzt es den voreingestellten Wert des Feldes auf den empfangenen Wert. Im Fall des HIDDEN-Felds kundennummer hat dies zur Folge, daß

    print hidden(-name  => 'kundennummer');

auch wie

    print hidden(-name  => 'kundennummer', 
                 -value => param('kundennummer');

reagiert und automatisch die Kundennummer weiterreicht.

Profis und Amateure

Damit der Besteller bei eventuell auftretenden Fehlern nicht das unschöne Internal Server Error zu sehen bekommt, steht der Hauptteil des Skripts ab Zeile 11 in einem eval-Konstrukt, Abstürze dazwischen landen in Zeile 52, die eine Vertröstungs-Meldung ausgibt und die wahre Fehlerursache in /tmp/errorlog samt Datum festhält. So kommt der Systemadministrator (bei komplexeren Systemen als dem vorgestellten) sporadisch auftretenden Fehlern auf die Schliche.

Schluß für heute! Nächstes Mal kommen Cookies und Server-seitiges Status-Speichern dran. See ya!

Abb.3: Startseite des Order-Systems

Abb.4: Bestellen ...

Abb.5: ... fertig!

Listing shop.pl

    01 #!/usr/bin/perl -w
    02 ######################################################################
    03 # Michael Schilli, 1998 (mschilli@perlmeister.com)
    04 ######################################################################
    05 
    06 use CGI qw/:standard/;                          # Cgi-Funktionen
    07 use Fcntl qw/:flock/;                           # LOCK_EX
    08 
    09 print header();                                 # Header ausgeben
    10 
    11 %products = (1 => 'Perl für Frauen, O\'Reilly, DM 59.90', # Produkte
    12              2 => 'Perl in 3 Tagen, SamsNet, DM 18.90', 
    13              3 => 'Go To Perl5, AWL, DM 59.00');
    14 
    15 eval {                                          # Fehler abfangen
    16 
    17   if(!defined param('kundennummer')) {          # Keine Kundennummer?
    18       print start_html('-title', 'Willkommen'), # -> Startseite
    19             h1('Willkommen im Perl-Buchladen!'),
    20             start_form(), "Ihre Kundennummer:", 
    21             textfield(-name => 'kundennummer'),
    22             submit(-value => "Einkaufen geh'n!"),
    23             end_form(), end_html();
    24 
    25   } elsif(!defined param('bestellung')) {       # Keine Bestellung?
    26       print start_html('-title', 'Bestellung'), # -> Bestellseite
    27             h1("Unser Sortiment:"), start_form(),
    28             checkbox_group(
    29                 '-name'      => 'bestellung',
    30                 '-values'    =>  [keys %products],
    31                 '-linebreak' => 'true',         # Untereinander
    32                 '-labels'    => \%products),    # Produkte
    33             p(), submit(-value => 'Bestellen'), # Bestellknopf
    34             hidden(-name  => 'kundennummer'),   # Weiterreichen
    35             end_form(), end_html();
    36 
    37   } else {                                      # Bestellung speichern
    38       open(ORDER, ">>orders.txt") || die "Cannot open orders.txt";
    39       flock(ORDER, LOCK_EX);                    # Lock setzen
    40       print ORDER "Kundennummer: ", param('kundennummer'),
    41                   " Bestellung: ", $products{param('bestellung')}, "\n";
    42       close(ORDER);
    43   
    44       print start_html('-title', 'Danke!'),     # Danke!-Seite
    45             p(), "Vielen Dank. ", 
    46             i($products{param('bestellung')}), 
    47             " geht Ihnen in den nächsten Tagen zu.", p(),
    48             p(), "Der fällige Betrag wird mit Kundennummer ", 
    49             param('kundennummer'), " verrechnet.",
    50             p(), "Viel Spaß damit!",
    51             start_form(), submit(-value => "Zurück zum Eingang"),
    52             end_form(), end_html();
    53   }
    54 };
    55 
    56 if ($@) {                                       # Fehler?
    57     print "Unser System kann Ihre Order leider momentan nicht " .
    58           "entgegennehmen. Bitte versuchen Sie es später nochmal.\n";
    59     open(ERRORLOG, ">>/tmp/errorlog");
    60     print ERRORLOG scalar localtime, "> $@";
    61     close(ERRORLOG);
    62 }

Michael Schilli

arbeitet 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.