Abschnitt 18 · Lektion 4
Anlegen und anzeigen
Jetzt kommt der Teil, für den die ganze Anwendung da ist: Jemand trägt etwas ein, und es bleibt
liegen. Technisch ist das ein Formular, eine Prüfung, ein INSERT und eine Weiterleitung. Drei davon
kennst du. Bei der Prüfung lohnt es sich, genauer hinzusehen.
Was eine Adresse zu einer Adresse macht
<?php
declare(strict_types=1);
// Was eine Adresse ist, entscheidet nicht der Geschmack, sondern die Frage:
// Was passiert, wenn ein Besucher darauf klickt?
function istErlaubteAdresse(string $url): bool
{
return str_starts_with($url, "http://") || str_starts_with($url, "https://");
}
$kandidaten = [
"https://astro.build",
"http://example.org/seite?a=1",
"javascript:alert(1)",
"javascript://astro.build/%0aalert(1)",
"ftp://example.org/datei.txt",
"astro.build",
" https://astro.build",
"HTTPS://astro.build",
"",
];
echo str_pad("Adresse", 40), str_pad("FILTER_VALIDATE_URL", 22), "eigene Pruefung\n";
echo str_repeat("-", 78), "\n";
foreach ($kandidaten as $url) {
echo str_pad('"' . $url . '"', 40),
str_pad(filter_var($url, FILTER_VALIDATE_URL) === false ? "nein" : "ja", 22),
istErlaubteAdresse($url) ? "ja" : "nein", "\n";
} Ein Lesezeichen ist eine Zeile, auf die später jemand klickt. Damit ist die Frage nicht, ob die Eingabe wie eine Adresse aussieht, sondern was der Browser tut, wenn er sie öffnet.
PHP bringt für die erste Frage etwas mit: filter_var($url, FILTER_VALIDATE_URL). Sieh dir im
ersten Beispiel die mittlere Spalte an. Die Funktion tut, was ihr Name sagt, und mehr nicht:
ftp://example.org/datei.txtist eine gültige Adresse. Für ein Lesezeichen in dieser Anwendung trotzdem nichts.javascript://astro.build/%0aalert(1)ist ebenfalls gültig. Es sieht aus wie eine Adresse, hat ein Schema, zwei Schrägstriche und einen Rechnernamen. Ein Klick darauf führt Code aus.
Deshalb steht in der dritten Spalte eine eigene Prüfung, und sie fragt nicht „ist das eine Adresse”,
sondern „fängt das mit http:// oder https:// an”. Das ist eine Erlaubnisliste: Was nicht
ausdrücklich erlaubt ist, kommt nicht durch. Bei allem, was mit Sicherheit zu tun hat, ist das die
Richtung, die hält. Eine Verbotsliste („alles außer javascript:”) ist immer nur so gut wie die
Fantasie dessen, der sie geschrieben hat.
Zwei Zeilen der Tabelle sind Grenzfälle mit Ansage. https://astro.build mit Leerzeichen davor
kommt nicht durch, deshalb steht in der Aufgabe ein trim() davor. Und HTTPS://astro.build in
Großbuchstaben kommt ebenfalls nicht durch, obwohl jeder Browser es öffnen würde. Wer das ändern
will, legt ein strtolower() um den Vergleich. Es ist die erste Zeile, die man an dieser Funktion
ergänzt, sobald sich jemand beschwert.
Absenden, weiterleiten, anzeigen
<?php
// Ein Formular, eine Liste, eine Meldung. Im Reiter Browser kannst du es
// bedienen: etwas eintragen, ein leeres Feld abschicken, danach auf Seite
// neu laden klicken. Die Liste liegt in der Sitzung, eine Datenbank
// braucht es dafuer nicht.
session_start();
$_SESSION["liste"] ??= [];
$pfad = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($pfad === "/eintragen" && $_SERVER["REQUEST_METHOD"] === "POST") {
$text = trim($_POST["text"] ?? "");
if ($text === "") {
// Die Meldung wird gemerkt, nicht ausgegeben. Ausgeben tut sie die
// Seite, auf der der Besucher gleich landet.
$_SESSION["meldung"] = "Da stand nichts drin.";
} else {
$_SESSION["liste"][] = $text;
$_SESSION["meldung"] = "Eingetragen: " . $text;
}
// Das P und das R aus Lektion 11.3: Der POST antwortet nie mit einer
// Seite, sondern mit einer Weiterleitung. Deshalb laedt ein Neuladen die
// Liste und schickt nicht das Formular noch einmal ab.
header("Location: /", true, 302);
exit;
}
// Eine Meldung wird beim Lesen verbraucht, sonst stuende sie ewig da.
$meldung = $_SESSION["meldung"] ?? null;
unset($_SESSION["meldung"]);
?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Absenden und weiterleiten</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}
.meldung{background:#eef;padding:.4rem .8rem}</style></head>
<body>
<h1>Einkaufszettel</h1>
<?php if ($meldung !== null) { ?>
<p class="meldung"><?= htmlspecialchars($meldung, ENT_QUOTES, "UTF-8") ?></p>
<?php } ?>
<form method="post" action="/eintragen">
<p><input type="text" name="text" size="30"> <button type="submit">Eintragen</button></p>
</form>
<?php if ($_SESSION["liste"] === []) { ?>
<p>Noch nichts eingetragen.</p>
<?php } else { ?>
<ul>
<?php foreach ($_SESSION["liste"] as $eintrag) { ?>
<li><?= htmlspecialchars($eintrag, ENT_QUOTES, "UTF-8") ?></li>
<?php } ?>
</ul>
<p>Eintraege: <?= count($_SESSION["liste"]) ?></p>
<?php } ?>
</body>
</html> Das zweite Beispiel ist ein Einkaufszettel in der Sitzung, ohne Datenbank, und es zeigt die drei Bewegungen, die dein Anlegen gleich macht. Trag im Reiter Browser etwas ein, schick danach ein leeres Feld ab und klick dann auf Seite neu laden.
Der POST antwortet nie mit einer Seite. Er antwortet mit einer Weiterleitung, und erst die
folgende Anfrage bekommt HTML. Das ist das Muster aus Lektion 11.3, und im Reiter Netzwerk steht
es nach jedem Eintragen als zwei Zeilen: POST /eintragen mit 302, dann GET / mit 200. Die Seite
vor dir ist die Antwort auf den GET. Deshalb holt Seite neu laden einfach / noch einmal, und
kein Browser fragt, ob er das Formular erneut abschicken soll.
Die Meldung wird gemerkt, nicht ausgegeben. Zwischen dem Absenden und der Anzeige liegt eine zweite Anfrage, und die weiß von der ersten nichts. Deshalb legt der POST seine Meldung in die Sitzung, und die Seite danach holt sie ab und löscht sie dabei. Ohne das Löschen stünde sie beim nächsten Aufruf noch einmal da. Auch das kennst du, aus Lektion 12.4.
Der abgewiesene Fall geht denselben Weg wie der erfolgreiche. Beide leiten weiter, beide hinterlassen eine Meldung. Nur der eine trägt etwas ein und der andere nicht. Das ist bequemer als es klingt: Die Funktion hat genau einen Ausgang, und man muss nicht an zwei Stellen daran denken, dass die Antwort eine Weiterleitung sein soll.
Wo maskiert wird
Was jemand einträgt, landet später auf einer Seite. Die Regel aus Abschnitt 15 gilt unverändert:
roh speichern, beim Ausgeben maskieren. In der Datenbank steht der Titel so, wie er eingetippt
wurde, und die Vorlage legt e() darum.
In diesem Projekt ist das schon erledigt, ansichten/liste.php ist gegeben und maskiert jeden Wert.
Der Grund, es trotzdem noch einmal zu erwähnen: Die Versuchung, beim Speichern zu maskieren, ist
groß, weil es sich sicherer anfühlt. Es ist die falsche Stelle. Man merkt es spätestens dann, wenn
derselbe Titel einmal in einer E-Mail und einmal in einer Datei landen soll, wo & nur stört.
Zum Mitnehmen
Eine Adresse entgegennehmen klingt nach dem einfachsten Teil des Projekts. Es ist die Stelle, an der fremder Text in deine Seite kommt, und die Frage lautet nicht, ob er richtig aussieht, sondern was passiert, wenn jemand darauf klickt.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 2 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.