PHP zip download bug - php

I have a script that lets users download a zip file on request. It works 100% on computer browsers but does not work on Android/mobile browsers, except for Opera (mobile) only.
Here's my script.
$leads = new Packer($zip_name);
$index = 1;
$count = 0;
foreach($cLeads->lastUnitInfo['leads'] as $lead)
{
// build a request string
$export = 'export_lead_'.$index;
$req = $_POST[$export];
// add it to the zip file
if(isset($req) && $req == '1')
{
// debug only
//echo 'adding lead: '.$lead['file_name'].'<br />';
$leads->addLead('leads/'.$lead['file_name'],$lead['item_name']);
$count++;
//echo 'count: '.$count.'<br/>';
}
$index++;
}
// debug
//exit('count: '.$count); // displays same results on all browsers.
// we got anything packed ?
if($count <= 0) //// <-------- BLOCK OF BUG ON MOBILE PHONE
{
if(file_exists($zip_name))
unlink($zip_name); // delete the zip file created.
exit('<h1>Nothing to export</h1>');
} ///// <---------------------- END BLOCK
// download the leads here.
$leads->purge();
exit;
Here's my purge() function
public function purge($zip_name = 'leads.zip')
{
header('Content-type: application/zip');
header('Content-Disposition: attachment; filename="'.$zip_name.'"');
ob_clean();
flush();
readfile($this->zip_name);
// errors will be disabled here
unlink($this->zip_name);
}
On my Android phone, the zip file gets downloaded but contains <h1>Nothing to export</h1> which renders it as an invalid zip file.
So my problem is, How is it that the block only executes on mobile browsers (except Opera) and then continues to download the zip when it should have exited if $count was zero at all?
I debugged it using Fiddler and the requests are all the same but the output is different, why ?
Is this a bug in PHP? Because if you look at my code the function purge() should output errors saying headers have already been sent but it just continues to download the zip file.
Browsers:
Dolphin (+Beta)
FireFox
Default Android browser
Boat Browser
PHP versions tested:
5.3.13 (Production, shared server)
5.1.4
This is driving me nuts now.
#Alix This is the weirdest bug I've ever seen. I don't seriously see any logical errors here. For the script to initiate a download, there actually has to be files added to the zip. Now on my phone, it says no files are added but there's a zip file in the temp folder. Moreover if there are no files added ($count = 0) then the script should terminate (hence the exit() function) with just a message <h1>Nothing to export</h1>. But it goes on to download the zip file (which at this time does not exist, but in the temp folder it does). The zip file ends up being corrupt as it contains <h1>Nothing to export</h1>
Alix wrote:
*> what happens if you comment out the exit call before serving the file?
It says readfile error then purges the zip file in gibberish UNICODE chars. I can tell it's the zip file because it starts with PK and contains the names of images being exported.
Alix wrote:
If this doesn't work, you may wanna change exit('<h1>Nothing to export</h1>'); to exit(var_dump($_REQUEST));, that way you may check for possible bugs in the form submission by inspecting the Zip file.
Interesting. It prints nothing but the cookie and a $_GET parameter only. If I put the code in your suggestion at the beginning on the script and in the block that adds the files to the zip it prints all the $_POST variables.
There's clearly a bug on PHP's part here. It should terminate when exit is called but it doesn't. And mind you this only happens on mobile browsers except Opera. I'm going to cry blood now.

Are you sure the other browsers are setting $_POST[$export] to the correct value?
Also, you should first ob_[end_]clean() and then output the headers, not the other way around:
public function purge($zip_name = 'leads.zip')
{
ob_end_clean();
flush();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="'.$zip_name.'"');
readfile($this->zip_name);
// errors will be disabled here
unlink($this->zip_name);
}
Additionally, you may wanna set these headers:
header('Content-Length: ' . intval(filesize($zip_name)));
header('Content-Transfer-Encoding: binary');
If this doesn't work, you may wanna change exit('<h1>Nothing to export</h1>'); to exit(var_dump($_REQUEST));, that way you may check for possible bugs in the form submission by inspecting the Zip file.

Related

Download abuse with php Content-Disposition: attachment and readfile

I'm having a download abuse issue with php Content-Disposition: attachment and readfile. It seems that my problem is with readfile, because although this script works, whether or not the client closes their browser, readfile reads the entire contents of the mp4, setting up the possibility of abuse with scripts initiating the download and immediately closing the progress. Something, somewhere, is running a script which clicks this link hundreds of times per second, running my php script and immediately cancelling their download, but my server is preparing that entire file to be offloaded each time.
Here's the script I'm running, when the user/abuser clicks a download link:
<?php
// get MP4 address
$MP4Address = $_GET["MP4Address"];
// We'll be outputting a MOV
header( 'Content-Type: application/octet-stream' );
$filename = basename($MP4Address);
// Name file
header('Content-Disposition: attachment; filename="'.$filename.'"');
// Source file
readfile($MP4Address);
?>
I suspect that readfile is the culprit here, but without it, the client will receive an empty file. There must be a more modern, proper way of doing this, but I'm not sure what it could be.
Unless you've called ignore_user_abort(true) PHP should get the signal that the connection has been aborted and cease execution. But it's possible that once you're inside the readfile() call PHP is not able to watch for that signal since it's busy doing low-level IO.
I would say you've got 2 options, the first being to simply write some code to detect and block the person that's abusing your service. You downloads are already backed by a PHP script, so adding in a bit of checking and filtering should be relatively simple.
The other would be to replace the readfile() call with a bit of [admittedly less efficient] code that should give PHP some breathing space to look for user aborts.
function read_file($filename, $chunksize=4096) {
if( ! $fh = fopen($filename, 'rb') ) {
throw new \Exception('Failed to open file');
}
while($chunk = fread($fh, $chunksize)) {
echo $chunk;
}
fclose($fh);
}

