Zurück

Valid CSS! Valid XHTML 1.0!

Programming Republic of Perl

Erstellung dynamischer HTML-Seiten unter Verwendung einer Datenbank (z.B. SQL-Server wie MySQL, PostgreSQL, ...)

Vorbemerkungen

Nun aber genug mit dem einleitenden Geplänkel. Legen wir einach los.

Erste Verbindung zur Datenquelle

Sicherheitshalber wird in den nachfolgenden Skripten das Perl-Compiler-Pragma strict verwendet. Dies ist insbesondere bei Servern absolut notwendig die einen mittels "mod_perl" eincompilierten Perl-Interpreter haben. Bei diesen Servern wird das Perl-Skript nur einmal eingelesen und anschließend im Speicher gehalten. Alle Variablen deren Lebensdauer nicht beschränkt wird, behalten dabei ihren Inhalt zwischen verschiedenen Aufrufen des Skripts. Dies gilt sogar für verschiedene Threads! Im ungünstigsten Fall können damit Daten von einem Skriptaufruf in einen anderen übertragen werden. Das obige Compiler-Pragma erzwingt die Definition aller Variablen mittels my. Damit ist aber die Lebensdauer der Variablen spätestens am Ende des Skripts erreicht und diese Probleme können nicht auftreten.

Wie bereits weiter oben bemerkt, wird für die Datenbankanbindung das Perl-Modul DBI.pm verwendet.

  1. Der erste Funktionsaufruf ist connect(). Damit wird eine Verbindung zwischen dem Skript und einer Datenbank auf dem SQL-Server aufgebaut. Der Befehl verwendet als minimale Argumente die Datenquelle, den User-Namen auf dem SQL-Server und das zughörige Passwort.
    $dbh = DBI->connect($data_source, $username, $password)
    wobei für die Datenquelle gilt:
    dbi:DriverName:database=database_name;host=hostname;port=port
    DriverName gibt den Namen des DBD-Treibers an, database ist der Name der Datenbank (hier dbi_demo), host ermöglicht die Benutzung eines anderen Rechners als Datenbankserver (nachfolgend ist der DB-Server und WWW-Server auf demselben Rechner - localhost) und port ermöglicht die Angabe einer von der Standardinstallation abweichenden Portnummer. In diesem Beispiel lautet $data_source also:

    dbi:mysql:dbi_demo
     
    Bei erfolgreichem Verbindungsaufbau ist das Ergebnis ein sogenannter "Datenbank-Handle". Er stellt vereinfacht gesagt eine Referenz zu der geöffneten Datenbankverbindung dar. Alle weiteren Zugriffe basieren auf diesem Handle.
     
  2. Bevor nun die eigentliche Datenbank-Applikation programmiert wird, soll gleich gezeigt werden wie die Verbindung zur Datenquelle wieder geschlossen wird. Nachdem die Datenbank nämlich nicht mehr benötigt wird, sollte auf jeden Fall mit Hilfe der disconnect()-Methode die Verbindung zur Datenquelle beendet werden. Obwohl es auch ohne diesen expliziten Verbindungsabbau geht, erspart man sich damit wenigstens die unschöne Fehlermeldung "Database handle destroyed without explicit disconnect".

Damit ergibt sich als erstes Beispiel der nachfolgende Code (nicht abtippen, sie können weiter unten alle Beispiele herunterladen):

#!/usr/bin/perl -w
#
# Dateiname: script1.pl
#     Autor: A. Grupp

# Compilerpragma strict um keine Fehler zu machen :-)
use strict;
# Benutzung der Perl-Module CGI und DBI
use CGI;
use DBI;

# Neues CGI-Objekt erzeugen
my $cgi_obj = new CGI;

# Datenbank-Verbindung aufbauen
my $dbh = DBI->connect( 'dbi:mysql:dbi_demo', 'andi', 'geheim') ||
     die "Kann keine Verbindung zum MySQL-Server aufbauen: $DBI::errstr\n";

