Vom: 12.09.2013

Pimcore Assets schützen

Assets sind unter Pimcore sämtlich völlig frei zum Download verfügbar. Erreicht wird dies dadurch, dass mod_rewrite des Webservers die Datei bei Existenz im Asset-Verzeichnis als Weiterleitung an den User ausliefert, ohne daß Pimcore hierbei involviert wird. Von einem Kunden hatten wir die Anforderung, daß einige dieser Files schützbar und zum Teil mit einem Freigabeprozess versehbar sein könnnen müssen. Es muss also dafür gesorgt werden, dass Pimcore den Download-Request zu Gesicht bekommt und ggfs ablehnt oder einen Freigabeprozess anstösst. Wir zeigen hier, wie wir dieses Problem angegangen sind.

Assets schützen: die Lösung allgemein

Die Möglichkeit des Downloads eines Assets beruht auf einer Regel für das Apache-Modul mod_rewrite, diese ist von Pimcore in der Datei .htaccess definiert. Im wesentlichen besagt diese "wenn die URI des GET-Requests exakt auf eine Verzeichnis/Datei-Kombination im Asset-Verzeichnis passt, mache einen Redirect auf das Assetverzeichnis + URI", was in einer Auslieferung des Files resultiert. Wir verbiegen diesen Redirect nun auf eine Methode eines Controllers, welche nun kontrolliert, ob das File überhaupt heruntergeladen werden darf und liefert die Daten dann ggfs. selbst aus. Die Schwierigkeit dieser Auslieferung bestand darin, dass die Dateien des Kunden zum Teil sehr groß sind (DVD-Images und Ähnliches), und "einfache" PHP-Funktionen wie file_get_contents() das File erst komplett in den Speicher lesen um es dann an den Browser zu pipen. Zum einen muss hier genug Hauptspeicher vorhanden sein um die Datei aufnehmen zu können, zum anderen entsehen hier auch lange Wartezeiten. Wir arbeiten hier also mit fread(), das Dateien in definierten Häppchen einlesen kann und pipen diese Stücke fortwährend per ob_flush()/flush() an den Browser. Die Zugriffsberechtigung speichern wir für unser Beispiel in den Properties des Assets selbst. Hat das Asset ein Property "protected" und steht dieses auf true, wird die Auslieferung verweigert. Eine konkrete Implementation könnte hier aber ausführlicher sein.

Beispielhaft: unsere Lösung

Hier also nun die veränderte .htaccess. Wir leiten hier auf den Controller "download",dessen Action "download" und übergeben als Parameter "filename" die URI des angefragten Assets. Wir bedienen uns dabei der Angabe von Controller/Action über GET-Parameter und ersparen uns so die Erstellung von statischen Routen.

# ASSETS: check if request method is GET (because of WebDAV) and if the requested file (asset) exists on the filesystem, if both match, deliver the asset directly 
RewriteCond %{REQUEST_METHOD} ^GET
RewriteCond %{DOCUMENT_ROOT}/website/var/assets%{REQUEST_URI} -f
# RewriteRule ^(.*)$ /website/var/assets%{REQUEST_URI} [PT,L]
RewriteRule ^(.*)$ /index.php?controller=download&action=download&filename=%{REQUEST_URI} [PT,L]

Dies ist der Quellcode unseres Beispielhaften Download-Controllers. Es wird für ein korrekts Handling der HTTP-Statuscodes, hier also z.B. 403 - Forbidden um die Suchmaschinen fernzuhalten, weiterhin eine Action "error" genutzt - die zugehörige View darf sich jeder selbst ausdenken ;)

 
class DownloadController extends Website_Controller_Action {
  private $chunk_size;
  public function init() {
    parent::init();
    $this->chunk_size = 1024*1024;
    $this->setLayout('content');
  }

  public function downloadAction() {
    $file_name = $this->_getParam("filename");
    $asset = Asset::getByPath($file_name);
    if ($asset->hasProperty("protected") && $asset->getProperty("protected") === true) {
      $this->setError(403, "Protected);
      return;
    }

    header("Content-Type:".$asset->getMimetype());
    $this->disableLayout(); // Nur nötig, wenn ein Layout verwendet wird
    $this->removeViewRenderer();

    $this->readfile_chunked(PIMCORE_ASSET_DIRECTORY."/".$file_name);
  }

  private function setError($code, $title, $message = "") {
    $Response = $this->getResponse();
    $Response->setHttpResponseCode($code);
    $this->forward("error", null, null, array(
      "title" => $title,
      "message" => $message
    ));
  }

  public function errorAction() {
    $this->view->title = $this->getParam("title");
    $this->view->message = $this->getParam("message");
  }

  private function readfile_chunked($filename, $return_bytes = true) {
    $bytes_delivered =0;

    $handle = fopen($filename, 'rb');
    if ($handle === false) throw new Exception("File not found");
    while (!feof($handle)) {
      $buffer = fread($handle, $this->chunk_size);
      print $buffer;
      ob_flush();
      flush();
      if ($return_bytes) $bytes_delivered += strlen($buffer);
    }
    $status = fclose($handle);
    if ($return_bytes && $status) {
      return $bytes_delivered; // return num. bytes delivered like readfile() does.
    }
    return $status;
  }
}