Josef(Sepp): PDF über Dateu Ausliefern, klappt nicht immer

Ich habe meine PDFs in einem geschützten Ordner liegen, von dort aus möcht ich sie ausgeben. Das verrückte darin ist, das es manchmal geht und manchmel nicht. Der Pfad ist in Ordnung. Aber irgendwas mache ich falsch.

$filename='/ www/users/12/docs/datei.pdf';
// Ausgabe	
header("Content-Type: application/pdf"); // MIME-Typ
header("Content-Disposition: filename=\"$filename\""); // Dateiname
header("Content-Length: ".filesize($filename)); // Dateigröße
readfile($filename); 
  1. Hallo,

    Ich habe meine PDFs in einem geschützten Ordner liegen, von dort aus möcht ich sie ausgeben. Das verrückte darin ist, das es manchmal geht und manchmel nicht.

    die Aussage ist unzureichend. Was genau heißt "geht nicht"? Wie äußert sich das? Was passiert genau?

    Aber irgendwas mache ich falsch.

    Vermutlich. Aber nicht in dem Stückchen Code, das du gezeigt hast.

    So long,
     Martin

    --
    Nothing travels faster than the speed of light with the possible exception of bad news, which obeys its own special laws.
    - Douglas Adams, The Hitchhiker's Guide To The Galaxy
    1. Vermutlich. Aber nicht in dem Stückchen Code, das du gezeigt hast.

      Doch. 1) $filename='/ www/users/12/docs/datei.pdf'; hat ein Leerzeichen vor www was mit Sicherheit Probleme macht und 2) im Parameter filename= hat eine Pfadangabe nichts zu suchen.

  2. Moin,

    fällt dir an diesem Pfad etwas auf:

    $filename='/ www/users/12/docs/datei.pdf';
    

    Du steckst jedenfalls den Inhalt der Variablen in einen HTTP-Header. Ich habe jetzt nicht in der Spezifikation nachgeschaut, aber ich glaube, dass im Folgenden ein relativer Pfad besser wäre:

    header("Content-Disposition: filename=\"$filename\""); // Dateiname
    

    Und hier zu,

    header("Content-Length: ".filesize($filename)); // Dateigröße
    readfile($filename); 
    

    empfehle ich, die Datei zu öffnen, mit fstat die Dateigröße zu ermitteln, die Datei auszugeben und dann zu schließen. Das spart einiges an doppeltem Aufwand, den filesize sowie readfile jeweils verursachen.

    Viele Grüße
    Robert

  3. Hallo

    Ich habe meine PDFs in einem geschützten Ordner liegen, von dort aus möcht ich sie ausgeben. Das verrückte darin ist, das es manchmal geht und manchmel nicht. Der Pfad ist in Ordnung. Aber irgendwas mache ich falsch.

    $filename='/ www/users/12/docs/datei.pdf';
    

    Das ist kein Dateiname, …

    header("Content-Disposition: filename=\"$filename\""); // Dateiname
    

    … weswegen diese Zeile einen ungültigen Wert enthält. Die Spezifikation für HTTP v1.1 sagt zu diesem Punkt, dass Browser jegliche Verzeichnisangaben ignorieren sollen. Das hört sich für mich an, als ob von deiner Vorgabe nur „datei.pdf“ übrig bliebe.

    „The receiving user agent SHOULD NOT respect any directory path information present in the filename-parm parameter, which is the only parameter believed to apply to HTTP implementations at this time. The filename SHOULD be treated as a terminal component only. “

    Wenn Browser aber Verzeichnisangaben „sauber“ ignorieren, sollten sie auch nicht darüber stolpern, dass das, was sie ignorieren sollen vorhanden ist. Zumal es sich um einen Vorschlag für den Dateinamen, der beim speichern der Datei vorgegeben ist, handelt. Also eigentlich nichts überlebenswichtiges.

    Tschö, Auge

    --
    Wo wir Mängel selbst aufdecken, kann sich kein Gegner einnisten.
    Wolfgang Schneidewind *prust*
  4. Die Leerstelle ist dir sicher irrtümlich durchgerutscht :)

    Der Path gehört sicher nicht in den filename der Content-Disposition, aber ich würde behaupten, dass das kein Grund für ein "Geht/Geht nicht" Problem ist.

    Guck auf jeden Fall mal ins PHP Handbuch, da machen die Herrschaften etwas ähnliches und da siehst Du, wie der Content-Disposition Header richtig aussehen muss und wie du den Path entfernst.

    Klickst Du hier

    Eine Abfrage auf Dateiexistenz kann auch nicht schaden. Wenn sie fehlt, kannst Du einen Redirect auf eine Error-Seite machen (oder zumindest am Server loggen, dass eine falsche Ressource angefordert wurde).

    Den Einwand von Robert, dass fileinfo und readfile zu Overhead führen, würde ich im Übrigen nicht gelten lassen, weil fileinfo() wohl kaum die Größe durch Einlesen der Datei bestimmt, sondern ins Directory schaut. Also intern auch nichts anderes tut als fopen+fstat+fclose. Da readfile() einen Namen braucht, kann ich die resource-Rückgabe von fopen auch nicht für den readfile wiederverwenden, sondern müsste das Kopieren selbst programmieren. Kann man machen, lohnt aber hier sicher nicht die Mühe.

    Aber trotzdem. Ein "mal geht's und mal nicht" ist damit noch nicht erklärt. Hast Du error_reporting eingeschaltet? Diese beiden Zeilen habe ich aus einem stackoverflow-Artikel, weiß nicht ob es anders eleganter geht.

    error_reporting(E_ALL);
    ini_set('display_errors', 1); 
    

    Gruß Rolf

    1. Hallo,

      Die Leerstelle ist dir sicher irrtümlich durchgerutscht :)

      das vermute ich auch, sonst würde es nämlich nicht "manchmal" nicht funktionieren, sondern nie.

      Eine Abfrage auf Dateiexistenz kann auch nicht schaden. Wenn sie fehlt, kannst Du einen Redirect auf eine Error-Seite machen (oder zumindest am Server loggen, dass eine falsche Ressource angefordert wurde).

      Nein, bitte kein Redirect im Fehlerfall, sondern direkt einen HTTP-Status 404 ausgeben. Und zwar unter der ursprünglich angefragten URL.

      Aber trotzdem. Ein "mal geht's und mal nicht" ist damit noch nicht erklärt. Hast Du error_reporting eingeschaltet? Diese beiden Zeilen habe ich aus einem stackoverflow-Artikel, weiß nicht ob es anders eleganter geht.

      Deswegen hatte ich ja schon gefragt, wie sich "geht nicht" eigentlich manifestiert. Aber dazu hat sich Sepp leider (noch?) nicht geäußert.

      So long,
       Martin

      --
      Nothing travels faster than the speed of light with the possible exception of bad news, which obeys its own special laws.
      - Douglas Adams, The Hitchhiker's Guide To The Galaxy
      1. Danke für Eure Hilfe. Wenn ich die Datei in das gleiche Verzeichnis lege wie das Script für die Ausgabe geht es jetzt.

            $file = 'test.pdf';
        
            header('Content-Description: File Transfer');
            header('Content-Type: application/pdf');
            header('Content-Disposition: filename="'.basename($file).'"');
            header('Expires: 0');
            header('Cache-Control: must-revalidate');
            header('Pragma: public');
            header('Content-Length: ' . filesize($file));
            readfile('ausgabedateien/'.$file);
            exit;
        	
        

        nur mit dem Pfad bei READFILE stimmt etwas nicht. Die Datei leigt im Verzeichniss ausgabedateien.

        Sepp

        1. Hallo

              $file = 'test.pdf';
          

          Du hast nun einen Dateinamen angegeben.

              header('Content-Disposition: filename="'.basename($file).'"');
          

          Der Dateiname wird nun korrekt in den HTTP-Header eingefügt.

              header('Content-Length: ' . filesize($file));
          

          Allerdings wird es ab hier etwas verwirrend. Die Funktion filesize braucht nicht nur einen Dateinamen, sondern Zugang zur Datei. Deshalb …

              readfile('ausgabedateien/'.$file);
          

          nur mit dem Pfad bei READFILE stimmt etwas nicht. Die Datei leigt im Verzeichniss ausgabedateien.

          … verwundert mich dein letzter Satz. Wenn die Datei „test.pdf“ wie du anfangs schriebst, im selben Verzeichnis wie das Skript liegt, ist das Voranstellen von „ausgabedateien/“ vor den Dateinamen, der in $file liegt, falsch. Ist jedoch die Angabe von „ausgabedateien/“ vor dem Dateinamen richtig, weil, wie du zum Schluss angibst, die Datei in eben diesem Verzeichnis liegt, kann filesize($file) nicht funktionieren, weil die Datei, falls nicht eine Kopie von „test.pdf“ im Skriptverzeichnis liegt, nicht gefunden werden kann.

          Tschö, Auge

          --
          Wo wir Mängel selbst aufdecken, kann sich kein Gegner einnisten.
          Wolfgang Schneidewind *prust*
    2. Moin,

      Den Einwand von Robert, dass fileinfo und readfile zu Overhead führen, würde ich im Übrigen nicht gelten lassen, weil fileinfo() wohl kaum die Größe durch Einlesen der Datei bestimmt, sondern ins Directory schaut. Also intern auch nichts anderes tut als fopen+fstat+fclose. Da readfile() einen Namen braucht, kann ich die resource-Rückgabe von fopen auch nicht für den readfile wiederverwenden, sondern müsste das Kopieren selbst programmieren. Kann man machen, lohnt aber hier sicher nicht die Mühe.

      filesize und readfile rufen beide fstat auf und das kann unter Umständen „teuer“ sein, also ein Flaschenhals, auch wenn beide im Inode nachschauen.

      Und das Kopieren einer ganzen Datei nach dem Öffnen muss ich nicht selbst programmieren, wenn es dafür die Funktion fpassthru gibt.

      Viele Grüße
      Robert