# Datenbank-Verbindung beenden
$dbh->disconnect;

Dieses erste Beispiel erzeugt natürlich noch keinen HTML-Output. Es zeigt nur den Datenbank-Verbindungsaufbau.

Ausführen einer Abfrage

Nachdem die Verbindug zu einer Datenbank aufgebaut wurde, können Abfragen ausgeführt werden. Die einzelnen Schritte hierfür sind:

  1. Vorbereiten der Abfrage mit Hilfe der pepare()-Methode. Damit wird der eigentliche Befehl noch nicht ausgeführt sondern vom Datenbanktreiber zur Ausführung vorbereitet. Obwohl MySQL einen Prepare nicht unterstützt muss dieser Methodenaufruf erfolgen da er vom DBI-Modul für alle Folgeoperationen vorgeschrieben ist. Das Ergebnis des prepare()-Aufrufs ist ein "Statement-Handle". Wie bereits der Database-Handle ist auch der Statement-Handle eine Referenz über die nachfolgend alle weiteren Zugriffe auf diese Abfrage bzw. das Abfrageergebnis erfolgen.

    Syntax des prepare()-Statements:

    $sth = $dbh->prepare($statement)
    Dabei muss das Argument $statement ein gültiger SQL-Befehl sein. Falls sie mit SQL (Structured Query Language) noch nicht so vertraut sind (auswendig schaffe ich es bei schwierigen Abfragen auch nicht immer so ohne weiteres), können sie die Abfrage auch erst mal mit der grafischen Entwurfsoberfläche eines anderen Datenbanksystems erstellen. Gerade Microsoft Access mit seinem "Query by Entry" hilft da oft weiter. Dort haben sie dann auch die Möglichkeit in eine SQL-Ansicht umzuschalten und sich das SQL-Statement anzuschauen. Nach und nach schafft man die einfacheren Statements dann auch aus eigener Kraft.

    Ergebnis dieser Prozedur ist wie schon erwähnt ein sogenannter Statement-Handle mit dessen Hilfe sie nachfolgend die Abfrage auch tatsächlich ausführen und auf das Ergebnis zugreifen können. Der Variablenname für den Handle kann natürlich frei gewählt werden.

  2. Das vorbereite Statement kann nun mit Hilfe der execute-Methode ausgeführt werden. Der zugehörige Syntax lautet:
    $sth->execute();
    Sie können sich bei Bedarf die Anzahl der gefundenen Datensätze auch gleich in einer Variablen merken. Selbiges ist nämlich eines der Ergebnisse der execute-Methode.
    $dsnum = $sth->execute();
  3. Anschließend kann man mit diversen weiteren Methoden auf das Abfrageergebnis zugreifen. Dazu jedoch im nächsten Abschnitt. Wird das Abfrageergebnis nämlich nicht mehr benötigt, sollten die zugehörigen Ressourcen möglichst schnell wieder freigegeben werden da hier erheblicher Speicher belegt sein kann. Diese Freigabe erfolgt mittels:
    $sth->finish();
# Befehl fuer Ausfuehrung vorbereiten. Referenz auf Statement
# Handle Objekt wird zurueckgeliefert
my $sth = $dbh->prepare( 'SELECT * FROM tbl_art' ) ||
     die "Kann Statement nicht vorbereiten: $DBI::errstr\n";

# Vorbereitetes Statement (Abfrage) ausfuehren
$sth->execute() ||
     die "Kann Abfrage nicht ausfuehren:  $DBI::errstr\n";

# Statement-Handle Resources freigeben
$sth->finish();

Wie bereits das vorige Beispiel wird immer noch kein HTML-Output erzeugt. Wir sind jetzt aber bereits im Besitz eines Abfrageergebnisses und können bald daran gehen mit den hier verfügbaren Daten auch eine HTML-Seite zu erzeugen.

