Showing posts with label HttpFoundation. Show all posts
Showing posts with label HttpFoundation. Show all posts

Thursday, April 14, 2016

Serving protected files with Symfony2

If you just want to serve a file without any restriction you just generate a  path like this:
<a href="http://myvirtualhost.localhost/uploads/flower.jpeg" download> Nice flower</a>

Where:
 -  the virtual host directory is pointing to  ...www\symfony_prj\web
 - "uploads" directory is found under Symfony's  "web" directory

In this approach anyone can access the files. In many situations this is not the desired behavior.

Serving protected files with Symfony2

First we should let Apache know that access to the files should be blocked, so in the "uploads" file I will add a .htaccess file with just one row:
deny from all
 If you try again to access the link you get an error that your browser cannot find it.

If I would use plain PHP the solution for serving the files could be the one described here http://php.net/manual/en/function.readfile.php

<?php
$file 
'monkey.gif';

if (
file_exists($file)) {
    
header('Content-Description: File Transfer');
    
header('Content-Type: application/octet-stream');
    
header('Content-Disposition: attachment; filename="'.basename($file).'"');
    
header('Expires: 0');
    
header('Cache-Control: must-revalidate');
    
header('Pragma: public');
    
header('Content-Length: ' filesize($file));
    
readfile($file);
    exit;
}
But I am using Symfony, and I have access to the HttpFoundation component:
The HttpFoundation component defines an object-oriented layer for the HTTP specification.
The official documentation  about serving files can be found here:
http://symfony.com/doc/current/components/http_foundation/introduction.html#serving-files

One of the options described is using a BinaryFileResponse.

The link from our page will not point direct to the file, instead will be a regular Symfony route to a controller.

The controller looks like this:


 
    /**
     * Serve a file
     *
     * @Route("/download/{id}", name="file_download", requirements={"id": "\d+"})
     * @Method("GET")
     */
    public function downloadFileAction(Request $request, File $file)
    {

        /*
         * $basePath can be either exposed (typically inside web/)
         * or "internal"
         */
        $filename= $file->getName();
        $basePath = $this->container->getParameter('my_upload_dir');
        $filePath = $basePath.'/'.$filename;
        // check if file exists
        $fs = new FileSystem();

        if (!$fs->exists($filePath)) {
            throw $this->createNotFoundException();
        }

        // prepare BinaryFileResponse
        $response = new BinaryFileResponse($filePath);
        $response->trustXSendfileTypeHeader();
        $response->setContentDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            $filename,
            iconv('UTF-8', 'ASCII//TRANSLIT', $filename)
        );
        return $response;
    }

I have a File class mapped with Doctrine, Symfony is smart enough to transform the route "id" parameter to the actual File object instance. In the File object only the name of the file is saved, and it can be retrieved with the method getName().

The function "setContentDisposition"  can receive as parameter the constant:
ResponseHeaderBag::DISPOSITION_ATTACHMENT and is asking for download, or you can force download with ResponseHeaderBag::DISPOSITION_INLINE

This article is inspired from the above mentioned documentation links and from this blog post:  http://symfonybricks.com/en/brick/how-to-force-file-download-from-controller-using-binaryfileresponse