From 8040fafaa8e06d611ae6eb8320bbec5de26466f2 Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Tue, 6 Aug 2019 17:10:56 +0200 Subject: [PATCH] MDL-62757 themes: validate theme preset files Preset files need to be validated to ensure compiling SCSS does not break. --- lang/en/admin.php | 1 + lib/adminlib.php | 92 ++++++++++++++++++++++++++++++++++++++ lib/outputlib.php | 7 ++- theme/boost/settings.php | 2 +- theme/classic/settings.php | 2 +- 5 files changed, 100 insertions(+), 4 deletions(-) diff --git a/lang/en/admin.php b/lang/en/admin.php index 400e094b728..3ec73135297 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -660,6 +660,7 @@ $string['intlrecommended'] = 'Intl extension is used to improve internationaliza $string['intlrequired'] = 'Intl extension is required to improve internationalization support, such as locale aware sorting and international domain names.'; $string['invalidagedigitalconsent'] = 'The digital age of consent is not valid: {$a}'; $string['invalidforgottenpasswordurl'] = 'The forgotten password URL is not a valid URL.'; +$string['invalidthemepreset'] = 'The chosen preset file is not compatible with this theme. The SCSS compile error was: "{$a}"'; $string['invalidsection'] = 'Invalid section.'; $string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.'; $string['ipblocked'] = 'This site is not available currently.'; diff --git a/lib/adminlib.php b/lib/adminlib.php index ac2142b7669..1f731a46f6e 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -11092,3 +11092,95 @@ class admin_settings_sitepolicy_handler_select extends admin_setting_configselec return true; } } + +/** + * Used to validate theme presets code and ensuring they compile well. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2019 Bas Brands + */ +class admin_setting_configthemepreset extends admin_setting_configselect { + + /** @var string The name of the theme to check for */ + private $themename; + + /** + * Constructor + * @param string $name unique ascii name, either 'mysetting' for settings that in config, + * or 'myplugin/mysetting' for ones in config_plugins. + * @param string $visiblename localised + * @param string $description long localised info + * @param string|int $defaultsetting + * @param array $choices array of $value=>$label for each selection + * @param string $themename name of theme to check presets for. + */ + public function __construct($name, $visiblename, $description, $defaultsetting, $choices, $themename) { + $this->themename = $themename; + parent::__construct($name, $visiblename, $description, $defaultsetting, $choices); + } + + /** + * Write settings if validated + * + * @param string $data + * @return string + */ + public function write_setting($data) { + $validated = $this->validate($data); + if ($validated !== true) { + return $validated; + } + return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin')); + } + + /** + * Validate the preset file to ensure its parsable. + * + * @param string $data The preset file chosen. + * @return mixed bool true for success or string:error on failure. + */ + public function validate($data) { + + if (in_array($data, ['default.scss', 'plain.scss'])) { + return true; + } + + $fs = get_file_storage(); + $theme = theme_config::load($this->themename); + $context = context_system::instance(); + + // If the preset has not changed there is no need to validate it. + if ($theme->settings->preset == $data) { + return true; + } + + if ($presetfile = $fs->get_file($context->id, 'theme_' . $this->themename, 'preset', 0, '/', $data)) { + // This operation uses a lot of resources. + raise_memory_limit(MEMORY_EXTRA); + core_php_time_limit::raise(300); + + // TODO: MDL-62757 When changing anything in this method please do not forget to check + // if the get_css_content_from_scss() method in class theme_config needs updating too. + + $compiler = new core_scss(); + $compiler->prepend_raw_scss($theme->get_pre_scss_code()); + $compiler->append_raw_scss($presetfile->get_content()); + if ($scssproperties = $theme->get_scss_property()) { + $compiler->setImportPaths($scssproperties[0]); + } + $compiler->append_raw_scss($theme->get_extra_scss_code()); + + try { + $compiler->to_css(); + } catch (Exception $e) { + return get_string('invalidthemepreset', 'admin', $e->getMessage()); + } + + // Try to save memory. + $compiler = null; + unset($compiler); + } + + return true; + } +} diff --git a/lib/outputlib.php b/lib/outputlib.php index bb36d5db81b..56a1110b15d 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -1409,6 +1409,9 @@ class theme_config { raise_memory_limit(MEMORY_EXTRA); core_php_time_limit::raise(300); + // TODO: MDL-62757 When changing anything in this method please do not forget to check + // if the validate() method in class admin_setting_configthemepreset needs updating too. + // Set-up the compiler. $compiler = new core_scss(); $compiler->prepend_raw_scss($this->get_pre_scss_code()); @@ -1485,7 +1488,7 @@ class theme_config { * * @return string The SCSS code to inject. */ - protected function get_extra_scss_code() { + public function get_extra_scss_code() { $content = ''; // Getting all the candidate functions. @@ -1515,7 +1518,7 @@ class theme_config { * * @return string The SCSS code to inject. */ - protected function get_pre_scss_code() { + public function get_pre_scss_code() { $content = ''; // Getting all the candidate functions. diff --git a/theme/boost/settings.php b/theme/boost/settings.php index c7cde2b2017..1f2bad2e87d 100644 --- a/theme/boost/settings.php +++ b/theme/boost/settings.php @@ -44,7 +44,7 @@ if ($ADMIN->fulltree) { $choices['default.scss'] = 'default.scss'; $choices['plain.scss'] = 'plain.scss'; - $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting = new admin_setting_configthemepreset($name, $title, $description, $default, $choices, 'boost'); $setting->set_updatedcallback('theme_reset_all_caches'); $page->add($setting); diff --git a/theme/classic/settings.php b/theme/classic/settings.php index 399b400bf6b..97619800db9 100644 --- a/theme/classic/settings.php +++ b/theme/classic/settings.php @@ -54,7 +54,7 @@ if ($ADMIN->fulltree) { $choices['default.scss'] = 'default.scss'; $choices['plain.scss'] = 'plain.scss'; - $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting = new admin_setting_configthemepreset($name, $title, $description, $default, $choices, 'classic'); $setting->set_updatedcallback('theme_reset_all_caches'); $page->add($setting);