Zugriff auf das Abfrageergebnis

Der Abfragehandle ist nach geglückter Ausführung der Abfrage das Tor zum Abfrageergebnis. Z.B. durch Anwendung der fetchrow_array()-Methode auf den Statement-Handle wird nämlich ein Datensatz aus dem Abfrageergebnis geholt, und in einem Array als Rückgabewert geliefert. Die einzelnen Datenfelder sind die Arrayelemente welche nun für für eine weitere Be/Verarbeitung zur Verfügung stehen. Außerdem wird auf den nächsten Datensatz im Ergebnis positioniert. Ein weiteres fetchrow_array() liefert dann den nächsten Datensatz und so fort. Wird nach dem letzten Datensatz erneut fetchrow_array() angewendet, ist das Ergebnis 'undef', was in Perl ja aber geprüft werden kann.

Nachfolgend ein Codebeispiel zur Anwendung von "fetchrow_array()":

# Bearbeitung des Abfrageergenisses
while ( my @ergebnis = $sth->fetchrow_array() ){
  # Im Array @ergebnis steht nun ein Datensatz
  print $ergebnis[0] . " " . $ergebnis[1] . "\n";
}
Wird das Skript nun von "Hand" ausgeführt erhält man das folgende Ergebnis:
user@server:~> script3.pl
1 Zweibeiner
2 Vierbeiner
3 Keinbeiner
4 Mehrbeiner
user@server:~>

Das Abfrageergebnis als CGI-Skript mit HTML-Output

Damit das ganze jetzt noch ein CGI-Skript mit HMTL-Output wird, müssen noch die entsprechenden Ausgabeteile um das vorige Beispiel gruppiert werden. Das ist jetzt aber bereits die Kür und hat mit dem eigentlichen Datenbank-Kontakt nichts mehr zu tun (siehe dazu die Erläterung zum Perl5-Modul CGI.pm bzw. benutzen sie den Befehl "perldoc CGI").

Und hier das Ergebnis unserer Bemühungen.

#==================================================================
# Bearbeitung des Abfrageergenisses und Ausgabe als HTML-CGI
#==================================================================

# HTTP-Header ausgeben (Perl 5 CGI Library)
print $cgi_obj->header( -type=>'text/html',
                        -expires=>'+1h');

# HTML-Header ausgeben (Perl 5 CGI Library)
print $cgi_obj->start_html(-title=>'Wieviele Beine hast den du?',
                           -author=>'grupp@elektronikschule.de',
                           );

print $cgi_obj->h2('Zu welcher Sorte gehörst denn du?');
print "<ul>\n";

while ( my @ergebnis = $sth->fetchrow_array() ){
   # Im Array @ergebnis steht nun ein Datensatz
   print '<li>' . $ergebnis[1] . "</li>\n";
}

print "</ul>\n";

# HTML-Dokument beenden
print $cgi_obj->end_html();

Und hier das ganze Skript nochmal im Überblick

#!/usr/bin/perl -w
#
# Dateiname: script4.pl
#     Autor: A. Grupp

# Compilerpragma strict um keine Fehler zu machen :-)
use strict;
# Benutzung der Perl-Module CGI und DBI
use CGI;
use DBI;

# Neues CGI-Objekt erzeugen
my $cgi_obj = new CGI;

# Datenbank-Verbindung aufbauen
my $dbh = DBI->connect( 'dbi:mysql:dbi_demo', 'root', '') ||
     die "Kann keine Verbindung zum MySQL-Server aufbauen: $DBI::errstr\n";

# Befehl fuer Ausfuehrung vorbereiten. Referenz auf Statement
# Handle Objekt wird zurueckgeliefert
my $sth = $dbh->prepare( 'SELECT * FROM tbl_art' ) ||
     die "Kann Statement nicht vorbereiten: $DBI::errstr\n";

