DSP-Logo
previous up next
contents index
Previous: PHP & MySQL Up: Einführung PHP Next: Reguläre Ausdrücke

Unterabschnitte

PHP & HTTP


Header

Neben der eigentlichen Seite schickt der Server an den Client (Browser) noch einige Zusatzinformationen. Diese werden vor der eigentlichen Seite im sog. Header gesendet. Mit diesen Informationen sagt der Server z.B., ob die angezeigte Seite wirklich die gewünschte Seite ist (Status `200 Found`), oder ob die Seite nicht gefunden werden konnte und deshalb eine Fehlerseite angezeigt wird (Status `404 Not Found`). Auch kann der Server dem Client mitteilen, daß die Seite sich unter einer anderen Adresse befindet (Status `301 Moved Permanently` oder `302 Found`). Es kann auch die Aufforderung geschickt werden, sich zu authentifizieren (Status `401 Unauthorized` ).

Zusätzlich zum Status einer Seite kann auch übermittelt werden, wann die Seite zum letzten Mal verändert wurde (Last-Modified), ob sie gecached werden darf (Cache-Control) und wenn ja wie lange (Expires) oder welchen Typ ihr Inhalt hat (Content-Type).

Normalerweise sendet der Webserver (in der Regel Apache) automatisch den richtigen Header. Mit PHP kann man den gesendeten Header allerdings beeinflussen. Zu beachten ist, daß kein einziges Zeichen vor der header-Anweisung ausgegeben werden darf! Ausgeben heißt in diesem Fall: Die Seite muß unbedingt mit PHP-Code (<?php) anfangen und darf vor dieser Codemarke nichts (nicht einmal ein Leerzeichen oder einen Zeilenumbruch) enthalten. Auch innerhalb der Codemarken dürfen Ausgaben mittels echo, print etc. erst nach dem Senden der Headerangaben gemacht werden.

Wenn PHP als CGI installiert ist, gibt es außerdem einige Einschränkungen, z.B. kann keine Authentifizierung gemacht werden (mehr dazu siehe weiter unten).

Wie der Header aussehen muß, ist in dem RFC[*] 2616 festgelegt. Er spezifiziert das HTTP/1.1 Protokoll. Im Folgenden zeige ich ein paar Möglichkeiten der Anwendung der header-Anweisung.


Weiterleiten

Wie bereits oben erwähnt, kann man, neben JavaScript und HTML[*], auch mit PHP den Client auf eine andere Seite weiterleiten. Dies geschieht mit folgender Anweisung:
header('Location: absolute_URL');
exit;
absolute_URL muß natürlich durch die gewünschte URL ersetzt werden. Es muß nach RFC die absolute URL angegeben werden, auch wenn fast alle Browser eine relative verstehen!

Das exit ist nicht unbedingt notwendig, allerdings würde es nichts bringen, nach dem header noch etwas auszugeben, da es sowieso nicht angezeigt wird.

Bei dieser Anweisung sendet Apache automatisch den Statuscode 302.


Nicht gefunden

Wenn du Apache so konfiguriert hast, daß er als Fehlerseite eine PHP-Seite anzeigt, wird als Statuscode 200 (OK) gesendet. Da dies aber unpraktisch ist, weil so z.B. Suchmaschinen deine Fehlerseite in ihren Index aufnehmen, solltest du den Statuscode 404 (Not Found) senden, wodurch diese Seite als Fehlerseite erkannt wird. Die Anweisung dazu lautet wie folgt:
header('HTTP/1.0 404 Not Found');


Authentifizierung

Mit PHP besteht die Möglichkeit, den Browser ein Fenster öffnen zu lassen, in dem Name und Paßwort eingetragen werden müssen. Wenn PHP nicht als Modul, sondern als CGI läuft, funktioniert das allerdings nicht[*].

Es ist eigentlich ganz einfach, eine solche Datei muß vom Prinzip her so aussehen:

<?php
  if($PHP_AUTH_USER!="Christoph" OR $PHP_AUTH_PW!="Reeg") {
    Header('HTTP/1.1 401 Unauthorized');
    Header('WWW-Authenticate: Basic realm="Top Secret"');
    echo "Mit Abbrechen kommst Du hier nicht rein. ;-) \n";
    exit;
  }
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
 <title>Authentification</title>
