From 9ba6076cff6a3b3e629863a7ac6c32bcdcabb924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Fri, 19 Jul 2013 11:53:16 +0200 Subject: [PATCH 1/5] MDL-23493 add support for theme fonts --- lib/classes/component.php | 2 +- lib/outputlib.php | 116 +++++++++++++++++++ theme/font.php | 235 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 theme/font.php diff --git a/lib/classes/component.php b/lib/classes/component.php index 195ec411407..09a9205bf44 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); */ class core_component { /** @var array list of ignored directories - watch out for auth/db exception */ - protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true); + protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true); /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */ protected static $supportsubplugins = array('mod', 'editor', 'local'); diff --git a/lib/outputlib.php b/lib/outputlib.php index 8a0b0464cc9..bf04bd34309 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -1032,6 +1032,23 @@ class theme_config { } } + // Now resolve all font locations. + if (preg_match_all('/\[\[font:([a-z_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { + $replaced = array(); + foreach ($matches as $match) { + if (isset($replaced[$match[0]])) { + continue; + } + $replaced[$match[0]] = true; + $fontname = $match[2]; + $component = rtrim($match[1], '|'); + $fonturl = $this->font_url($fontname, $component)->out(false); + // We do not need full url because the font.php is always in the same dir. + $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl); + $css = str_replace($match[0], $fonturl, $css); + } + } + // now resolve all theme settings or do any other postprocessing $csspostprocess = $this->csspostprocess; if (function_exists($csspostprocess)) { @@ -1088,6 +1105,42 @@ class theme_config { return $url; } + /** + * Return the URL for a font + * + * @param string $font the name of the font (including extension). + * @param string $component specification of one plugin like in get_string() + * @return moodle_url + */ + public function font_url($font, $component) { + global $CFG; + + $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; + } + + $params['font'] = $font; + + $url = new moodle_url("$CFG->httpswwwroot/theme/font.php"); + if (!empty($CFG->slasharguments) and $rev > 0) { + $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font']; + $url->set_slashargument($path, 'noparam', true); + } else { + $url->params($params); + } + + return $url; + } + /** * Returns URL to the stored file via pluginfile.php. * @@ -1236,6 +1289,69 @@ class theme_config { } } + /** + * Resolves the real font location. + * + * @param string $font name of font file + * @param string $component + * @return string full file path + */ + public function resolve_font_location($font, $component) { + global $CFG; + + if ($component === 'moodle' or $component === 'core' or empty($component)) { + if (file_exists("$this->dir/fonts_core/$font")) { + return "$this->dir/fonts_core/$font"; + } + foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. + if (file_exists("$parent_config->dir/fonts_core/$font")) { + return "$parent_config->dir/fonts_core/$font"; + } + } + if (file_exists("$CFG->dataroot/fonts/$font")) { + return "$CFG->dataroot/fonts/$font"; + } + if (file_exists("$CFG->dirroot/lib/fonts/$font")) { + return "$CFG->dirroot/lib/fonts/$font"; + } + return null; + + } else if ($component === 'theme') { // Exception. + if (file_exists("$this->dir/fonts/$font")) { + return "$this->dir/fonts/$font"; + } + foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. + if (file_exists("$parent_config->dir/fonts/$font")) { + return "$parent_config->dir/fonts/$font"; + } + } + return null; + + } else { + if (strpos($component, '_') === false) { + $component = 'mod_'.$component; + } + list($type, $plugin) = explode('_', $component, 2); + + if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) { + return "$this->dir/fonts_plugins/$type/$plugin/$font"; + } + foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. + if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) { + return "$parent_config->dir/fonts_plugins/$type/$plugin/$font"; + } + } + if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) { + return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font"; + } + $dir = core_component::get_plugin_directory($type, $plugin); + if (file_exists("$dir/fonts/$font")) { + return "$dir/fonts/$font"; + } + return null; + } + } + /** * Return true if we should look for SVG images as well. * diff --git a/theme/font.php b/theme/font.php new file mode 100644 index 00000000000..8b26d256828 --- /dev/null +++ b/theme/font.php @@ -0,0 +1,235 @@ +. + +/** + * This file is responsible for serving the fonts used in CSS. + * + * Note: it is recommended to use only WOFF (Web Open Font Format) fonts. + * + * @package core + * @copyright 2013 Petr Skoda (skodak) {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Disable moodle specific debug messages and any errors in output, +// comment out when debugging or better look into error log! +define('NO_DEBUG_DISPLAY', true); + +define('ABORT_AFTER_CONFIG', true); +require('../config.php'); + +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 3) { + font_not_found(); + } + list($themename, $component, $rev, $font) = explode('/', $slashargument, 4); + $themename = min_clean_param($themename, 'SAFEDIR'); + $component = min_clean_param($component, 'SAFEDIR'); + $rev = min_clean_param($rev, 'INT'); + $font = min_clean_param($font, 'RAW'); + +} else { + $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); + $component = min_optional_param('component', 'core', 'SAFEDIR'); + $rev = min_optional_param('rev', -1, 'INT'); + $font = min_optional_param('font', '', 'RAW'); +} + +if (!$font) { + font_not_found(); +} + +if (empty($component) or $component === 'moodle' or $component === 'core') { + $component = 'core'; +} + +if (preg_match('/^[a-z0-9_-]+\.woff$/i', $font, $matches)) { + // This is the real standard! + $font = $matches[0]; + $mimetype = 'application/font-woff'; + +} else if (preg_match('^[a-z0-9_-]+/\.ttf$/i', $font, $matches)) { + $font = $matches[0]; + $mimetype = 'application/x-font-ttf'; + +} else if (preg_match('^[a-z0-9_-]+/\.otf$/i', $font, $matches)) { + $font = $matches[0]; + $mimetype = 'application/x-font-opentype'; + +} else if (preg_match('^[a-z0-9_-]+/\.oet$/i', $font, $matches)) { + // IE8 must die!!! + $font = $matches[0]; + $mimetype = 'application/vnd.ms-fontobject'; + +} else { + font_not_found(); +} + +if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { + // Normal theme exists. +} else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) { + // Theme exists in alternative location. +} else { + font_not_found(); +} + +$candidatelocation = "$CFG->cachedir/theme/$rev/$themename/fonts/$component"; +$etag = sha1("$rev/$themename/$component/$font"); + +if ($rev > 0) { + if (file_exists("$candidatelocation/$font.error")) { + font_not_found(); + } + + if (file_exists("$candidatelocation/$font")) { + 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: '.$mimetype); + header('Etag: "'.$etag.'"'); + die; + } + send_cached_font("$candidatelocation/$font", $etag, $font, $mimetype); + } +} + +// Ok, now we need to start normal moodle script, we need to load all libs and $DB. +define('ABORT_AFTER_CONFIG_CANCEL', true); + +define('NO_MOODLE_COOKIES', true); // Session not used here. +define('NO_UPGRADE_CHECK', true); // Ignore upgrade check. + +require("$CFG->dirroot/lib/setup.php"); + +$theme = theme_config::load($themename); +$themerev = theme_get_revision(); + +$fontfile = $theme->resolve_font_location($font, $component); + +if ($themerev <= 0 or $rev != $themerev) { + // Do not send caching headers if they do not request current revision, + // we do not want to pollute browser caches with outdated fonts. + if (empty($fontfile) or !is_readable($fontfile)) { + font_not_found(); + } + send_uncached_font($fontfile, $font, $mimetype); +} + +make_cache_directory('theme', false); + +if (empty($fontfile) or !is_readable($fontfile)) { + if (!file_exists($candidatelocation)) { + @mkdir($candidatelocation, $CFG->directorypermissions, true); + } + // Make note we can not find this file. + $cachefont = "$candidatelocation/$font.error"; + $fp = fopen($cachefont, 'w'); + fclose($fp); + font_not_found(); +} + +$cachefont = cache_font($font, $fontfile, $candidatelocation); +if (connection_aborted()) { + die; +} +// Make sure nothing failed. +clearstatcache(); +if (file_exists($cachefont)) { + send_cached_font($cachefont, $etag, $font, $mimetype); +} + +send_uncached_font($fontfile, $font, $mimetype); + + + +// Utility functions. + +function send_cached_font($fontpath, $etag, $font, $mimetype) { + 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="'.$font.'"'); + header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($fontpath)) .' GMT'); + header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); + header('Pragma: '); + header('Cache-Control: public, max-age='.$lifetime); + header('Accept-Ranges: none'); + header('Content-Type: '.$mimetype); + header('Content-Length: '.filesize($fontpath)); + + if (xsendfile($fontpath)) { + die; + } + + // No need to gzip already compressed fonts. + + readfile($fontpath); + die; +} + +function send_uncached_font($fontpath, $font, $mimetype) { + header('Content-Disposition: inline; filename="'.$font.'"'); + header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); + header('Expires: '. gmdate('D, d M Y H:i:s', time() + 15) .' GMT'); + header('Pragma: '); + header('Accept-Ranges: none'); + header('Content-Type: '.$mimetype); + header('Content-Length: '.filesize($fontpath)); + + readfile($fontpath); + die; +} + +function font_not_found() { + header('HTTP/1.0 404 not found'); + die('font was not found, sorry.'); +} + +/** + * Caches a given font file. + * + * @param string $font The name of the font that was requested. + * @param string $fontfile The location of the font file we want to cache. + * @param string $candidatelocation The location to cache it in. + * @return string The path to the cached font. + */ +function cache_font($font, $fontfile, $candidatelocation) { + global $CFG; + $cachefont = "$candidatelocation/$font"; + + clearstatcache(); + if (!file_exists($candidatelocation)) { + @mkdir($candidatelocation, $CFG->directorypermissions, true); + } + + // Prevent serving of incomplete file from concurrent request, + // the rename() should be more atomic than copy(). + ignore_user_abort(true); + if (@copy($fontfile, $cachefont.'.tmp')) { + rename($cachefont.'.tmp', $cachefont); + @chmod($cachefont, $CFG->filepermissions); + @unlink($cachefont.'.tmp'); // Just in case anything fails. + } + return $cachefont; +} From 37824e73f9de89519ed7f405b83c577cd6383217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 20 Jul 2013 13:08:35 +0200 Subject: [PATCH 2/5] MDL-23493 allow numbers in components used in CSS placeholders --- lib/outputlib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/outputlib.php b/lib/outputlib.php index bf04bd34309..0aed961612e 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -1016,7 +1016,7 @@ class theme_config { */ public function post_process($css) { // now resolve all image locations - if (preg_match_all('/\[\[pix:([a-z_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { + if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { $replaced = array(); foreach ($matches as $match) { if (isset($replaced[$match[0]])) { @@ -1033,7 +1033,7 @@ class theme_config { } // Now resolve all font locations. - if (preg_match_all('/\[\[font:([a-z_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { + if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { $replaced = array(); foreach ($matches as $match) { if (isset($replaced[$match[0]])) { From 85b3806114776da1c327dc392cdc9b0d3b47b993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sun, 7 Jul 2013 21:42:03 +0200 Subject: [PATCH 3/5] MDL-40545 add $CFG->localcachedir intended for local caching on cluster nodes --- admin/cli/install.php | 5 ++- admin/tool/behat/cli/util.php | 1 + config-dist.php | 7 ++- install.php | 1 + lib/moodlelib.php | 6 +++ lib/setup.php | 8 +++- lib/setuplib.php | 61 +++++++++++++++++++++++++- lib/testing/classes/util.php | 1 + lib/tests/setuplib_test.php | 80 ++++++++++++++++++++++++++++++++++- lib/upgrade.txt | 1 + lib/upgradelib.php | 11 +++-- 11 files changed, 170 insertions(+), 12 deletions(-) diff --git a/admin/cli/install.php b/admin/cli/install.php index c2d08d129df..2ab997e445d 100644 --- a/admin/cli/install.php +++ b/admin/cli/install.php @@ -394,8 +394,9 @@ if ($interactive) { cli_error(get_string('pathserrcreatedataroot', 'install', $a)); } } -$CFG->tempdir = $CFG->dataroot.'/temp'; -$CFG->cachedir = $CFG->dataroot.'/cache'; +$CFG->tempdir = $CFG->dataroot.'/temp'; +$CFG->cachedir = $CFG->dataroot.'/cache'; +$CFG->localcachedir = $CFG->dataroot.'/localcache'; // download required lang packs if ($CFG->lang !== 'en') { diff --git a/admin/tool/behat/cli/util.php b/admin/tool/behat/cli/util.php index cef4ea7e126..4ee20fe7b01 100644 --- a/admin/tool/behat/cli/util.php +++ b/admin/tool/behat/cli/util.php @@ -151,6 +151,7 @@ $CFG->jsrev = 1; // Unset cache and temp directories to reset them again with the new $CFG->dataroot. unset($CFG->cachedir); +unset($CFG->localcachedir); unset($CFG->tempdir); // Continues setup. diff --git a/config-dist.php b/config-dist.php index e22d246d37d..22829ff85ba 100644 --- a/config-dist.php +++ b/config-dist.php @@ -213,6 +213,7 @@ $CFG->admin = 'admin'; // $CFG->xsendfilealiases = array( // '/dataroot/' => $CFG->dataroot, // '/cachedir/' => '/var/www/moodle/cache', // for custom $CFG->cachedir locations +// '/localcachedir/' => '/var/local/cache', // for custom $CFG->localcachedir locations // '/tempdir/' => '/var/www/moodle/temp', // for custom $CFG->tempdir locations // '/filedir' => '/var/www/moodle/filedir', // for custom $CFG->filedir locations // ); @@ -353,10 +354,12 @@ $CFG->admin = 'admin'; // // It is possible to specify different cache and temp directories, use local fast filesystem // for normal web servers. Server clusters MUST use shared filesystem for cachedir! +// Localcachedir is intended for server clusters, it does not have to be shared by cluster nodes. // The directories must not be accessible via web. // -// $CFG->tempdir = '/var/www/moodle/temp'; -// $CFG->cachedir = '/var/www/moodle/cache'; +// $CFG->tempdir = '/var/www/moodle/temp'; // Files used during one HTTP request only. +// $CFG->cachedir = '/var/www/moodle/cache'; // Directory MUST BE SHARED by all cluster nodes, locking required. +// $CFG->localcachedir = '/var/local/cache'; // Intended for local node caching. // // Some filesystems such as NFS may not support file locking operations. // Locking resolves race conditions and is strongly recommended for production servers. diff --git a/install.php b/install.php index f591b00c523..412952bef1d 100644 --- a/install.php +++ b/install.php @@ -170,6 +170,7 @@ $CFG->httpswwwroot = $CFG->wwwroot; $CFG->dataroot = $config->dataroot; $CFG->tempdir = $CFG->dataroot.'/temp'; $CFG->cachedir = $CFG->dataroot.'/cache'; +$CFG->localcachedir = $CFG->dataroot.'/localcache'; $CFG->admin = $config->admin; $CFG->docroot = 'http://docs.moodle.org'; $CFG->langotherroot = $CFG->dataroot.'/lang'; diff --git a/lib/moodlelib.php b/lib/moodlelib.php index f79a2dbe7ca..997048e6485 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -1560,6 +1560,12 @@ function purge_all_caches() { // hack: this script may get called after the purifier was initialised, // but we do not want to verify repeatedly this exists in each call make_cache_directory('htmlpurifier'); + + // This is the only place where we purge local caches, we are only adding files there. + // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes. + remove_dir($CFG->localcachedir, true); + set_config('localcachedirpurged', time()); + make_localcache_directory('', true); } /** diff --git a/lib/setup.php b/lib/setup.php index d0c2ab4abf0..56c22c54321 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -37,7 +37,8 @@ * - $CFG->dirroot - Path to moodle's library folder on server's filesystem. * - $CFG->libdir - Path to moodle's library folder on server's filesystem. * - $CFG->tempdir - Path to moodle's temp file directory on server's filesystem. - * - $CFG->cachedir - Path to moodle's cache directory on server's filesystem. + * - $CFG->cachedir - Path to moodle's cache directory on server's filesystem (shared by cluster nodes). + * - $CFG->localcachedir - Path to moodle's local cache directory (not shared by cluster nodes). * * @global object $CFG * @name $CFG @@ -156,6 +157,11 @@ if (!isset($CFG->cachedir)) { $CFG->cachedir = "$CFG->dataroot/cache"; } +// Allow overriding of localcachedir. +if (!isset($CFG->localcachedir)) { + $CFG->localcachedir = "$CFG->dataroot/localcache"; +} + // The current directory in PHP version 4.3.0 and above isn't necessarily the // directory of the script when run from the command line. The require_once() // would fail, so we'll have to chdir() diff --git a/lib/setuplib.php b/lib/setuplib.php index 4c9ba973708..81ebd50e205 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -529,7 +529,7 @@ function get_exception_info($ex) { // Remove some absolute paths from message and debugging info. $searches = array(); $replaces = array(); - $cfgnames = array('tempdir', 'cachedir', 'themedir', + $cfgnames = array('tempdir', 'cachedir', 'localcachedir', 'themedir', 'langmenucachefile', 'langcacheroot', 'dataroot', 'dirroot'); foreach ($cfgnames as $cfgname) { if (property_exists($CFG, $cfgname)) { @@ -1311,7 +1311,10 @@ function make_upload_directory($directory, $exceptiononerror = true) { debugging('Use make_temp_directory() for creation of temporary directory and $CFG->tempdir to get the location.'); } else if (strpos($directory, 'cache/') === 0 or $directory === 'cache') { - debugging('Use make_cache_directory() for creation of chache directory and $CFG->cachedir to get the location.'); + debugging('Use make_cache_directory() for creation of cache directory and $CFG->cachedir to get the location.'); + + } else if (strpos($directory, 'localcache/') === 0 or $directory === 'localcache') { + debugging('Use make_localcache_directory() for creation of local cache directory and $CFG->localcachedir to get the location.'); } protect_directory($CFG->dataroot); @@ -1340,6 +1343,8 @@ function make_temp_directory($directory, $exceptiononerror = true) { /** * Create a directory under cachedir and make sure it is writable. * + * Note: this cache directory is shared by all cluster nodes. + * * @param string $directory the full path of the directory to be created under $CFG->cachedir * @param bool $exceptiononerror throw exception if error encountered * @return string|false Returns full path to directory if successful, false if not; may throw exception @@ -1355,6 +1360,58 @@ function make_cache_directory($directory, $exceptiononerror = true) { return make_writable_directory("$CFG->cachedir/$directory", $exceptiononerror); } +/** + * Create a directory under localcachedir and make sure it is writable. + * The files in this directory MUST NOT change, use revisions or content hashes to + * work around this limitation - this means you can only add new files here. + * + * The content of this directory gets purged automatically on all cluster nodes + * after calling purge_all_caches() before new data is written to this directory. + * + * Note: this local cache directory does not need to be shared by cluster nodes. + * + * @param string $directory the relative path of the directory to be created under $CFG->localcachedir + * @param bool $exceptiononerror throw exception if error encountered + * @return string|false Returns full path to directory if successful, false if not; may throw exception + */ +function make_localcache_directory($directory, $exceptiononerror = true) { + global $CFG; + + make_writable_directory($CFG->localcachedir, $exceptiononerror); + + if ($CFG->localcachedir !== "$CFG->dataroot/localcache") { + protect_directory($CFG->localcachedir); + } else { + protect_directory($CFG->dataroot); + } + + if (!isset($CFG->localcachedirpurged)) { + $CFG->localcachedirpurged = 0; + } + $timestampfile = "$CFG->localcachedir/.lastpurged"; + + if (!file_exists($timestampfile)) { + touch($timestampfile); + @chmod($timestampfile, $CFG->filepermissions); + + } else if (filemtime($timestampfile) < $CFG->localcachedirpurged) { + // This means our local cached dir was not purged yet. + remove_dir($CFG->localcachedir, true); + if ($CFG->localcachedir !== "$CFG->dataroot/localcache") { + protect_directory($CFG->localcachedir); + } + touch($timestampfile); + @chmod($timestampfile, $CFG->filepermissions); + clearstatcache(); + } + + if ($directory === '') { + return $CFG->localcachedir; + } + + return make_writable_directory("$CFG->localcachedir/$directory", $exceptiononerror); +} + /** * Checks if current user is a web crawler. * diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php index 71ebacc7de7..4dfa12c4cb6 100644 --- a/lib/testing/classes/util.php +++ b/lib/testing/classes/util.php @@ -599,6 +599,7 @@ abstract class testing_util { make_temp_directory(''); make_cache_directory(''); make_cache_directory('htmlpurifier'); + make_localcache_directory(''); // Reset the cache API so that it recreates it's required directories as well. cache_factory::reset(); // Purge all data from the caches. This is required for consistency. diff --git a/lib/tests/setuplib_test.php b/lib/tests/setuplib_test.php index 63431ee0efc..12dcb0e6784 100644 --- a/lib/tests/setuplib_test.php +++ b/lib/tests/setuplib_test.php @@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2012 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class core_setuplib_testcase extends basic_testcase { +class core_setuplib_testcase extends advanced_testcase { /** * Test get_docs_url_standard in the normal case when we should link to Moodle docs. @@ -126,7 +126,7 @@ class core_setuplib_testcase extends basic_testcase { global $CFG; // This doesn't test them all possible ones, but these are set for unit tests. - $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'cachedir'); + $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'cachedir', 'localcachedir'); $fixture = ''; $expected = ''; @@ -142,4 +142,80 @@ class core_setuplib_testcase extends basic_testcase { $this->assertContains($expected, $exceptioninfo->message, 'Exception message does not contain system paths'); $this->assertContains($expected, $exceptioninfo->debuginfo, 'Exception debug info does not contain system paths'); } + + public function test_localcachedir() { + global $CFG; + + $this->resetAfterTest(true); + + // Test default location - can not be modified in phpunit tests because we override everything in config.php. + $this->assertSame("$CFG->dataroot/localcache", $CFG->localcachedir); + + $now = time(); + $timestampfile = "$CFG->localcachedir/.lastpurged"; + + $dir = make_localcache_directory('', false); + $this->assertSame($CFG->localcachedir, $dir); + $this->assertFileNotExists("$CFG->localcachedir/.htaccess"); + $this->assertFileExists($timestampfile); + $this->assertGreaterThanOrEqual($now, filemtime($timestampfile)); + $this->assertLessThanOrEqual(time(), filemtime($timestampfile)); + + $dir = make_localcache_directory('test/test', false); + $this->assertSame("$CFG->localcachedir/test/test", $dir); + + // Test custom location. + $CFG->localcachedir = "$CFG->dataroot/testlocalcache"; + $now = time(); + $timestampfile = "$CFG->localcachedir/.lastpurged"; + $this->assertFileNotExists($timestampfile); + + $dir = make_localcache_directory('', false); + $this->assertSame($CFG->localcachedir, $dir); + $this->assertFileExists("$CFG->localcachedir/.htaccess"); + $this->assertFileExists($timestampfile); + $this->assertGreaterThanOrEqual($now, filemtime($timestampfile)); + $this->assertLessThanOrEqual(time(), filemtime($timestampfile)); + + $dir = make_localcache_directory('test', false); + $this->assertSame("$CFG->localcachedir/test", $dir); + + $prevtime = filemtime($timestampfile); + $dir = make_localcache_directory('pokus', false); + $this->assertSame("$CFG->localcachedir/pokus", $dir); + $this->assertSame($prevtime, filemtime($timestampfile)); + + + // Test purging. + $testfile = "$CFG->localcachedir/test/test.txt"; + $this->assertTrue(touch($testfile)); + + $now = time(); + set_config('localcachedirpurged', $now - 2); + purge_all_caches(); + $this->assertFileNotExists($testfile); + $this->assertFileNotExists(dirname($testfile)); + $this->assertFileExists($timestampfile); + $this->assertGreaterThanOrEqual($now, filemtime($timestampfile)); + $this->assertLessThanOrEqual(time(), filemtime($timestampfile)); + $this->assertGreaterThanOrEqual($now, $CFG->localcachedirpurged); + $this->assertLessThanOrEqual(time(), $CFG->localcachedirpurged); + + // Simulates purge_all_caches() on another server node. + make_localcache_directory('test', false); + $this->assertTrue(touch($testfile)); + set_config('localcachedirpurged', $now - 1); + $this->assertTrue(touch($timestampfile, $now - 2)); + clearstatcache(); + $this->assertSame($now - 2, filemtime($timestampfile)); + + $now = time(); + $dir = make_localcache_directory('', false); + $this->assertSame("$CFG->localcachedir", $dir); + $this->assertFileNotExists($testfile); + $this->assertFileNotExists(dirname($testfile)); + $this->assertFileExists($timestampfile); + $this->assertGreaterThanOrEqual($now, filemtime($timestampfile)); + $this->assertLessThanOrEqual(time(), filemtime($timestampfile)); + } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 4c865c0c853..65fa261ee16 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -10,6 +10,7 @@ information provided here is intended especially for developers. core_component::get_plugin_list() does not accept empty parameter any more. * Use core_text::* instead of textlib:: and also core_collator::* instead of collatorlib::*. * Use new function moodleform::mock_submit() to simulate form submission in unit tests (backported). +* New $CFG->localcachedir setting useful for cluster nodes. Admins have to update X-Sendfile aliases if used. DEPRECATIONS: Various previously deprecated functions have now been altered to throw DEBUG_DEVELOPER debugging notices diff --git a/lib/upgradelib.php b/lib/upgradelib.php index 32e4637f6ae..1f552d5a57d 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -1467,12 +1467,17 @@ function install_core($version, $verbose) { global $CFG, $DB; // We can not call purge_all_caches() yet, make sure the temp and cache dirs exist and are empty. - make_cache_directory('', true); remove_dir($CFG->cachedir.'', true); - make_temp_directory('', true); + make_cache_directory('', true); + + remove_dir($CFG->localcachedir.'', true); + make_localcache_directory('', true); + remove_dir($CFG->tempdir.'', true); - make_writable_directory($CFG->dataroot.'/muc', true); + make_temp_directory('', true); + remove_dir($CFG->dataroot.'/muc', true); + make_writable_directory($CFG->dataroot.'/muc', true); try { set_time_limit(600); From cb8108dd22174f24f335c4479107695c10be1f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 13 Jul 2013 00:05:58 +0200 Subject: [PATCH 4/5] MDL-40545 convert js and theme resources to localcachedir --- lib/javascript.php | 2 +- lib/outputlib.php | 2 -- lib/outputrequirementslib.php | 2 -- theme/image.php | 4 ++-- theme/javascript.php | 4 ++-- theme/styles.php | 4 ++-- 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/javascript.php b/lib/javascript.php index 7a97dfc7ea8..998b8960544 100644 --- a/lib/javascript.php +++ b/lib/javascript.php @@ -79,7 +79,7 @@ $etag = sha1($rev.implode(',', $jsfiles)); // Use the caching only for meaningful revision numbers which prevents future cache poisoning. if ($rev > 0 and $rev < (time() + 60*60)) { - $candidate = $CFG->cachedir.'/js/'.$etag; + $candidate = $CFG->localcachedir.'/js/'.$etag; if (file_exists($candidate)) { if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { diff --git a/lib/outputlib.php b/lib/outputlib.php index 0aed961612e..08da9d89ea3 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -44,7 +44,6 @@ require_once($CFG->libdir.'/outputrequirementslib.php'); */ function theme_reset_all_caches() { global $CFG, $PAGE; - require_once("$CFG->libdir/filelib.php"); $next = time(); if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) { @@ -55,7 +54,6 @@ function theme_reset_all_caches() { } set_config('themerev', $next); // time is unique even when you reset/switch database - fulldelete("$CFG->cachedir/theme"); if ($PAGE) { $PAGE->reload_theme(); diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index bce03a5001b..49551c09d45 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -1744,7 +1744,6 @@ class YUI_config { */ function js_reset_all_caches() { global $CFG; - require_once("$CFG->libdir/filelib.php"); $next = time(); if (isset($CFG->jsrev) and $next <= $CFG->jsrev and $CFG->jsrev - $next < 60*60) { @@ -1755,5 +1754,4 @@ function js_reset_all_caches() { } set_config('jsrev', $next); - fulldelete("$CFG->cachedir/js"); } diff --git a/theme/image.php b/theme/image.php index 9e4ba1bffa8..e3839b28b21 100644 --- a/theme/image.php +++ b/theme/image.php @@ -74,7 +74,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { image_not_found(); } -$candidatelocation = "$CFG->cachedir/theme/$rev/$themename/pix/$component"; +$candidatelocation = "$CFG->localcachedir/theme/$rev/$themename/pix/$component"; $etag = sha1("$rev/$themename/$component/$image"); if ($rev > 0) { @@ -142,7 +142,7 @@ if ($themerev <= 0 or $rev != $themerev) { send_uncached_image($imagefile); } -make_cache_directory('theme', false); +make_localcache_directory('theme', false); // We're not using SVG and there is no cached version of this file (in any format). // As we're going to be caching a format other than svg, and because svg use is conditional we need to ensure that at the same diff --git a/theme/javascript.php b/theme/javascript.php index a5142374cfd..8c3457308a7 100644 --- a/theme/javascript.php +++ b/theme/javascript.php @@ -63,7 +63,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { die('Theme was not found, sorry.'); } -$candidate = "$CFG->cachedir/theme/$rev/$themename/javascript_$type.js"; +$candidate = "$CFG->localcachedir/theme/$rev/$themename/javascript_$type.js"; $etag = sha1("$rev/$themename/$type"); if ($rev > 0 and file_exists($candidate)) { @@ -93,7 +93,7 @@ if ($themerev <= 0 or $rev != $themerev) { js_send_uncached($theme->javascript_content($type)); } -make_cache_directory('theme', false); +make_localcache_directory('theme', false); js_write_cache_file_content($candidate, js_minify($theme->javascript_files($type))); // Verify nothing failed in cache file creation. diff --git a/theme/styles.php b/theme/styles.php index f0129c3895b..0279b5177ea 100644 --- a/theme/styles.php +++ b/theme/styles.php @@ -87,7 +87,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { die('Theme was not found, sorry.'); } -$candidatedir = "$CFG->cachedir/theme/$rev/$themename/css"; +$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; $etag = "$rev/$themename/$type"; $candidatename = $type; if (!$usesvg) { @@ -148,7 +148,7 @@ if ($themerev <= 0 or $themerev != $rev) { $etag = sha1($etag); } -make_cache_directory('theme', false); +make_localcache_directory('theme', false); if ($type === 'editor') { $cssfiles = $theme->editor_css_files(); From 64a8058e18cf4c26b6119f9a75dec5ced3e98ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 20 Jul 2013 14:11:40 +0200 Subject: [PATCH 5/5] MDL-40545 convert font cache to localcachedir --- theme/font.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/theme/font.php b/theme/font.php index 8b26d256828..f093421e993 100644 --- a/theme/font.php +++ b/theme/font.php @@ -87,7 +87,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { font_not_found(); } -$candidatelocation = "$CFG->cachedir/theme/$rev/$themename/fonts/$component"; +$candidatelocation = "$CFG->localcachedir/theme/$rev/$themename/fonts/$component"; $etag = sha1("$rev/$themename/$component/$font"); if ($rev > 0) { @@ -133,7 +133,7 @@ if ($themerev <= 0 or $rev != $themerev) { send_uncached_font($fontfile, $font, $mimetype); } -make_cache_directory('theme', false); +make_localcache_directory('theme', false); if (empty($fontfile) or !is_readable($fontfile)) { if (!file_exists($candidatelocation)) {