I have a PHP file get_css.php which generates CSS code more than 60 KB long. This code does not change very often. I want this code to be cached in user's browser.
Now, when i visit a HTML page several times which includes get_css.php url to fetch css, my browser is loading all CSS contents from the server each time i visit the page.
Browsers should get the contents from server only if the CSS code is changed on server side. If the css code is not changed, browser will use the css code from the browser cache.
I cannot use any PHP function which is not allowed in Server Safe Mode.
Is it possible? How can i achieve this?
You cannot force a client to revalidate its cache so easily.
Setting a variable query string to its resource won't play well with proxies, but seems to suffice with browsers. Browsers do tend to only redownload the css file if there's a query string change.
<link rel="stylesheet" type="text/css" href="/get_css.php?v=1.2.3">
Potentially, you could play with the naming of the CSS, such as add numbers, but this isn't a great alternative.
You cannot control browser behaviour from PHP, but you can use HTTP codes to tell the browser something.
If the CSS is not changed, just reply with a 304 Not Modified response code:
if ($css_has_not_changed && $browser_has_a_copy) {
http_response_code(304);
} else {
// regenerate CSS
}
This way, the browser will ask for the document (which you cannot control), but you tell him to use the cached copy.
Of course this needs testing, as I have now idea how it will work 'the first time' a browser requests the file (perhaps the request headers can tell you more). A quick firebug test reveals that Firefox requests Cache-Control: no-cache when it is requesting a fresh copy, and Cache-Control: max-age=0 when it has cache.
add normal GET parameter when you including get_css.php like so
<link rel="stylesheet" type="text/css" href="get_css.php?v=1">
Browser will think that it is new link and will load it again.
and in get_css.php use this to make browser cache data
<?php
header("Content-type: text/css");
header('Cache-Control: public');
header('Expires: ' . gmdate('D, d M Y H:i:s', strtotime('+1 year')) . ' GMT');
ob_start("ob_gzhandler");
//echo css here
The browser wants to cache your document by default, but you have to give it enough info to make that possible. One fairly easy way is to send the Last-Modified header, containing the date/time at which your script was last changed. You'll also need to handle the browser's "revalidation" request correctly by checking the incoming Last-Modified date, comparing it to the actual modified date of your script, and returning a 304 Not Modified response (with an empty response body), if the file is unchanged.
It's also a good idea to be sure that your server isn't "magically" sending any other "no-cache" directives. The easiest way to do this is to send a Cache-Control directive that tells the browser exactly what behavior you expect.
Here is a quick explanation of each Cache-Control option.
Something like the following should do the trick:
<?php
// this must be at the top of your file, no content can be output before it
$modified = filemtime(__FILE__);
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$if_modified_since=strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]);
if( $modified > $if_modified_since ) {
header('HTTP/1.0 304 Not Modified');
exit();
}
}
header('Cache-Control: must-revalidate');
header('Last-Modified: '.date("r",$modified));
// ... and the rest of your file goes here...
The above example was based heavily on the example, and writeup found here.
Related
I'm having problems with serving CSS files from PHP. For test I'm just loading content from existing CSS file into PHP variable and than echo it. I want to set headers to allow caching of file until it was modified.
PHP code
$css_file_path = "path-to-existing-css-file";
$file_content = file_get_contents ($css_file_path);
$fmtime = date ("r", filemtime ($css_file_path));
header ("Content-type: text/css");
header ("X-Content-Type-Options: nosniff");
header ("Last-Modified: " . $fmtime);
die ($file_contents);
This is done by simple PHP code as shown above. For some reason it's never cached (tested in latest Firefox only).
I have tried to put this line before die() function to test it.
echo date ("r", time());
And it gets updated all the time. I'm such a caching noob, I admit it, so all I want to do is to make file being cached until new modification arrives.
So far, I have read tones of different posts here and web-wide and mostly found nothing or very poor information on this subject.
What am I missing and is it possible to achieve at all?
To start with
I want to do is to make file being cached until new modification arrives
The only way a browser can know there is a new modification, is by asking the server whether their cached version is still valid.
This is done as followed:
1. Browser requests /style.css
GET /style.css
2. Server sends to browser
HTTP/1.1 200 OK
Last-Modified: Wed 2 Aug 2017 21:28:00 GMT
Cache-Control: must-revalidate, max-age=31536000
... file-contents ...
// 31536000 is about 1 year
3. Next time browser wants that file it sends
GET /style.css
If-Modified-Since: Wed 2 Aug 2017 21:28:00 GMT
4a. Your server can read that header, and verify if the file isn't modified after
the given date. If it isn't, you can reply with a single:
HTTP/1.1 304 Not Modified
... without sending the contents again
4b. If your file was hower modified after Aug 2, you should sent a response simalar
as in step 2
So in code, step 2, add the Cache-Control-header:
header('Cache-Control: must-revalidate, max-age=31536000');
And step 4a, act to the If-Modified-Since request-header:
$css_file_path = "path-to-existing-css-file";
$fmtimestamp = filemtime ($css_file_path);
// Check header set by browser
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $fmtimestamp <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
die(); // We're done here
}
// Otherwise continue as ussualy
$file_content = file_get_contents ($css_file_path);
Alternative solution, without using the If-Modified-Since, but it depends on the situation if this is usable for you:
// Somewhere in your HTML
<link rel="stylesheet" href="/style.css?version=<?php echo filemtime($pathToStyle.css) ?>" />
When your file changes, the link changes and the browser would see it as a new file. In that case you can leave the must-revalidate-part out of the Cache-Control-header and the browser won't reload the style.css unless the max-age expires or cache is cleaned up.
My idea is simple, take all css files and generate one minified in a time of change in some css file. Then tell the browser to clear the cache. If there is an unchanged file in browser cache then use it - so user don't need to redownload it every time.
I'm using the following snip of code to do that. But the part with using cache is a bit buggy, most of time it works but sometimes it tell the browser to use the cached version (as there is no change) and browser is using the old one and user must do client side cache refresh.
Could you give me some advice how to do that, so it would refresh client side browser cache everytime when the change occurs and if there is no change just use the cache?
$cssFiles = getCssFiles();
$fm = new FileMinifier(FileMinifier::TYPE_CSS);
$lastModified = $fm->lastModification($cssFiles);
$savedLastModified = DateUtils::convertToTimestamp($this->system->systemSettings['cssLastChange']);
$etagFile = md5('css-file');
header("Content-type: text/css");
header("Pragma: public");
header('Cache-Control: public');
header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastModified) . " GMT");
header("Etag: $etagFile");
// if there is a change - generate new minified css
if ($lastModified > $savedLastModified)
{
// take files minify them, save it and redirect to output, update last change time
...
}
// or use already generated
else
{
$ifModifiedSince = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : 0);
$etagHeader = (isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : false);
// if it is in chache use it! - no need for redownloading
if (strtotime($ifModifiedSince) == $lastModified || $etagHeader == $etagFile)
{
header("HTTP/1.1 304 Not Modified");
exit;
}
$this->data['text'] = file_get_contents(SystemInfo::getServerRoot() . '/public/css/minified.css');
}
What you're trying to do is admirable, but it's a bit of re-inventing the wheel. As far as CSS files, JavaScript files, etc are concerned, modern browsers already do a fine job of pulling unchanged files from the cache.
Manipulating the HTTP headers to notify the browser of a file change is do-able, but there are browser differences (especially older browsers) in how the headers are interpreted which makes that approach fraught with nuance.
It is far easier to accomplish your goal by versioning your CSS includes. A change in file version will prompt the browser to re-download the file.
Example:
Before file change:
<link href="http://yourwebsite.com/file.css?_=1.0.0.1" rel="stylesheet" type="text/css">
After file change:
<link href="http://yourwebsite.com/file.css?_=1.0.0.2" rel="stylesheet" type="text/css">
All browsers will interpret the change in URI parameter as a new file and will re-download it.
It's also possible to automate the versioning so that you don't need to manually edit the include line after every change. Here's one way to do it...
Example:
<?php
$ver = filemtime($filename);
echo '<link href="http://yourwebsite.com/file.css?_='.$ver.'" rel="stylesheet" type="text/css">';
?>
That code will place append modified date of the file (Unix timestamp format) to the URI of the file include.
On an HTML page constructed using php + jsquery + javascript (e.g. index.php), a video tag has a source that is another php page, with a GET field specifying which video to load (e.g. "getfile.php?file=111").
Buttons switch which video is playing; e.g. javascript
var video = document.getElementById('flyover');
var source = video.getElementsByTagName('source')[0];
source.setAttribute('src', "getfile.php?file=222");
getfile.php emits HTTP headers, then fpassthru of file contents.
...
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename='.basename($file->FileName));
header('Content-Transfer-Encoding: binary');
$seconds_to_keep = ...
header ("Expires: " . gmdate("D, d M Y H:i:s", time() + $seconds_to_keep) . " GMT");
header('Cache-Control: public, max-age=' . $seconds_to_keep);
header('Content-Length: ' . filesize($filename));
fpassthru($fp);
exit;
Fiddler proxy used to confirm headers:
# Result Protocol Host URL Body Caching Content-Type
47 200 HTTP ... /getfile.php?file=2639 10,113 public, max-age=31536000; Expires: Thu, 06 Aug 2015 20:20:30 GMT video/mp4
Test actions:
Load page
Wait for video #1 to finish playing (And Fiddler updates Caching info from "-1" to "max-age / Expires" details)
Push button for video #2
Wait for video #2 to finish playing (And Fiddler updates Caching info)
Push button for video #1
On Chrome, the result is that video #1 immediately starts playing (and buffering bar shows halfway loaded, which is the most I ever see at video start). Fiddler does NOT show a new "getfile" request to server.
On IE 11, there is a delay while video #1 buffers (and buffering bar shows zero loaded at video start). Fiddler DOES show a new "getfile" request to server.
IE's cache setting is "automatic". (Temporary Internet Files / Check for newer versions of stored pages = "Automatically"). Cache size is 250 mb, videos are ~ 6 mb each, and cache was emptied prior to start of testing.
Confirmed that URL is exactly the same (according to fiddler, and using alert pop-up in javascript).
Q: What else could affect IE's failure to cache these videos?
UPDATE
IMAGES, obtained via the same url, but with different query field fileid value, and different Content-Type header, ARE caching in IE: If quit browser, and restart browser, and go the the same page, Fiddler does not show any "/getfile.php?fileid=333" requests for those images. (It did show those requests the first time page was loaded after cache clear.)
The only change in php code executed (for images versus video) is a single if / else if statement, that controls what Content-Type header is emitted.
Perhaps it is IE 11's caching policy to not cache videos?
The logic does emit a Content-Length header with file size, and the client internet options cache (250 mbs) is much larger than the file size (6 mb), so it "should" be able to cache it. Disk space free is many GBs.
UPDATE #2
Restarting IE, after using Security tab to turn "Enable Protected Mode" off or on, does not change the above results.
Increasing disk space to the maximum (1024 MB) does not change the above results.
Setting IE's policy to "Check for newer versions of stored pages: Never" doesn't seem to "stick": when close Internet Options, then re-open it, the radio button has returned to "Automatically".
...
Repeating Chrome test after the IE tests confirms that caching is still working correctly within Chrome.
UPDATE #3
php code on server does NOT test for HTTP_IF_MODIFIED_SINCE; I'm not sending Last-Modified header. I was assuming maxage would be sufficient. It is possible that IE would be smarter about caching video files if Last-Modified was present. If you have any experience with video over slow server connections, and have succeeded using a specific set of headers, then an answer with the approach you used would be useful.
Give this a shot, from http://php.net/manual/en/function.header.php#85146:
$last_modified_time = filemtime($file);
$etag = md5_file($file);
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $last_modified_time)." GMT");
header("Etag: $etag");
if (#strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified_time ||
trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
header("HTTP/1.1 304 Not Modified");
exit;
}
Here's the thing.
I'm serving somewhat compressed CSS content
(`str_replace(array("\r", "\n", "\t", '\s\s+'), '', cssGoesHere)`)
via a PHP file in my page:
<link rel="stylesheet" type="text/css" href="/css/loader.css.php" />
The question is: how do I make the browser cache the css returned, BUT update the cache if the content is changed?
The PHP file is not being modified, so appending something like
<?php echo filemtime('/css/loader.css.php'); ?>
to the href attribute is not an option. Can this be solved with headers, and if so, how? Because AFAIK if I serve it like I wrote above, the browser will just cache the result and keep reusing the cache (provided, of course, the browser is enabled/capable of doing so), but I need it to know when the content is changed.
Edit: I've made a github project with my code (though I did change it alot for more flexibility since I wrote this). Here's the link: https://github.com/jurchiks/YACC
If you have any suggestions, write them to my e-mail or smth.
It can be solved by headers cache-control, BUT this have proved to be not very effective. Because some browser do overwrite or modify your forced settings.
I can't see where you getting the CSS data from, if it's a CSS file you could get the filemtime of the css file as version indicator or simply supply somewhere a version string which has to be changed each time the CSS file changes.
By the way, str_replace(array("\r", "\n", "\t", '\s\s+'), does not effectively remove all newlines. Your should use something like
$foo = nl2br($string, FALSE);
$bar = str_replace(array('<br>', '\s\s+'), '', cssGoesHere)
instead.
HTH :)
The only way to reliably make the file load upon change is the method you state. Otherwise, the page will cache the file and keep it for as long as it thinks necessary. It can't check to see whether the page has changed or not without requesting and downloading at least the headers, and if it's gone that far it might as well just download the rest of the page, as the bandwidth will be minimal.
The best alternative if you really can't append the modified date to the HTML, would be to set the cache headers via PHP.
header("Cache-Control: max-age=time-in-seconds");
This doesn't always work though, as server settings and browser settings can override this, especially in aggressive caching browsers such as Internet Explorer.
You could add a version number to the CSS link href and use mod rewrite to route it back to the original CSS file.
For example. Let's say you start with version 1 (note the version appendix to the file name):
<link rel="stylesheet" type="text/css" href="/css/loader-v001.css.php" />
The browser will cache this file. The next time you update the CSS file and you don't want the browser to use the cache file, simply change the version number like this:
<link rel="stylesheet" type="text/css" href="/css/loader-v002.css.php" />
Then, in your mod rewrite, route any request for the file loader-v{x}.css.php to loader.css.php.
I am not sure if this is 'automated' enough for your needs, as it does require manual changing of the version number. However, the different file name will ensure the browser sees it as an actual 'new' file and re-downloads it.
use this in your css php page:
header('Content-type: text/css');
header("Cache-Control: no-cache, must-revalidate");
you can see more information about headers in PHP's official documentation
i would suggest to keep somewhere the time that the cache content was generated for example
style.php which holds the CSS data was last modified at 21/06/2012
and cache content was last generated at 20/06/2012 running a comparison on dates in strtotime() integers, you see that the cached content must be generated again from the new content.
So my application uses a LOT of js files. thats a lot of http requests. I decided to combine them dynamically at the server in packs of 3-4 files clubbed by functionality.
My client side request is:
...script type="text/javascript" src="http://mydomain.com/core-js.php" ...
My server side does:
--core-js.php--
header("Content-type: application/x-javascript");
include_once('file1.js');
include_once('file2.js');
include_once('file3.js');
include_once('file4.js');
I am setting a far future expire header on core-js.php. My question is, would core-js.php be cached at the client side? If it would be, could someone please explain how?
Thanks!
The client doesn't know or care that what got sent to it was satisfied by bringing together several files server-side. The client should cache it if the caching headers are correct. You'll want to check them carefully to be sure that your PHP install isn't sending other headers that conflict (Firefox+Firebug is good for this), since PHP pages tend to be used for dynamic stuff where you don't want caching.
Please see: http://www.jonasjohn.de/snippets/php/caching.htm, you have to check the incoming request headers to send the right response.
You can do something like below:
<?php
ob_start();
$filemtimes = array();
foreach(array('file1.js','file2.js') as $file)
{
include_once($file);
$filemtimes[]= filemtime($file);
}
$date = gmdate('D, d M Y H:i:s', max($filemtimes)).' GMT';
$length = ob_get_length();
$etag = md5($date.$lengte);
$headers = apache_request_headers();
if(!empty($headers['If-None-Match']) && !empty($headers['If-Modified-Since']))
{
if
(
$etag == md5($headers['If-Modified-Since'].$length)
)
{
ob_end_clean();
header("Content-type: application/x-javascript");
header('Last-Modified: '.$date."\r\n");
header('Expires: '.gmdate('D, d M Y H:i:s', (time()+3600)).' GMT'."\r\n");
header('Cache-Control: max-age=3600'."\r\n");
header('ETag: '.$headers['If-None-Match']."\r\n");
header('HTTP/1.1 304 Not Modified');
header('Connection: close');
exit;
}
}
header("Content-type: application/x-javascript");
header('Last-Modified: '.$date."\r\n");
header('Expires: '.gmdate('D, d M Y H:i:s', (time()+3600)).' GMT'."\r\n");
header('Cache-Control: max-age=3600'."\r\n");
header('ETag: '.$headers['If-None-Match']."\r\n");
header('Content-Length: '.$length."\r\n");
header('Accept-Ranges: bytes'."\r\n");
ob_end_flush();
exit;
?>
Your script will be cached. No data is send to the client. Server side the includes and modification calculation is done for every request. Maybe store etag and modification time in session or cookie to do the check before includes and calculations. Or check filesizes instead of includes.
The vast majority of browsers and caching proxies will respect the expiry header (if set).
Yes it will. The client doesn't know that the js file he's requesting is a bunch of other files chunked into one, he's just seeing one js file, the one he requested and it's telling him to cache it, core-js.php. As long as you don't change the name of the file (core-js.php) there should be no problem.
On another note, you should take a look at Minify http://code.google.com/p/minify/
You can merge and cache not only js but css in groups, basically what you're doing. I've been using it for a while with no problems and it's pretty nice.
Yes, but it's complicated. PHP by default adds a bunch of headers which prevent caching. You'll have to make sure you're removing all of them. Also, does your PHP script understand If-Modified-Since and If-None-Match headers? Do you even generate Last-Modified and ETag headers in the first place? This is tricky to get right, and why bother, when your webserver has all that built into it?
I'd do this differently. Make the request to core.js, not core.php. Of course, core.js does not exist, so .htaccess catches the request and directs it to index.php. Now index.php generates the required javascript and serves it to the client. It also creates the file core.js. Future requests for core.js will be handled by Apache as normal for static files, without going near PHP.
And if you want to be able to update the javascript, you can instead use URLs of the form last-modified-timestap.core.js. Changing the timestamp in the HTML will generate a new javascript file on the first request.
I do this for dynamically created CSS (the designer can edit CSS in the administration panel, with values saved into the database), and it works well.