template

Index :: PHP/MySQL :: Downloadstream erzeugen

Mithilfe seines Browsers kann sich der Benutzer normalerweise nur solche Daten downloaden, die als Datei im Webpfad liegen. Dies muss aber nicht immer der Fall sein. Manche Sites lagern - vor allem private oder sicherheitsrelevante - Daten ausserhalb des Webpfades in einem Archivordner oder speichern sie (binär) in einer Datenbank.
Da per PHP ein Datenstrom jedoch auch direkt an den Browser übermittelt werden kann, wie es z.B. bei echo() geschieht, ist es leicht, mittels Header-Manipulation beliebige Ausgaben zu generieren und zu senden.

In den aufgeführten Beispielen werden der Einfachheit halber nur Dateien ausgelesen und übermittelt. In der Praxis können aber natürlich auch Datenbankfelder oder sonstige zur Laufzeit erzeugte Daten ausgegeben werden.

Im nachfolgenden Fall befinden sich im lesegeschützten Ordner privat eine Reihe von Bildern, die lediglich über ein PHP-Skript geladen werden können. Das Skript benötigt hierzu einen GET-Parameter, um den realen Bildnamen zu ermitteln (z.B. <a href='script.php?nr=7>Bild Nr.7 laden</a>).
Cache-Control und Pragma werden leer angegeben, damit der Client die Datei nicht aus seinem Cache holt, sondern wirklich vom Server herunterlädt.
Die Datei wird hier zwar stückchenweise in einer while-Schleife per print() übermittelt, man könnte sie jedoch auch in einem Rutsch per fpassthru(), include() oder readfile() senden. Eine weitere Alternative wäre die Ausgabe mittels echo().
<?php
$f = "privat/bild". $_GET['nr'] .".jpg";

header('Cache-Control:');
header('Pragma:');
header('Content-Type: image/jpg');
header('Content-Length: '. filesize($f));
header('Content-Transfer-Encoding: binary');
if ($fp = fopen($f, 'rb')) {
  while (!feof($fp)) {
    print(fread($fp, 1024*8)); flush();
  } fclose($fp);
}
?>
Der Ausdruck privat ist in diesem Beispiel natürlich irreführend. Die Bilder befinden sich hier zwar in einem lesegeschützten Order und sind nicht auf direktem Wege erreichbar, aber ein gewiefter Benutzer sieht ja am Link (script.php?nr=7), dass man das Skript auch mit anderen Werten für den GET-Parameter nr aufrufen könnte.
Im Skript müsste also zusätzlich eine Benutzerauthentifizierung (über Session, Datenbank, Cookie o.Ä.) erfolgen, um den Mechanismus gegen Missbrauch abzusichern.

Beim obigen Beispiel würde der Client das Bild im Browserfenster anzeigen, weil es sich laut unserem Header (Content-Type) um Daten des Mime-Typs image/jpg handelt und der Browser dies per Default darstellen kann.
Jenes Verhalten (Downloaden und Ausführen/Darstellen) ist jedoch nicht immer erwünscht. Manchmal soll der Benutzer nämlich aufgefordert werden, die Datei - ungeachtet ihres Mime-Typs - auf seiner Platte zu speichern.
Auch in solchen Fällen kann uns die Verwendung eines entsprechenden Headers (hier: Content-Disposition: attachment) helfen. Ebenso kann der Dateiname angegeben werden, der im 'Speichern unter...'-Dialog voreingestellt wird.
Dies wird im folgenden Beispiel gezeigt: hier handelt es sich um ein Online-Archiv, aus dem der Benutzer diverse Dateien (Dokumente und Multimedia) herunterladen kann.
Der Content-Type wird individuell anhand der Extension ermittelt und im Header übergeben, damit der Client alternativ zum Abspeichern auch die Möglichkeit hat, die Datei gleich ausführen/anzeigen zu lassen, falls er eine entsprechende Applikation installiert hat.
Mittels ob_end_clean() wird der Ausgabebuffer vorher sicherheitshalber geleert, damit gewährleistet ist, dass ausschliesslich die Datei übertragen wird.
Mit set_time_limit(0) schalten wir (für dieses Skript) die Zeitlimitierung - i.d.R. 30 Sekunden - ab, falls eine längere Datei, z.B. MP3 oder ein Video, übertragen werden muss.
Falls der Client den Microsoft IE (MSIE) benutzt, müssen wir den angebotenen Dateinamen (hier: der Originalname der gespeicherten Datei) formatieren, weil der MSIE Probleme mit verketteten Extensionen hat.
Cache-Control und Pragma werden leer angegeben, damit der Client die Datei nicht aus seinem Cache holt, sondern wirklich vom Server herunterlädt. Dies ist normalerweise nur für Dateien nötig, deren Inhalt sich ändern kann, z.B. bei RSS oder News-Feeds.
Der connection_status() wird fortlaufend geprüft, damit wir erkennen, ob der Benutzer den Download evtl. abgebrochen hat.
Wir senden in Paketen von 8 KB (1024*8), damit wir zwischendurch den connection_status prüfen können; die Paketgröße ist hier willkürlich gewählt. Theoretisch kann die Übertragung, gerade bei kleinen Dateien, auch am Stück erfolgen.
<?php

// ... Dateiname $fnam ermitteln ...

switch (strtoupper(substr(strrchr($fnam, "."), 1))) {
  case "PDF": $ctype="application/pdf"; break;
  case "EXE": $ctype="application/octet-stream"; break;
  case "ZIP": $ctype="application/x-zip"; break;
  case "DOC": $ctype="application/msword"; break;
  case "XLS": $ctype="application/vnd.ms-excel"; break;
  case "GIF": $ctype="image/gif"; break;
  case "PNG": $ctype="image/png"; break;
  case "JPG": $ctype="image/jpg"; break;
  case "MP3": $ctype="audio/mpeg"; break;
  case "WAV": $ctype="audio/x-wav"; break;
  case "MPG": $ctype="video/mpeg"; break;
  case "MOV": $ctype="video/quicktime"; break;
  case "AVI": $ctype="video/x-msvideo"; break;
  default: $ctype="application/force-download";
}
ob_end_clean();
set_time_limit(0);
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE"))
  $fnam = preg_replace('/\./', '%2e', $fnam, substr_count($fnam, '.') - 1);
header("Cache-Control:");
header("Pragma:");
header("Content-Type: ". $ctype);
header("Content-Length: ". filesize($f));
header('Content-Disposition: attachment; filename="'. $fnam .'"');
header("Content-Transfer-Encoding: binary");
if ($fp = fopen($f, 'rb')) {
  while ((!feof($fp)) && (connection_status()==0)) {
    print(fread($fp, 1024*8)); flush();
  } fclose($fp);
}
?>
Im Anhang befindet sich eine Liste gebräuchlicher Mime-Typen.
Bei textuellen Inhalten wie text/html kann zusätzlich ein Zeichensatz mit angegeben werden, z.B.:
header("Content-Type: text/html; charset=UTF-8");

Zum Abschluss noch ein simples Beispiel, wie man dem Benutzer ein Zertifikat anbieten könnte. Es muss hierzu ein Link auf das PHP-Skript existieren, z.B.
Hallo, Herr Meier, <a href='getcert.php'>Ihr Zertifikat</a>.
Das PHP-Skript getcert.php könnte so aussehen:
<?php
$crtFileName = "/usr/local/apache/conf/ssl.crt/meier.crt";

header("Content-type: application/x-x509-ca-cert");
header("Content-length: ". filesize($crtFileName));
include($crtFileName);
flush();
?>
Natürlich fehlt auch hier noch eine Benutzerauthentifizierung, falls dies nicht schon durch das Protokoll selbst gewährleistet ist.

Index :: PHP/MySQL


template