I have a really weird problem regarding a small piece of code in a CodeIgniter application. Basically, there's a page with links to various PDF files. When a user clicks on the link, the request is parsed by PHP, an observer is notified, writing the click event in the database (activity log), and then the file is outputted by using readfile().
So far, so good. Tested it, it works like a charm. The PDF is outputted for download, and the event is written in the database as it should.
The problem comes when a user clicks on such link, then cancels the download and clicks on another link no later than 9-10 seconds. When that happens, the event is registered in the database twice.
I did triple check of the observers that record the event, but they appear to be fine. Besides, there's a similar function for a video links, only it redirects to another page instead of outputting the file directly, and it works just fine.
After a few hours of scratching my head, I figured there's an issue with the readfile() function, because, if I put a var_dump();die(); or anything that outputs some text before the download and force it to come as text, the download event is recorded only once.
Here's the code in question:
public function downloadPDF($id = NULL)
{
if (($id == NULL) OR (!$this->validateId($id))) {
// redirect with error
}
$item = // code for fetching the PDF properties from the DB
$this->notify('ActivityObserver'); // writes the download event in the DB
$file = '.' . urldecode($item['link']);
$size = filesize($file);
$name = urldecode(basename($file));
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header("Content-Disposition: attachment; filename=\"$name\"");
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit();
}
Tried to test it with different browsers, the behaviour is the same. All inspector tools show only 1 request being made on click.
What am I missing in this big ugly picture? Why could it sometimes write twice instead of only once?
Thanks for your time to read this wall of text.
Related
I am working on a little resize and download image kind of php script wherein the events must happen in the following fashion :
A visitor uploads an image file.
The image is resized by the script.
A download link to the resized image appears in the front-end.
My resizing code is ready and working but I need help with the third part, that is how do I offer a download link to the recently created image. Here is a gist of how the resized image is created i the php script.
<?php
//normal validation stuff happens here
//resizing stuff happens here
//here is last part of the code that creates the resized image
$filename = uniqid();
$file = 'uploads/'.$filename.'.jpeg';
imagejpeg($new, $file, 80);
imagedestroy($new);
This download link must prompt as a download box instead of opening a new tab. Headers is what I know is needed in case we need to offer a download link. But this is useless to me unless I understand a right approach to this. Also please understand that my situation may be a little different as this is not a download link for a static-resource. Here the images to be downloaded are created dynamically at every request, everytime.
While I am struggling on my own a solution, will be great otherwise please provide me a road-map of what steps should be taken or what are functions that I will need to get this done. Thanks in advance.
Step 1 - User is uploading picture and you are saving it on server (you can resize it now) with new unique name (maybe md5 from time()?). On response page you can give link like this www.mypage.com/downloadimg.php?fileid=54gj6hg45h654g where last part is generated file name.
Step 2 - User clicks download link. You just have to send headers from Hanky 웃 Panky answer and then file_get_contents().
You can force a download using proper headers.
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
I'm building a PHP application, one section of which will export an Excel file when a user submits the last of three pages of HTML forms. This takes a while to process, so on form submit I'm bringing up a "Please Wait" popup with JavaScript prior to processing beginning. The file is then created and downloaded on the users machine by setting the headers below
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename="' . $fileName . '.xls' .'"');
header('Cache-Control: max-age=0');
The problem I'm having, is that I then need to redirect the user back to the first page of the form. I can't echo a javascript location.href call, as the content-type has already been changed before this, so it never makes it to screen. Neither can I use a standard PHP header('Location: x') redirect for the same reason.
My question is, after diverting output to a file in the way above, how can I then either get the output back to screen to echo a JavaScript redirect, or redirect the user to a new page in some other way?
As always, any help is much appreciated.
James
Try this code
<?php
echo "<script>
window.location='page.php';
</script>";
?>
As described above there's no way to send new headers after the page has loaded, but there is a way to get the functionality I wanted.
The answer was to create an AJAX call, running in a setInterval loop which looks for a $_SESSION variable in the PHP. This session variable is set after the Excel file is created (where I was previously trying to place the redirect), causing the AJAX function to return success, and then perform a location.href redirect to the correct page.
the below code can do it. it can redirect you last page from where the download request is sent no need to echo content only readfile can do it so the page will automatically unload after download box appear.
$path is the path of your file to make user download
header("Content-type: application/vnd.ms-excel");
header('Content-Disposition: attachment; filename="'.$path.'"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header("Cache-Control: public");
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
readfile($path);
This is question about downloading files from server using php script. When user clicks on download link, it processed to download.php file, and using header it started downloading.
After downloading file there is one function in download.php file, which updates mysql database about file downloaded and deduct money from user's account. Everything works fine.
Now, the problem occurs when user have download manager installed in pc. Sometimes download started in browser and download manager both. So, at the end there are two download entries in database, And money deducted from user's account two times.
Question : Is there any way to start only one download at time? Or any other way to do this thing?
Download link I provide to user.
<a href='download.php?id=1'>Download Test Video</a>
The script I am using for downloading file. (download.php)
$file = "c:/test.avi"; // $file = $_GET['id'];
$title = "Test Video";
header("Pragma: public");
header('Content-disposition: attachment; filename='.$title);
header("Content-type: ".mime_content_type($file));
header("Content-Length: " . filesize($file) ."; ");
header('Content-Transfer-Encoding: binary');
ob_clean();
flush();
$chunksize = 1 * (1024 * 1024); // how many bytes per chunk
if (filesize($file) > $chunksize) {
$handle = fopen($file, 'rb');
$buffer = '';
while (!feof($handle)) {
$buffer = fread($handle, $chunksize);
echo $buffer;
ob_flush();
flush();
}
fclose($handle);
} else {
readfile($file);
}
record_download('user id', 'file id');
Moving the comment down here for anyone interested.
The function that I had to come up with was a unique download link after a payment had been processed. These are the steps that were taken.
Process the Payment and capture IP address, file and path of the downloadable file - save this information to the database.
Once payment has been deducted successfully, trigger a function that generates a unique token, e.g: sha1(microtime().$transactionid), and save this to the database (note: please don't use microtime() in production, use a random string generator).
Using .htaccess we generated a download link, e.g.: http://domain.com/download/<token> the .htaccess contents:
RewriteRule ^download/([a-z0-9-]) /download.php?token=$1
Optional: If their IP matches what we have in the database, go ahead and allow the user to download the file. If it doesn't, we ask the user to log in so we can update their IP address and begin downloading.
Once you have the token, you can pretty much do any form of validation you would like from here, such as preventing multiple downloads by adding a column in the database download_downloaded INT(1) DEFAULT 0 where if it is set to 1, then it has been downloaded. I would suggest giving the user about a day before locking them out after downloading, just in case their file was corrupt in the process.
Any other additional items, such as download counter etc.
Finally use your code above after to start the download. I would have it structured a little differently though.
download.php
$token = $_GET['token'];
$allow_download = FALSE; // Can never be too careful..
...
//Database lookup to get the file ID
...
$file = "c:/test.avi"; // now from the database call
$title = "Test Video";
// Do any additional validation here
// returns TRUE or FALSE (Boolean) -- Custom function to query the database
$allow_download = check_if_downloadable('user_id', 'file_id', 'token_id');
record_download('user id', 'file id');
// After validation is complete, allow them to download
if ($allow_download === TRUE){
header("Pragma: public");
...
If a user lost their download link (as it has happened many times), we show their download link on their member home page once they have logged in, and they can start downloading again (if you allow it).
I hope this helps. I'm sorry that I can't give out some code examples at this time.
Most of this is just querying the database.
Sometime in our website , need a download link. When click this link then any type of file,image, pdf will be download.You can do this using a simple php script.
want to download a picture name “shafiq_photo.jpg” so parameter is file name “shafiq_photo.jpg”.
Then Create a php file name “download.php” which you use in above file.
<?php
$file = $_GET["file"];
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header("Content-Type: application/force-download");
header('Content-Disposition: attachment; filename=' . urlencode(basename($file)));
// header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
}
?>
I would recommend to alter the logic:
Deduct money first.
Generate a random string. Save file and user info with it.
In the client profile dump a download link with the random identifier. I mean http://www.example.com/download.php?file=254fd1f5df4df2sd5fsd5f4sdfsd0fsdf5sd4fsdf5dfsdf
Onload complete remove the database entry.
But there is a security hole!
If two people start download that file with that link, then they can bypass payment for one.
But what if someone download that file and send it to other. So, that isn't so much problem. isn't it?
But there is a option attach CSRF token if you think you should really safe.
Im having some major problems with an site im developing, basically whats happening is a user fills out a form, then jquery takes over and posts all the information to sendfile.php, it then is meant to force the user to download a specific file, but its just not doing anything at all and im not seeing any errors either, the file exists.
The code im using is as follows:
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-disposition: attachment; filename="http://website.com/wp-content/uploads/2012/02/303lowe-logo.jpg"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
readfile("http://website.com/wp-content/uploads/2012/02/logo.jpg");
Any help would be awesome.
Based on what you said about using jQuery, I assume you are using AJAX to post the form results to the server. I think you will find that you cannot download a file using AJAX.
Perhaps consider doing the AJAX request then redirecting the user to a new page to download the file. If the redirected page serves the file directly, then the user won't even know they have been redirected (the browser will stay on the same page, usually).
I need to know if a user selected download then clicked the cancel button, which is not the same as readfile having an error. I have inspected the count returned by the readfile function, but it shows the bytes in the file even if the user canceled the download from the Save As dialog.
The reason this is needed is because my site has a one-time download, where a member gives permission for another use to download their file one time, then the permission goes away. But if a member clicks the download button then decides not to download it right then, I dont' want my database to get updated to show they got the file.
This deals with intellectual property protection since the files are the property of the member who uploaded them, and I need to keep an audit trail of exactly what other members downloaded the file in case they start floating around the internet. But if the readfile function always reflects the filesize (meaning those bytes were transferred in some way), I have no way to know if the file was actually downloaded.
I have seen a number of posts about this subject, but no real solutions to what has to be a frequent need - did they download it or not? Just knowing that they clicked the download button doesn't really say whether they decided to go through with it since the Save As dialog box allows someone to cancel the actual completion of the download.
For completeness, here is my download code up until the readfile function:
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=$download_name");
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize("sub/$doc_file"));
ob_clean();
flush();
$wasdownloaded = readfile("sub/$doc_file");
I fear the correct answer is "Impossible" - let me explain: You might be able to correctly figure out, when the file has crossed the wire, but you can't figure out reliably, whether the client threw it away or not.
Example (chronological sequence):
A user on MSIE clicks download and is presented with the "Save where" Dialog.
While this dialog is open, the download is started in the background.
The user navigates around in the dialog or simply does nothing (phone rang, he talks)
The background download is finished, your script sees the download as complete
The user clicks on "cancel"
MSIE deletes the tempfile, the download is never stored in a user-accessible form
Result:
The user sees the file as "not downloaded" - and he is correct
Your app sees the file as "correctly downloaded" - and it is correct
You would first need ignore_user_abort().
This would allow your script to continue on after the user has hit cancel, or escape.
You would then have to print out the file and continuously check with connection_aborted().
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=$download_name");
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize("sub/$doc_file"));
ob_clean();
flush();
$fp=fopen("sub/$doc_file","rb");
while(!feof($fp))
{
print(fread($fp,1024*8));
flush();
ob_flush();
if( connection_aborted() )
{
//do code for handling aborts
}
}
Use this comment on php.net : http://www.php.net/manual/en/function.fread.php#72716
On fclose you would be able to determine if file has been downloaded successful, because your are checking if user aborted connection with connection_status()