Dr. O. Hoffmann
Denglish für Anfänger:
Gern downlüdete ich, was du upgeloadet hast, aber du hast das file nicht verlinkt. Dann loggte das form auch nicht in, weil keine action da war und das dann bestenfalls mit js performt. Da muß man tighter an der spec bleiben, sonst hat der user da accessmäßig und von der usability her voll abgeloost.
Olaf Hoffmann
Dem ungeduldigen Leser sei gesagt, um
einen download anzubieten, braucht man
kein PHP, dazu reicht ein normaler
Verweis mit (X)HTML auf die jeweilige Datei.
Mit der rechten Maustaste kann jeder Nutzer
den Inhalt einer solchen Datei herunterladen.
Mit PHP kann man aber sehr schön
erreichen, daß der download nur
von einer eigenen Seite aus durchgeführt
werden kann - und Protokollieren der Zugriffe
ist auch gar kein Problem. So behält man
immer im Blick, was vorgeht und ob das
Angebot überhaupt genutzt wird.
Falls sessions
oder die Änderung der ini-Daten
auf dem server aus sicherheitstechnischen oder weltanschaulichen
Gründen nicht zuverlässig
funktionieren, habe ich hier eine einfache
Zeitkontrolle als Ersatz eingebaut, die aber
nur einigermaßen sicher ist, wenn
diese beim eigenen Einsatz variiert oder
verfeinert wird.
Bei dem zweiten Beispiel werden
in recht einfacher Weise sessions
verwendet
ebenso wie der Zufallsgenerator,
die Initialisierung des PHP-parsers wird
geändert.
Gelernt werden kann in beiden Fällen,
wie man einigermaßen
vorsichtig mit Benutzerangaben umgeht und
ebenso der Einsatz von einigen Dateifunktionen.
Das Herzstück der Zeilen ist jedoch die
header
-Funktion, die sich unter anderem auch
zur Steuerung des downloads eignet.
Wenn man das so verwenden will, sollte man
das Verzeichnis "geheim" natürlich
umbenennen und den Namen nicht bekannt werden
lassen. Sofern möglich sollte man sie auch in
einem Verzeichnis unterbringen, welches nicht direkt
per HTTP zugänglich ist.
Die darin enthaltene Datei "statistik"
muß auch Schreibrechte für den HTTP-Nutzer
haben.
Hier ist das Beispiel:
PHP-download
Schauen wir uns einfach einmal an, wie es
geht:
<?php # php-Labor: php-download # Fehlerbericht anschalten: error_reporting(E_ALL); # Wenn sessions auf dem server # nicht zuverlaessig funktionieren, # muss man die durch etwas ersetzen # welches zwar hinsichtlich sessions weniger # sicher ist, dafuer aber sicher funktioniert. # Hier wird nur kontrolliert, # ob zwischen dem Absenden des Formulars # und dem Darstellen des Formulars nicht # eine zu grosse Zeit verstrichen ist. # Gucken, ob eine Referenzzeit per POST gesendet # wurde, sonst Zeit gleich 0 setzen. if(isset($_POST['k'])) { $kon=$_POST['k']; } else { $kon=0; } # wir bestimmen uns eine eigene Zeit, wenn # man dabei die drei Zahlen aendert und # diese nicht bekannt werden laesst, kann # auf die downloads auch nicht von anderen # Formularen dauerhaft mit der POST-Methode # zugegriffen werden: # Zeiteinheit, hier ungefaehr eine Minute $ze=67.3127101; # Zeitnullpunkt - relativ zum Beginn des # Jahres 1970 in eigenen Zeiteinheiten - # das sollte ein kleinerer Wert sein als # der aktuelle Zeitpunkt: $zo=12592812.5; # Zeittoleranz, da sollte etwa eine # halbe Stunde reichen, um einen download # aus dem Formular auszuwaehlen $zt=1853.18943/$ze; # Refenrenzzeit k und Zeitdifferenz dt ermitteln: $zk = time()/$ze-$zo ; $dt = $zk-$kon; $k=floor($zk); # Verzeichnis fuer downloads und Statistik # (das Verzeichnis geheim sollte natuerlich # im wirklichen Leben einen geheimeren Namen bekommen, # der dem spaeteren Nutzer nicht bekannt sein darf, damit # dieser die Dateien nicht doch unbemerkt herunterladen # kann): $geheim="./geheim/"; # vermeiden, dass was anderes heruntergeladen # wird als vorgesehen, daher explizit angeben: $anz=0; $angebot[$anz++]="download.php"; $angebot[$anz++]="downloads.php"; $angebot[$anz++]="projekt.html"; $angebot[$anz++]="projekt.ps"; $angebot[$anz++]="projekt.ps.gz"; $angebot[$anz++]="projekt.pdf"; $angebot[$anz++]="projekt.tex"; # Wenn die Zeitdifferenz zwischen Aufruf der # Datei und Abschicken des Formulars zu gross # ist oder jemand eine Phantasiezeit uebermittelt # hat, wird wieder ein Formular rausgehauen: if (($dt < 0) OR ($dt > $zt)) { ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/transitional.dtd"> <html lang="de"> <head> <title>Download</title> </head> <body> <form action="download.php" method="post"> <p>Was herunterladen?:</p> <p> <input type="hidden" name="k" value="<?php echo $k;?>"> <select name="deins" size="3"> <?php # Angebot mit einer for-Schleife ins Formular pappen: for ($i = 0; $i < $anz; $i++) { echo"<option value=\"$i\">$angebot[$i] </option>"; } ?> </select> </p> <input type="submit" value=" Absenden "> </form> <br> <form action="download.php" method="post"> <input type="hidden" name="k" value="<?php echo $k;?>"> <input type="hidden" name="deins" value="-1"> <input type="submit" value=" Statistik "> </form> </body> </html> <?php # Wenn es aber die Zeitdifferenz kurz genug ist, # schauen wir mal, was wir tun sollen: } else { $down="ok"; # klar, gucken sollte man schon, ob Variablen # gesetzt sind und ob die richtigen Werte # drin stehen, wenn nicht, sucht man # sich selber was Schoenes aus: if(isset($_POST['deins'])) { $deins=$_POST['deins']; if (($deins >= 0) AND ($deins < $anz)) { $datei=$angebot[$deins]; } elseif ($deins == -1){ # Das soll mal die Statistik anzeigen $down="ko"; } else { $datei=$angebot[0]; } } else { $datei=$angebot[0]; } # Wenn alles in Ordnung ist, kommt der # download, sonst die Statistik if ($down=="ok"){ # statistische Daten ermitteln und gegen groben # Schabernack absichern $datum = (date("d.m.Y G:i:s")); if(isset($_SERVER['REMOTE_ADDR'])) { $remad=htmlentities($_SERVER['REMOTE_ADDR']); # nur fuer das Beispiel ueberschrieben, # sonst die folgende Zeile und diesen Kommentar # entfernen: $remad="0815.4711"; } else { # ohne ip kein download... die("ohne ip kein download..."); } if(isset($_SERVER['HTTP_USER_AGENT'])) { $agent=htmlentities($_SERVER['HTTP_USER_AGENT']); } else { $agent = " - "; } if(isset($_SERVER['HTTP_REFERER'])) { $refer=htmlentities($_SERVER['HTTP_REFERER']); } else { $refer = " - "; } # Statistik anlegen: # die Maskierung der Variable \$nri wird uns beim Einlesen zu Gute # kommen, damit erreichen wir da eine einfach Numerierung: $raus = "<tr><td><?php echo \$nri++ ?></td><td>$datum</td>"; $raus .= "<td>$datei</td><td>$remad</td>\t<td>$agent</td>\t<td>$refer</td></tr>\n"; $file=$geheim."statistik"; $fp = fopen ($file, "a+"); fputs ($fp, $raus); fclose($fp); # Wir werden irgendeine Datei mit Namensvorschlag # ausgeben, also einen passenden MIME-Typ setzen... # das x- im Subtyp gibt an, dass es sich um ein # experimentelles Format handelt, also prinzipiell nicht # eindeutig zugeordnet werden kann, was fuer alle Formate # gilt, bei denen der Subtyp mit x- beginnt. $content="Content-type: application/x-download "; header($content); # Namensvorschlag wird angeboten # (mag sein, dass das aeltere browser noch nicht # drauf haben...): $header="Content-Disposition: attachment; filename=".$datei." "; header($header); # Die originale Datei raushauen: $file=$geheim.$datei; readfile($file); exit; } else { # also wenn $down !="ok" Statistik anzeigen: $nri=1; echo "<html><head><title>Download</title></head><body><table border=\"1\">\n"; echo "<tr><th>Nr.</th><th>Datum</th><th>Datei</th>"; echo "<th>IP</th><th>User-Agent</th><th>referrer</th></tr>\n"; $file=$geheim."statistik"; # Damit die Numerierung funktioniert, muss das Einlesen der Statistik mit # include erfolgen und nicht readfile. # Dass das funktioniert, zeigt uebrigens, dass man durch schlecht kontrollierte # Benutzereingaben und Ausgabe derselben mit include sehr leicht seinen account # schreddern koennte - so ist natuerlich alles in Ordnung. Den eigenen # account durch eine geeignete Formulareingabe eines Nutzers zu schreddern, # bleibt dem geneigten Leser zur Uebung ueberlassen. include($file); echo "</table><br></body></html>"; } } ?>
Beispiel mit sessions
:
<?php # php-Labor: php-download # Fehlerbericht anschalten: error_reporting(E_ALL); # Zufallsgerator initialisieren: mt_srand ((double)microtime()*1000000); # Dem Nutzer nicht laestigfallen und # Cookies abschalten: ini_set("session.use_cookies", "0"); # session automatisch an url anhaengen anschalten # (macht der server eigentlich nach abschalten # der cookies von alleine ini_set("session.use_trans_sid", "1"); # Session starten: session_start(); # Verzeichnis fuer downloads und Statistik # (das Verzeichnis geheim sollte natuerlich # im wirklichen Leben einen geheimeren Namen bekommen, # der dem spaeteren Nutzer nicht bekannt sein darf, damit # dieser die Dateien nicht doch unbemerkt herunterladen # kann) $geheim="./geheim/"; # vermeiden, dass was anderes heruntergeladen # wird als vorgesehen, daher explizit angeben: $anz=0; $angebot[$anz++]="downloads.php"; $angebot[$anz++]="download.php"; $angebot[$anz++]="projekt.html"; $angebot[$anz++]="projekt.ps"; $angebot[$anz++]="projekt.ps.gz"; $angebot[$anz++]="projekt.pdf"; $angebot[$anz++]="projekt.tex"; # Wenn in der Session nichts drin steht, # machen wir mal was rein, ist ja sonst langweilig: if(!isset($_SESSION["ok"])) { $_SESSION["ok"]=mt_rand(10000,99999); # (man koennte diesen Parameter jetzt zum Beispiel auch # noch zusaetzlich uebergeben und nachkontrollieren). # und nur dann hauen wir eine html-Ausgabe mit # ein Formular zum Runterholen raus: ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/transitional.dtd"> <html lang="de"> <head> <title>Download</title> </head> <body> <form action="download.php" method="get"> <p>Was herunterladen?:</p> <p> <select name="deins" size="3"> <?php # Angebot mit einer for-Schleife ins Formular pappen: for ($i = 0; $i < $anz; $i++) { echo"<option value=\"$i\">$angebot[$i] </option>"; } ?> </select> </p> <input type="submit" value=" Absenden "> </form> <a href="download.php?deins=-1">Statistik anzeigen</a> </body> </html> <?php # Wenn es aber eine session-Variable $ok gibt, # schauen wir mal, was wir tun sollen: } else { $down="ok"; # klar, gucken sollte man schon, ob Variablen # gesetzt sind und ob die richtigen Werte # drin stehen, wenn nicht, sucht man # sich selber was Schoenes aus: if(isset($_GET['deins'])) { $deins=$_GET['deins']; if (($deins >= 0) AND ($deins < $anz)) { $datei=$angebot[$deins]; } elseif ($deins == -1){ # Das soll mal die Statistik anzeigen $down="ko"; } else { $datei=$angebot[0]; } } else { $datei=$angebot[0]; } # Wenn alles in Ordnung ist, kommt der # download, sonst die Statistik if ($down=="ok"){ # statistische Daten ermitteln und gegen groben # Schabernack absichern $datum = (date("d.m.Y G:i:s")); if(isset($_SERVER['REMOTE_ADDR'])) { $remad=htmlentities($_SERVER['REMOTE_ADDR']); # nur fuer das Beispiel ueberschrieben, # sonst die folgende Zeile und diesen Kommentar # entfernen: $remad="0815.4711"; } else { # ohne ip kein download... die("ohne ip kein download..."); } if(isset($_SERVER['HTTP_USER_AGENT'])) { $agent=htmlentities($_SERVER['HTTP_USER_AGENT']); } else { $agent = " - "; } if(isset($_SERVER['HTTP_REFERER'])) { $refer=htmlentities($_SERVER['HTTP_REFERER']); } else { $refer = " - "; } # Statistik anlegen: # die Maskierung der Variable \$nri wird uns beim Einlesen zu Gute # kommen, damit erreichen wir da eine einfach Numerierung: $raus = "<tr><td><?php echo \$nri++ ?></td><td>$datum</td>"; $raus .= "<td>$datei</td><td>$remad</td>\t<td>$agent</td>\t<td>$refer</td></tr>\n"; $file=$geheim."statistik"; $fp = fopen ($file, "a+"); fputs ($fp, $raus); fclose($fp); # php-Fehler verhindern, dass die session-id # ueberhaupt und dann auch noch falsch in die # download-Datei eingefuegt wird: ini_set("session.use_trans_sid", "0"); # Wir werden irgendeine Datei mit Namensvorschlag # ausgeben, also einen passenden MIME-Typ setzen... # das x- im Subtyp gibt an, dass es sich um ein # experimentelles Format handelt, also prinzipiell nicht # eindeutig zugeordnet werden kann, was fuer alle Formate # gilt, bei denen der Subtyp mit x- beginnt. $content="Content-type: application/x-download "; header($content); # Namensvorschlag wird angeboten # (mag sein, dass das aeltere browser noch nicht # drauf haben...): $header="Content-Disposition: attachment; filename=".$datei." "; header($header); # Die originale Datei raushauen: $file=$geheim.$datei; readfile($file); exit; } else { # also wenn $down !="ok" Statistik anzeigen: $nri=1; echo "<html><head><title>Download</title></head><body><table border=\"1\">\n"; echo "<tr><th>Nr.</th><th>Datum</th><th>Datei</th>"; echo "<th>IP</th><th>User-Agent</th><th>referrer</th></tr>\n"; $file=$geheim."statistik"; # Damit die Numerierung funktioniert, muss das Einlesen der Statistik mit # include erfolgen und nicht readfile. # Dass das funktioniert, zeigt uebrigens, dass man durch schlecht kontrollierte # Benutzereingaben und Ausgabe derselben mit include sehr leicht seinen account # schreddern koennte - so ist natuerlich alles in Ordnung. Den eigenen # account durch eine geeignete Formulareingabe eines Nutzers zu schreddern, # bleibt dem geneigten Leser zur Uebung ueberlassen. include($file); echo "</table><br></body></html>"; } } ?>