Asked  7 Months ago    Answers:  5   Viewed   33 times

I'm trying to force a download of a protected zip file (I don't want people to access it without logging in first.

I have the function created for the login and such , but I'm running into a problem where the downloaded file is corrupting.

Here's the code I have:

$file='../downloads/'.$filename;
header("Content-type: application/zip;n");
header("Content-Transfer-Encoding: Binary");
header("Content-length: ".filesize($file).";n");
header("Content-disposition: attachment; filename="".basename($file).""");
readfile("$file");
exit();

Here's the error: Cannot open file: It does not appear to be a valid archive.

The file downloads fine otherwise, so it must be something I'm doing wrong with the headers.

Any ideas?

 Answers

67

This issue can have several causes. Maybe your file is not found or it can not be read and thus the file’s content is just the PHP error message. Or the HTTP header is already sent. Or you have some additional output that then corrupts your file’s content.

Try to add some error handling into your script like this:

$file='../downloads/'.$filename;
if (headers_sent()) {
    echo 'HTTP header already sent';
} else {
    if (!is_file($file)) {
        header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
        echo 'File not found';
    } else if (!is_readable($file)) {
        header($_SERVER['SERVER_PROTOCOL'].' 403 Forbidden');
        echo 'File not readable';
    } else {
        header($_SERVER['SERVER_PROTOCOL'].' 200 OK');
        header("Content-Type: application/zip");
        header("Content-Transfer-Encoding: Binary");
        header("Content-Length: ".filesize($file));
        header("Content-Disposition: attachment; filename="".basename($file).""");
        readfile($file);
        exit;
    }
}
Wednesday, March 31, 2021
 
Jimenemex
answered 7 Months ago
36

Checking the engine source for headers_list and http_response_code, notice that the value for general headers and status code are separated:

// headers_list
SG(sapi_headers).headers

// http_response_code
SG(sapi_headers).http_response_code

But HTTP response code isn't the only header with dedicated storage: Content-Type does, too:

SG(sapi_headers).mimetype = NULL;

So what's going on here? The complete header() algorithm specifically checks for the following strings to adjust state:

  • HTTP/
  • Content-Type
  • Content-Length
  • Location
  • WWW-Authenticate

HTTP/ is checked specifically because that's how one set the status code explicitly before PHP 5.4: after that, http_response_code is available and is recommended for clarity. That header() was used is confusing, for the reason you're asking in this question and on general principle: the http header BNF clearly doesn't include status line:

header-field   = field-name ":" OWS field-value OWS

PHP handles the others separately because they are single-value headers and/or their value matters for efficiency in later calculations.

TL;DR: HTTP/ set by header() isn't included in headers_list() because HTTP/ status lines are not headers in the strict RFC sense. But for the PHP < 5.4 limitation that header() was the only way to set HTTP/ status, it'd likely have never been a confusing issue.

Wednesday, March 31, 2021
 
hillz
answered 7 Months ago
72

Instead of using:

$fp = fopen($file, "r");
while (!feof($fp)) {
    echo fread($fp, 65536);
    flush(); // this is essential for large downloads
}
fclose($fp);

Use readfile()

readfile($file);
Saturday, May 29, 2021
 
SpiderLinked
answered 5 Months ago
18

The url is probably a script that may be redirecting you. Use CURL instead

$fh = fopen('file.zip', 'w');
$ch = curl_init()
curl_setopt($ch, CURLOPT_URL, $url); 
curl_setopt($ch, CURLOPT_FILE, $fh); 
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // this will follow redirects
curl_exec($ch);
curl_close($ch);
fclose($fh);
Saturday, May 29, 2021
 
the_e
answered 5 Months ago
15

You can use the ZipArchive class to create a ZIP file and stream it to the client. Something like:

$files = array('readme.txt', 'test.html', 'image.gif');
$zipname = 'file.zip';
$zip = new ZipArchive;
$zip->open($zipname, ZipArchive::CREATE);
foreach ($files as $file) {
  $zip->addFile($file);
}
$zip->close();

and to stream it:

header('Content-Type: application/zip');
header('Content-disposition: attachment; filename='.$zipname);
header('Content-Length: ' . filesize($zipname));
readfile($zipname);

The second line forces the browser to present a download box to the user and prompts the name filename.zip. The third line is optional but certain (mainly older) browsers have issues in certain cases without the content size being specified.

Tuesday, June 1, 2021
 
muaddhib
answered 5 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :