Symfony-Zone | Timo Haberkern blogs about symfony

Aug/09

3

Serving PDF Files through Symfony Controllers

A lot of web applications offer to download some kind of PHP file. Maybe you want to implement some kind of export feature or you have paid content in form of a PDF or other media or you just want to secure your PDF files in the same way as the rest of your application (i.e. username/password). If this is the case you wouldn’t place your PDF-files in a subfolder of the web-Directory of your Symfony project, because all these file can be accessed directly via browser.

So how to serve your PDF-Files through a Symfony controller? it is an easy one. Just assume you put your pdf-files in a directory called “media” on the root-level of your Symfony-project.

root
  |-- apps
  |-- cache
  |-- config
  |  |-- demo.pdf
  |-- media
  |-- lib
  |-- web

These files cannot be reached by Browser, so we  need a new routing information for Symfony to get them accessible via the webbrowser. For that open the routing.yml of your application and add the following rule topmost in this file.

mediafile:
  url: /media/:filename.pdf/*
  param: { module: media, action: show }

From now on if you try to access a PDF-File in the media-folder (i.e. http://domain/media/demo.pdf) the media-Module with the action show is called. That was easy but how to implement this action. First you need to create the media module with the symfony command line tool:

$> symfony generate:module appname media

Afterwards add the following method to the class in the media/actions/actions.class.php file:

public function executeShow(sfWebRequest $request) {
  // being sure no other content wil be output
  $this->setLayout(false);
  sfConfig::set('sf_web_debug', false);

  $pdfpath = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.'media'
                       .DIRECTORY_SEPARATOR
                       .$request->getParameter('filename').'.pdf';

  // check if the file exists
  $this->forward404Unless(file_exists($pdfpath));

  // Adding the file to the Response object
  $this->getResponse()->clearHttpHeaders();
  $this->getResponse()->setHttpHeader('Pragma: public', true);
  $this->getResponse()->setContentType('application/pdf');
  $this->getResponse()->sendHttpHeaders();
  $this->getResponse()->setContent(readfile($pdfpath));

  return sfView::NONE;
}

Thats it! You can improve this Action method a little bit. For example you can add a “download”-switch. If this switch is set to 1 the file is downloaded instead of shown in the browser (i.e. http://domain/media/demo.pdf/download/1).

public function executeShow(sfWebRequest $request) {
  // being sure no other content wil be output

  $this->setLayout(false);
  sfConfig::set('sf_web_debug', false);

  $pdfpath = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.'media'
                                .DIRECTORY_SEPARATOR
                                .$request->getParameter('filename').'.pdf';

  // check if the file exists
  $this->forward404Unless(file_exists($pdfpath));

  // Adding the file to the Response object
  $this->getResponse()->clearHttpHeaders();
  $this->getResponse()->setHttpHeader('Pragma: public', true);
  $this->getResponse()->setContentType('application/pdf');

  // download-switch
  if ($request->getParameter('download', 0) == 1) {
    $this->getResponse()->setHttpHeader('Content-Disposition',
                            'attachment; filename="'.
                            $request->getParameter('filename').'"');
  }

  $this->getResponse()->sendHttpHeaders();
  $this->getResponse()->setContent(readfile($pdfpath));

  return sfView::NONE;
}

As we now use a Symfony-Controller you can do just use the complete Symfony infrastructure. You like to secure the PDF-files? Just enable the Symfony security mechanisms (is_secure: on in the security.yml) and only logged in users can download them. In the same way you can create paid content.  Or you can add watermarks to your PDF using Zend_Pdf for example.

Hope this is helpful for you.

No tags

10 comments

  • BarbouK · 3. August 2009 at 13:40

    Thank you very much for these clear explanations !

    Exactly what I needed ;)

  • manubing · 3. August 2009 at 21:03

    very helpful, thank you!

  • Leonardo Diaz · 3. August 2009 at 23:24

    i dont know if this is the best option to do it, I mean loading symfony by it’s controller (index.php), and even more how much memory will be used on this instruction (important when using shared hosts):

    $this->getResponse()->setContent(readfile($pdfpath));

    what if the pdf is huge???

    on the other hand I think this is the only way to do some things like statistics and as you say some way to control access to the files.

    Maybe a different approach like copying the accessed file to a temp location, and executing some redirection so apache server (or any other) can handle the download without executing php script for it.

    Thanks for the article, is an excelent starting point for looking on this field.

  • Aro · 27. August 2009 at 03:02

    thx, it’s Exactly what i need!! I’m changing from java to php and symfony and I really need that. thx again

  • News from the world of Symfony - September 2009 · 29. September 2009 at 03:51

    [...] them privately, or share them. The files, of course, need to be stored out of the document root. I found this article by Timo Haberkern of Symfony Zone very helpful: As we now use a Symfony-Controller you can do just use the complete Symfony infrastructure. You [...]

  • Hassan · 26. October 2009 at 05:01

    Thank you very much for these clear explanations!

    Exactly what I needed :D

  • Seb. · 16. November 2009 at 16:15

    Nice wrap up. You might also set the headers in the template:

    $sf_response->clearHttpHeaders();
    $sf_response->setContentType(‘application/pdf’);
    $sf_response->setHttpHeader(‘Content-Disposition’, ‘attachment; filename=’ . $pdfFileName);

    readfile($pdfFilePath);

  • Sean · 23. December 2009 at 18:19

    There’s a potential security hole here that you may want to be wary of – the file path you accept is handed straight from the user… this is dangerous!

    For example, say I visited
    http://domain/media/..%2Fconfig%2Fdatabases.yml/download/1

    Your script would then push out the content of your database config file – including your DB username, password, and domain! And then they can do all sorts of malicious things.

    Essentially, since you’re not filtering your files and just concatenating it with the directory path, it’s possible to use “..” to navigate around the file system, and choose any file you want.

    A better option is to provide some sort of restricted key that maps to a file path. That way, you have very tight control over which files will be served.

    Happy coding!

  • Kristof Taveirne · 2. January 2010 at 19:01

    ALL RIGHT!
    Thanks a lot!

    Exactly what I was looking for!

  • Peter · 13. April 2010 at 17:53

    Great article Timo! Thanks a lot :-) Keep them coming, symfony is great.

Leave a Reply

<<

>>

Theme Design by devolux.nh2.me