Datei-Downloads im Zend Framework

Wir kennen das: an irgendeiner Stelle unseres Webauftritts sollen Dateien zum Download angeboten werden. So lange die Dateien mit genau dem Dateinamen auf der Platte des Besuchers landen sollen, wie diese im Dateisystem des Servers abgelegt sind, ist das fast trivial. Im einfachsten Fall setzt man einfach einen Link auf eine Datei. Was aber, wenn die (womöglich auch noch dynamisch) unter einem anderen Namen ausgeliefert werden soll?

Anforderungen

Was sollte die Implementierung berücksichtigen?

  • eine Datei sollte dynamisch einen beliebigen Namen bekommen können
  • HTTP-Standards sollen erfüllt werden

Der zweite Punkt erfordert ein wenig Schläfenmassage, denn es gibt leider eine Firma, die sich schwer damit tut, Standards zu lesen, die nicht aus dem eigenen Hause stammen. Üblicherweise sorgt der HTTP-Header Content-Disposition mit einem Wert, der mit „attachment; …“ beginnt, dafür, dass der Client den Anwender fragt, wohin die Datei gespeichert werden soll. Beginnt der Wert mit „inline; …“, so versucht der Client die Datei entweder direkt zu öffnen oder an das passende Programm zu übergeben. Nicht so der Internet Explorer. Diverse Bugs und „Interpretationen“ säumen den Weg der Implementierung dieses Headers im IE1. Der sichere Weg liegt meiner (und vieler anderer Leute) Erkenntnis nach darin, im Content-Type-Header anstelle des korrekten MIME-Typs den Typ für ausführbare Dateien „application/octet-stream“ zu verwenden. Also erweitern wir die Liste der Anforderungen:

  • bescheuerte Bugs einer Browserimitation aus Redmond kleinere Besonderheiten Internet Explorers sollen umschifft werden

Eine sehr gelungende Umsetzung als ActionHelper hat Rolando Espinoza La fuente als ZF-Snippet abgelegt. Allerdings setzt er das mod_xsendfile Apache-Modul, welches zugegebenermaßen das Leben an dieser Stelle enorm vereinfachen würde, voraus. Das bieten leider sicher die wenigsten Hoster standardmäßig an. Zum Glück ist eine auf dem Xsendfile-Helper basierende Lösung relativ einfach umzusetzen. Das erweitert unsere Anforderungen weiter um:

  • es sollen keine zusätzlich zu installierenden Apache-Module vorausgesetzt werden
  • der Code sollte so ressourcenschonend wie möglich arbeiten

Da wir den Umgang mit der Datei also nicht allein einem Apache-Modul überlassen können, müssen wir die HTTP-Header im PHP setzen und auch die Ausgabe der Datei mit PHP erledigen. Mit der PHP-Funktion file_get_contents würde das zum Beispiel funktionieren. Dummerweise lädt diese Funktion den Inhalt der Datei erstmal in den RAM. Das ist nicht nur unnötig und ineffizient – es würde sicher auch zu Fehlern führen, da RAM meist nicht unbegrenzt zur Verfügung steht. Die Lösung ist readfile, denn diese Funktion leitet den Inhalt der Datei direkt in den Ausgabepuffer. Wenn man den deaktiviert, dann landet die Ausgabe von readfile direkt im Ausgabestream.

In der Informatik soll es ja angeblich nur zwei wirklich schwierige Probleme geben: Dinge zu benennen und Caches zu invalidieren2. Caching in HTTP ist jedenfalls ein Thema für sich. Um sicher zu stellen, dass ein Client immer die aktuelleste Version eines Downloads erhält, teilen wir dem Client mit, dass die Datei zu einem Zeitpunkt in der Vergangenheit abgelaufen ist. Beim nächsten Download muss der Client also die Datei auf jeden Fall neu vom  Server anfordern und darf diese nicht aus dem Cache holen. Dazu setzen wir den Expires-Header auf den ungültigen Wert „-1“. Daher kommt noch

  • sicher stellen, dass Downloads immer frisch sind

als Anforderung hinzu.

Nun denn, auf ans Werk!

Die Bausteine

Das Klassengerüst könnte zum Beispiel so aussehen:

Damit hätten wir den Helper und die direct-Methode3, dank derer der Helper in der Action-Methode einfach aufgerufen werden kann.

Die eigentliche Arbeit wird die Methode downloader erledigen:

Jetzt brauchen wir noch eine Eigenschaft, in der abgespeichert wird, ob ein Download erzwungen werden soll:

Diese Eigenschaft ist standardmäßig auf true gesetzt, da das der häufigere Fall sein wird.

Um diese Eigenschaft ändern zu können, brauchen wir noch eine passende Methode:

Schlussgedanken

Dieser Helper kümmert sich nur darum, eine Datei zum Client zu befördern. Der ganze Aspekt der Sicherheit der Parameter muss anderswo im Code erledigt werden (sinnvollerweise im Controller). Der Helper überprüft auch nicht, ob der gewünschte Dateiname gültig ist4, oder ob überhaupt eine passende Datei im Dateisystem abgelegt ist5.

Der Einfachheit halber hier noch einmal der Code in Gänze:

  1. Mehr dazu findet sich in dieser Zusammenstellung: http://greenbytes.de/tech/tc2231/
  2. Angeblich stammt der Ausspruch „There are only two hard things in Computer Science: cache invalidation and naming things.“ von Phil Carlton (http://martinfowler.com/bliki/TwoHardThings.html)
  3. Mehr Hintergründe zu diesem Pattern hat Rob Allen schon einmal zusammen gefasst: http://akrabat.com/zend-framework/using-action-helpers-in-zend-framework/
  4. Dazu müsste mal jemand einen kleinen Validator schreiben. *Hüstel*
  5. Das sieht doch nach einer lohnenden Aufgabe für Zend_Validate_File_Exists aus: http://framework.zend.com/manual/1.12/de/zend.file.transfer.validators.html#zend.file.transfer.validators.exists