I want that my script will be executed after downloading a file so in the first part of this code it will get the .txt file and change word license to the database result, then i want that the download starts and after that i want to clear the .txt file for the next use. If i write it as below i don't get the database result in the text file because it executes first the whole code before it downloads. If i remove the last part it all works but it wont reset the text.
<?php
$userID= $_SESSION['user_id'];
$license=$dbConnection->getOne("SELECT license FROM valid_license where discordid = '$userID' ");
$license2 = $license['license'];
$zip = new ZipArchive;
$fileToModify = 'license.txt';
if ($zip->open('test.zip') === TRUE) {
$oldContents = $zip->getFromName($fileToModify);
$newContents = str_replace('license', $license2, $oldContents);
$zip->deleteName($fileToModify);
$zip->addFromString($fileToModify, $newContents);
$zip->close();
echo 'ok';
} else {
echo 'failed';
}
header("Location: test.zip");
$userID= $_SESSION['user_id'];
$license=$dbConnection->getOne("SELECT license FROM valid_license where discordid = '$userID' ");
$license2 = $license['license'];
$zip = new ZipArchive;
$fileToModify = 'license.txt';
if ($zip->open('test.zip') === TRUE) {
$oldContents = $zip->getFromName($fileToModify);
$newContents = str_replace($license2, 'license', $oldContents);
$zip->deleteName($fileToModify);
$zip->addFromString($fileToModify, $newContents);
$zip->close();
echo 'ok';
} else {
echo 'failed';
}
?>
There are two potential reasons why this is happening:
Your web server is waiting for PHP to finish executing before serving any response to the user, so the second half is overwriting the zip file before the header is ever sent.
Your web server is sending the Location: header without delay, but also your PHP code is still executing while that response and the subsequent request are in-flight, overwriting the data before the request for the file comes back.
Either of those will break your intended flow.
Instead of using a Location: header, set the appropriate Content-Type: header for a zip file, dump the data out to the user, and then clean up the file.
header("Content-Type: application/zip");
header("Content-Disposition: attachment; filename=$file_name");
header("Content-Length: " . filesize($yourfile));
readfile($yourfile);
Additionally, do not modify the original zip file like this. If you get two overlapping requests you'll wind up either serving the wrong license, or just corrupt the file for one or both or all subsequent.
Make a copy, modify the copy, serve the copy, delete the copy.
Related
I have a script which is used to download large files in pdf and media format. I can't download it it gives http internal error sometimes it give 500 internal server error sometimes.
<?php
//The directory where the download files are kept - keep outside of the web document root
$strDownloadFolder = "uploads/";
//If you can download a file more than once
$boolAllowMultipleDownload = 0;
// 1. Create a database connection
//connect to the DB
$resDB = mysql_connect("localhost", "root", "");
mysql_select_db("downloader", $resDB);
if(!empty($_GET['key'])){
//check the DB for the key
$resCheck = mysql_query("SELECT * FROM downloads WHERE downloadkey = '".mysql_real_escape_string($_GET['key'])."' LIMIT 1");
if($resCheck == FALSE) { echo "QUERY FAILED: " . mysql_error(); }
$arrCheck = mysql_fetch_assoc($resCheck);
if(!empty($arrCheck['file'])){
//check that the download time hasnt expired
if($arrCheck['expires']>=time()){
if(!$arrCheck['downloads'] OR $boolAllowMultipleDownload){
//everything is hunky dory - check the file exists and then let the user download it
$strDownload = $strDownloadFolder.$arrCheck['file'];
if(file_exists($strDownload)){
//get the file content
$strFile = file_get_contents($strDownload);
//set the headers to force a download
header("Content-type: application/force-download");
header("Content-Disposition: attachment; filename=\"".str_replace(" ", "_", $arrCheck['file'])."\"");
//echo the file to the user
echo $strFile;
//update the DB to say this file has been downloaded
mysql_query("UPDATE downloads SET downloads = downloads + 1 WHERE downloadkey = '".mysqli_real_escape_string($_GET['key'])."' LIMIT 1");
exit;
}else{
echo "We couldn't find the file to download.";
}
}else{
//this file has already been downloaded and multiple downloads are not allowed
echo "This file has already been downloaded.";
}
}else{
//this download has passed its expiry date
echo "This download has expired.";
}
}else{
//the download key given didnt match anything in the DB
echo "No file was found to download.";
}
}else{
//No download key wa provided to this script
echo "No download key was provided. Please return to the previous page and try again.";
}
?>
Since http force headers are not working for larger files... I wanna use href with download attribute so when user clicks on it it should have direct encrypted link! with token or limited session!
file_get_contents reads the entire file into memory. If the file is larger than the available memory, it fails.
Instead you can read the file contents directly to the output stream:
//read whole file into memory, whoops
//$strFile = file_get_contents($strDownload);
//set the headers to force a download
header("Content-type: application/force-download");
header("Content-Disposition: attachment; filename=\"".str_replace(" ", "_", $arrCheck['file'])."\"");
//read file to output stream
readfile($strDownload);
http://php.net/manual/en/function.readfile.php
I have a function that creates different file types depending on a variable, I have it generating an XML, but when I click the link to the page to do so (XML), nothing happens. If I click to open it in a new tab or manually enter the url in the title bar then the file will download as I want it to.
function asset($asset_id, $display = ''){
$this->load->model('model_asset');
$asset = $this->model_asset->get_by_id($asset_id, true);
switch($display) {
case 'xml':
$this->load->helper('array_to_xml_helper');
$asset_arr = get_object_vars($asset);
$filename = $asset->title .'-'. $asset->subtitle . '.xml';
$xml = Array2XML::createXML('asset', $asset_arr);
header ("Content-Type:text/xml");
header('Content-Disposition: attachment; filename="'. $filename .'"');
echo $xml->saveXML();
break;
}
}
How can I make this work with dynamically generated files (I'm using a arraytoxml utility function I found here)
You can try to set file in attachement
Add this to your headers :
Content-disposition: attachment
filename=huge_document.pdf
End goal:
Click link on page 1, end up with file downloaded and refresh page 1. Using PHP to serve downloads that are not in public html.
Approach:
Page 1.
Link transfers to page 2 with get variable reference of which file I am working with.
Page 2.
Updates relevant SQL databases with information that needs to be updated before refresh of page 1. Set "firstpass" session variable. Set session variable "getvariablereference" from get variable. Redirect to page 1.
Page 1.
If first pass session variable set. Set Second pass session variable. Unset first pass variable. Refresh Page. On reload the page will rebuild using updated SQL database info (changed on page 2.).
Refreshed Page 1.
If second pass session variable set. Run download serving header sequence.
This is page 1. I am not showing the part of page 1 that has the initial link. Since it doesn't matter.
// REFERSH IF FIRSTPASS IS LIVE
if ($_SESSION["PASS1"] == "YES"){
$_SESSION["PASS1"] = "no";
$_SESSION["PASS2"] = "YES";
echo "<script>document.location.reload();</script>";
}
if ($_SESSION["PASS2"] == "YES"){
// Grab reference data from session:
$id = $_SESSION['passreference'];
// Serve the file download
//First find the file location
$query = "SELECT * from rightplace
WHERE id = '$id'";
$result = mysql_query($query);
$row = mysql_fetch_array($result);
$filename = $row['file'];
$uploader = $row['uploader'];
// Setting up download variables
$string1 = "/home/domain/aboveroot/";
$string2 = $uploader;
$string3 = '/';
$string4 = $filename;
$file= $string1.$string2.$string3.$string4;
$ext = strtolower (end(explode('.', $filename)));
//Finding MIME type
if($ext == "pdf" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: application/pdf');
readfile($file);
}
if($ext == "doc" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: application/msword');
readfile($file);
}
if($ext == "txt" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: text/plain');
readfile($file);
}
if($ext == "rtf" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: application/rtf');
readfile($file);
}
if($ext == "docx" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
readfile($file);
}
if($ext == "pptx" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: application/vnd.openxmlformats-officedocument.presentationml.presentation');
readfile($file);
}
if($ext == "ppt" && file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: application/vnd.ms-powerpoint');
readfile($file);
}
}
The script on page 2 is working correctly. It updates the sql database and redirects to the main page properly. I have also checked that it sets the "$_SESSION['passreference'];" correctly and nothing on page 1 would unset it.
So, thats the whole long explanation of the situation. I am stumped. What happens is, as I said page 2 works fine. Then it kicks to page 1, refreshes and then doesnt push any download. I know that the download script works and that the files are there to be downloaded (checked without the whole refresh sequence).
I essentially have two questions:
Can anyone spot whats going wrong?
Can anyone conceptualize a better approach?
It is hard to debug something like this remotely even given the code, the segment you posted works as you say. Have you checked your error logs? The most likely culprit is a problem with sending header() after other output has been done.
When dealing with file downloads, I think it is easier wherever possibly to initiate the download on a new page/window so there can be no risk of breaking headers. Maybe a slightly altered sequence using a third page that initiates the actual download:
Page 1 links to the second page to do magic, which redirects back to page 1
Page 1 then spawns page 3 in a new window, which initiates the download.
There's a good example code for loading a new window for a download in this answer.
Looking at your code, the download problem may be that the $ext variable contains an unexpected value or that the $file variable contains the name of a file that really doesn't exist.
In either this cases, none of your "if" conditions would be true, so the download would'nt start.
My suggestion is to add the following statements just before the "//Finding MIME type" comment line:
$log = "file='".$file."'\n";
$log .= "ext='".$ext."'\n";
#file_put_contents("/tmp/page1.log", $log, FILE_APPEND);
This way, looking at the "/tmp/page1.log" file you should be able to check if the $file and $ext variabiles effectively contain the expected values.
I've used "/tmp/page1.log" as the log file name since I suppose that you're working on linux; if not, please adjust the first argument of the "file_put_contents" function with a valid path and file name for your enviroment.
Also, I would replace the sequence of "if" tests with the following code:
$content_types = array(
"pdf" => "application/pdf",
"doc" => "application/msword",
"txt" => "text/plain",
"rtf" => "application/rtf",
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation"
);
if (isset($content_types[$ext])) {
if (file_exists($file)) {
header("Content-disposition: attachment; filename= '$filename'");
header('Content-type: '.$content_types[$ext]);
readfile($file);
die("");
} else {
die("** '".$file."' does not exist **");
}
} else {
die("** Unhandled '".$ext."' extension **");
}
Obviously, you should implement the error handling in a much more robust way, not simply using the "die()" function as I did, but this is only an example.
Finally, be aware that there are also better ways of getting the content-type corresponding to the file extension; for example one solution is to use the PHP Fileinfo functions.
Have a look at this answer for further information about this topic.
Keep also in mind that in safe mode the file_exists function always returns FALSE, and that the results of the file_exists function are cached; see the clearstatcache() function on the PHP manual for further details.
I just reworked your PHP code a bit. Especially you'll get more information about what's going wrong. Just try this code and read the following comments, which explain what happend, if you get one of the new error messages. Also read the NOTE part below, which explains why you probably can't access a file from PHP, even it's existing and is in the right directory.
Using window.location.reload(); instead of document.location...
I added an error()-function. You can add more HTML to it, so it's producing a page in the layout you want. And you could log the error to a local file, too. There is a private info parameter used to pass sensible information as database errors (can contain SQL) to the function. For productive use you shouldn't display that to the user. Instead you can log it into a file or only display it for privileged users (e.g. Administrators).
Checks weather $id is set. Returns error() message if not; Could happen if session was not updated correctly.
I added "$id = addslashes($id);" for security reasons. If your id could be set to values like $id = "' OR 1" (SQL-Injection) for example, you could get into trouble. If you are sure this can not happen, you can remove it.
It checks the $result variable after the DB query. If e.g. your database connection wasn't established or the script cannot connect this will produce an error()-output that informs you. The same happens if you have an error in your SQL syntax, e.g. wrong table name.
It's also checked weather a valid $row is fetched from the database. If there isn't a row returned your $id is problably wrong (there isn't such an entry in your database).
I rewrote your string operations to $filepath = $rootpath . "/" . $uploader . "/" . $filename; where $rootpath is set before without "/" at the end; This is easier to read...
Extensions and MIME-Types are now put into an array, instead of using a lot of "if-then"-blocks, that's easier to maintain. Also the code inside that blocks were similar... so we only need to write it once.
A default MIME type (Content-Type:"application/octet-stream) is sent, if the file extension is not known.
We check for file_exists() and output an error message, with $filename given to allow checking weather the path is correct...
So here is the source code:
<?php
function error($message, $info = "") {
echo "ERROR: $message<br>";
echo "PRIVATE-INFO: $info"; // probably you only want to log that into a file?
exit;
}
// REFERSH IF FIRSTPASS IS LIVE
if ($_SESSION["PASS1"] == "YES") {
$_SESSION["PASS1"] = "no";
$_SESSION["PASS2"] = "YES";
echo "<script>window.location.reload();</script>";
exit;
}
if ($_SESSION["PASS2"] == "YES") {
// Grab reference data from session:
$id = $_SESSION['passreference'];
if (!$id) error("Internal Error ('id' not set)");
// Select file location from DB
$id = addslashes($id);
$query = "SELECT * from rightplace WHERE id = '$id'";
$result = mysql_query($query);
if (!$result) error("DB-query execution error", mysql_error());
$row = mysql_fetch_array($result);
mysql_free_result($result);
if (!$row) error("File with ID '$id' was not found in DB.");
$filename = $row['file'];
$uploader = $row['uploader'];
// Setting up download variables
$rootpath = "/home/domain/aboveroot";
$filepath = $rootpath . "/" . $uploader . "/" . $filename;
$ext = strtolower(end(explode('.', $filename)));
// Serve the file download
// List of known extensions and their MIME-types...
$typelist = array(
"pdf" => "application/pdf",
"doc" => "application/msword",
"txt" => "text/plain",
"rtf" => "application/rtf",
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt" => "application/vnd.ms-powerpoint"
);
// set default content-type
$type = "application/octet-stream";
// for known extensions, assign specific content-type
if (!isset($typelist[$ext])) $type = $typelist[$ext];
if (file_exists($filepath)) {
header("Content-disposition: attachment; filename= '$filename'");
header("Content-type: $type");
readfile($filepath);
} else {
error("Error: File '$filepath' was not found!", $filepath);
}
}
?>
NOTES:
The file not found error can happen even the file exists. If this happens, this is most probably a security mechanism that prevents the PHP script to access files outside the HTML-root directory. For example php scripts could be executed in a "chrooted" environment, where the root directory "/" is mapped e.g. to "/home/username/". So if you want to access "/home/username/dir/file" you would need to write "/dir/file" in your PHP script. It can be even worse, if your root is set like "/home/username/html"; then you'll not be able to access directories below your "html" directory. To work around that, you can create a directory inside the HTML-root and put a file named ".htaccess" there. Write "DENY FROM ALL" in it, which prevents access to the directory by browser request (only scripts can access it). This works for apache servers only. But there are solutions like that for other server software too... More info on this can be found under: http://www.php.net/manual/en/ini.core.php#ini.open-basedir
Another possibility is that your file access right (for uploaded files) are not set in a way, that your script is allowed to access them. With some security settings enabled (on a linux server), your PHP script can only access files owned by the same user as the "owner" set for the script file. After upload via "ftp" this is most probably the usersname of the ftp user. If edited on the shell, this will be the current users username. => But: Uploaded files are sometimes assigned to the user the webserver is running as (e.g. "www-data", "www-run" or "apache"). So find out which it is and assign your script to this owner.
For file uploads you should use move_uploaded_file(...) which is explained here: www.php.net/manual/en/function.move-uploaded-file.php ; If you don't do this, the file access right may be wrong or you might not be able to access the file.
I'm using WSO2 WS Framework and I managed to run example in which web service returns an image as a MTOM attachment which is then saved by the client using file_put_contents(...) command.
Service:
<?php
function sendAttachment($msg){
$responsePayloadString = <<<XML
<ns1:download xmlns:ns1="http://wso2.org/wsfphp/samples/mtom">
<ns1:fileName>test.jpg</ns1:fileName>
<ns1:image xmlmime:contentType="image/jpeg" xmlns:xmlmime="http://www.w3.org/2004/06/xmlmime">
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="cid:myid1"></xop:Include>
</ns1:image>
</ns1:download>
XML;
$f = file_get_contents("test.jpg");
$responseMessage = new WSMessage($responsePayloadString,
array( "attachments" => array("myid1" => $f)));
return $responseMessage;
}
$operations = array("download" => "sendAttachment");
$service = new WSService(array("operations" => $operations, "useMTOM" => TRUE));
$service->reply();
?>
Client:
<?php
$requestPayloadString = '<download></download>';
try {
$client = new WSClient(
array( "to" => "http://SPLINTER/MTOM/service.php",
"useMTOM" => TRUE,
"responseXOP" => TRUE));
$requestMessage = new WSMessage($requestPayloadString);
$responseMessage = $client->request($requestMessage);
printf("Response = %s \n", $responseMessage->str);
$cid2stringMap = $responseMessage->attachments;
$cid2contentMap = $responseMessage->cid2contentType;
$imageName;
if($cid2stringMap && $cid2contentMap){
foreach($cid2stringMap as $i=>$value){
$f = $cid2stringMap[$i];
$contentType = $cid2contentMap[$i];
if(strcmp($contentType,"image/jpeg") ==0){
$imageName = $i."."."jpg";
if(stristr(PHP_OS, 'WIN')) {
file_put_contents($imageName, $f);
}else{
file_put_contents("/tmp/".$imageName, $f);
}
}
}
}else{
printf("attachments not received ");
}
} catch (Exception $e) {
if ($e instanceof WSFault) {
printf("Soap Fault: %s\n", $e->Reason);
} else {
printf("Message = %s\n",$e->getMessage());
}
}
?>
Instead of that I would like to open a "Save dialog" to choose between opening or saving the file. When searching for solution I read about setting heders like:
header('Content-type: application/octet-stream');
header('Content-disposition: attachment; filename="test.jpg"');
But it didn't work well. "Save dialog" poped up, but when image couldn't be opened saying that file is empty.
Actually I don't understand very good how this MTOM attachments thing is working. In client code, I think $f is a string and when I do printf($f) it prints 0(zero) so how can I save this string as an image?
If you want to use that headers, you have to output the file content, not save it somewhere.
header('Content-type: application/octet-stream');
header('Content-disposition: attachment; filename="test.jpg"');
// Output file. This must be the ONLY output of the whole script
echo $rawFileContents;
The basics is that at the moment you have your whole file content loaded in a variable (and it seems is $f in your code), you output it instead of write it in a file (as I think you're doing now). So, the three lines of code I gave you should replace the file_put_contents() calls.
Instead, if you want to save the file in your /tmp folder, ok, do it, but then use instead
header('Location: /tmp/' . $imageName);
This way you redirect the user browser directly to the saved file, and let users do what they want with it.
I store all of my images behind the webroot (before /var/www/), which means that the web server is unable to send cache headers back for my pictures. What do I need to add to this to make the user's web cache work? Currently, this is getting hit every time by the same browser.
My <img> path on my pages look something like this:
<img src="pic.php?u=1134&i=13513&s=0">
Edit: Could it be that it is because "pic.php?u=1134&i=13513&s=0" is not a valid file name or something?
// pic.php
<?php
// open the file in a binary mode
$user = $_GET['u'];
$id = $_GET['i'];
$s = $_GET['s'];
if (!isset($user) && !isset($s) && $isset($id))
{
// display a lock!
exit(0);
}
require_once("bootstrap_minimal.php"); //setup db connection, etc
// does this image_id belong to the user?
$stmt = $db->query('SELECT image_id, user_id, file_name, private FROM images WHERE image_id = ?', $id);
$obj = $stmt->fetchObject();
if (is_object($obj))
{
// is the picture is the users?
if ($obj->user_id != $_SESSION['user_id'])
{
// is this a private picture?
if ($obj->private == 1)
{
// check permissions...
// display a lock in needed!
}
}
}
else
{
// display a error pic?!
exit(0);
}
if ($s == 0)
{
$picture = $common->getImagePathThumb($obj->file_name);
}
else
{
$picture = $common->getImagePath($obj->file_name);
}
// send the right headers
header("Content-Type: image/png");
header("Content-Length: " . filesize($picture));
$fp = fopen($picture, 'rb');
// dump the picture and stop the script
fpassthru($fp);
exit;
?>
You need to add something like:
$expiry = 3600*24*7; // A week
header('Expires: ' . gmdate('D, d M Y H:i:s' time() + $expiry) . ' GMT');
header('Cache-control: private, max-age=' . $expiry);
Apache only caches static files by default. You need to send a cache control header via the header() function. This article has a lot of information on the topic.
Alternatively, you could use the PHP file to redirect to the actual location of the image. (This is probably the easiest way if you don't know anything about headers.)
You might try:
header("Cache-Control: max-age=3600");
That should send a cache timeout of one hour on the file.
What I would do in your situation is to stream the bytes of the image using a .php file. Don't link to images directly; instead, link to a php file that:
- outputs the cache headers
- reads the file off of disk, from behind the webroot
- sends the image bits down the wire
Simple answer: you aren't telling your users' browser to cache it