# Vorbereitetes Statement (Abfrage) ausfuehren
$sth->execute ||
     die "Kann Abfrage nicht ausfuehren: $DBI::errstr\n";

# HTTP-Header ausgeben (Perl 5 CGI Library)
print $cgi_obj->header( -type=>'text/html',
                        -expires=>'+1h');

# HTML-Header ausgeben (Perl 5 CGI Library)
print $cgi_obj->start_html(-title=>'Wieviele Beine hast den du?',
                           -author=>'grupp@elektronikschule.de',
                           );

print $cgi_obj->h2('Zu welcher Sorte gehörst denn du?');
print "<ul>\n";

while ( my @ergebnis = $sth->fetchrow_array() ){
   # Im Array @ergebnis steht nun ein Datensatz
   print '<li>' . $ergebnis[1] . "</li>\n";
}

print "</ul>\n";

# HTML-Dokument beenden
print $cgi_obj->end_html();

# Statement-Handle Resources freigeben
$sth->finish;

# Datenbank-Verbindung beenden
$dbh->disconnect;

Verknüpfung des Resultats des 1. Skripts mit Detaildatensätzen via Hyperlink auf 2. Skript

Die Beispiel-Datenbank besteht aus zwei Tabellen. Bisher haben wir nur die erste Tabelle abgefragt. Das Ergebnis dieser Bemühungen kann man nun aber gleich als Hyperlink gestalten. Über den Hyperlink wird dann ein weiteres Skript aufgerufen das zu einer Art zugehörigen Detaildatensätze aus der zweiten Tabelle extrahiert und anzeigt. Hierzu sind im ersten Skript geringfügige Änderungen bei der Ausgabe nötig.

while ( my @ergebnis = $sth->fetchrow_array() ){
   # Im Array @ergebnis steht nun ein Datensatz
   print '<li><a href="script6.pl?art=' . $ergebnis[0]
          . '">' . $ergebnis[1] . "</a></li>\n";
}

Das erste Datenfeld $ergebnis[0] enthät den Index um in der zweiten Tabelle die verwandten Datensätze aufzufinden. In die Ausgabe wird nun ein Hyperlink integriert der auf das Perlskript "script6.pl" verweist. Diesem Skript wird aber noch ein Parameter mit auf die Reise gegeben. Dies geschieht durch Anhängen von z.B. "?art=1". Den "?art="-Teil findet man ja noch ganz einfach. Die Nummer soll aber flexibel von Hyperlink zu Hyperlink wechseln und die richtige Kennziffer beinhalten. Durch Stringverkettung gelingt dies mit Hilfe des Datenfelds $ergebnis[0]. Innerhalb des Hyperlinks wird der Obergriff aus Datenfeld $ergebnis[1] angezeigt.

Das Perlskript "script6.pl" muß nun auf die übergebene Kennziffer "art=x" zugreifen. Hierfür gibt es ja die elegante Variante via CGI.pm.

my $art;
if ( defined($cgi_obj->param('art')) ){
   $art = $cgi_obj->param('art');
}
else{
   die "Dieses Skript muß anders aufgerufen werden!";
}

Nun haben wir es schon fast. Mit Hilfe der Variablen "$art" können wir unsere ursprüngliche Abfrage etwas modifizieren. Es wird natürlich die zweite Tabelle abgefragt und es wird eine kleine WHERE-Klausel an die Abfrage angehängt.

# Befehl fuer Ausfuehrung vorbereiten. Referenz auf Statement
# Handle Objekt wird zurueckgeliefert
my $sth = $dbh->prepare( "SELECT * FROM tbl_bsp WHERE bsp_artID=$art" ) ||
     die "Kann Statement nicht vorbereiten: $DBI::errstr\n";

Das restliche Folgeskript "script6.pl" müssten Sie eigentlich schon selbst erstellen können.

Auch das Zusammenspiel dieser beiden Skripte lässt sich natürlich wie immer ausprobieren.

