diff --git a/admin/renderer.php b/admin/renderer.php index 56c905673df..293bf52f225 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 3c47f6a76b7..a9a49d9b7d5 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -3784,7 +3784,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..3223ee48f72 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.) @@ -942,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]])) { @@ -977,6 +988,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 +1003,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 +1026,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 +1069,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 +1085,92 @@ 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; + } + + /** + * 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. * + * 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 00000000000..5860abeff95 Binary files /dev/null and b/pix/i/test.png differ 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..75b7c599617 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,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); - $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)) { @@ -139,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; } @@ -229,10 +251,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'; @@ -241,3 +265,32 @@ function get_contenttype_from_ext($ext) { } 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