Previous: PHP & MySQL
Up: Einführung PHP
Next: Reguläre Ausdrücke
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.
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.
header('HTTP/1.0 404 Not Found');
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.
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.5nur 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.
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.
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.
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.
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.2Bei 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: Thu, 01 Dec 1994 16:00:00 GMTDiese Seite wäre bis zum 1. Dezember 1994 0 Uhr aktuell gewesen. Danach muß sie wieder auf Aktualität überprüft werden.
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.
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.
Up: Einführung PHP
Previous: PHP & MySQL
Next: Reguläre Ausdrücke