diff --git a/lib/csslib.php b/lib/csslib.php index a8217223cf2..109711f36d2 100644 --- a/lib/csslib.php +++ b/lib/csslib.php @@ -292,7 +292,7 @@ function css_send_cached_css($csspath, $etag) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime.', immutable'); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); if (!min_enable_zlib_compression()) { @@ -318,7 +318,7 @@ function css_send_cached_css_content($csscontent, $etag) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime.', immutable'); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); if (!min_enable_zlib_compression()) { diff --git a/lib/editor/tinymce/plugins/loader.php b/lib/editor/tinymce/plugins/loader.php index 141a6ee7b2c..537ac791d53 100644 --- a/lib/editor/tinymce/plugins/loader.php +++ b/lib/editor/tinymce/plugins/loader.php @@ -62,7 +62,7 @@ if ($allowcache) { // Set it to expire a year later. Note that this means we should never get // If-Modified-Since requests so there is no need to handle them specially. header('Expires: ' . date('r', time() + 365 * 24 * 3600)); - header('Cache-Control: max-age=' . 365 * 24 * 3600); + header('Cache-Control: max-age=' . 365 * 24 * 3600 . ', immutable'); // Pragma is set to no-cache by default so must be overridden. header('Pragma:'); } diff --git a/lib/filelib.php b/lib/filelib.php index a3ca2cbd09c..a99d293b788 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2155,6 +2155,7 @@ function file_safe_save_content($content, $destination) { * and should not be reopened. * @param array $options An array of options, currently accepts: * - (string) cacheability: public, or private. + * - (string|null) immutable * @return null script execution stopped unless $dontdie is true */ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring=false, $forcedownload=false, $mimetype='', @@ -2195,6 +2196,14 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring } if ($lifetime > 0) { + $immutable = ''; + if (!empty($options['immutable'])) { + $immutable = ', immutable'; + // Overwrite lifetime accordingly: + // 90 days only - based on Moodle point release cadence being every 3 months. + $lifetimemin = 60*60*24*90; + $lifetime = max($lifetime, $lifetimemin); + } $cacheability = ' public,'; if (!empty($options['cacheability']) && ($options['cacheability'] === 'public')) { // This file must be cache-able by both browsers and proxies. @@ -2207,7 +2216,7 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring $cacheability = ' private,'; } $nobyteserving = false; - header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform'); + header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform'.$immutable); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); @@ -2294,6 +2303,8 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring * (string|null) cacheability - force the cacheability setting of the HTTP response, "private" or "public", * when $lifetime is greater than 0. Cacheability defaults to "private" when logged in as other than guest; otherwise, * defaults to "public". + * (string|null) immutable - set the immutable cache setting in the HTTP response, when served under HTTPS. + * Note: it's up to the consumer to set it properly i.e. when serving a "versioned" URL. * * @category files * @param stored_file $stored_file local file object diff --git a/lib/jslib.php b/lib/jslib.php index d3114c104ae..c39ce4472c5 100644 --- a/lib/jslib.php +++ b/lib/jslib.php @@ -41,7 +41,7 @@ function js_send_cached($jspath, $etag, $filename = 'javascript.php') { header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($jspath)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime.', immutable'); header('Accept-Ranges: none'); header('Content-Type: application/javascript; charset=utf-8'); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index ba7874fc69e..c7346c2bd4a 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -1,6 +1,10 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. +=== 3.2.4 === + +* The caching option 'immutable' has been added to send_stored_file() and send_file(). + === 3.2.3 === * Behat compatibility changes are now being documented at diff --git a/theme/font.php b/theme/font.php index d6d5e22208a..c4a16eaddc5 100644 --- a/theme/font.php +++ b/theme/font.php @@ -180,7 +180,7 @@ function send_cached_font($fontpath, $etag, $font, $mimetype) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($fontpath)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime.', immutable'); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Content-Length: '.filesize($fontpath)); diff --git a/theme/image.php b/theme/image.php index ed5080f1147..7af9ed6f0be 100644 --- a/theme/image.php +++ b/theme/image.php @@ -230,7 +230,7 @@ function send_cached_image($imagepath, $etag) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($imagepath)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age='.$lifetime.', no-transform'); + header('Cache-Control: public, max-age='.$lifetime.', no-transform, immutable'); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Content-Length: '.filesize($imagepath)); diff --git a/theme/jquery.php b/theme/jquery.php index f4527d2cf0d..da5e961dbf9 100644 --- a/theme/jquery.php +++ b/theme/jquery.php @@ -125,7 +125,7 @@ header('Content-Disposition: inline; filename="'.$filename.'"'); header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($file)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); -header('Cache-Control: public, max-age='.$lifetime); +header('Cache-Control: public, max-age='.$lifetime.', immutable'); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); diff --git a/theme/yui_combo.php b/theme/yui_combo.php index 9e4e536e5ab..d237d441655 100644 --- a/theme/yui_combo.php +++ b/theme/yui_combo.php @@ -401,7 +401,7 @@ function combo_send_cached($content, $mimetype, $etag, $lastmodified) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime.', immutable'); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Etag: "'.$etag.'"'); diff --git a/theme/yui_image.php b/theme/yui_image.php index 5865aa37f7a..894ece8e932 100644 --- a/theme/yui_image.php +++ b/theme/yui_image.php @@ -123,7 +123,7 @@ function yui_image_cached($imagepath, $imagename, $mimetype, $etag) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($imagepath)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); - header('Cache-Control: public, max-age=315360000, no-transform'); + header('Cache-Control: public, max-age=315360000, no-transform, immutable'); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Content-Length: '.filesize($imagepath));