From 436dbeecdaf3f345a01a6b454f89d5ccf4dce06d Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Thu, 27 Sep 2012 10:58:42 +1200 Subject: [PATCH 1/3] MDL-22955 theme: Added ability to use SVG format for icons --- admin/renderer.php | 2 +- config-dist.php | 12 +++ lib/editor/tinymce/adminlib.php | 2 +- lib/filelib.php | 2 +- lib/javascript-static.js | 11 ++- lib/outputlib.php | 112 +++++++++++++++++++++++---- lib/outputrequirementslib.php | 1 + lib/tests/outputlib_test.php | 97 +++++++++++++++++++++++ pix/i/test.png | Bin 0 -> 10871 bytes pix/i/test.svg | 133 ++++++++++++++++++++++++++++++++ theme/image.php | 27 +++++-- 11 files changed, 370 insertions(+), 29 deletions(-) create mode 100644 pix/i/test.png create mode 100644 pix/i/test.svg diff --git a/admin/renderer.php b/admin/renderer.php index 3a3c2bd2ffd..65eadc18b00 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -683,7 +683,7 @@ class core_admin_renderer extends plugin_renderer_base { $row = new html_table_row(); $row->attributes['class'] = 'type-' . $plugin->type . ' name-' . $plugin->type . '_' . $plugin->name; - if ($this->page->theme->resolve_image_location('icon', $plugin->type . '_' . $plugin->name)) { + if ($this->page->theme->resolve_image_location('icon', $plugin->type . '_' . $plugin->name, null)) { $icon = $this->output->pix_icon('icon', '', $plugin->type . '_' . $plugin->name, array('class' => 'smallicon pluginicon')); } else { $icon = $this->output->pix_icon('spacer', '', 'moodle', array('class' => 'smallicon pluginicon noicon')); diff --git a/config-dist.php b/config-dist.php index feb2902b674..f047a152f6b 100644 --- a/config-dist.php +++ b/config-dist.php @@ -442,6 +442,18 @@ $CFG->admin = 'admin'; // // $CFG->disableupdatenotifications = true; // +// As of version 2.4 Moodle serves icons as SVG images if the users browser appears +// to support SVG. +// For those wanting to control the serving of SVG images the following setting can +// be defined in your config.php. +// If it is not defined then the default (browser detection) will occur. +// +// To ensure they are always used when available: +// $CFG->svgicons = true; +// +// To ensure they are never used even when available: +// $CFG->svgicons = false; +// //========================================================================= // 8. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!! //========================================================================= diff --git a/lib/editor/tinymce/adminlib.php b/lib/editor/tinymce/adminlib.php index 2aa9caa49df..5c15e1b0475 100644 --- a/lib/editor/tinymce/adminlib.php +++ b/lib/editor/tinymce/adminlib.php @@ -198,7 +198,7 @@ class tiynce_subplugins_settings extends admin_setting { $displayname = html_writer::tag('span', $namestr, array('class'=>'dimmed_text')); } - if ($PAGE->theme->resolve_image_location('icon', 'tinymce_' . $name)) { + if ($PAGE->theme->resolve_image_location('icon', 'tinymce_' . $name, false)) { $icon = $OUTPUT->pix_icon('icon', '', 'tinymce_' . $name, array('class' => 'smallicon pluginicon')); } else { $icon = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'smallicon pluginicon noicon')); diff --git a/lib/filelib.php b/lib/filelib.php index 401d781aa3c..e0e8fdeba85 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -3779,7 +3779,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } // no redirect here because it is not cached $theme = theme_config::load($themename); - $imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle'); + $imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle', null); send_file($imagefile, basename($imagefile), 60*60*24*14); } diff --git a/lib/javascript-static.js b/lib/javascript-static.js index 84bf7bbceb5..b9ed5278fae 100644 --- a/lib/javascript-static.js +++ b/lib/javascript-static.js @@ -38,10 +38,17 @@ M.util.image_url = function(imagename, component) { component = 'core'; } + var url = M.cfg.wwwroot + '/theme/image.php'; 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; + if (!M.cfg.svgicons) { + url += '/_s'; + } + url += '/' + 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; + url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename; + if (!M.cfg.svgicons) { + url += '&svg=0'; + } } return url; diff --git a/lib/outputlib.php b/lib/outputlib.php index 69c13934e52..f6f28cd79e8 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -334,6 +334,12 @@ class theme_config { */ public $supportscssoptimisation = true; + /** + * Used to determine whether we can serve SVG images or not. + * @var bool + */ + private $usesvg = null; + /** * Load the config.php file for a particular theme, and return an instance * of this class. (That is, this is a factory method.) @@ -977,6 +983,7 @@ class theme_config { global $CFG; $params = array('theme'=>$this->name); + $svg = $this->use_svg_icons(); if (empty($component) or $component === 'moodle' or $component === 'core') { $params['component'] = 'core'; @@ -991,11 +998,22 @@ class theme_config { $params['image'] = $imagename; + $url = new moodle_url("$CFG->httpswwwroot/theme/image.php"); 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); + $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image']; + if (!$svg) { + // We add a simple /_s to the start of the path. + // The underscore is used to ensure that it isn't a valid theme name. + $path = '/_s'.$path; + } + $url->set_slashargument($path, 'noparam', true); } else { - $url = new moodle_url("$CFG->httpswwwroot/theme/image.php", $params); + if (!$svg) { + // We add an SVG param so that we know not to serve SVG images. + // We do this because all modern browsers support SVG and this param will one day be removed. + $params['svg'] = '0'; + } + $url->params($params); } return $url; @@ -1003,26 +1021,41 @@ class theme_config { /** * Resolves the real image location. + * + * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG + * and we need a way in which to turn it off. + * By default SVG won't be used unless asked for. This is done for two reasons: + * 1. It ensures that we don't serve svg images unless we really want to. The admin has selected to force them, of the users + * browser supports SVG. + * 2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded + * by the user due to security concerns. + * * @param string $image name of image, may contain relative path * @param string $component + * @param bool $svg If set to true SVG images will also be looked for. * @return string full file path */ - public function resolve_image_location($image, $component) { + public function resolve_image_location($image, $component, $svg = false) { global $CFG; + if (!is_bool($svg)) { + // If $svg isn't a bool then we need to decide for ourselves. + $svg = $this->use_svg_icons(); + } + if ($component === 'moodle' or $component === 'core' or empty($component)) { - if ($imagefile = $this->image_exists("$this->dir/pix_core/$image")) { + if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) { return $imagefile; } foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last - if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image")) { + if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) { return $imagefile; } } - if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image")) { + if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) { return $imagefile; } - if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image")) { + if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) { return $imagefile; } return null; @@ -1031,11 +1064,11 @@ class theme_config { if ($image === 'favicon') { return "$this->dir/pix/favicon.ico"; } - if ($imagefile = $this->image_exists("$this->dir/pix/$image")) { + if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) { return $imagefile; } foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last - if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image")) { + if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) { return $imagefile; } } @@ -1047,36 +1080,81 @@ class theme_config { } list($type, $plugin) = explode('_', $component, 2); - if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image")) { + if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) { return $imagefile; } foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last - if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image")) { + if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image", $svg)) { return $imagefile; } } - if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image")) { + if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) { return $imagefile; } $dir = get_plugin_directory($type, $plugin); - if ($imagefile = $this->image_exists("$dir/pix/$image")) { + if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) { return $imagefile; } return null; } } + /** + * Return true if we should look for SVG images as well. + * + * @staticvar bool $svg + * @return bool + */ + public function use_svg_icons() { + global $CFG; + if ($this->usesvg === null) { + if (!isset($CFG->svgicons) || !is_bool($CFG->svgicons)) { + // IE 5 - 8 don't support SVG at all. + if (empty($_SERVER['HTTP_USER_AGENT'])) { + // Can't be sure, just say no. + $this->usesvg = false; + } else if (preg_match('#MSIE +[5-8]\.#', $_SERVER['HTTP_USER_AGENT'])) { + // IE < 9 doesn't support SVG. Say no. + $this->usesvg = false; + } else if (preg_match('#Android +[0-2]\.#', $_SERVER['HTTP_USER_AGENT'])) { + // Android < 3 doesn't support SVG. Say no. + $this->usesvg = false; + } else { + // Presumed fine. + $this->usesvg = true; + } + } else { + // Force them on/off depending upon the setting. + $this->usesvg = $CFG->svgicons; + } + } + return $this->usesvg; + } + /** * Checks if file with any image extension exists. * + * The order to these images was adjusted prior to the release of 2.4 + * At that point the were the following image counts in Moodle core: + * + * - png = 667 in pix dirs (1499 total) + * - gif = 385 in pix dirs (606 total) + * - jpg = 62 in pix dirs (74 total) + * - jpeg = 0 in pix dirs (1 total) + * + * There is work in progress to move towards SVG presently hence that has been prioritiesed. + * * @param string $filepath + * @param bool $svg If set to true SVG images will also be looked for. * @return string image name with extension */ - private static function image_exists($filepath) { - if (file_exists("$filepath.gif")) { - return "$filepath.gif"; + private static function image_exists($filepath, $svg = false) { + if ($svg && file_exists("$filepath.svg")) { + return "$filepath.svg"; } else if (file_exists("$filepath.png")) { return "$filepath.png"; + } else if (file_exists("$filepath.gif")) { + return "$filepath.gif"; } else if (file_exists("$filepath.jpg")) { return "$filepath.jpg"; } else if (file_exists("$filepath.jpeg")) { diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index 98b52096942..1dc66c7a5e3 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -264,6 +264,7 @@ class page_requirements_manager { 'slasharguments' => (int)(!empty($CFG->slasharguments)), 'theme' => $page->theme->name, 'jsrev' => ((empty($CFG->cachejs) or empty($CFG->jsrev)) ? -1 : $CFG->jsrev), + 'svgicons' => $page->theme->use_svg_icons() ); if (debugging('', DEBUG_DEVELOPER)) { $this->M_cfg['developerdebug'] = true; diff --git a/lib/tests/outputlib_test.php b/lib/tests/outputlib_test.php index 5fe9ea5c304..52771c9467e 100644 --- a/lib/tests/outputlib_test.php +++ b/lib/tests/outputlib_test.php @@ -133,3 +133,100 @@ class xhtml_container_stack_testcase extends advanced_testcase { $this->assertDebuggingNotCalled(); } } + +/** + * Tests the theme config class. + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_config_testcase extends advanced_testcase { + /** + * This function will test directives used to serve SVG images to make sure + * this are making the right decisions. + */ + public function test_svg_image_use() { + global $CFG; + + $this->resetAfterTest(); + + if (isset($_SERVER['HTTP_USER_AGENT'])) { + $ua = $_SERVER['HTTP_USER_AGENT']; + } else { + $ua = null; + } + + // The two required tests. + $this->assertTrue(file_exists($CFG->dirroot.'/pix/i/test.svg')); + $this->assertTrue(file_exists($CFG->dirroot.'/pix/i/test.png')); + + $theme = theme_config::load(theme_config::DEFAULT_THEME); + + // First up test the forced setting. + $imagefile = $theme->resolve_image_location('i/test', 'moodle', true); + $this->assertEquals('test.svg', basename($imagefile)); + $imagefile = $theme->resolve_image_location('i/test', 'moodle', false); + $this->assertEquals('test.png', basename($imagefile)); + + // Now test the use of the svgicons config setting. + // We need to clone the theme as usesvg property is calculated only once. + $testtheme = clone $theme; + $CFG->svgicons = true; + $imagefile = $testtheme->resolve_image_location('i/test', 'moodle', null); + $this->assertEquals('test.svg', basename($imagefile)); + $CFG->svgicons = false; + // We need to clone the theme as usesvg property is calculated only once. + $testtheme = clone $theme; + $imagefile = $testtheme->resolve_image_location('i/test', 'moodle', null); + $this->assertEquals('test.png', basename($imagefile)); + unset($CFG->svgicons); + + // Finally test a few user agents. + $useragents = array( + // IE7 on XP. + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)' => false, + // IE8 on Vista. + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)' => false, + // IE8 on Vista in compatability mode. + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0)' => false, + // IE8 on Windows 7. + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)' => false, + // IE9 on Windows 7. + 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)' => true, + // IE9 on Windows 7 in compatability mode. + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/5.0)' => false, + // Chrome 11 on Windows. + 'Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.652.0 Safari/534.17' => true, + // Chrome 22 on Windows. + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1' => true, + // Chrome 21 on Ubuntu 12.04. + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1' => true, + // Firefox 4 on Windows. + 'Mozilla/5.0 (Windows NT 6.1; rv:1.9) Gecko/20100101 Firefox/4.0' => true, + // Firefox 15 on Windows. + 'Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0.1' => true, + // Firefox 15 on Ubuntu. + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1' => true, + // Opera 12.02 on Ubuntu. + 'Opera/9.80 (X11; Linux x86_64; U; en) Presto/2.10.289 Version/12.02' => true, + // Android browser pre 1.0 + 'Mozilla/5.0 (Linux; U; Android 0.5; en-us) AppleWebKit/522+ (KHTML, like Gecko) Safari/419.3' => false, + // Android browser 2.3 (HTC) + 'Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; HTC Vision Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' => false, + // Android browser 3.0 (Motorola) + 'Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13' => true + ); + foreach ($useragents as $agent => $expected) { + $_SERVER['HTTP_USER_AGENT'] = $agent; + // We need to clone the theme as usesvg property is calculated only once. + $testtheme = clone $theme; + $imagefile = $testtheme->resolve_image_location('i/test', 'moodle', null); + $this->assertEquals($expected ? 'test.svg' : 'test.png', basename($imagefile), + 'Incorrect image returned for user agent `'.$agent.'`'); + } + + if ($ua !== null) { + $_SERVER['HTTP_USER_AGENT'] = $ua; + } + } +} \ No newline at end of file diff --git a/pix/i/test.png b/pix/i/test.png new file mode 100644 index 0000000000000000000000000000000000000000..5860abeff9567f679b2f1ad92f2b5e1db1ab7ce0 GIT binary patch literal 10871 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 N002ovPDHLkV1jSrvatXF literal 0 HcmV?d00001 diff --git a/pix/i/test.svg b/pix/i/test.svg new file mode 100644 index 00000000000..522af087fbb --- /dev/null +++ b/pix/i/test.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/theme/image.php b/theme/image.php index 1ddeb266382..b488fa85ace 100644 --- a/theme/image.php +++ b/theme/image.php @@ -37,6 +37,13 @@ if ($slashargument = min_get_slash_argument()) { if (substr_count($slashargument, '/') < 3) { image_not_found(); } + if (strpos($slashargument, '_s/') === 0) { + // Can't use SVG + $slashargument = substr($slashargument, 3); + $usesvg = false; + } else { + $usesvg = true; + } // image must be last because it may contain "/" list($themename, $component, $rev, $image) = explode('/', $slashargument, 4); $themename = min_clean_param($themename, 'SAFEDIR'); @@ -49,6 +56,7 @@ if ($slashargument = min_get_slash_argument()) { $component = min_optional_param('component', 'core', 'SAFEDIR'); $rev = min_optional_param('rev', -1, 'INT'); $image = min_optional_param('image', '', 'SAFEPATH'); + $usesvg = (bool)min_optional_param('svg', '1', 'INT'); } if (empty($component) or $component === 'moodle' or $component === 'core') { @@ -77,12 +85,15 @@ if ($rev > -1) { image_not_found(); } $cacheimage = false; - if (file_exists("$candidatelocation/$image.gif")) { - $cacheimage = "$candidatelocation/$image.gif"; - $ext = 'gif'; + if ($usesvg && file_exists("$candidatelocation/$image.svg")) { + $cacheimage = "$candidatelocation/$image.svg"; + $ext = 'svg'; } else if (file_exists("$candidatelocation/$image.png")) { $cacheimage = "$candidatelocation/$image.png"; $ext = 'png'; + } else if (file_exists("$candidatelocation/$image.gif")) { + $cacheimage = "$candidatelocation/$image.gif"; + $ext = 'gif'; } else if (file_exists("$candidatelocation/$image.jpg")) { $cacheimage = "$candidatelocation/$image.jpg"; $ext = 'jpg'; @@ -120,7 +131,7 @@ define('NO_UPGRADE_CHECK', true); // Ignore upgrade check require("$CFG->dirroot/lib/setup.php"); $theme = theme_config::load($themename); -$imagefile = $theme->resolve_image_location($image, $component); +$imagefile = $theme->resolve_image_location($image, $component, $usesvg); $rev = theme_get_revision(); $etag = sha1("$themename/$component/$rev/$image"); @@ -229,10 +240,12 @@ function image_not_found() { function get_contenttype_from_ext($ext) { switch ($ext) { - case 'gif': - return 'image/gif'; + case 'svg': + return 'image/svg+xml'; case 'png': return 'image/png'; + case 'gif': + return 'image/gif'; case 'jpg': case 'jpeg': return 'image/jpeg'; @@ -240,4 +253,4 @@ function get_contenttype_from_ext($ext) { return 'image/vnd.microsoft.icon'; } return 'document/unknown'; -} +} \ No newline at end of file From f7d6a556cf105f1c60a5d11356171afec2a7b3a1 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Wed, 3 Oct 2012 08:32:40 +1300 Subject: [PATCH 2/3] MDL-22955 output: Added block to ensure that SVG is not used within CSS --- lib/outputlib.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/outputlib.php b/lib/outputlib.php index f6f28cd79e8..3223ee48f72 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -948,6 +948,11 @@ 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)) { + // We are going to disable the use of SVG images when available in CSS background-image properties + // as support for it in browsers is at best quirky. + // When we choose to support SVG in background css we will need to remove this code and implement a solution that is + // either consistent or varies the URL for serving CSS depending upon SVG being used if available, or not. + $this->force_svg_use(false); $replaced = array(); foreach ($matches as $match) { if (isset($replaced[$match[0]])) { @@ -1131,6 +1136,17 @@ class theme_config { return $this->usesvg; } + /** + * Forces the usesvg setting to either true or false, avoiding any decision making. + * + * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred. + * + * @param bool $setting True to force the use of svg when available, null otherwise. + */ + private function force_svg_use($setting) { + $this->usesvg = (bool)$setting; + } + /** * Checks if file with any image extension exists. * From 27c1c305b1641d2410c32ee20227c647b927c874 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Wed, 3 Oct 2012 09:01:58 +1300 Subject: [PATCH 3/3] MDL-22955 output: Fixed bug by ensuring we generate an SVG candidate even for regular format requests --- theme/image.php | 78 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/theme/image.php b/theme/image.php index b488fa85ace..75b7c599617 100644 --- a/theme/image.php +++ b/theme/image.php @@ -131,11 +131,38 @@ define('NO_UPGRADE_CHECK', true); // Ignore upgrade check require("$CFG->dirroot/lib/setup.php"); $theme = theme_config::load($themename); -$imagefile = $theme->resolve_image_location($image, $component, $usesvg); - $rev = theme_get_revision(); $etag = sha1("$themename/$component/$rev/$image"); +// 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 +// time we cache a version of the SVG if it exists. If we don't do this other users who ask for SVG would not ever get it as +// there is a cached image already of another format. +// Remember this only gets run once before any candidate exists, and only if we want a cached revision. +if (!$usesvg && $rev > -1) { + $imagefile = $theme->resolve_image_location($image, $component, true); + if (!empty($imagefile) && is_readable($imagefile)) { + $cacheimage = cache_image($image, $imagefile, $candidatelocation); + $pathinfo = pathinfo($imagefile); + // There is no SVG equivilant, we've just successfully cached an image of another format. + if ($pathinfo['extension'] !== 'svg') { + // Serve the file as we would in a normal request. + if (connection_aborted()) { + die; + } + // make sure nothing failed + clearstatcache(); + if (file_exists($cacheimage)) { + send_cached_image($cacheimage, $etag); + } + send_uncached_image($imagefile); + exit; + } + } +} + +// Either SVG was requested or we've cached a SVG version and are ready to serve a regular format. +$imagefile = $theme->resolve_image_location($image, $component, $usesvg); if (empty($imagefile) or !is_readable($imagefile)) { if ($rev > -1) { if (!file_exists($candidatelocation)) { @@ -150,23 +177,7 @@ if (empty($imagefile) or !is_readable($imagefile)) { } if ($rev > -1) { - $pathinfo = pathinfo($imagefile); - $cacheimage = "$candidatelocation/$image.".$pathinfo['extension']; - - clearstatcache(); - if (!file_exists(dirname($cacheimage))) { - @mkdir(dirname($cacheimage), $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($imagefile, $cacheimage.'.tmp')) { - rename($cacheimage.'.tmp', $cacheimage); - @chmod($cacheimage, $CFG->filepermissions); - @unlink($cacheimage.'.tmp'); // just in case anything fails - } - ignore_user_abort(false); + $cacheimage = cache_image($image, $imagefile, $candidatelocation); if (connection_aborted()) { die; } @@ -253,4 +264,33 @@ function get_contenttype_from_ext($ext) { return 'image/vnd.microsoft.icon'; } return 'document/unknown'; +} + +/** + * Caches a given image file. + * + * @param string $image The name of the image that was requested. + * @param string $imagefile The location of the image file we want to cache. + * @param string $candidatelocation The location to cache it in. + * @return string The path to the cached image. + */ +function cache_image($image, $imagefile, $candidatelocation) { + global $CFG; + $pathinfo = pathinfo($imagefile); + $cacheimage = "$candidatelocation/$image.".$pathinfo['extension']; + + clearstatcache(); + if (!file_exists(dirname($cacheimage))) { + @mkdir(dirname($cacheimage), $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($imagefile, $cacheimage.'.tmp')) { + rename($cacheimage.'.tmp', $cacheimage); + @chmod($cacheimage, $CFG->filepermissions); + @unlink($cacheimage.'.tmp'); // just in case anything fails + } + return $cacheimage; } \ No newline at end of file