XHTML SVG CSS PHP

Dr. O. Hoffmann

PHP-Labor

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

PHP-download

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>";
  }
}
?>