Merge branch 'w19_MDL-32683_m23_publiccache' of git://github.com/skodak/moodle

This commit is contained in:
Dan Poltawski
2012-05-07 17:55:31 +08:00
12 changed files with 491 additions and 258 deletions
+32 -1
View File
@@ -108,4 +108,35 @@ function min_enable_zlib_compression() {
@ini_set('zlib.output_compression', 65536);
return true;
}
}
/**
* 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;
}
+45 -12
View File
@@ -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.
*
@@ -85,21 +87,33 @@ 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) {
$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 :-( **/";
$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($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($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');
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));
@@ -115,16 +129,17 @@ function css_send_ie_css($themename, $rev) {
* 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');
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()) {
@@ -176,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.
*/
@@ -3630,4 +3663,4 @@ class css_style_paddingleft extends css_style_padding {
public function consolidate_to() {
return 'padding';
}
}
}
+6 -5
View File
@@ -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;
+38 -51
View File
@@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@@ -18,8 +17,7 @@
/**
* This file is serving optimised JS
*
* @package core
* @subpackage lib
* @package core_lib
* @copyright 2010 Petr Skoda (skodak)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -31,15 +29,22 @@ define('NO_DEBUG_DISPLAY', true);
// we need just the values from config.php and minlib.php
define('ABORT_AFTER_CONFIG', true);
require('../config.php'); // this stops immediately at the beginning of lib/setup.php
require_once("$CFG->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 = <<<EOD
try {console.log('Error: Minimisation of javascript failed!');} catch (e) {}
// Error: $error
// Problem detected during javascript minimisation, please review the following code
// =================================================================================
EOD;
echo $js;
foreach ($files as $jsfile) {
echo file_get_contents($jsfile)."\n";
} else {
$content = '';
foreach ($jsfiles as $jsfile) {
$content .= file_get_contents($jsfile)."\n";
}
js_send_uncached($candidate, $etag);
}
+156
View File
@@ -0,0 +1,156 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 = <<<EOD
try {console.log('Error: Minimisation of javascript failed!');} catch (e) {}
// Error: $error
// Problem detected during javascript minimisation, please review the following code
// =================================================================================
EOD;
foreach ($files as $jsfile) {
$js .= file_get_contents($jsfile)."\n";
}
return $js;
}
+39 -11
View File
@@ -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
@@ -763,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);
}
}
/**
@@ -872,8 +890,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();
@@ -886,7 +902,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 +926,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;
}
/**
+16 -7
View File
@@ -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,
@@ -316,6 +318,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),
);
@@ -401,8 +404,14 @@ class page_requirements_manager {
throw new coding_exception('Attempt to require a JavaScript file that does not exist.', $url);
}
}
if (!empty($CFG->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);
}
+35 -14
View File
@@ -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();
}
@@ -50,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")) {
@@ -78,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: max-age='.$lifetime);
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);
}
}
@@ -103,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) {
@@ -126,7 +147,7 @@ if ($rev > -1) {
}
copy($imagefile, $cacheimage);
}
send_cached_image($cacheimage, $rev);
send_cached_image($cacheimage, $etag);
} else {
send_uncached_image($imagefile);
@@ -138,22 +159,22 @@ 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: max-age='.$lifetime);
header('Cache-Control: public, max-age='.$lifetime);
header('Accept-Ranges: none');
header('Content-Type: '.$mimetype);
header('Content-Length: '.filesize($imagepath));
+28 -115
View File
@@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@@ -18,7 +17,7 @@
/**
* This file is responsible for serving the one huge CSS of each theme.
*
* @package moodlecore
* @package core
* @copyright 2009 Petr Skoda (skodak) {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -31,10 +30,24 @@ define('NO_DEBUG_DISPLAY', true);
// we need just the values from config.php and minlib.php
define('ABORT_AFTER_CONFIG', true);
require('../config.php'); // this stops immediately at the beginning of lib/setup.php
require_once("$CFG->dirroot/lib/jslib.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');
@@ -51,19 +64,15 @@ 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
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('Content-Type: application/javascript; charset=utf-8');
die;
js_send_unmodified(filemtime($candidate), $etag);
}
send_cached_js($candidate, $rev);
js_send_cached($candidate, $etag);
}
//=================================================================================
@@ -74,118 +83,22 @@ 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);
$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()
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);
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) {
global $CFG;
require("$CFG->dirroot/lib/xsendfilelib.php");
$lifetime = 60*60*24*30; // 30 days
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: 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 = <<<EOD
try {console.log('Error: Minimisation of theme javascript failed!');} catch (e) {}
// Error: $error
// Problem detected during javascript minimisation, please review the following code
// =================================================================================
EOD;
foreach ($files as $jsfile) {
$js .= file_get_contents($jsfile)."\n";
}
return $js;
js_send_uncached($theme->javascript_content($type));
}
+24 -12
View File
@@ -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,23 +65,19 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
}
if ($type === 'ie') {
css_send_ie_css($themename, $rev);
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'])) {
// 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: max-age='.$lifetime);
header('Content-Type: text/css; charset=utf-8');
die;
css_send_unmodified(filemtime($candidatesheet), $etag);
}
css_send_cached_css($candidatesheet, $rev);
css_send_cached_css($candidatesheet, $etag);
}
//=================================================================================
@@ -82,6 +91,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);
@@ -106,4 +118,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);
+36 -13
View File
@@ -33,10 +33,13 @@ 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();
}
$etag = sha1($parts);
$parts = trim($parts, '&');
// find out what we are serving - only one type per request
@@ -53,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: max-age='.$lifetime);
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)) {
@@ -108,13 +113,20 @@ 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=');
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'.$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)/', '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
@@ -122,15 +134,19 @@ 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'.$sep.$version.'/$1.$2', $filecontent);
}
}
$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);
}
@@ -140,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));
}
@@ -193,10 +212,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'])) {
return $_SERVER['QUERY_STRING'];
} else if (isset($_SERVER['QUERY_STRING']) and strpos($_SERVER['QUERY_STRING'], '?') !== false) {
return array($_SERVER['QUERY_STRING'], false);
} else if ($slashargument = min_get_slash_argument()) {
$slashargument = ltrim($slashargument, '/');
return array($slashargument, true);
} else {
// unsupported server, sorry!
+36 -17
View File
@@ -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);
@@ -65,35 +69,50 @@ 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');
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));
header('Etag: '.$etag);
if (xsendfile($imagepath)) {
die;