ACHTUNG: In diesem Beispiel werden erstmals Daten die über das Internet gesendet wurden in einen SQL-Befehl eingebaut. Normalerweise stammen diese Daten aus den von uns selbst erzeugten Hyperlinks und sollten deshalb stimmen und gefahrlos sein. Nun gibt es aber böse Menschen die versuchen derartige Codestellen auszunutzen und "selbstgeschnitzte" Daten einschleusen. Damit stürzt im besten Fall das Skript ab - im schlechtesten Fall tut es Dinge die Sie nicht haben wollen (z.B. Löschen von Datensätzen, Dateien, ..., Einschleusen weiteren Codes, ...) und ist dann eine riesige Gefahr!!! So wie im obigen Beispiel dürfen Sie deshalb von Außen stammende Daten nie weiterverwenden. Hier sind vorher Tests durchzuführen und problematische Zeichen zu entfernen. Hierzu finden Sie weiter unten einen kurzen Abschnitt den Sie unbedingt lesen sollten bevor Sie eigene Anwendungen auf einem Internet-Server platzieren.

Einfügen neuer Datensätzen

In eine Datenbank lassen sich auf diese Art und Weise auch neue Datensätze einfügen. So könnte es im obigen Beispiel z.B. den Wunsch geben zusätzliche "Füssler" in die Datenbank zu schreiben. Dazu muß natürlich der Wert für den neuen Eintrag beim Benutzer mit Hilfe eines Formulars abgefragt werden. Das Formular wird dabei 2 Dinge abfragen:

  1. Die Sorte des neuen "Füsslers" (Feldname sorte mit einem Wertebereich von 1-5).
  2. Die Bezeichnung des neuen "Füsslers" (Feldname bezeichnung als freies Textfeld).

Gegenüber den bisherigen Skripten gibt es aber auch hier nicht allzu viel Neues da das eigentlich Prinzip gleich bleibt. Die vom Formular übergebenen Werte werden mit Hilfe des CGI.pm-Moduls in eine Variable des Skripts eingelesen (beachten Sie auch hier wieder, dass die Daten von Außen kommen und vor der Verwendung eigentlich erst "unschädlich" gemacht werden müssen!!!)

my $sorte;
if ( defined($cgi_obj->param('sorte')) ){
   $sorte = $cgi_obj->param('sorte');
}
my $bezeichnung;
if ( defined($cgi_obj->param('bezeichnung')) ){
   $bezeichnung = $cgi_obj->param('bezeichnung');
}

Die Hauptänderung betrifft den SQL-String der Abfrage. Bislang hatten wir hier immer Abragen im eigentlichen Sinn. Der nun verwendete SQL-String ist eher als höfliche Anfrage an den Datenbankserver zu verstehen. Würde man sie in Worte fassen, so könnte man dies so versuchen:

Lieber Datenbankserver, könntest du mir in die Tabelle tbl_bsp einen neuen Datensatz mit den Werten ... eintragen?.

In SQL-Syntax sieht das etwas nüchterner aus. Soll beispielsweise für die Sorte 2 (Vierbeiner) eine "Kuh" eingetragen werden, so ist das in SQL folgendermaßen zu formulieren:

INSERT INTO tbl_bsp SET bsp_artID='2', bsp_text='Kuh';

Anstelle der beiden festen Werte (2 und Kuh) werden die beiden obigen Variablen $sorte und $bezeichnung eingesetzt. Das entsprechende Statement hat damit dieses Aussehen:

my $sql = "INSERT INTO tbl_bsp SET bsp_artID='$sorte', bsp_text='$bezeichnung'";
my $sth = $dbh->prepare( $sql ) ||
     die "Kann Statement nicht vorbereiten: $DBI::errstr\n";

Das Ganze wird noch mit ein wenig Ausgabekosmetik versehen und verhält sich dann so.