</head>
<body>
<h1>Hier ist der Top-Secret Bereich</h1>
<h2><?php
  echo "Username: ".$PHP_AUTH_USER." Paßwort: ".$PHP_AUTH_PW;
?></h2>
</body>
</html>
Das Funktionsprinzip ist ganz einfach: Beim ersten Aufruf sind im Array ,, _SERVER`` die beiden Stellen `,,PHP_AUTH_USER` und ` PHP_AUTH_PW` nicht gesetzt. Dadurch wird der Bereich in der IF-Abfrage bearbeitet. Hier werden die beiden Header zurückgegeben, die den Browser veranlassen, nach Usernamen und Paßwort zu fragen. Diese beiden Zeilen müssen fast genau so übernommen werden, damit es funktioniert![*] Das einzige, was geändert werden darf, ist das `Top Secret`. Der Text danach wird nur dann ausgegeben, wenn jemand bei der Paßwortabfrage auf ,Abbrechen` klickt (oder, im Falle des Internet Explorers, drei Versuche, sich zu authentifizieren, mißlungen sind); dann springt der Webserver nach dem `echo` aus der Datei und der Rest wird nicht mehr ausgegeben. Wenn jedoch jemand das richtige Paßwort mit dem richtigen Usernamen eingegeben hat, wird der Bereich in der IF-Abfrage nicht bearbeitet und der Rest der Datei wird abgearbeitet. In unserem Fall wird die Überschrift ,,Hier ist der Top-Secret Bereich`` und die Zeile ,,Username: Christoph Paßwort: Reeg`` im HTML-Format ausgegeben.

Es gibt noch ein kleines Sicherheitsproblem bei der ganzen Sache - der Browser behält sich nämlich den Usernamen und das Paßwort, so daß die Autoren derjenigen Seiten, die man nach der Paßworteingabe abruft, theoretisch das Paßwort abfragen könnten. Dies kann man jedoch ganz einfach verhindern, indem man den Browser komplett beendet.

Auf fast dieselbe Weise kann man sich natürlich auch direkt für den Zugriff auf eine Datenbank authentifizieren. Der folgende Quelltext zeigt, wie man dies erreicht:

<?php
if ($_SERVER["PHP_AUTH_USER"] == ""
      OR !@mysql_connect("localhost",
                         $_SERVER["PHP_AUTH_USER"],
                         $_SERVER["PHP_AUTH_PW"])) {
    Header('HTTP/1.0 401 Unauthorized');
    Header('WWW-Authenticate: Basic realm="Top Secret"');
    echo "Mit Abbrechen kommst Du hier nicht rein. ;-)\n";
    exit;
 }
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
 <title>Authentification</title>
</head>
<body>
<h1>Hier ist der Top-Secret Bereich</h1>
<h2><?php
  echo "Username: ".$_SERVER["PHP_AUTH_USER"];
  echo " Paßwort: ".$_SERVER["PHP_AUTH_PW"];
?></h2>
</body>
</html>
Das `@`-Zeichen vor dem mysql_connect hat nichts mit der if-Abfrage zu tun. Es sorgt dafür, daß keine Fehlermeldung beim Aufruf von mysql_connect ausgegeben wird. Die Fehlermeldung würde nicht nur stören, sondern sie würde auch die Paßwortabfrage zuverlässig verhindern. Vor dem header-Aufruf darf nichts ausgegeben werden.

Der Bereich in der obigen IF-Abfrage wird genau dann nicht bearbeitet, wenn mittels Benutzername und Paßwort eine Verbindung zur Datenbank aufgebaut werden konnte. In jedem anderen Fall wird, wie im ersten Beispiel, abgebrochen und (in diesem Fall) der Text ,,Mit Abbrechen...`` ausgegeben. Um sich Probleme zu ersparen, sollte man obige Bedingung der IF-Anweisung einfach 1:1 übernehmen, denn diese ist bestens erprobt! :-)

Noch eine Anmerkung zum Schluß: Anstatt der Zeichenkette ``HTTP/1.0 401 Unauthorized`` kann auch ``Status: 401 Unauthorized`` benutzt werden. Im Falle des o.g. PHP-CGI-Problems scheint es dann so, als ob die Authentifizierung funktionieren würde (es tritt kein Fehler 500 mehr auf); dies ist jedoch ein Trugschluß, denn trotz allem werden die beiden benötigten Authentifizierungs-Variablen nicht mit den Werten gefüllt, die der Browser nach der Eingabe durch den Benutzer im entsprechenden Dialog zurückliefert.


Download

In bestimmten Fällen wäre es schön, wenn ein PHP-Script die von ihm erzeugten Daten nicht einfach in Form einer HTML-Seite ausgeben, sondern diese an den Client senden könnte. Dieser sollte dann die Daten z.B. in Form einer Datei abspeichern oder auf sonstige Weise verarbeiten (an spezielle Applikationen übergeben).

Solche Situationen gibt es z.B. bei Anhängen (Attachments) in einem Webmail-System. Normalerweise wird die Ausgabe eines PHP-Scripts als HTML interpretiert, welches der Browser anzeigen soll. Damit der Browser die Datei aber direkt auf die Platte speichert (bzw. dem Benutzer überläßt, was er damit machen will), muß die Angabe über den Typ des Dateiinhalts für die Übertragung geändert werden. Das geschieht mit folgender Anweisung (siehe auch weiter unten):

header("Content-Type: application/octetstream");

Wenn nichts anderes angegeben wird, benutzt der Browser den Dateinamen des Scripts aus der URL als Dateinamen zum Abspeichern.

header("Content-Disposition: attachment; filename=datei_name.ext");
Mit diesem Header wird der Dateiname auf ,,datei_name.ext`` gesetzt. Man beachte das Fehlen von Quoting-Zeichen wie etwa Hochkommata[*]. Grund hierfür ist, daß bestimmte Browser wie der IE sonst die Quoting-Zeichen als Teil des Dateinamens ansehen. Natürlich kann anstelle des hier beispielhaft eingetragenen jeder mögliche Dateiname stehen. Eventuelle Pfadangaben sollen explizit ignoriert werden. D.h. es ist möglich den Dateinamen festzulegen, aber nicht in welches Verzeichnis die Datei gespeichert werden sollte.

Microsoft liest die RFCs scheinbar anders als alle anderen (oder gar nicht?), so daß der IE 5.5[*]nur folgenden Header versteht:

header("Content-Disposition: filename=datei_name.ext");

Über die Variable _SERVER["HTTP_USER_AGENT"] können wir PHP auch entscheiden lassen, welche Variante wahrscheinlich die richtige ist.

header("Content-Disposition: ".
       (strpos($_SERVER["HTTP_USER_AGENT"],"MSIE 5.5")?""
                                           :"attachment; ").
       "filename=datei_name.ext");
Die Variante, den Dateinamen über Header festzulegen, hat einen kleinen Nachteil: Wenn der Nutzer später im Browser nicht auf den Link klickt, um dann die Datei zu speichern, sondern direkt über ,,Save Link as`` speichern will, konnte noch kein Header gesendet werden, so daß der Browser den Dateinamen nicht kennt und wieder den Dateinamen des Scripts vorschlägt. Das kann nur umgangen werden, indem man dafür sorgt, daß der gewünschte Dateiname in der URL steht. Dies ist wiederum nur über Funktionen des Webservers möglich. Beim Apache sind das die Funktionen Rewrite und Redirect.

Die Erfahrung hat gezeigt, daß ein ,,Content-Transfer-Encoding`` Header die ganze Sache sicherer macht, auch wenn er laut RFC 2616 nicht benutzt wird.

header("Content-Transfer-Encoding: binary");

Die ,,großen`` Browser zeigen beim Download häufig einen Fortschrittsbalken an. Dies funktioniert allerdings nur dann, wenn der Browser weiß, wie groß die Datei ist. Die Größe der Datei in Bytes wird über den ,,Content-Length`` Header angegeben.

header("Content-Length: {Dateigröße}");

Zusammenfassend können wir nun folgenden Header benutzen, wenn die Ausgabe eines Scripts heruntergeladen werden soll:

// Dateityp, der immer abgespeichert wird
header("Content-Type: application/octetstream");
// Dateiname
// mit Sonderbehandlung des IE 5.5
header("Content-Disposition: ".
     (!strpos($HTTP_USER_AGENT,"MSIE 5.5")?"attachment; ":"").
     "filename=datei name.ext");
// eigentlich ueberfluessig, hat sich aber wohl bewaehrt
header("Content-Transfer-Encoding: binary");
// Zwischenspeichern auf Proxies verhindern
// (siehe weiter unten)
header("Cache-Control: post-check=0, pre-check=0");
// Dateigröße für Downloadzeit-Berechnung
header("Content-Length: {Dateigroesse}");
Diese Headerkombination sollte zuverlässig funktionieren. Bei der Vielzahl von Browsern, die sich nicht immer an die RFCs halten, ist jedoch nicht ausgeschlossen, daß das ganze angepaßt werden muß. Sollte jemand eine Kombination haben, die besser funktioniert, freue ich mich natürlich über eine Rückmeldung.

Ein letztes Wort noch zur Header-Kombination: Wie sich zeigte, funktioniert diese Download-Methode nicht mehr, wenn vor dem Senden o.g. Header schon bestimmte andere Header, wie die für das Nicht-Cachen (11.1.6), gesandt wurden. Man sollte also immer auf die Reihenfolge der Header achten und sicherstellen, daß vor den Headern für den Download keine oder nur definitiv nicht störende Header verschickt werden.


Content-Type

Neben dem oben schon verwendeten Content-Type gibt es natürlich noch andere. So lange ,,nur`` ,,normale`` Webseiten ausgegeben werden, interessiert uns der Typ nicht. Wenn aber z.B. mit den PHP image functions ein Bild dynamisch erzeugt wird, was auch als Bild im Browser angezeigt wird, muß der Typ explizit angegeben werden. Die Tabelle 11.1 zeigt die wichtigsten Typen.

Bei statischen Dateien entscheidet der Webserver in der Regel anhand der Endung, welchen Typ er sendet. Normalerweise beachtet der Client den Typ, den der Server sendet. Es gibt jedoch IE Versionen, die der Meinung sind, anhand der Endung selbst besser entscheiden zu können, um was für eine Datei es sich handelt.


Tabelle 11.1: Content-Type Typen
Bezeichnung Bedeutung
application/octetstream wird als Datei (Binärdaten) angesehen (wird gespeichert)
image/gif GIF-Bild
image/jpeg JPEG-Bild
image/png PNG-Bild
text/html wird als Webseite angesehen, HTML wird interpretiert
text/plain wird als reiner Text angesehen, HTML wird nicht beachtet


Die Bedeutung des Type können wir uns an den folgenden Dateien ansehen.

<?php

?>
<html>
<body>
<h1>Hallo Welt!</h1>
</body>
</html>
Die Datei ist im Endeffekt eine ganz normale Webseite, enthält zwar einen Bereich für PHP Anweisungen, der ist jedoch leer. Sie wird auch dem entsprechend im Browser angezeigt.

<?php
header("Content-Type: text/plain");
?>
<html>
<body>
<h1>Hallo Welt!</h1>
</body>
</html>
Nachdem das Script nun von sich behauptet, es wäre ein normaler Text, werden die HTML-Anweisung komplett mißachtet und der Text so wie er ist ausgegeben.

<?php
header("Content-Type: text/html");
?>
<html>
<body>
<h1>Hallo Welt!</h1>
</body>
</html>
Mit dem richtigen Typ ist die Welt aber wieder in Ordnung.

<?php
header("Content-Type: image/png");
?>
<html>
<body>
<h1>Hallo Welt!</h1>
</body>
</html>
Als Bild taugt der HTML-Code nun wirklich nicht und Netscape zeigt das Symbol für ein defektes Bild an.

<?php
header("Content-Type: application/octetstream");
?>
<html>
<body>
<h1>Hallo Welt!</h1>
</body>
</html>
Und den Octetstream will Netscape ordnungsgemäß als Datei abspeichern.


Cache

Über die richtigen Headerangaben kann auch das Verhalten der Proxies beeinflußt werden. So verhindert folgende Kombination zuverlässig das Zwischenspeichern.
header("Expires: -1");
header("Cache-Control: post-check=0, pre-check=0");
header("Pragma: no-cache");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");

Zusätzlich braucht der IE ab Version 5 offenbar noch folgenden HTML-Code am Ende der Datei, d.h. zwischen </body> und </html>

<head>
<meta http-equiv="pragma" content="no-cache">
</head>

Das Nicht-Cachen sollte aber nur benutzt werden, wenn es wirklich notwendig ist, d.h. die Seite sich bei jedem Aufruf ändert. Es ist sonst nämlich zum einen für den Besucher nervig, da die Seite bei jedem Aufruf neu geladen werden muß - normalerweise kommt sie ja nach dem ersten Aufruf für eine gewisse Zeit aus dem Cache. Zum anderen belastet das Neuladen den Server unnötig. Davon abgesehen ignorieren viele Suchmaschinen Seiten, die nicht gecached werden dürfen, denn warum soll eine Seite aufgenommen werden, wenn die Seite von sich behauptet, daß sie sich ständig ändert? Von daher sollte die Seite, wenn sie in Suchmaschinen auftauchen soll, einen gescheiten Last-Modified und Expires Header senden.

Was tun bei einer Seite, die sich jede Minute ändert? Ganz einfach: Die entsprechenden Header richtig setzen. Die Tabelle 11.2 zeigt die entsprechenden Header. Es muß unterschieden werden zwischen den META-Tags, die im HTML stehen und den HTTP-Headern. Erstere werden nur vom Browser gelesen, letztere auch von Proxies.


Tabelle 11.2: Cache Header
Bezeichnung Bedeutung
Cache-Control Anweisungen für Proxies und Browser
Expires Zeitpunkt, ab dem der Inhalt wahrscheinlich veraltet sein wird
Last-Modified letzte Änderung des Inhalts



Cache-Control

Das RFC 2616 sagt zum Thema Syntax des Cache-Control (hier nur ein Ausschnitt):
Cache-Control   = "Cache-Control" ":" 1#cache-directive

    cache-directive = cache-request-directive
         | cache-response-directive

    cache-request-directive =
           "no-cache"                          ; Section 14.9.1
         | "no-store"                          ; Section 14.9.2

     cache-response-directive =
         | "no-cache" [ "=" <"> 1#field-name <"> ]; Section 14.9.1
         | "no-store"                             ; Section 14.9.2
Bei einem Cache-Control: no-cache Header muß der Proxy bei jeder Anfrage überprüfen, ob eine aktuellere Version vorliegt. Wenn dies nicht der Fall ist, kann er die gespeicherte Seite senden. Bei Cache-Control: no-store darf der Proxy die Seite nicht speichern, das heißt, sie muß bei jedem Aufruf neu übertragen werden.


Expires

Beim Expire-Header muß die Uhrzeit richtig formatiert sein und in Greenwich Mean Time[*]. Kleines Beispiel:
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Diese Seite wäre bis zum 1. Dezember 1994 0 Uhr aktuell gewesen. Danach muß sie wieder auf Aktualität überprüft werden.


Last-Modified

Bei Last-Modified sollte die Zeit der letzten Änderung angegeben werden. Bei statischen Seiten wird dafür häufig das Änderungsdatum der Datei genommen. Bei dynamischen Seiten gestaltet sich die Sache etwas schwieriger.

Alternative: GET-Parameter

Eine Alternative zum Header ist, einen zufälligen GET-Parameter an die URL zu hängen. Zum Beispiel wird dann aus der URL http://reeg.net/index.html die Adresse http://reeg.net/index.html?IRGENDETWAS. Es muß natürlich gewährleistet sein, daß IRGENDETWAS immer etwas anderes ist.

URLs parsen

Beispiel: PHP-Manual

Niemand, der PHP programmiert, kennt alle Befehle und deren Syntax auswendig. Wozu auch - es gibt schließlich das PHP-Manual, ein kurzer Blick auf http://php.net/ genügt. Die Betreiber dieser Site haben sich etwas besonders schlaues einfallen lassen, damit man sich nicht immer durch zig Unterseiten hangeln muß, bis man den Befehl gefunden hat, den man sucht: Man kann einfach den Namen des gesuchten Befehls hinter die o.g. URL hängen und landet automatisch auf der richtigen Seite des PHP-Manuals. Beispiel: http://php.net/echo zeigt die Beschreibung des Befehls echo an. Wie man sich denken kann, wurde diese Annehmlichkeit mit einem PHP-Script realisiert. Im Folgenden werde ich also erst einmal zeigen, wie dieser Trick funktioniert und im zweiten Schritt dann noch ein anderes Beispiel geben.

Doch nun los: Zuerst einmal muß man verstehen, was passiert, wenn der Webserver, der die HTML-Seiten (auch die durch PHP-Scripte dynamisch erzeugten!) an den Client (d.h. Browser) schickt, eine Seite nicht findet. Dann nämlich schickt er den Status-Code 404 (siehe 11.1.2) in Verbindung mit einer Fehlerseite, die dann im Browser angezeigt wird. Diese kann man abfangen und statt der Standard-Fehlerseite eine eigene angeben - und das ist es, was wir hier ausnutzen möchten.

Um einem Apache-Webserver mitzuteilen, daß er beim Status-Code 404 zu einer anderen als der Standard-Fehlerseite umleiten soll, erstellt man eine Datei namens .htaccess mit folgendem Inhalt:

ErrorDocument 404 /pfad/zur/alternativseite.php4

Auf diese Weise wird an Stelle der normalen Fehlerseite die Alternativseite aufgerufen, wobei der Benutzer davon nichts mitbekommt. In dieser Seite kann man dann in dem Fall, daß mit Status-Code 404 umgeleitet wurde, auf den gesuchten letzten Teil der URL wie folgt zugreifen:

<?php
  if ($_SERVER["REDIRECT_STATUS"]==404) {
    $keyword = substr($_SERVER["REDIRECT_URL"],
      strrpos($_SERVER["REDIRECT_URL"],"/")+1);
  }
?>

Mit etwas Erfahrung sieht man dem Script direkt an, daß es einfach alles abschneidet, was hinter dem letzten Slash / kommt, und es in die Variable $keyword schreibt. Letztere kann man nach Belieben im weiteren Scriptverlauf auswerten (und natürlich auch ganz anders nennen!). Im Falle des PHP-Manuals wird aus dieser Information eine neue URL zusammengestellt, was recht leicht zu bewerkstelligen ist, da die meisten Dateinamen der Seiten des Manuals die Form function.FUNKTIONSNAME.html haben - lediglich Underscores (_) werden durch das Minuszeichen ersetzt. Mit der neuen URL wird dann eine einfache Header-Weiterleitung (siehe 11.1.1) durchgeführt.

Anderes Beispiel: Akronyme

Angenommen, du hast eine recht bekannte Homepage, die auf einer serverseitigen Datenbank (z.B. mit MySQL) aufsetzt. Hier soll einmal eine Akronym-DB als Beispiel dienen. Nun möchtest du vielleicht den Besuchern dieser Seite die Möglichkeit geben, Akronyme schnell und einfach nachzuschlagen, indem sie ein gesuchtes Akronym einfach ans Ende der ihnen schon bekannten URL hängen. Mit http://akronyme.junetz.de/ als Basis-URL ergibt sich so z.B. die Möglichkeit, das Akronym ROTFL nachzuschlagen, indem man einfach http://akronyme.junetz.de/rotfl eintippt.

Doch wie soll das funktionieren? ,,Soll ich etwa für jede mögliche Eingabe eine eigene Seite bauen?`` höre ich euch schon rufen. Natürlich nicht, wofür dann eine Datenbank benutzen?! Das geht viel eleganter:

Erstelle eine Datei namens .htaccess mit folgendem Inhalt:

ErrorDocument 404 /akronyme/index.php4

Wobei hier /akronyme/index.php4 der absolute Pfad (relativ zur Server-HTTP-Wurzel) zu unserem Akronyme-Skript ist. Die weitere Behandlung der REDIRECT_URL wurde bereits im vorigen Beispiel beschrieben.


previous up next
contents index
Up: Einführung PHP Previous: PHP & MySQL Next: Reguläre Ausdrücke

Christoph Reeg