Commit 436dbeec authored by Sam Hemelryk's avatar Sam Hemelryk
Browse files

MDL-22955 theme: Added ability to use SVG format for icons

parent ccd90e76
......@@ -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'));
......
......@@ -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!!!
//=========================================================================
......
......@@ -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'));
......
......@@ -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);
}
......
......@@ -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;
......
......@@ -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;
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);
if (!empty($CFG->slasharguments) and $rev > 0) {
$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")) {
......
......@@ -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;
......
......@@ -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
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="114.88917"
height="114.88917"
id="svg2"
version="1.1"
inkscape:version="0.48.3.1 r9886"
sodipodi:docname="New document 1">
<defs
id="defs4">
<linearGradient
id="linearGradient3988">
<stop
style="stop-color:#000000;stop-opacity:0"
offset="0"
id="stop3990" />
<stop
style="stop-color:#ff8000;stop-opacity:1;"
offset="1"
id="stop3992" />
</linearGradient>
<linearGradient
id="linearGradient3982">
<stop
id="stop3984"
offset="0"
style="stop-color:#ffab00;stop-opacity:1;" />
<stop
id="stop3986"
offset="1"
style="stop-color:#ff8000;stop-opacity:1;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3982"
id="radialGradient3980"
cx="360.68207"
cy="435.11246"
fx="360.68207"
fy="435.11246"
r="45.40649"
gradientTransform="matrix(1,0,0,0.98110214,0,8.2226956)"
gradientUnits="userSpaceOnUse"
spreadMethod="pad" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3988"
id="linearGradient4005"
x1="323.18604"
y1="445.29483"
x2="375.84158"
y2="514.34235"
gradientUnits="userSpaceOnUse" />
<filter
inkscape:collect="always"
id="filter4055"
color-interpolation-filters="sRGB">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="2.1197264"
id="feGaussianBlur4057" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.7480769"
inkscape:cx="-31.349998"
inkscape:cy="12.481512"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1855"
inkscape:window-height="1056"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-304.95364,-378.52595)">
<path
sodipodi:type="arc"
style="fill:url(#radialGradient3980);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path3204"
sodipodi:cx="360.68207"
sodipodi:cy="435.11246"
sodipodi:rx="44.90649"
sodipodi:ry="44.048405"
d="m 405.58856,435.11246 c 0,24.32726 -20.10532,44.0484 -44.90649,44.0484 -24.80117,0 -44.90649,-19.72114 -44.90649,-44.0484 0,-24.32726 20.10532,-44.04841 44.90649,-44.04841 24.80117,0 44.90649,19.72115 44.90649,44.04841 z" />
<path
sodipodi:type="arc"
id="path3994"
sodipodi:cx="374.98349"
sodipodi:cy="445.12344"
sodipodi:rx="52.343235"
sodipodi:ry="52.343235"
d="m 427.32673,445.12344 c 0,28.90837 -23.43487,52.34324 -52.34324,52.34324 -28.90837,0 -52.34324,-23.43487 -52.34324,-52.34324 0,-28.90837 23.43487,-52.34323 52.34324,-52.34323 28.90837,0 52.34324,23.43486 52.34324,52.34323 z"
transform="matrix(0.95405918,-0.29961823,0.29961823,0.95405918,-128.72531,123.64832)"
style="opacity:0.47736631;fill:url(#linearGradient4005);fill-opacity:1;filter:url(#filter4055)" />
</g>
</svg>
......@@ -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';
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment