From 8c672cf9f35a1578b706b3a05713be5763299f3f Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 13:43:57 +0200 Subject: [PATCH 01/12] MDL-32683 explicitly allow more caching of static content --- lib/csslib.php | 4 ++-- theme/image.php | 4 ++-- theme/javascript.php | 4 ++-- theme/styles.php | 2 +- theme/yui_combo.php | 2 +- theme/yui_image.php | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/csslib.php b/lib/csslib.php index 5fc0f9b16ab..2fdbb7357a8 100644 --- a/lib/csslib.php +++ b/lib/csslib.php @@ -99,7 +99,7 @@ function css_send_ie_css($themename, $rev) { 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: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); header('Content-Length: '.strlen($css)); @@ -124,7 +124,7 @@ function css_send_cached_css($csspath, $rev) { 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: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); if (!min_enable_zlib_compression()) { diff --git a/theme/image.php b/theme/image.php index b76bd26c4e6..d13aabbb2e6 100644 --- a/theme/image.php +++ b/theme/image.php @@ -82,7 +82,7 @@ if ($rev > -1) { $mimetype = get_contenttype_from_ext($ext); header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); - header('Cache-Control: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: '.$mimetype); die; } @@ -153,7 +153,7 @@ function send_cached_image($imagepath, $rev) { 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: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Content-Length: '.filesize($imagepath)); diff --git a/theme/javascript.php b/theme/javascript.php index 4151172a711..cfc1eb04877 100644 --- a/theme/javascript.php +++ b/theme/javascript.php @@ -59,7 +59,7 @@ if ($rev > -1 and file_exists($candidate)) { $lifetime = 60*60*24*30; // 30 days header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); - header('Cache-Control: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: application/javascript; charset=utf-8'); die; } @@ -108,7 +108,7 @@ function send_cached_js($jspath) { 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: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: application/javascript; charset=utf-8'); diff --git a/theme/styles.php b/theme/styles.php index 5f6fd90ee68..07bb0a0edd5 100644 --- a/theme/styles.php +++ b/theme/styles.php @@ -64,7 +64,7 @@ if (file_exists($candidatesheet)) { $lifetime = 60*60*24*30; // 30 days header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); - header('Cache-Control: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: text/css; charset=utf-8'); die; } diff --git a/theme/yui_combo.php b/theme/yui_combo.php index 8683cd6953d..e68d1c19600 100644 --- a/theme/yui_combo.php +++ b/theme/yui_combo.php @@ -56,7 +56,7 @@ if (strpos($parts, '/-1/') === false and (!empty($_SERVER['HTTP_IF_NONE_MATCH']) $lifetime = 60*60*24*30; // 30 days header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); - header('Cache-Control: max-age='.$lifetime); + header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: '.$mimetype); die; } diff --git a/theme/yui_image.php b/theme/yui_image.php index 1c2012e1d39..b6555db5545 100644 --- a/theme/yui_image.php +++ b/theme/yui_image.php @@ -90,7 +90,7 @@ function yui_image_cached($imagepath) { 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: max-age=315360000'); + header('Cache-Control: public, max-age=315360000'); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Content-Length: '.filesize($imagepath)); From 9d473266a7c045ab6e2f46e34c6ce1545ca00207 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 14:45:26 +0200 Subject: [PATCH 02/12] MDL-32683 use slashargument urls for theme images --- lib/configonlylib.php | 33 ++++++++++++++++++++++++++++++++- lib/javascript-static.js | 11 ++++++----- lib/outputlib.php | 22 +++++++++++++++++----- lib/outputrequirementslib.php | 1 + theme/image.php | 28 +++++++++++++++++++++++----- 5 files changed, 79 insertions(+), 16 deletions(-) diff --git a/lib/configonlylib.php b/lib/configonlylib.php index 185415d1f37..9ceee1e58fd 100644 --- a/lib/configonlylib.php +++ b/lib/configonlylib.php @@ -108,4 +108,35 @@ function min_enable_zlib_compression() { @ini_set('zlib.output_compression', 65536); return true; -} \ No newline at end of file +} + +/** + * Returns the slashargument part of the URL. + * Note: ".php" is NOT allowed in slasharguments! + * + * @return string + */ +function min_get_slash_argument() { + // Note: This code has to work in the same cases as normal get_slash_argument(), + // but at the same time it may be simpler because we do not have to deal + // with encodings and other tricky stuff. + + $relativepath = ''; + + if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) { + if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') { + $relativepath = urldecode($_SERVER['PATH_INFO']); + } + } else { + if (isset($_SERVER['PATH_INFO'])) { + $relativepath = $_SERVER['PATH_INFO']; + } + } + + $matches = null; + if (preg_match('|^.+\.php(.*)$|i', $relativepath, $matches)) { + $relativepath = $matches[1]; + } + + return $relativepath; +} diff --git a/lib/javascript-static.js b/lib/javascript-static.js index f2666474058..961a2da023e 100644 --- a/lib/javascript-static.js +++ b/lib/javascript-static.js @@ -33,14 +33,15 @@ M.str = M.str || {}; * @return {String} */ M.util.image_url = function(imagename, component) { - var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename; - if (M.cfg.themerev > 0) { - url = url + '&rev=' + M.cfg.themerev; + if (component == '' || component == 'moodle' || component == 'core') { + component = 'core'; } - if (component && component != '' && component != 'moodle' && component != 'core') { - url = url + '&component=' + component; + if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) { + var url = M.cfg.wwwroot + '/theme/image.php/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename; + } else { + var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename; } return url; diff --git a/lib/outputlib.php b/lib/outputlib.php index 280e19f53e8..e9599f64325 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -886,7 +886,7 @@ class theme_config { $component = rtrim($match[1], '|'); $imageurl = $this->pix_url($imagename, $component)->out(false); // we do not need full url because the image.php is always in the same dir - $imageurl = str_replace("$CFG->httpswwwroot/theme/", '', $imageurl); + $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl); $css = str_replace($match[0], $imageurl, $css); } } @@ -910,17 +910,29 @@ class theme_config { public function pix_url($imagename, $component) { global $CFG; - $params = array('theme'=>$this->name, 'image'=>$imagename); + $params = array('theme'=>$this->name); + + if (empty($component) or $component === 'moodle' or $component === 'core') { + $params['component'] = 'core'; + } else { + $params['component'] = $component; + } $rev = theme_get_revision(); if ($rev != -1) { $params['rev'] = $rev; } - if (!empty($component) and $component !== 'moodle'and $component !== 'core') { - $params['component'] = $component; + + $params['image'] = $imagename; + + if (!empty($CFG->slasharguments) and $rev > 0) { + $url = new moodle_url("$CFG->httpswwwroot/theme/image.php"); + $url->set_slashargument('/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'], 'noparam', true); + } else { + $url = new moodle_url("$CFG->httpswwwroot/theme/image.php", $params); } - return new moodle_url("$CFG->httpswwwroot/theme/image.php", $params); + return $url; } /** diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index 8dc60055c17..af7a5d4236d 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -316,6 +316,7 @@ class page_requirements_manager { 'sesskey' => sesskey(), 'loadingicon' => $renderer->pix_url('i/loading_small', 'moodle')->out(false), 'themerev' => theme_get_revision(), + 'slasharguments' => (int)(!empty($CFG->slasharguments)), 'theme' => $page->theme->name, 'jsrev' => ((empty($CFG->cachejs) or empty($CFG->jsrev)) ? -1 : $CFG->jsrev), ); diff --git a/theme/image.php b/theme/image.php index d13aabbb2e6..9659b91ed01 100644 --- a/theme/image.php +++ b/theme/image.php @@ -32,12 +32,30 @@ define('NO_DEBUG_DISPLAY', true); define('ABORT_AFTER_CONFIG', true); require('../config.php'); // this stops immediately at the beginning of lib/setup.php -$themename = min_optional_param('theme', 'standard', 'SAFEDIR'); -$component = min_optional_param('component', 'moodle', 'SAFEDIR'); -$image = min_optional_param('image', '', 'SAFEPATH'); -$rev = min_optional_param('rev', -1, 'INT'); +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 3) { + image_not_found(); + } + // image must be last because it may contain "/" + list($themename, $component, $rev, $image) = explode('/', $slashargument, 4); + $themename = min_clean_param($themename, 'SAFEDIR'); + $component = min_clean_param($component, 'SAFEDIR'); + $rev = min_clean_param($rev, 'INT'); + $image = min_clean_param($image, 'SAFEPATH'); -if (empty($component) or empty($image)) { +} else { + $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); + $component = min_optional_param('component', 'core', 'SAFEDIR'); + $rev = min_optional_param('rev', -1, 'INT'); + $image = min_optional_param('image', '', 'SAFEPATH'); +} + +if (empty($component) or $component === 'moodle' or $component === 'core') { + $component = 'moodle'; +} + +if (empty($image)) { image_not_found(); } From 7070b7f2612c73ecf943935f93a6cdfb1c1deebc Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 15:25:20 +0200 Subject: [PATCH 03/12] MDL-32683 use slashargument urls for theme style sheets --- lib/csslib.php | 15 +++++++++++---- lib/outputlib.php | 20 +++++++++++++++----- theme/styles.php | 21 +++++++++++++++++---- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/lib/csslib.php b/lib/csslib.php index 2fdbb7357a8..3f2b4a08f3b 100644 --- a/lib/csslib.php +++ b/lib/csslib.php @@ -85,14 +85,21 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles) { * * @param string $themename The name of the theme we are sending CSS for. * @param string $rev The revision to ensure we utilise the cache. + * @param bool $slasharguments */ -function css_send_ie_css($themename, $rev) { +function css_send_ie_css($themename, $rev, $slasharguments) { $lifetime = 60*60*24*30; // 30 days $css = "/** Unfortunately IE6/7 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/"; - $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=plugins);"; - $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=parents);"; - $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=theme);"; + if ($slasharguments) { + $css .= "\n@import url(styles.php/$themename/$rev/plugins);"; + $css .= "\n@import url(styles.php/$themename/$rev/parents);"; + $css .= "\n@import url(styles.php/$themename/$rev/theme);"; + } else { + $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=plugins);"; + $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=parents);"; + $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=theme);"; + } header('Etag: '.md5($rev)); header('Content-Disposition: inline; filename="styles.php"'); diff --git a/lib/outputlib.php b/lib/outputlib.php index e9599f64325..48d18220e13 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -518,8 +518,14 @@ class theme_config { $rev = theme_get_revision(); if ($rev > -1) { - $params = array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor'); - return new moodle_url($CFG->httpswwwroot.'/theme/styles.php', $params); + if (!empty($CFG->slasharguments)) { + $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php"); + $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true); + return $url; + } else { + $params = array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor'); + return new moodle_url($CFG->httpswwwroot.'/theme/styles.php', $params); + } } else { $params = array('theme'=>$this->name, 'type'=>'editor'); return new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php', $params); @@ -589,7 +595,13 @@ class theme_config { $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev, 'type'=>'parents')); $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev, 'type'=>'theme')); } else { - $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev)); + if (!empty($CFG->slasharguments)) { + $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php"); + $url->set_slashargument('/'.$this->name.'/'.$rev.'/all', 'noparam', true); + $urls[] = $url; + } else { + $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev, 'type'=>'all')); + } } } else { // find out the current CSS and cache it now for 5 seconds @@ -872,8 +884,6 @@ class theme_config { * @return string The processed CSS. */ public function post_process($css) { - global $CFG; - // now resolve all image locations if (preg_match_all('/\[\[pix:([a-z_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { $replaced = array(); diff --git a/theme/styles.php b/theme/styles.php index 07bb0a0edd5..254e493e0fa 100644 --- a/theme/styles.php +++ b/theme/styles.php @@ -33,9 +33,22 @@ define('ABORT_AFTER_CONFIG', true); require('../config.php'); // this stops immediately at the beginning of lib/setup.php require_once($CFG->dirroot.'/lib/csslib.php'); -$themename = min_optional_param('theme', 'standard', 'SAFEDIR'); -$type = min_optional_param('type', 'all', 'SAFEDIR'); -$rev = min_optional_param('rev', 0, 'INT'); +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 2) { + image_not_found(); + } + // image must be last because it may contain "/" + list($themename, $rev, $type) = explode('/', $slashargument, 3); + $themename = min_clean_param($themename, 'SAFEDIR'); + $rev = min_clean_param($rev, 'INT'); + $type = min_clean_param($type, 'SAFEDIR'); + +} else { + $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); + $rev = min_optional_param('rev', 0, 'INT'); + $type = min_optional_param('type', 'all', 'SAFEDIR'); +} if (!in_array($type, array('all', 'ie', 'editor', 'plugins', 'parents', 'theme'))) { header('HTTP/1.0 404 not found'); @@ -52,7 +65,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { } if ($type === 'ie') { - css_send_ie_css($themename, $rev); + css_send_ie_css($themename, $rev, !empty($slashargument)); } $candidatesheet = "$CFG->cachedir/theme/$themename/css/$type.css"; From ecbad2ad1c4a20d04be5aa0455cce117c518d4d9 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 15:31:27 +0200 Subject: [PATCH 04/12] MDL-32683 use slashargument urls for theme javascript files --- lib/outputlib.php | 8 +++++++- theme/javascript.php | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/outputlib.php b/lib/outputlib.php index 48d18220e13..7cd7abe0733 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -775,7 +775,13 @@ class theme_config { $params = array('theme'=>$this->name,'rev'=>$rev); $params['type'] = $inhead ? 'head' : 'footer'; - return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params); + if (!empty($CFG->slasharguments) and $rev > 0) { + $url = new moodle_url("$CFG->httpswwwroot/theme/javascript.php"); + $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true); + return $url; + } else { + return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params); + } } /** diff --git a/theme/javascript.php b/theme/javascript.php index cfc1eb04877..a7ec470d912 100644 --- a/theme/javascript.php +++ b/theme/javascript.php @@ -32,9 +32,22 @@ define('NO_DEBUG_DISPLAY', true); define('ABORT_AFTER_CONFIG', true); require('../config.php'); // this stops immediately at the beginning of lib/setup.php -$themename = min_optional_param('theme', 'standard', 'SAFEDIR'); -$rev = min_optional_param('rev', 0, 'INT'); -$type = min_optional_param('type', 'head', 'RAW'); +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 2) { + image_not_found(); + } + // image must be last because it may contain "/" + list($themename, $rev, $type) = explode('/', $slashargument, 3); + $themename = min_clean_param($themename, 'SAFEDIR'); + $rev = min_clean_param($rev, 'INT'); + $type = min_clean_param($type, 'SAFEDIR'); + +} else { + $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); + $rev = min_optional_param('rev', 0, 'INT'); + $type = min_optional_param('type', 'head', 'RAW'); +} if ($type !== 'head' and $type !== 'footer') { header('HTTP/1.0 404 not found'); From 6e7b46018edc865ef238374ada11e4b85018b6bb Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 15:59:43 +0200 Subject: [PATCH 05/12] MDL-32683 use slashargument urls for yui combo loader --- lib/outputrequirementslib.php | 12 +++++++----- theme/yui_combo.php | 14 ++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index af7a5d4236d..e30f8800f16 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -152,6 +152,8 @@ class page_requirements_manager { public function __construct() { global $CFG; + $sep = empty($CFG->slasharguments) ? '?' : '/'; + require_once("$CFG->libdir/yui/phploader/phploader/loader.php"); $this->yui3loader = new stdClass(); @@ -174,8 +176,8 @@ class page_requirements_manager { } else { $this->yui3loader->base = $CFG->httpswwwroot . '/lib/yui/'. $CFG->yui3version . '/build/'; $this->yui2loader->base = $CFG->httpswwwroot . '/lib/yui/'. $CFG->yui2version . '/build/'; - $this->yui3loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php?'; - $this->yui2loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php?'; + $this->yui3loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep; + $this->yui2loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep; } // enable combo loader? this significantly helps with caching and performance! @@ -201,8 +203,8 @@ class page_requirements_manager { $this->M_yui_loader->groups = array( 'moodle' => array( 'name' => 'moodle', - 'base' => $CFG->httpswwwroot . '/theme/yui_combo.php?moodle/'.$jsrev.'/', - 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php?', + 'base' => $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep.'moodle/'.$jsrev.'/', + 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep, 'combine' => $this->yui3loader->combine, 'filter' => '', 'ext' => false, @@ -218,7 +220,7 @@ class page_requirements_manager { 'local' => array( 'name' => 'gallery', 'base' => $CFG->wwwroot.'/lib/yui/gallery/', - 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php?', + 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php'.$sep, 'combine' => $this->yui3loader->combine, 'filter' => $this->M_yui_loader->filter, 'ext' => false, diff --git a/theme/yui_combo.php b/theme/yui_combo.php index e68d1c19600..46978ff3ea9 100644 --- a/theme/yui_combo.php +++ b/theme/yui_combo.php @@ -109,12 +109,14 @@ foreach ($parts as $part) { } $filecontent = file_get_contents($contentfile); + $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot); + if ($mimetype === 'text/css') { if ($version == 'moodle') { - $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', 'yui_image.php?file='.$version.'/'.$frankenstyle.'/'.array_shift($bits).'/$1.$2', $filecontent); + $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php?file='.$version.'/'.$frankenstyle.'/'.array_shift($bits).'/$1.$2', $filecontent); } else if ($version == 'gallery') { // search for all images in gallery module CSS and serve them through the yui_image.php script - $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', 'yui_image.php?file='.$version.'/'.$bits[0].'/'.$bits[1].'/$1.$2', $filecontent); + $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php?file='.$version.'/'.$bits[0].'/'.$bits[1].'/$1.$2', $filecontent); } else { // First we need to remove relative paths to images. These are used by YUI modules to make use of global assets. // I've added this as a separate regex so it can be easily removed once @@ -122,7 +124,7 @@ foreach ($parts as $part) { $filecontent = preg_replace('#(\.\./\.\./\.\./\.\./assets/skins/sam/)?([a-z0-9_-]+)\.(png|gif)#', '$2.$3', $filecontent); // search for all images in yui2 CSS and serve them through the yui_image.php script - $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', 'yui_image.php?file='.$version.'/$1.$2', $filecontent); + $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php?file='.$version.'/$1.$2', $filecontent); } } @@ -195,9 +197,13 @@ function combo_params() { $parts = explode('?', $_SERVER['REQUEST_URI'], 2); return $parts[1]; - } else if (isset($_SERVER['QUERY_STRING'])) { + } else if (isset($_SERVER['QUERY_STRING']) and strpos($_SERVER['QUERY_STRING'], '?') !== false) { return $_SERVER['QUERY_STRING']; + } else if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + return $slashargument; + } else { // unsupported server, sorry! combo_not_found('Unsupported server - query string can not be determined, try disabling YUI combo loading in admin settings.'); From d5222fae5f2e262503b6ac111d491a650151c6cf Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 16:14:48 +0200 Subject: [PATCH 06/12] MDL-32683 use slashargument urls for yui images --- theme/yui_combo.php | 17 ++++++++++------- theme/yui_image.php | 6 +++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/theme/yui_combo.php b/theme/yui_combo.php index 46978ff3ea9..63cabc2f272 100644 --- a/theme/yui_combo.php +++ b/theme/yui_combo.php @@ -33,7 +33,9 @@ define('ABORT_AFTER_CONFIG', true); require('../config.php'); // this stops immediately at the beginning of lib/setup.php // get special url parameters -if (!$parts = combo_params()) { + +list($parts, $slasharguments) = combo_params(); +if (!$parts) { combo_not_found(); } @@ -110,13 +112,14 @@ foreach ($parts as $part) { $filecontent = file_get_contents($contentfile); $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot); + $sep = ($slasharguments ? '/' : '?file='); if ($mimetype === 'text/css') { if ($version == 'moodle') { - $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php?file='.$version.'/'.$frankenstyle.'/'.array_shift($bits).'/$1.$2', $filecontent); + $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php'.$sep.$version.'/'.$frankenstyle.'/'.array_shift($bits).'/$1.$2', $filecontent); } else if ($version == 'gallery') { // search for all images in gallery module CSS and serve them through the yui_image.php script - $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php?file='.$version.'/'.$bits[0].'/'.$bits[1].'/$1.$2', $filecontent); + $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php'.$sep.$version.'/'.$bits[0].'/'.$bits[1].'/$1.$2', $filecontent); } else { // First we need to remove relative paths to images. These are used by YUI modules to make use of global assets. // I've added this as a separate regex so it can be easily removed once @@ -124,7 +127,7 @@ foreach ($parts as $part) { $filecontent = preg_replace('#(\.\./\.\./\.\./\.\./assets/skins/sam/)?([a-z0-9_-]+)\.(png|gif)#', '$2.$3', $filecontent); // search for all images in yui2 CSS and serve them through the yui_image.php script - $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php?file='.$version.'/$1.$2', $filecontent); + $filecontent = preg_replace('/([a-z0-9_-]+)\.(png|gif)/', $relroot.'/theme/yui_image.php'.$sep.$version.'/$1.$2', $filecontent); } } @@ -195,14 +198,14 @@ function combo_params() { // note: buggy or misconfigured IIS does return the query string in REQUEST_URL if (isset($_SERVER['REQUEST_URI']) and strpos($_SERVER['REQUEST_URI'], '?') !== false) { $parts = explode('?', $_SERVER['REQUEST_URI'], 2); - return $parts[1]; + return array($parts[1], false); } else if (isset($_SERVER['QUERY_STRING']) and strpos($_SERVER['QUERY_STRING'], '?') !== false) { - return $_SERVER['QUERY_STRING']; + return array($_SERVER['QUERY_STRING'], false); } else if ($slashargument = min_get_slash_argument()) { $slashargument = ltrim($slashargument, '/'); - return $slashargument; + return array($slashargument, true); } else { // unsupported server, sorry! diff --git a/theme/yui_image.php b/theme/yui_image.php index b6555db5545..2fce5b02d6d 100644 --- a/theme/yui_image.php +++ b/theme/yui_image.php @@ -32,7 +32,11 @@ define('NO_DEBUG_DISPLAY', true); define('ABORT_AFTER_CONFIG', true); require('../config.php'); // this stops immediately at the beginning of lib/setup.php -$path = min_optional_param('file', '', 'SAFEPATH'); +if ($slashargument = min_get_slash_argument()) { + $path = ltrim($slashargument, '/'); +} else { + $path = min_optional_param('file', '', 'SAFEPATH'); +} $parts = explode('/', $path); $version = array_shift($parts); From 71d4c60389d7fe26d8d95eeaf05dcbf87c6535ad Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 16:40:33 +0200 Subject: [PATCH 07/12] MDL-32683 fine tune theme image caching --- theme/image.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/theme/image.php b/theme/image.php index 9659b91ed01..58b564bafe4 100644 --- a/theme/image.php +++ b/theme/image.php @@ -68,6 +68,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { } $candidatelocation = "$CFG->cachedir/theme/$themename/pix/$component"; +$etag = sha1("$themename/$component/$rev/$image"); if ($rev > -1) { if (file_exists("$candidatelocation/$image.error")) { @@ -96,15 +97,16 @@ if ($rev > -1) { if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // we do not actually need to verify the etag value because our files // never change in cache because we increment the rev parameter - $lifetime = 60*60*24*30; // 30 days + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often $mimetype = get_contenttype_from_ext($ext); header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: '.$mimetype); + header('Etag: '.$etag); die; } - send_cached_image($cacheimage, $rev); + send_cached_image($cacheimage, $etag); } } @@ -121,6 +123,7 @@ $theme = theme_config::load($themename); $imagefile = $theme->resolve_image_location($image, $component); $rev = theme_get_revision(); +$etag = sha1("$themename/$component/$rev/$image"); if (empty($imagefile) or !is_readable($imagefile)) { if ($rev > -1) { @@ -144,7 +147,7 @@ if ($rev > -1) { } copy($imagefile, $cacheimage); } - send_cached_image($cacheimage, $rev); + send_cached_image($cacheimage, $etag); } else { send_uncached_image($imagefile); @@ -156,19 +159,19 @@ if ($rev > -1) { // we are not using filelib because we need to fine tune all header // parameters to get the best performance. -function send_cached_image($imagepath, $rev) { +function send_cached_image($imagepath, $etag) { global $CFG; require("$CFG->dirroot/lib/xsendfilelib.php"); - $lifetime = 60*60*24*30; // 30 days + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often $pathinfo = pathinfo($imagepath); $imagename = $pathinfo['filename'].'.'.$pathinfo['extension']; $mimetype = get_contenttype_from_ext($pathinfo['extension']); - header('Etag: '.md5("$rev/$imagepath")); + header('Etag: '.$etag); header('Content-Disposition: inline; filename="'.$imagename.'"'); - header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); + 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); From 8475b9704ff38c3de23735f8f1f50389e55546c3 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 16:54:06 +0200 Subject: [PATCH 08/12] MDL-32683 fine tune theme javascript caching --- theme/javascript.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/theme/javascript.php b/theme/javascript.php index a7ec470d912..01b14501aa2 100644 --- a/theme/javascript.php +++ b/theme/javascript.php @@ -64,19 +64,21 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { } $candidate = "$CFG->cachedir/theme/$themename/javascript_$type.js"; +$etag = sha1("$themename/$rev/$type"); if ($rev > -1 and file_exists($candidate)) { if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // we do not actually need to verify the etag value because our files // never change in cache because we increment the rev parameter - $lifetime = 60*60*24*30; // 30 days + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: application/javascript; charset=utf-8'); + header('Etag: '.$etag); die; } - send_cached_js($candidate, $rev); + send_cached_js($candidate, $etag); } //================================================================================= @@ -93,6 +95,9 @@ require_once('Minify.php'); $theme = theme_config::load($themename); +$rev = theme_get_revision(); +$etag = sha1("$themename/$rev/$type"); + if ($rev > -1) { // note: cache reset might have purged our cache dir structure, // make sure we do not use stale file stat cache in the next check_dir_exists() @@ -101,7 +106,7 @@ if ($rev > -1) { $fp = fopen($candidate, 'w'); fwrite($fp, minify($theme->javascript_files($type))); fclose($fp); - send_cached_js($candidate); + send_cached_js($candidate, $etag); } else { send_uncached_js($theme->javascript_content($type)); } @@ -111,12 +116,13 @@ if ($rev > -1) { // we are not using filelib because we need to fine tune all header // parameters to get the best performance. -function send_cached_js($jspath) { +function send_cached_js($jspath, $etag) { global $CFG; require("$CFG->dirroot/lib/xsendfilelib.php"); - $lifetime = 60*60*24*30; // 30 days + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often + header('Etag: '.$etag); header('Content-Disposition: inline; 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'); From 2c61e7cc4a6e57ad9b2ae1a77d9ba8293651217a Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 17:06:58 +0200 Subject: [PATCH 09/12] MDL-32683 fine tune theme style sheet caching --- lib/csslib.php | 30 ++++++++++++++++++------------ theme/styles.php | 11 ++++++++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/csslib.php b/lib/csslib.php index 3f2b4a08f3b..c9db0931ded 100644 --- a/lib/csslib.php +++ b/lib/csslib.php @@ -85,23 +85,28 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles) { * * @param string $themename The name of the theme we are sending CSS for. * @param string $rev The revision to ensure we utilise the cache. + * @param string $etag The revision to ensure we utilise the cache. * @param bool $slasharguments */ -function css_send_ie_css($themename, $rev, $slasharguments) { - $lifetime = 60*60*24*30; // 30 days +function css_send_ie_css($themename, $rev, $etag, $slasharguments) { + global $CFG; + + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often + + $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot); $css = "/** Unfortunately IE6/7 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/"; if ($slasharguments) { - $css .= "\n@import url(styles.php/$themename/$rev/plugins);"; - $css .= "\n@import url(styles.php/$themename/$rev/parents);"; - $css .= "\n@import url(styles.php/$themename/$rev/theme);"; + $css .= "\n@import url($relroot/styles.php/$themename/$rev/plugins);"; + $css .= "\n@import url($relroot/styles.php/$themename/$rev/parents);"; + $css .= "\n@import url($relroot/styles.php/$themename/$rev/theme);"; } else { - $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=plugins);"; - $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=parents);"; - $css .= "\n@import url(styles.php?theme=$themename&rev=$rev&type=theme);"; + $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=plugins);"; + $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=parents);"; + $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=theme);"; } - header('Etag: '.md5($rev)); + header('Etag: '.$etag); header('Content-Disposition: inline; filename="styles.php"'); 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'); @@ -122,11 +127,12 @@ function css_send_ie_css($themename, $rev, $slasharguments) { * request, then optimised/minified, and finally cached for serving. * * @param string $csspath The path to the CSS file we want to serve. - * @param string $rev The revision to make sure we utilise any caches. + * @param string $etag The revision to make sure we utilise any caches. */ -function css_send_cached_css($csspath, $rev) { - $lifetime = 60*60*24*30; // 30 days +function css_send_cached_css($csspath, $etag) { + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often + header('Etag: '.$etag); header('Content-Disposition: inline; filename="styles.php"'); 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'); diff --git a/theme/styles.php b/theme/styles.php index 254e493e0fa..7eeb2114e13 100644 --- a/theme/styles.php +++ b/theme/styles.php @@ -65,10 +65,11 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { } if ($type === 'ie') { - css_send_ie_css($themename, $rev, !empty($slashargument)); + css_send_ie_css($themename, $rev, $etag, !empty($slashargument)); } $candidatesheet = "$CFG->cachedir/theme/$themename/css/$type.css"; +$etag = sha1("$themename/$rev/$type"); if (file_exists($candidatesheet)) { if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { @@ -79,9 +80,10 @@ if (file_exists($candidatesheet)) { header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: text/css; charset=utf-8'); + header('Etag: '.$etag); die; } - css_send_cached_css($candidatesheet, $rev); + css_send_cached_css($candidatesheet, $etag); } //================================================================================= @@ -95,6 +97,9 @@ require("$CFG->dirroot/lib/setup.php"); $theme = theme_config::load($themename); +$rev = theme_get_revision(); +$etag = sha1("$themename/$rev/$type"); + if ($type === 'editor') { $cssfiles = $theme->editor_css_files(); css_store_css($theme, $candidatesheet, $cssfiles); @@ -119,4 +124,4 @@ if ($type === 'editor') { $cssfile = "$CFG->cachedir/theme/$themename/css/all.css"; css_store_css($theme, $cssfile, $allfiles); } -css_send_cached_css($candidatesheet, $rev); +css_send_cached_css($candidatesheet, $etag); From dbe14f39c02984186d987ca14c11f7b68868412b Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 17:25:27 +0200 Subject: [PATCH 10/12] MDL-32683 fine tune yui resource caching --- theme/yui_combo.php | 24 +++++++++++++++++++----- theme/yui_image.php | 45 ++++++++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/theme/yui_combo.php b/theme/yui_combo.php index 63cabc2f272..77fe9011816 100644 --- a/theme/yui_combo.php +++ b/theme/yui_combo.php @@ -39,6 +39,7 @@ if (!$parts) { combo_not_found(); } +$etag = sha1($parts); $parts = trim($parts, '&'); // find out what we are serving - only one type per request @@ -55,16 +56,18 @@ if (substr($parts, -3) === '.js') { // If-Modified-Since header, we can send back a 304 Not Modified since the // content never changes (the rev number is increased any time the content changes) if (strpos($parts, '/-1/') === false and (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))) { - $lifetime = 60*60*24*30; // 30 days + $lifetime = 60*60*24*360; // 1 year, we do not change YUI versions often, there are a few custom yui modules header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: '.$mimetype); + header('Etag: '.$etag); die; } $parts = explode('&', $parts); $cache = true; +$lastmodified = 0; foreach ($parts as $part) { if (empty($part)) { @@ -110,6 +113,10 @@ foreach ($parts as $part) { continue; } $filecontent = file_get_contents($contentfile); + $fmodified = filemtime($contentfile); + if ($fmodified > $lastmodified) { + $lastmodified = $fmodified; + } $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot); $sep = ($slasharguments ? '/' : '?file='); @@ -134,8 +141,12 @@ foreach ($parts as $part) { $content .= $filecontent; } +if ($lastmodified == 0) { + $lastmodified = time(); +} + if ($cache) { - combo_send_cached($content, $mimetype); + combo_send_cached($content, $mimetype, $etag, $lastmodified); } else { combo_send_uncached($content, $mimetype); } @@ -145,17 +156,20 @@ if ($cache) { * Send the JavaScript cached * @param string $content * @param string $mimetype + * @param string $etag + * @param int $lastmodified */ -function combo_send_cached($content, $mimetype) { - $lifetime = 60*60*24*30; // 30 days +function combo_send_cached($content, $mimetype, $etag, $lastmodified) { + $lifetime = 60*60*24*360; // 1 year, we do not change YUI versions often, there are a few custom yui modules header('Content-Disposition: inline; filename="combo"'); - header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); + 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: max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); + header('Etag: '.$etag); if (!min_enable_zlib_compression()) { header('Content-Length: '.strlen($content)); } diff --git a/theme/yui_image.php b/theme/yui_image.php index 2fce5b02d6d..2b5b91f4db3 100644 --- a/theme/yui_image.php +++ b/theme/yui_image.php @@ -69,26 +69,40 @@ if (!file_exists($imagepath)) { yui_image_not_found(); } -yui_image_cached($imagepath); +$pathinfo = pathinfo($imagepath); +$imagename = $pathinfo['filename'].'.'.$pathinfo['extension']; + +switch($pathinfo['extension']) { + case 'gif' : $mimetype = 'image/gif'; break; + case 'png' : $mimetype = 'image/png'; break; + case 'jpg' : $mimetype = 'image/jpeg'; break; + case 'jpeg' : $mimetype = 'image/jpeg'; break; + case 'ico' : $mimetype = 'image/vnd.microsoft.icon'; break; + default: $mimetype = 'document/unknown'; +} + +// if they are requesting a revision that's not -1, and they have supplied an +// If-Modified-Since header, we can send back a 304 Not Modified since the +// content never changes (the rev number is increased any time the content changes) +if (strpos($parts, '/-1/') === false and (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))) { + $lifetime = 60*60*24*360; // 1 year, we do not change YUI versions often, there are a few custom yui modules + header('HTTP/1.1 304 Not Modified'); + 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('Cache-Control: public, max-age='.$lifetime); + header('Content-Type: '.$mimetype); + header('Etag: '.$etag); + die; +} + +yui_image_cached($imagepath, $imagename, $mimetype, $etag); - -function yui_image_cached($imagepath) { +function yui_image_cached($imagepath, $imagename, $mimetype, $etag) { global $CFG; require("$CFG->dirroot/lib/xsendfilelib.php"); - $lifetime = 60*60*24*300; // 300 days === forever - $pathinfo = pathinfo($imagepath); - $imagename = $pathinfo['filename'].'.'.$pathinfo['extension']; - - switch($pathinfo['extension']) { - case 'gif' : $mimetype = 'image/gif'; break; - case 'png' : $mimetype = 'image/png'; break; - case 'jpg' : $mimetype = 'image/jpeg'; break; - case 'jpeg' : $mimetype = 'image/jpeg'; break; - case 'ico' : $mimetype = 'image/vnd.microsoft.icon'; break; - default: $mimetype = 'document/unknown'; - } + $lifetime = 60*60*24*360; // 1 year, we do not change YUI versions often, there are a few custom yui modules header('Content-Disposition: inline; filename="'.$imagename.'"'); header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($imagepath)) .' GMT'); @@ -98,6 +112,7 @@ function yui_image_cached($imagepath) { header('Accept-Ranges: none'); header('Content-Type: '.$mimetype); header('Content-Length: '.filesize($imagepath)); + header('Etag: '.$etag); if (xsendfile($imagepath)) { die; From 8ab297434182069f36c4f3b8e449a88d2e2cace4 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 18:16:50 +0200 Subject: [PATCH 11/12] MDL-32683 improve and standardise JS serving --- lib/javascript.php | 89 +++++++++---------- lib/jslib.php | 156 ++++++++++++++++++++++++++++++++++ lib/outputrequirementslib.php | 10 ++- theme/javascript.php | 122 ++------------------------ 4 files changed, 210 insertions(+), 167 deletions(-) create mode 100644 lib/jslib.php diff --git a/lib/javascript.php b/lib/javascript.php index 994c4748775..2594eeaaaa1 100644 --- a/lib/javascript.php +++ b/lib/javascript.php @@ -1,5 +1,4 @@ dirroot/lib/jslib.php"); -ini_set('zlib.output_compression', 'Off'); +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 1) { + image_not_found(); + } + // image must be last because it may contain "/" + list($rev, $file) = explode('/', $slashargument, 2); + $rev = min_clean_param($rev, 'INT'); + $file = '/'.min_clean_param($file, 'SAFEPATH'); -// setup include path -set_include_path($CFG->libdir . '/minify/lib' . PATH_SEPARATOR . get_include_path()); -require_once('Minify.php'); - -$file = min_optional_param('file', '', 'RAW'); -$rev = min_optional_param('rev', 0, 'INT'); +} else { + $rev = min_optional_param('rev', 0, 'INT'); + $file = min_optional_param('file', '', 'RAW'); +} // some security first - pick only files with .js extension in dirroot $jsfiles = array(); @@ -70,50 +75,32 @@ if (!$jsfiles) { die(); } -minify($jsfiles); +$etag = sha1($rev.implode(',', $jsfiles)); +$candidate = $CFG->cachedir.'/js/'.$etag; -function minify($files) { - global $CFG; +if ($rev > -1) { + if (file_exists($candidate)) { + if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + // we do not actually need to verify the etag value because our files + // never change in cache because we increment the rev parameter + js_send_unmodified(filemtime($candidate), $etag); + } + js_send_cached($candidate, $etag); - $cachedir = $CFG->cachedir.'/js'; - // make sure the cache dir exist - if (!file_exists($cachedir)) { - @mkdir($cachedir, $CFG->directorypermissions, true); + } else { + if (!file_exists(dirname($candidate))) { + @mkdir(dirname($candidate), $CFG->directorypermissions, true); + } + $fp = fopen($candidate, 'w'); + fwrite($fp, js_minify($jsfiles)); + fclose($fp); + js_send_cached($candidate, $etag); } - if (0 === stripos(PHP_OS, 'win')) { - Minify::setDocRoot(); // IIS may need help - } - Minify::setCache($cachedir, true); - - $options = array( - // Maximum age to cache - 'maxAge' => (60*60*24*20), - // The files to minify - 'files' => $files - ); - - try { - Minify::serve('Files', $options); - die(); - } catch (Exception $e) { - $error = $e->getMessage(); - $error = str_replace("\r", ' ', $error); - $error = str_replace("\n", ' ', $error); - } - - // minification failed - try to inform the developer and include the non-minified version - $js = <<. + +/** + * This file contains various javascript related functions, + * all functions here are self contained and can be used in ABORT_AFTER_CONFIG scripts. + * + * @package core_lib + * @copyright 2012 Petr Skoda (skodak) {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +//NOTE: do not verify MOODLE_INTERNAL here, this is used from themes too + +/** + * Send javascript file content with as much caching as possible + * @param string $jspath + * @param string $etag + * @param string $filename + */ +function js_send_cached($jspath, $etag, $filename = 'javascript.php') { + require(__DIR__ . '/xsendfilelib.php'); + + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often + + header('Etag: '.$etag); + header('Content-Disposition: inline; filename="'.$filename.'"'); + 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('Accept-Ranges: none'); + header('Content-Type: application/javascript; charset=utf-8'); + + if (xsendfile($jspath)) { + die; + } + + if (!min_enable_zlib_compression()) { + header('Content-Length: '.filesize($jspath)); + } + + readfile($jspath); + die; +} + +/** + * Send javascript without any caching + * @param string $js + * @param string $filename + */ +function js_send_uncached($js, $filename = 'javascript.php') { + header('Content-Disposition: inline; filename="'.$filename.'"'); + header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); + header('Expires: '. gmdate('D, d M Y H:i:s', time() + 2) .' GMT'); + header('Pragma: '); + header('Accept-Ranges: none'); + header('Content-Type: application/javascript; charset=utf-8'); + header('Content-Length: '.strlen($js)); + + echo $js; + die; +} + +/** + * Send file not modified headers + * @param int $lastmodified + * @param string $etag + */ +function js_send_unmodified($lastmodified, $etag) { + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often + header('HTTP/1.1 304 Not Modified'); + header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); + header('Cache-Control: public, max-age='.$lifetime); + header('Content-Type: application/javascript; charset=utf-8'); + header('Etag: '.$etag); + if ($lastmodified) { + header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT'); + } + die; +} + +/** + * Minify javascript files + * @param array $files + * @return string + */ +function js_minify($files) { + // setup include path + set_include_path(__DIR__ . '/minify/lib' . PATH_SEPARATOR . get_include_path()); + require_once('Minify.php'); + + if (empty($files)) { + return ''; + } + + if (0 === stripos(PHP_OS, 'win')) { + Minify::setDocRoot(); // IIS may need help + } + // disable all caching, we do it in moodle + Minify::setCache(null, false); + + $options = array( + 'bubbleCssImports' => false, + // Don't gzip content we just want text for storage + 'encodeOutput' => false, + // Maximum age to cache, not used but required + 'maxAge' => 1800, + // The files to minify + 'files' => $files, + // Turn orr URI rewriting + 'rewriteCssUris' => false, + // This returns the CSS rather than echoing it for display + 'quiet' => true + ); + + $error = 'unknown'; + try { + $result = Minify::serve('Files', $options); + if ($result['success']) { + return $result['content']; + } + } catch (Exception $e) { + $error = $e->getMessage(); + $error = str_replace("\r", ' ', $error); + $error = str_replace("\n", ' ', $error); + } + + // minification failed - try to inform the theme developer and include the non-minified version + $js = <<cachejs) and !empty($CFG->jsrev) and strpos($url, '/lib/editor/') !== 0 and substr($url, -3) === '.js') { - return new moodle_url($CFG->httpswwwroot.'/lib/javascript.php', array('file'=>$url, 'rev'=>$CFG->jsrev)); + if (!empty($CFG->cachejs) and !empty($CFG->jsrev) and $CFG->jsrev > 0 and strpos($url, '/lib/editor/') !== 0 and substr($url, -3) === '.js') { + if (empty($CFG->slasharguments)) { + return new moodle_url($CFG->httpswwwroot.'/lib/javascript.php', array('rev'=>$CFG->jsrev, 'file'=>$url)); + } else { + $returnurl = new moodle_url($CFG->httpswwwroot.'/lib/javascript.php'); + $returnurl->set_slashargument('/'.$CFG->jsrev.$url); + return $returnurl; + } } else { return new moodle_url($CFG->httpswwwroot.$url); } diff --git a/theme/javascript.php b/theme/javascript.php index 01b14501aa2..e34e488ab16 100644 --- a/theme/javascript.php +++ b/theme/javascript.php @@ -1,5 +1,4 @@ dirroot/lib/jslib.php"); if ($slashargument = min_get_slash_argument()) { $slashargument = ltrim($slashargument, '/'); @@ -70,15 +70,9 @@ if ($rev > -1 and file_exists($candidate)) { if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // we do not actually need to verify the etag value because our files // never change in cache because we increment the rev parameter - $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often - header('HTTP/1.1 304 Not Modified'); - header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); - header('Cache-Control: public, max-age='.$lifetime); - header('Content-Type: application/javascript; charset=utf-8'); - header('Etag: '.$etag); - die; + js_send_unmodified(filemtime($candidate), $etag); } - send_cached_js($candidate, $etag); + js_send_cached($candidate, $etag); } //================================================================================= @@ -89,9 +83,6 @@ define('NO_MOODLE_COOKIES', true); // Session not used here define('NO_UPGRADE_CHECK', true); // Ignore upgrade check require("$CFG->dirroot/lib/setup.php"); -// setup include path -set_include_path($CFG->libdir . '/minify/lib' . PATH_SEPARATOR . get_include_path()); -require_once('Minify.php'); $theme = theme_config::load($themename); @@ -104,107 +95,10 @@ if ($rev > -1) { clearstatcache(); check_dir_exists(dirname($candidate)); $fp = fopen($candidate, 'w'); - fwrite($fp, minify($theme->javascript_files($type))); + fwrite($fp, js_minify($theme->javascript_files($type))); fclose($fp); - send_cached_js($candidate, $etag); + js_send_cached($candidate, $etag); + } else { - send_uncached_js($theme->javascript_content($type)); -} - -//================================================================================= -//=== utility functions == -// we are not using filelib because we need to fine tune all header -// parameters to get the best performance. - -function send_cached_js($jspath, $etag) { - global $CFG; - require("$CFG->dirroot/lib/xsendfilelib.php"); - - $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often - - header('Etag: '.$etag); - header('Content-Disposition: inline; 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('Accept-Ranges: none'); - header('Content-Type: application/javascript; charset=utf-8'); - - if (xsendfile($jspath)) { - die; - } - - if (!min_enable_zlib_compression()) { - header('Content-Length: '.filesize($jspath)); - } - - readfile($jspath); - die; -} - -function send_uncached_js($js) { - header('Content-Disposition: inline; filename="javascript.php"'); - header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); - header('Expires: '. gmdate('D, d M Y H:i:s', time() + 2) .' GMT'); - header('Pragma: '); - header('Accept-Ranges: none'); - header('Content-Type: application/javascript; charset=utf-8'); - header('Content-Length: '.strlen($js)); - - echo $js; - die; -} - -function minify($files) { - if (empty($files)) { - return ''; - } - - if (0 === stripos(PHP_OS, 'win')) { - Minify::setDocRoot(); // IIS may need help - } - // disable all caching, we do it in moodle - Minify::setCache(null, false); - - $options = array( - 'bubbleCssImports' => false, - // Don't gzip content we just want text for storage - 'encodeOutput' => false, - // Maximum age to cache, not used but required - 'maxAge' => 1800, - // The files to minify - 'files' => $files, - // Turn orr URI rewriting - 'rewriteCssUris' => false, - // This returns the CSS rather than echoing it for display - 'quiet' => true - ); - - $error = 'unknown'; - try { - $result = Minify::serve('Files', $options); - if ($result['success']) { - return $result['content']; - } - } catch (Exception $e) { - $error = $e->getMessage(); - $error = str_replace("\r", ' ', $error); - $error = str_replace("\n", ' ', $error); - } - - // minification failed - try to inform the theme developer and include the non-minified version - $js = <<javascript_content($type)); } From 869b12554d5cad85009e3d97b5d13ab21434d58b Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 18:23:51 +0200 Subject: [PATCH 12/12] MDL-32683 minor css related refactoring --- lib/csslib.php | 22 +++++++++++++++++++++- theme/styles.php | 8 +------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/csslib.php b/lib/csslib.php index c9db0931ded..5f5d88ebb32 100644 --- a/lib/csslib.php +++ b/lib/csslib.php @@ -25,6 +25,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +//NOTE: do not verify MOODLE_INTERNAL here, this is used from themes too + /** * Stores CSS in a file at the given path. * @@ -189,6 +191,24 @@ function css_send_uncached_css($css) { die; } +/** + * Send file not modified headers + * @param int $lastmodified + * @param string $etag + */ +function css_send_unmodified($lastmodified, $etag) { + $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often + header('HTTP/1.1 304 Not Modified'); + header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); + header('Cache-Control: public, max-age='.$lifetime); + header('Content-Type: text/css; charset=utf-8'); + header('Etag: '.$etag); + if ($lastmodified) { + header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT'); + } + die; +} + /** * Sends a 404 message about CSS not being found. */ @@ -3643,4 +3663,4 @@ class css_style_paddingleft extends css_style_padding { public function consolidate_to() { return 'padding'; } -} \ No newline at end of file +} diff --git a/theme/styles.php b/theme/styles.php index 7eeb2114e13..a8b1048fc73 100644 --- a/theme/styles.php +++ b/theme/styles.php @@ -75,13 +75,7 @@ if (file_exists($candidatesheet)) { if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // we do not actually need to verify the etag value because our files // never change in cache because we increment the rev parameter - $lifetime = 60*60*24*30; // 30 days - header('HTTP/1.1 304 Not Modified'); - header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); - header('Cache-Control: public, max-age='.$lifetime); - header('Content-Type: text/css; charset=utf-8'); - header('Etag: '.$etag); - die; + css_send_unmodified(filemtime($candidatesheet), $etag); } css_send_cached_css($candidatesheet, $etag); }