Readfile is best solution to download external files?

I need to get a remote file and give it to user without saving it to my server disk (for hiding original URL) and found a lot of posts about download external files with various functions like file_get_contents or readfile. Already I'm using this one:
function startDownload($url){
if($this->url_exists($url))
{
//get filename from url
$name=$this->getFileName($url);
//first flush clear almost output
ob_end_flush();
//final clear
ob_clean();
//set headers
header('Content-Type: application/octet-stream');
header("Content-Transfer-Encoding: Binary");
header("Content-disposition: attachment; filename=\"" . $name . "\"");
//send file to client;
readfile($url);
//exit command is important
exit;
}
else JFactory::getApplication()->enqueueMessage(JText::_('URL_NOT_FOUND'), 'error');
}
And that's working but there is a problem! For a file with 200 MB size it takes ~ 10 seconds to start download in client browser. I think it's because readfile first downloads whole file to my server buffer and then give it to user. Is that right?
And is it possible to make it faster? for example download be started before fetch ended or it isn't possible technically?
In fact I don't know that this method is optimised or not. Any technical advice would be appreciated.
Note :
I know that this function should be changed for big files and that's not my concern now.
I consider to buy the external server in the same datacenter to make this download faster.
Target is that [File server] be separate than the file [online shop].
I tested curl method that mentioned by #LawrenceCherone. It worked nicely but when moved it to my project the result was the same as readfile (white screen for a few seconds).
So suspect to readfile() function. Separate my previous code to a single PHP file and result was amazing! Download starts immediately.
So I think my guess wasn't right and problem was not related to readfile function.
After a little search found a minor modification. I added below line :
while (ob_get_level()) ob_end_clean();
before the :
readfile($url);
And now download starts before whole file fetched in my server.

Is it possible to send a header from a PHP file, that does absolutely nothing

On a page where I offer music sample downloads, I have several <a> tags whose href points to a PHP file. Various data included as GET vars allow the proper file to be downloaded. Normally the PHP will respond with typical download headers followed by a readfile(). (the code for that is below, FYI). This results in a clean download (or download / play dialog box on some browsers). By "clean", I mean the download is completed with no disturbance in the visitors page.
However, in the unlikely event that the requested file is unavailable, I don't know what to do. I know it should not happen, but if it does I would like the download link to simply do NOTHING. Unfortunately since it is an <a> tag referencing a PHP file, doing nothing results in the browser clearing the page, with the URL of the PHP file in the address bar. Not a good visitor experience! So I'd like way to avoid disturbing the page and doing NOTHING if there is is an errant request. I'll use javascript to alert the visitor about what went wrong, but I can't have the errant file request clear the page!
I thought I'd had a solution by issuing a header('Location: #'); when the script detected an impossible file download. But after a few seconds the browser cleared the page and put up a message indicating the page "redirected you too many times." (indeed, my script log fills up with over 100 entries, even though i only clicked the tag once.)
So far the only solution I have that works (works in the sense of NOT disturbing the visitors page if an "unavailable" file is requested) is to point my download headers at a "dummy" file. An actual "silence.mp3" or "nosong.mp3" file. But is there a way to call a header() that does nothing to the calling page? Simply calling exit or exit() won't work (the visitor page is redirected a blank.)
Not that it matters, but this is the code I normally call in response to the d/l request...
function downloadFile($path) {
$path_parts = pathinfo($path);
$ext = strtolower($path_parts["extension"]); // don't need this.
$fsize =fileExists($path);
if ($fsize == 0)
{
header('Location: #'); // this doesn't work!!! (too many redirectcts)
exit;
}
//$dlname = $path_parts['filename'] . "." . strtolower($path_parts["extension"]);
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Disposition: filename=\"" . $path_parts["basename"]."\"");
header("Content-Type: application/x-file-to-save");
header("Content-Transfer-Encoding: binary");
if($fsize) header("Content-length: $fsize");
$bytesRead = readfile($path);
return $bytesRead;
}
If you are using HTTP/1.x with a standard anchor tag, without JavaScript or other client-side interception. An HTTP/1.0 204 No Content status header will cause the user-agent to simply seem like nothing happened when clicking a link that returns a 204 status header.
HTTP/1.0 204 No Content
The server has fulfilled the request but there is no new information
to send back. If the client is a user agent, it should not change its
document view from that which caused the request to be generated. This
response is primarily intended to allow input for scripts or other
actions to take place without causing a change to the user agent's
active document view. The response may include new metainformation in
the form of entity headers, which should apply to the document
currently in the user agent's active view.
Source: https://www.w3.org/Protocols/HTTP/1.0/spec.html#Code204
This is also compatible with the HTTP/1.1 protocol.
I recommend using output buffering to ensure no other content is being sent by your application by mistake. Additionally there should be no need to send a Content-Length header.
function downloadFile($path) {
if (!is_file($path) || !($fsize = filesize($path))) {
header('HTTP/1.0 204 No Content');
exit;
}
$path_parts = pathinfo($path);
header('Cache-Control: public');
header('Content-Description: File Transfer');
header('Content-Disposition: filename="' . $path_parts['basename'] . '"');
header('Content-Type: application/x-file-to-save');
header('Content-Transfer-Encoding: binary');
header('Content-length: ' . $fsize); //fsize already validated above.
return readfile($path);
}
Performing the file checks before creating the links is the simplest way to do this.
If I understand your request correctly you have files that you wish to allow a client to download, and links to PHP scripts that download certain files.
The problem with your implementation is that when the file is empty, the PHP script still must load and change the content of the clients page(from the action of loading the script), which is the incorrect behavior (correct being no action at all).
Since you are using tags on the main download page, really the only way to not change the content of the page in the case of a missing file is to compute the content of the tags in advance. With a simple PHP function you could check the contents of a list of files and their directories, and then generate links for the ones that exist, and blank links for the ones that do not.
Overall, I think separating the functionality of checking whether a file exists and actually downloading the file to a client is the only way to allow the functionality you desire.

