From 8ab297434182069f36c4f3b8e449a88d2e2cace4 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Sat, 5 May 2012 18:16:50 +0200 Subject: [PATCH] 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)); }