HACK #55 Hack Doppeltes Abschicken von Formularen verhindern Durch die Verwendung einer Transaktionstabelle in Ihrer Datenbank können Sie das klassische Problem der doppelt abgeschickten Formulare verhindern. Beim Entwurf von Webapplikationen gibt es zwei Dinge, die mir so richtig auf die Nerven gehen: Eines davon ist die Menge an schlechtem Code, mit dem das Problem des doppelten Abschickens von Formularen gelöst werden soll. Wie oft haben Sie schon eine E-Commerce-Website gesehen, die Sie anfleht:»bitte den Senden-Button nicht zweimal anklicken.«? Dieses klassische Problem tritt auf, wenn ein Browser den Inhalt eines Webformulars mehr als einmal an den Server schickt. Andererseits ist das genau das Verhalten, das von einem gut erzogenen Webbrowser erwartet wird, wenn der Benutzer den Senden-Button zweimal anklickt; es liegt also beim Server herauszufinden, ob ein Fehler vorliegt oder nicht. In Abbildung 6-8 sehen Sie eine grafische Darstellung des Problems. Der Browser schickt zwei identische Anfragen an den Server, da der Benutzer zweimal geklickt hat. Die erste Senden-Anweisung wird akzeptiert, und bevor der Server die Antwort schicken kann, wird das Formular ein zweites Mal gesendet. Dann wird die erste Antwort an den Browser zurückgegeben, gefolgt von der zweiten Serverantwort. #55 Abbildung 6-8: Abfolge beim doppelten Abschicken von Formularen Abbildung 6-9 zeigt eine Möglichkeit, das Problem mit dem doppelten Abschicken in den Griff zu bekommen. Die erste Abfrage speichert in der zu verarbeitenden Seite eine einmalige ID. Kommt eine zweite Abfrage mit der gleichen ID beim Server an, kann die redundante Transaktion abgelehnt werden. Hack #55: Doppeltes Abschicken von Formularen verhindern 243
#55 Doppeltes Abschicken von Formularen verhindern Abbildung 6-9: Die Lösung des Problems besteht darin, die zweite Anforderung abzulehnen Der Code Sichern Sie den Code aus Beispiel 6-7 als db.sql. Beispiel 6-7: Der SQL-Code für die Transaktionstabelle DROP TABLE IF EXISTS transcheck; CREATE TABLE transcheck ( transid TEXT, posted TIMESTAMP ); Speichern Sie nun den in Beispiel 6-8 gezeigten Code als index.php. Beispiel 6-8: Das HTML-Formular, das die Transaktions-ID enthält <? require_once( "trans.php" );?> <html> <body> <form action="handler.php" method="post"> <input type="hidden" name="transid" value="<?php echo( get_transid( ) );?>" /> Name: <input type="text" /><br/> Menge: <input type="text" size="5" /><br/> <input type="submit" /> </format> Der Code aus Beispiel 6-9 gehört in die Datei handler.php. Beispiel 6-9: Der Code, der die Formulardaten entgegennimmt und die Transaktion überprüft <? require_once( "trans.php" );?> <html> <body> <?php if ( check_transid( $_POST["transid"] ) )?> Dieses Formular wurde bereits abgeschickt. <?php else 244 Kapitel 6: Applikationsdesign
Beispiel 6-9: Der Code, der die Formulardaten entgegennimmt und die Transaktion überprüft (Fortsetzung) add_transid( $_POST["transid"] );?> Herzlichen Glückwunsch zum Kauf dieses unglaublich wertvollen Produkts. Vielen Dank! <?php?> </body> </html> Speichern Sie den Code aus Beispiel 6-10 in der Datei trans.php. Beispiel 6-10: Die Bibliothek zum Überprüfen der Transaktion <?php require_once( "DB.php" ); $dsn = 'mysql://benutzername:passwort@localhost/transtest'; $db =& DB::Connect( $dsn, array( ) ); if (PEAR::isError($db)) die($db->getmessage( )); function check_transid( $id ) global $db; $res = $db->query( "SELECT COUNT(transid) FROM transcheck WHERE transid=?", array($id) ); $res->fetchinto($row); return $row[0]; function add_transid( $id ) global $db; $sth = $db->prepare( "INSERT INTO transcheck VALUES(?, now( ) )" ); $db->execute( $sth, array( $id ) ); function get_transid( ) $id = mt_rand( ); while( check_transid( $id ) ) $id = mt_rand( ); return $id;?> Den Hack ausführen Laden Sie die Dateien auf Ihren Server hoch und verwenden Sie dann den Befehl mysql, um das Schema aus db.sql in Ihre Datenbank zu laden: mysql --user=benutzername --password=passwort mydb < db.sql Hack #55: Doppeltes Abschicken von Formularen verhindern 245
#55 Doppeltes Abschicken von Formularen verhindern Wenn Sie nun die Datei index.php in Ihrem Browser aufrufen, wird das einfache E-Commerce-Formular angezeigt, zu sehen in Abbildung 6-10. Abbildung 6-10: Das einfache E-Commerce-Formular Geben Sie ein paar Daten ein und klicken Sie auf Senden. Das Ergebnis sollte aussehen wie die Anzeige in Abbildung 6-11. Das ist ein guter Anfang, da wir offensichtlich in der Lage sind, erfolgreiche Transaktionen durchzuführen. Abbildung 6-11: Ein erfolgreicher Geschäftsabschluss Klicken Sie nun auf den Zurück-Button Ihres Browsers und schicken Sie das Formular erneut ab. Das Ergebnis sollte nun aussehen wie Abbildung 6-12. Im Verlauf dieser Transaktion fordert index.php eine einmalige ID vom Skript trans.php an. Das Skript handler.php, das die Formulardaten entgegennimmt, überprüft durch einen Aufruf der Funktion check_transid( ), ob die ID bereits in Benutzung ist. In diesem Fall sollte der Code das Ergebnis in Abbildung 6-12 zurückgeben. Befindet sich die ID noch nicht in der Datenbank, verwenden Sie die Funktion add_transid( ), um eine neue ID in die Datenbank einzufügen und dem 246 Kapitel 6: Applikationsdesign
Abbildung 6-12: Die Anzeige nach einem erneuten Abschicken des Formulars Benutzer anzuzeigen, dass die Transaktion erfolgreich war, wie in Abbildung 6-11 gezeigt. Der aufmerksame Leser wird gemerkt haben, dass hier eine Konkurrenzsituation entstehen kann. Muss zwischen dem Aufruf von check_transid( ) und add_transid( ) ein weiteres Formular bearbeitet werden, kann es zu einem doppelt abgeschickten Formular kommen, dessen Verarbeitung gültig ist. Unterstützt Ihre Datenbank gespeicherte Prozeduren (»stored procedures«), können Sie eine einzelne Prozedur schreiben, die überprüft, ob die Transaktion beendet wurde, und diese dann einer entsprechenden Liste hinzufügen. Auf diese Weise kann das Konkurrenzproblem vermieden werden, wobei gleichzeitig sichergestellt wird, dass Formulare nicht doppelt abgeschickt werden. Ab MySQL 5.0 werden auch gespeicherte Prozeduren unterstützt. Hack #55: Doppeltes Abschicken von Formularen verhindern 247