WICHTIG: Sicherheitsüberprüfung der CGI-Parameter als "Abfangjäger" für Angreifer!

Bei Verwendung des obigen SQL-Statements kann es noch gravierende Probleme geben. Stellen Sie sich einfach vor jemand versucht über das Formular den Begriff "Has'" einzugeben. Beachten Sie hierbei das einzelne Hochkomma an der schwäbischen Bezeichnung für einen "Hase" :-). Wenn Sie nun diesen als CGI-Parameter übergebenen Datenwert einfach ungeprüft in Ihren SQL-Befehl einfügen, dann sieht das Resultat so aus:

INSERT INTO tbl_bsp SET bsp_artID='2', bsp_text='Has'';

Am Ende des Befehls sehen sie die zwei aufeinander folgenden Hochkommas. Das eine Hochkomma gehört zum Begriff und soll in die Datenbank übernommen werden. Das zweite Hochkomma schließt den Sollinhalt des Datenfelds ab. Leider ist das aber ein SQL-Syntaxfehler und das bisherige Erfassungsskript stürzt hier mit einem Fehler ab. Je nach Datenbank und SQL-Know-How des "Benutzers" ist ein böswilliger Angreifer auf diese Art und Weise sogar in der Lage Ihren SQL-Befehl so abzuändern, dass er ganz andere Dinge tut als Sie ursprünglich vorgesehen hatten! In diesem Fall wird der "Benutzer" ganz einfach zum "Angreifer" :-(! Deshalb sollten sie DRINGEND alle Parameter die Ihnen von Außen in Ihr Skript gereicht werden nach Zeichen "abklopfen" die solche Effekte erzielen könnten (' " % * ...). Diesen Zeichen ist dann z.B. in MySQL ein \ (Backslash) voranzustellen! Beispiel: \'. Netterweise liefert das DBI-Modul bereits eine entsprechende Funktion! Sie heißt $dbh->quote() und

Sie sollten prinzipiell jeden von Außen gelieferten Parameter auf diese Weise quoten. Diese Aufforderung betrifft auch Parameter die Sie in Ihrem Formular fest vorgegeben haben oder auch Hidden-Felder. Es gibt nälich auch die Möglichkeit Ihrem Skript die Daten unter Umgehung Ihres Formulars "unterzuschieben". Dabei lassen sich dann beliebige Daten an das Skript schicken. Achtung: Das gilt auch wenn Sie nur die POST-Methode zulassen und zusätzlich den Referer auswerten (alles nur eine Frage der Angriffsenergie)!

Das Quoten könnte beispielsweise folgendermaßen in unser bisheriges Skript eingefügt werden:

my $bezeichnung = $dbh->quote($bezeichnung);
my $sorte = $dbh->quote($sorte);
my $sql = "INSERT INTO tbl_bsp SET bsp_artID=$sorte, bsp_text=$bezeichnung";
my $sth = $dbh->prepare( $sql ) ||
     die "Kann Statement nicht vorbereiten: $DBI::errstr\n";

Beachten Sie, dass im SQL-Befehl die Hochkommas um die beiden Variablen $sorte und $bezeichnung entfernt wurden. Die Hochkommas befinden sich ja, wie bereits erwähnt, im Variableninhalt! Dieses korrigierte Skript kann dann auch mit Hochkommas umgehen und verhält sich dann so.

Vorläufiges Schlusswort (die Seite lebt :-), kommen Sie also gelegentlich mal wieder vorbei)

Falls sie das alles in Ruhe zuhause ausprobieren wollen, können sie hier alle Beispielskripte dieses kleinen Kurses herunterzuladen.

Falls sie es bis hierher durchgehalten habe, wünsche ich ihnen viel Erfolg und würde mich freuen von ihnen zu hören (Kritik, Anregungen, Lob, ...)


Zurück

Zugriffszähler Lokal bzw. Remote-Zugriffe seit dem 20. Dezember 1998