I am trying to zip a bunch of PDFs together and download in the browser, at the moment the PDF files are zipped and downloaded to the folder the PDFs are stored in, not via the users browser and into their download folder, I have a similar (and much simpler) function which downloads a single PDF so feel like I'm missing something fairly obvious here..
$id is a comma seperated list of filenames, this is then split into an array for looping through and adding to zip file. this bit works, thinking it may be a header issue or with the response.
Any help much appreciated.
public function downloadMultiple($id) {
$id_array = explode(',', $id);
$public_dir = storage_path();
$zipFileName = time().'.zip';
$zip = new ZipArchive;
if ($zip->open($public_dir . '/' . $zipFileName, ZipArchive::CREATE) === TRUE) {
foreach($id_array as $file) {
$file_path = storage_path($file).".pdf";
if (file_exists($file_path)) {
$zip->addFile($file_path,$file.".pdf");
}
}
if ($zip->close()) {
$filetopath = $public_dir.'/'.$zipFileName;
$headers = [
'Cache-control: maxage=1',
'Pragma: no-cache',
'Expires: 0',
'Content-Type : application/octet-stream',
'Content-Transfer-Encoding: binary',
'Content-Type: application/force-download',
'Content-Disposition: attachment; filename='.time().'.zip',
"Content-length: " . filesize($filetopath)
];
if (file_exists($filetopath)) {
$response = response()->download($filetopath, $zipFileName, $headers);
//$response->deleteFileAfterSend(true);
} else {
return ['status'=>'zip file does not exist'];
}
} else {
return ['status'=>'zip file could not close'];
}
} else {
return ['status'=>'Could not create new zip'];
}
}
Update:
Definitely gets to the return and does create the file, it just doesn't seem to download for the user, the below is what is brought back in the inspector so clearly something not working as expected
may be worth while mentioning the code which is sent to the controller
let xhr = new XMLHttpRequest(), self = this;
xhr.open('GET', window.location.origin+'/download-multiple/' + this.selected);
xhr.onload = function () {
};
xhr.send();
Assuming you're getting to that part of the controller method, I believe the problem is that you're not returning your response:
if (file_exists($filetopath)) {
// $response = response()->download($filetopath, $zipFileName, $headers);
// $response->deleteFileAfterSend(true);
return response()->download($filetopath, $zipFileName, $headers)->deleteFileAfterSend(true);
} else {
return ['status'=>'zip file does not exist'];
}
EDIT: The problem is you're trying to load the file via AJAX, which you can't do the way that you're trying to do it (see here for examples on how to do it). Change your javascript to:
let xhr = new XMLHttpRequest(), self = this;
window.location = window.location.origin+'/download-multiple/' + this.selected
Related
In Wordpress (PHP) i'm using Contact Form 7 (CF7) as the UI and sending the data via an API with the function on_submit below. Would like to have the success or fail response data logged to make sure everything is working so thought i'd write that to a simple text file using the function custom_logs. It does write to the text file but then the process never continues on to the CF7 plugin to indicate the POST was a success or fail, it just hangs after hitting submit on the UI side. I believe something about writing to the file is interrupting the flow and then is just stops from CF7's POV. I rarely write anything in PHP so not 100% sure what the issue is? Anyone have an idea how to solve it, I can't find anything. Thanks
function on_submit( $form, &$abort, $submission )
{
$data = $submission->get_posted_data();
$firstname = sanitize_text_field($data['first-name']);
$response = wp_safe_remote_post("https://www.api.com", [
'body' => json_encode([
'firstname' => $firstname,
]),
]);
if ( is_wp_error($response) ) {
$abort = TRUE;
$body = wp_remote_retrieve_body($response);
$result = json_decode($body);
$submission->set_response($result->error);
$submission->set_status('api_failed');
} else {
$abort = FALSE;
$body_success = wp_remote_retrieve_body($response);
$result_success = json_decode($body_success);
custom_logs("WP API: " . $result_success);
}
}
add_action('wpcf7_before_send_mail', 'on_submit', 10, 3);
function custom_logs($message) {
if(is_array($message)) {
$message = json_encode($message);
}
$file = fopen("custom_logs.log","a");
echo fwrite($file, "\n" . date('Y-m-d h:i:s') . " :: " . $message);
fclose($file);
}
I'm generating a large PDF with 2000 pages in symfony (4.2) framework. What I'm doing is just save the HTML content to the .HTML file by getting content from the twig.
Then I'm using the headless chrome to generate the PDF from the URL using the below command.
/usr/bin/google-chrome --headless --disable-gpu --run-all-compositor-stages-before-draw --print-to-pdf [URL of HTML file] --virtual-time-budget=10000
Now, the requirement is while the above command is running I have to display the loader with the progress bar in the front.
What I did is as below to get the stream response and display them on the browser.
Controller
public function streamAction()
{
$process = new Process(["pwd"]);
$process->run();
$output = new StreamedOutputService(fopen('php://stdout', 'w'));
$response = new StreamedResponse(function() use ($output, $process) {
// $process->isRunning() always returns false.
while ($process->isRunning()) {
$output->writeln($process->getOutput());
}
});
$response->headers->set('X-Accel-Buffering', 'no');
return $response;
}
Streamed Response Class
protected function doWrite($message, $newline)
{
if (
false === #fwrite($this->getStream(), $message) ||
(
$newline &&
(false === #fwrite($this->getStream(), PHP_EOL))
)
) {
throw new RuntimeException('Unable to write output.');
}
echo $message;
ob_flush();
flush();
}
What is the buggy on the above code? I'm not able to get the output of the command hence can not write it to the browser.
Below code is working fine and sending response at every 2 seconds on the browser
public function streamAction()
{
$output = new StreamedOutputService(fopen('php://stdout', 'w'));
$response = new StreamedResponse(function() use ($output) {
for($i = 0; $i <= 5; $i++) {
$output->writeln($i);
sleep(2);
}
});
$response->headers->set('X-Accel-Buffering', 'no');
return $response;
}
I am going the codebase of slim php for educational purposes, I kind of understand alot from reading it. however, i am finding it really difficult to understand the purpose of the buffer used in the 'main' run method of the App class.
public function run($silent = false)
{
$response = $this->container->get('response');
try {
ob_start();
$response = $this->process($this->container->get('request'), $response);
} catch (InvalidMethodException $e) {
$response = $this->processInvalidMethod($e->getRequest(), $response);
} finally {
$output = ob_get_clean();
}
if (!empty($output) && $response->getBody()->isWritable()) {
$outputBuffering = $this->container->get('settings')['outputBuffering'];
if ($outputBuffering === 'prepend') {
// prepend output buffer content
$body = new Http\Body(fopen('php://temp', 'r+'));
$body->write($output . $response->getBody());
$response = $response->withBody($body);
} elseif ($outputBuffering === 'append') {
// append output buffer content
$response->getBody()->write($output);
}
}
$response = $this->finalize($response);
if (!$silent) {
$this->respond($response);
}
return $response;
}
i have tried to dump the value of ob_get_clean() but it is always empty.
This is done in order to always return a PSR-7 Response. If you echo or print_r() inside your routes/middleware this will get prepended to the response body if the outputBuffering setting is set to prepend or if set to append it will be appended.
I get my file via:
require_once 'google/appengine/api/cloud_storage/CloudStorageTools.php';
use google\appengine\api\cloud_storage\CloudStorageTools;
$public_link = CloudStorageTools::getPublicUrl("gs://bucket/file.pdf", false);
If I go to $public_link in the browser, it shows the PDF inside the browser. I am trying to figure out how I can force the download of this file.
Google App Engine only has a 60 second timeout so I'm afraid the serve function wont work via GAE. Does anyone have any suggestions?
--
EDIT
Andrei Volga's previous answer in this post suggests I use a Signed URL with a response-content-distribution header.
So far, I am able to create a signed URL that successfully shows the file but I am not able to generate a signed url that has any sort of header at all aka create a signed URL that will force the download instead of just showing it.
This is what I have so far, most of which is courtesy of mloureiro.
function googleBuildConfigurationString($method, $expiration, $file, array $options = [])
{
$allowedMethods = ['GET', 'HEAD', 'PUT', 'DELETE'];
// initialize
$method = strtoupper($method);
$contentType = $options['Content_Type'];
$contentMd5 = $options['Content_MD5'] ? base64_encode($options['Content_MD5']) : '';
$headers = $options['Canonicalized_Extension_Headers'] ? $options['Canonicalized_Extension_Headers'] . PHP_EOL : '';
$file = $file ? $file : $options['Canonicalized_Resource'];
// validate
if(array_search($method, $allowedMethods) === false)
{
throw new RuntimeException("Method '{$method}' is not allowed");
}
if(!$expiration)
{
throw new RuntimeException("An expiration date should be provided.");
}
return <<<TXT
{$method}
{$contentMd5}
{$contentType}
{$expiration}
{$headers}{$file}
TXT;
}
function googleSignString($p12FilePath, $string)
{
$certs = [];
if (!openssl_pkcs12_read(file_get_contents($p12FilePath), $certs, 'notasecret'))
{
echo "Unable to parse the p12 file. OpenSSL error: " . openssl_error_string(); exit();
}
$RSAPrivateKey = openssl_pkey_get_private($certs["pkey"]);
$signed = '';
if(!openssl_sign( $string, $signed, $RSAPrivateKey, 'sha256' ))
{
error_log( 'openssl_sign failed!' );
$signed = 'failed';
}
else $signed = base64_encode($signed);
return $signed;
}
function googleBuildSignedUrl($serviceEmail, $file, $expiration, $signature)
{
return "http://storage.googleapis.com{$file}" . "?GoogleAccessId={$serviceEmail}" . "&Expires={$expiration}" . "&Signature=" . urlencode($signature);
}
$serviceEmail = '<EMAIL>';
$p12FilePath = '../../path/to/cert.p12';
$expiration = (new DateTime())->modify('+3hours')->getTimestamp();
$bucket = 'bucket';
$fileToGet = 'picture.jpg';
$file = "/{$bucket}/{$fileToGet}";
$string = googleBuildConfigurationString('GET', $expiration, $file, array("Canonicalized_Extension_Headers" => ''));
$signedString = googleSignString($p12FilePath, $string);
$signedUrl = googleBuildSignedUrl($serviceEmail, $file, $expiration, $signedString);
echo $signedUrl;
For small files you can use serve option instead of public URL with save-as option set to true. See documentation.
For large files you can use a Signed URL with response-content-disposition parameter.
You can add and additional query string only.
https://cloud.google.com/storage/docs/xml-api/reference-headers#responsecontentdisposition
response-content-disposition
A query string parameter that allows content-disposition to be overridden for authenticated GET requests.
Valid Values URL-encoded header to return instead of the content-disposition of the underlying object.
Example
?response-content-disposition=attachment%3B%20filename%3D%22foo%22
I am writing a REST API and currently testing some things. I am trying to make it send an error response when it does not find anything in the database.
The part that is running (because i am testing currently by just entering the url into my browser) is below:
else if ($request->getHttpAccept() === 'xml')
{
if(isset($data['s']) && isset($data['n'])) {
$id = $db->getAlcoholIDByNameSize($data['n'], $data['s']);
$prices = $db->pricesByAlcohol($id);
}
if(isset($id)) {
$resData = array();
if(!empty($prices)) {
foreach($prices as $p) {
$store = $db->store($p['store']);
array_push($resData, array('storeID' => $p['store'], 'store_name' => $store['name'], 'store_gps' => $store['gps'], 'price' => round($p['price'], 2)));
}
RestUtils::sendResponse(200, json_encode($resData), 'application/json');
} else {
RestUtils::sendResponse(204, 'error', 'application/json');
}
} else {
RestUtils::sendResponse(204, 'error', 'application/json');
}
//RestUtils::sendResponse(501, "xml response not implemented", 'application/xml');
}
everything works fine if the queries return something to be stored in $id and $prices. If they do not exist in the database, however, it tries to load the page, and then goes back to the previous page you were on. You can see the behavior by going to:
http://easyuniv.com/API/alc/coorsa/2 <-- works
http://easyuniv.com/API/alc/coors/3 <-- works
http://easyuniv.com/API/alc/coorsa/5 <-- doesn't work(or anything else, the two above are the only ones)
here is my sendResponse function:
public static function sendResponse($status = 200, $body = '', $content_type = 'text/html')
{
$status_header = 'HTTP/1.1 ' . $status . ' ' . RestUtils::getStatusCodeMessage($status);
// set the status
header($status_header);
// set the content type
header('Content-type: ' . $content_type);
// pages with body are easy
if($body !== '')
{
$temp = json_decode($body);
$body = json_encode(array('result' => array('status' => $status, 'message' => RestUtils::getStatusCodeMessage($status)), 'data' => $temp));
// send the body
echo $body;
exit;
}
// we need to create the body if none is passed
else
{
$body = "else".json_encode(array('result' => array('status' => $status, 'message' => RestUtils::getStatusCodeMessage($status))));
echo $body;
exit;
}
}
I have tried debugging using echos but I cant seem to narrow down what the issue is. Any help would be appreciated, thanks.
The problem is that when there is no appropriate data found in the data base you are returning HTTP 204 which is telling the browser there is absolutely nothing for it to display. This is not true in your case.
You still want to output the message that there was nothing found.
To fix you need to replace the two instances of 204 in your code with 200.
I modified tested your code using: Note, nothing will display as is. To get the message to display change 204 to 200 in the $status_header variable.
<?php
$status_header = 'HTTP/1.1 204';
// set the status
header($status_header);
// set the content type
header('Content-type: text/html');
echo "Can you see me???";
?>
Note: When testing this always close the tab and use a fresh tab for each call or else it will look like it is loading data from the previous call, like you have explained.