I have a php script (actually https://drupal.org/project/file_force) that is forcing users who click on a link to download that link by adding the correct headers to the response.
This link works fine 90% of the time. Occasionally the incorrect content-length is being passed so users are getting apparently truncated files. The mistake happens consistently on particular files, but if those files are re-uploaded, the error may not appear on the new instance, which makes me think this is not an issue with the files, but instead a cache somewhere. So I ran clearstatcache() every time to no avail. What is odd is that php is passing the correct file size, or says it is when I pass the string it's inserting to a log file.
Here's the relevant code:
clearstatcache();
return array(
'Content-Type: ' . $mimeinfo,
'Content-Disposition: ' . $disposition . '; filename="' . basename($filepath) . '";',
// Content-Length is also a good header to send, as it allows the browser to
// display a progress bar correctly.
// There's a trick for determining the file size for files over 2 GB. Nobody
// should be using this module with files that large, but… the sprintf()
// trickery makes sure the value is correct for files larger than 2GB. See
// note at http://php.net/filesize
'Content-Length: ' . sprintf('%u', filesize($filepath)),
);
A sample output from sprintf('%u', filesize($filepath)) on a file that isn't working is 2682059 which somehow gets translated to 1740048 when the browser gets to see it.
I've tried removing the sprintf function to no avail.
I've also tried not including a Content-Length declaration at all, but someone one is getting attached with the incorrect value anyway. This last piece of evidence perhaps suggests some other code is overriding the content headers I'm setting here, yet it appears to be leaving alone any other headers that I change in the above code to test that theory.
Any thoughts for where to look?
I resolved the issue.
Turns out another module within Drupal was adding its own content-length header and getting the value from a database rather than the file directly (weird), and it was happening down stream. By reversing the order that the modules got their hands on the headers, the issue went away. I have filed a bug report against the offending module.
SOURCES: PHP official documentation - "header" function
There's a function in PHP called header that you would like to use to set headers, before the actual page loads:
Here's the skeleton of the function:
void header ( string $string [, bool $replace = true [, int $http_response_code ]] )
Explanation:
Parameters:
string
The header string.
There are two special-case header calls. The first is a header that
starts with the string "HTTP/" (case is not significant), which will
be used to figure out the HTTP status code to send. For example, if
you have configured Apache to use a PHP script to handle requests for
missing files (using the ErrorDocument directive), you may want to
make sure that your script generates the proper status code.
replace
The optional replace parameter indicates whether the header
should replace a previous similar header, or add a second header of
the same type. By default it will replace, but if you pass in FALSE as
the second argument you can force multiple headers of the same type.
http_response_code
Forces the HTTP response code to the specified value. Note that this
parameter only has an effect if the string is not empty.
===
Return Values:
No value is returned.
Example:
<?php
header('Location: http://www.example.com/'); //redirect to www.example.com
?>
Note: The function must be called before any output is sent, i.e. before any HTML tags or before any echo calls.
in your case:
<?php
header('Content-Length: ' . sprintf('%u', filesize($filepath)));
?>
the replace parameter which is by default - true, will cause your program to overwrite all previously set 'Content-Length' headers..
Hope all the explanation was worth it... :)
Related
Trying to use /update-cache/ requests to update some AMP pages, but i'm getting 403 errors.
Removed the opening part/protocol from the urls since i don't have the reputation to post this many links, but everything is https.
I have a page at: www.qponverzum.hu/ajanlat/budapest-elozd-meg-a-hajhullast-mikrokameras-hajdiagnosztika-hajhagyma-es-fejborvizsgalattal-tanacsadas-5000-ft-helyett-2500-ft-ert-biohajklinika-szepsegapolas-egeszseg/amp
From the AMP cache: www-qponverzum-hu.cdn.ampproject.org/c/s/www.qponverzum.hu/ajanlat/budapest-elozd-meg-a-hajhullast-mikrokameras-hajdiagnosztika-hajhagyma-es-fejborvizsgalattal-tanacsadas-5000-ft-helyett-2500-ft-ert-biohajklinika-szepsegapolas-egeszseg/amp
I've been following the documentation at developers.google.com/amp/cache/update-ping
If i make an /update-ping request, it seems to work fine, returns a 200 no content response, but due to the high amount of urls/pages we would like to use /update-cache since it allows for a higher request rate.
I've created a private and public RSA key and made the public key acessible at www.qponverzum.hu/.well-known/amphtml/apikey.pub
I've been trying to use the following php code to generate the update-cache url
$ampBaseUrl = "https://www-qponverzum-hu.cdn.ampproject.org";
$signatureUrl = '/update-cache/c/s/www.qponverzum.hu/ajanlat/budapest-elozd-meg-a-hajhullast-mikrokameras-hajdiagnosztika-hajhagyma-es-fejborvizsgalattal-tanacsadas-5000-ft-helyett-2500-ft-ert-biohajklinika-szepsegapolas-egeszseg/amp?amp_action=flush&_ts='.time();
// opening the private key
$pkeyid = openssl_pkey_get_private("file://private-key.pem");
// generating the signature
openssl_sign($signatureUrl, $signature, $pkeyid)
// urlsafe base64 encoding
$signature = urlsafe_b64encode($signature);
// final url for updating
$ampUrl = $ampBaseUrl.$signatureUrl."&_url_signature=".$signature;
The urlsafe_b64encode function I'm using:
function urlsafe_b64encode($string) {
return str_replace(array('+','/','='),array('-','_',''), base64_encode($string));
}
$ampUrl ends up looking like this: https://www-qponverzum-hu.cdn.ampproject.org/update-cache/c/s/www.qponverzum.hu/ajanlat/budapest-elozd-meg-a-hajhullast-mikrokameras-hajdiagnosztika-hajhagyma-es-fejborvizsgalattal-tanacsadas-5000-ft-helyett-2500-ft-ert-biohajklinika-szepsegapolas-egeszseg/amp?amp_action=flush&_ts=1500362660&_url_signature=NjTCnmqUGpMY_CokGxchoczSOxnTLQvcQsX4fv2gIhW3H8hVw24mKCpmNoyV-9LND3OAR9ld80KeMH3lip863p_wBorIy1BAag7bRfvWcxsPrbqbox87VMrUWCEsry5epWxKYl2qNCT1GMv8SYAJ5WR0QZR0Qjvw5MXfZjohmbvrxJ7mWlc7fcvWoIXuO_q_yFkhi7A-fOZWm9sy8UDIlq-zNEPkVUwfqfWc_HbNHgvrk9Z6zZSNzB-dWAOT6QYAc1KeEVOIbvQxKkLkGMArTpydj5iLxz0aERvglKRl215Bqh6_jZu95T5yKv7X4R127ylpWYW2YDlTR9bgRE7Faw
If I make a simple GET request to this url(with a browser or curl) i get a 403 error('Your client does not have permission to get URL').
I've checked the webserver logs, but it doesn't seem like there're any requests made to the public keys url.
I think I'm missing something very obvious, so any feedback would be greatly appreciated.
There's an error in $signatureUrl - it should be amp_ts instead of _ts in the query params and in openssl_sign add the fourth parameter
openssl_sign($signatureUrl, $signature, $pkeyid, OPENSSL_ALGO_SHA256);
The signature needs to be signed with SHA256, if you omit the last parameter it uses SHA1
I've used your script with these 2 changes for my work project and it's working fine.
It should return "OK" in the response body if it's fine.
Check this https://gist.github.com/krzysztofbukowski/739ccf4061d69360b5b2c8306f5878bd
Try to set the response content type to "text/plain" for https://www.qponverzum.hu/.well-known/amphtml/apikey.pub as recommended in here
=========
I use the script for update cache, but I got the 403 forbidden error.
It's hard to debug and find out the root cause.
Did someone succed?
All the other answers have really helped - thanks. I'm adding a little here which may hopefully also help.
As #kul3r4 points out (I missed it first time round) the apikey.pub file needs to be served as plain text. Here is the Nginx config rule for that;
location /.well-known/amphtml/apikey.pub { ## serve amp public key as plain/text
default_type text/plain;
}
If you are echoing out #Krzysztof Bukowski's answer to the screen, the fact that & is in the url parameters means my browser was stripping out the amp part of the amp_ts and amp_url_signature so be aware of that.
If you are struggling with the file paths and syntax of this;
$pkeyid = openssl_pkey_get_private("file://amp-private-key.pem");
Then just follow this answer here and put the contents of your private key in a variable -> Openssl and PHP
According to the documentation: http://php.net/manual/en/function.headers-list.php, and this comment: http://php.net/manual/en/function.headers-list.php#110330, php code:
<?php var_dump(header_list()); ?>
Does not show the status headers.
This strange behavior is strange. So there are two questions:
Why? (I'm not sure if this question is opinion based, if it is, and there is no REAL explanation please omit it. I mean that sometimes opinion based questions aren't opinion based, and really have explanation, and this cannot be predict before they are asked).
I know that I can use my own function to set header, which will set header and additionally remember that this header was set. But this is kind of... workaround, as header_list() is quite sure HERE, FOR THAT. Additionally those headers are somewhere in the php engine memory so saving them second time inside script is not memory efficient. So... What is the back-door to get all headers, not as stupid as workaround below? This can be useful for example as a part of debug / developer class that is rendering all the "developer" data as html comments at the end of the page. Of course I'm omitting the content length header which is too soon to predict.
It looks like this function omit all the headers that don't have colon... Is it right?
To post more code, simple workaround to header function (linear not object, using globals and not static class just to show the idea). With the assumption that header function is omitting headers without colons (which may not be quite true...):
<?php
// Mechanism:
$headers = array();
function setHeader($header) {
header($header);
if (strpos($header, ':') === false) {
global $headers;
$headers[] = $header;
}
}
function getHeaders() {
global $headers;
return array_merge($headers, header_list());
}
// Example:
setHeader('HTTP/1.1 404 Not Found');
var_dump(getHeaders());
?>
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.
It seems that only the status code is missing from the header_list.
You can get the current status code (they probably overwrite one another) using another function: http_response_code.
Error : Unable to add cookies, header already sent.
I would like to know if there is any way I can diagnose above error efficiently.
I understand the cause of this error - when cookies are being sent after something else has been sent to the browser such as headers, html etc.
There are hundreds of files with thousands of lines of codes. If anyone knows a method (even if 3rd party, it doesn't matter) that will help me to find which contents are sent to the browser before cookies so I can manipulate that part of code easily. I'm having trouble locating actual place or code patch causing this error.
Generally this happens to many vBulletin users out there and vB staff tells you to disable mods/plugins etc. But i would like to know any efficient way to find problem location. There is no problem with <?php etc. etc.
Refer to header_sent() function at point of cookie setting. php function header_sent() can help you.
bool headers_sent ([ string &$file [, int &$line ]] )
If the optional file and line parameters are set, headers_sent() will put the PHP source file name and line number where output started in the file and line variables.
The PHP documentation states that php://input can only be read once.
In my application I need to read it twice, once for authentication purposes and once for actually processing the content, and both functions are handled by different, independent modules. The crazy thing is: it works.
Can I count on this working everywhere, or is this a fluke in my version of PHP (5.2.10)? The only documentation I can find about this is the one that states that it shouldn't work, with no version limitation mentioned.
Following Dennis' hunch, I did this test:
$in = fopen('php://input', 'r');
echo fread($in, 1024) . "\n";
fseek($in, 0);
echo fread($in, 1024) . "\n";
fclose($in);
echo file_get_contents('php://input') . "\n";
Curling:
$ curl http://localhost:8888/tests/test.php -d "This is a test"
This is a test
This is a test
Apparently it's limited to one read per open handle.
A little more digging revealed that indeed php://input can only be read once, ever, for PUT requests. The above example used a POST request.
A little inspection of the source code yields the answers.
First, yes, you're limited to one read per handle because the underlying stream does not implement the seek handler:
php_stream_ops php_stream_input_ops = {
php_stream_input_write,
/* ... */
"Input",
NULL, /* seek */
/* ... */
};
Second, the read handler has two different behaviors depending on whether the "POST data" has been read and stored in SG(request_info).raw_post_data.
if (SG(request_info).raw_post_data) {
read_bytes = SG(request_info).raw_post_data_length - *position;
/* ...*/
if (read_bytes) {
memcpy(buf, SG(request_info).raw_post_data + *position, read_bytes);
}
} else if (sapi_module.read_post) {
read_bytes = sapi_module.read_post(buf, count TSRMLS_CC);
/* ... */
} else {
stream->eof = 1;
}
So we have three possibilities here:
The request body data has already been read and stored in SG(request_info).raw_post_data. In this case, since the data is stored, we can open and read multiple handles for php://input.
The request body data has been read, but its contents were not stored anywhere. php://input cannot give us anything.
The request data hasn't been read yet. This means we can open php://input and read it only once.
NOTE: What follows is the default behavior. Different SAPIs or additional extensions may change this behavior.
In case of POST requests, PHP defines a different POST reader and a POST handler depending on the content-type.
Case 1. This happens when we have a POST request:
With content-type application/x-www-form-encoded. sapi_activate detects a POST request with a content-type and calls sapi_read_post_data. This detects the content-type and defines the POST reader/handler pair. The POST reader is sapi_read_standard_form_data, which is immediately called and just copies the request body to SG(request_info).post_data. The default post reader php_default_post_reader is then called, which fills $HTTP_RAW_POST_DATA if the ini setting always_populate_post_data is set and then copies SG(request_info).post_data to SG(request_info).raw_post_data and clears the first. The call to the handler doesn't matter here and is deferred until the superglobals are built (which may not happen, in case JIT is activated and the superglobals are not used).
With an unrecognized or inexistent content-type. In this case, there's no defined POST reader and handler. Both cases end up in php_default_post_reader without any data read. Since this is a POST request and there's no reader/handler pair, sapi_read_standard_form_data will be called. This is the same function as the read handler the content type application/x-www-form-encoded, so all the data gets swallowed to SG(request_info).post_data. The only differences from now on is that $HTTP_RAW_POST_DATA is always populated (no matter the value of always_populate_post_data) and there's no handler for building the superglobals.
Case 2. This happens when we have a form request with content-type "multipart/form-data". The POST reader is NULL, so the handler, which is rfc1867_post_handler acts as a mixed reader/handler. No data whatsoever is read in the sapi_activate phase. The function sapi_handle_post is eventually called in a later phase, which, in its turn calls the POST handler. rfc1867_post_handler reads the request data, populates POST and FILES, but leaves nothing in SG(request_info).raw_post_data.
Case 3. This last case takes place with requests different from POST (e.g. PUT). php_default_post_reader is directly called. Because the request is not a POST request, the data is swallowed by sapi_read_standard_form_data. Since no data is read, there's not anything left to be done.
Maybe they mean fseek() or rewind() aren't available.
Have you tried one of those functions on an opened php://input ?
I'm a PHP virgin (first day), so please type slowly.
I have a number of images, bg_001.jpg, bg_002.jpg, etc., which I want to rotate through each time the page is refreshed. I tried this:
if (isset($_COOKIE["bg1"])) {
$img_no = $_COOKIE["bg1"] + 1;
} else {
$img_no = 1;
}
$filename = 'bg_' . sprintf("%03d", $img_no) . '.jpg';
if (!file_exists("/img/" . $filename)) {
$img_no = 1;
$filename = 'bg_' . sprintf("%03d", $img_no) . '.jpg';
}
setcookie("bg1", $img_no, time() + 86400);
print '<img src="img/' . $filename . '" alt="" height="175" width="800"> ';
Instead of a cookie I get a
Warning: Cannot modify header information - headers already sent by (output
started at /home2/.../about.php:7) in /home2/.../about.php on line 31
Line 31 being the line with the setcookie. I already found pointers about PHP having trouble with Unicode's BOM, but I have no idea how to fix it (if it is the problem here in the first place).
So, to make it official (and avoid a "not a real question" label), how do I fix this? :-)
Constructive criticism on my code is welcome too.
epilogue:
Seemed like a common newbie error: several answers toward the same solution within fifteen minutes. Thanks guyz/galz.
So I moved everything except the print to the start of the file, and indeed: fixed.
cookies can only be sent, if there are no information sent prior. This means that the first thing in your PHP file has to be <?php, and nothing can get before that part (not even the dreaded UTF-8 BOM), because if something is before the first <?php (like <html>), then php will send those data for the browser and after data is sent you can't use setcookie. Refactor your code to look something like this:
<?php
(...)
setcookie(...)
(...)
?>
<HTML>
(...)
</HTML>
And double check that editors like notepad didn't put any UTF-8 BOM signatures before the first <?php in the file.
With HTTP your header (request/response info) and Content (actual textor binary content) are set separately and Header must precede the Content.
Setting a cookie actually adds a command to the header response, so any modifications to the cookie need to happen before you start outputting any content.
Move all your code that references your cookie before the opening html element on your page and you should be ok.
You might also find it easier to use sessions instead of manipulating the cookies themselves. the session_start() still needs to come before any other content but it makes it easier to store data structures, etc.
have a look at http://php.net/session_start
To get around this without major code changes, use Output Buffering like so,--
<?php
ob_start();
// ... my code here
?>
<html></html>
<?php
// ... end of the file
ob_end_flush();
?>
The error/warning you got says it all: headers are already sent
This means that output has been already sent to the browser before the setcookie() method was called.
As you can understand cookies "should be set" before any output is send to the browser.
So check line 7 at about.php. You should have html code there or you might have a call to print or echo.