phpslim

Slim PHP download loads a page with gibberish


I'm using Slim 4. I am trying to download a file, but when I click on the link, instead of the file downloading, the browser window loads a lot of gibberish.

Project structure

app (Controller is in here)
public (public site is here)
routes
uploads (file to download is here)
vendor
views

Route:

$app->get('/adm/download/', DownloadController::class . ':download')->setName('adm.download');

Controller:

public function download($request, $res, $args) {
    $file = "/home/acct/public_html/project/uploads/application/ourfile.jpg";
    
    $response = $res->withHeader('Content-Description', 'File Transfer')
    ->withHeader('Content-Type', 'application/octet-stream')
    ->withHeader('Content-Disposition', 'attachment;filename="'.basename($file).'"')
    ->withHeader('Content-Transfer-Encoding', 'binary')
    ->withHeader('Expires', '0')
    ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
    ->withHeader('Pragma', 'public')
    ->withHeader('Content-Length', filesize($file));
    ob_clean();
    flush();
    readfile($file);
    return $response;
}

View:

<a href="{{ url_for('adm.download') }}">Download file</a>

I have tried removing ob_clean() and flush() and moving it to different positions. Nothing works. The file is never downloaded.


Solution

  • The gibberish you describe is most likely the raw JPEG file, because it's a binary picture format and that's what binary looks like when you try to display it as text. But, why is browser trying to display it as text, if you set application/octet-stream and binary, and why is it displaying it in line rather than downloading, if you set attachment?

    Because you add headers to $response but you never populate it with file contents. Instead, you call readfile() to write file into output buffer. So, unless you have an active output buffer, this will happen:

    1. PHP detects that readfile() starts emitting response body, so it prints builtin and previously set headers, then appends file contents.
    2. Slim processes response and tries to emit your custom headers. That isn't possible, because headers have already been sent. Slim detects so at \Slim\ResponseEmitter::emit() and ignores your headers altogether, because doing otherwise will trigger a Cannot modify header information - headers already sent by PHP warning.

    If you are using Slim, you need to use it all the way through. The most straightforward way to is to populate response body:

    $response
        ->getBody()
        ->write(file_get_contents($file));
    

    But there's a dedicated method for files:

    $streamFactory = new \Slim\Psr7\Factory\StreamFactory();
    
    $response = $res->withHeader('Content-Description', 'File Transfer')
        ->withHeader('Content-Type', 'application/octet-stream')
        ->withHeader('Content-Disposition', 'attachment;filename="' . basename($file) . '"')
        ->withHeader('Content-Transfer-Encoding', 'binary')
        ->withHeader('Expires', '0')
        ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
        ->withHeader('Pragma', 'public')
        ->withHeader('Content-Length', filesize($file))
        ->withBody($streamFactory->createStreamFromFile($file));
    
    return $response;