Blocks can only contain strings error when saving to disk

Background
I'm generating an Excel sheet that get's emailed to a certain address. It works perfectly on localhost (saves perfectly, can't test the mailing as I have no local mailserver running). When I host it, I run into some problems.
What problems
I've narrowed it down to this line:
$this->PhpExcel->saveToDisk($path);
$path is a custom path that is set at runtime. When I run that, it saves the file to disk but it gives me this:
This is a function in a custom helper class. Below is the code for the function:
public function saveToDisk($path) {
// set layout
$this->_View->layout = '';
// headers
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="'.$filename.'"');
header('Cache-Control: max-age=0');
// writer
$objWriter = PHPExcel_IOFactory::createWriter($this->xls, 'Excel2007');
ob_end_clean();
$objWriter->save($path);
// clear memory
$this->xls->disconnectWorksheets();
}
But here is the catch. When I output it to browser, by changing $objWriter->save($path); to $objWriter->save('php://output'); it gives me no error and exports the file perfectly.
Question
I've googled the error and tried my best to find the solution, but to no avail. The amount of info available on this error is staggeringly scarce or outdated. Could someone please tell me what it is that I'm missing or doing wrong?
When you've saved the file to disk, CakePHP still expects you to give it something to return to the browser that made the request, even if it's just a message to say that the file has been saved successfully, or that the email has been sent successfully.
If you're saving to disk, you don't want to send the headers because the server isn't going to be returning an Excel file, so no headers.
I fixed it by removing this line:
ob_end_clean();

Download file size is unknown although file size is present in header

I am doing the follwoing on a web page: A click on an element sends a set of data (attached to the element) to my server which will then generate a custom zip-file:
$.post(urlprefix + 'makeZip.php', params, function(data){
window.location = urlprefix + 'getZip.php?file=' + data; //get file
}).error(handleAJAXError);
makeZip.php works just fine and returns the name of the (temporary) zip file that the client should then download. As I want to keep my server clean I route the file through another script called getZip.php which does the following:
/* RETURN REQUESTED FILE AND DELETE FROM SERVER*/
$filename = $_GET['file'];
/* TRANSFER FILE CONTENTS */
header('Content-type: application/zip');
header('Content-Disposition: attachment; filename="customDownload.zip"');
header('Content-Length: '.filesize($filename));
readfile($filename);
/* REMOVE FILE FROM SERVER */
unlink($filename);
All browsers will download the file successfully, yet I am facing one problem: the files can get rather big (up to 200MB) so I thought it would be nice to have an estimate of how long the download's going to take. That's why I am sending the Content-Length header (the specified filesize is correct). Yet, all browsers I tested this is are telling me the filesize is unknown (which might lead to the user skipping the download).
Is this some kind of problem with my header information? Is it a client-side problem? Should I use another approach to getting the client to download the file?
If the client shows "unknown filesize" in the dialog when downloading a file provided via readfile, you'd better check if mod_deflate, mod_gzip, mod_something-that-shrinks-http is installed on your server, and put an exception for given download.
More info here
EDIT by m90:
In my particular case (running Apache) I turned shrinking off by using:
#apache_setenv('no-gzip', 1);
#ini_set('zlib.output_compression', 0);
Filesize headers are sent and received correctly now in all browsers

Categories