mitmario.dev

Anlegen und anzeigen

PHP Sandbox 3 Min Lesezeit 2 BeispieleLektion 4 von 7

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

Welche Adresse durchkommt
<?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.txt ist 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

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 &amp; 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, kostenlos

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