diff --git a/classes/form/colourpicker_form_element.php b/classes/form/colourpicker_form_element.php new file mode 100644 index 00000000000..7ccb0296e30 --- /dev/null +++ b/classes/form/colourpicker_form_element.php @@ -0,0 +1,205 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/lib/form/editor.php'); + +/** + * Form element for handling the colour picker. + * + * @package theme_boost_union + * @copyright 2023 Mario Wehr + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_boost_union_colourpicker_form_element extends HTML_QuickForm_element implements templatable { + + // String html for help button, if empty then no help. + public $_helpbutton = ''; + + /** + * Class constructor + * + * @param string Name of the element + * @param mixed Label(s) for the element + * @param mixed Associative array of tag attributes or HTML attributes name="value" pairs + * @since 1.0 + * @access public + * @return void + */ + public function __construct($elementname=null, $elemenlabel=null, $attributes=null) { + parent::__construct($elementname, $elemenlabel, $attributes); + $this->_type = 'static'; + } + + /** + * Sets name of editor + * + * @param string $name name of element + */ + // @codingStandardsIgnoreStart + public function setName($name) { + $this->updateAttributes(array('name' => $name)); + } + // @codingStandardsIgnoreEnd + /** + * Returns name of element + * + * @return string + */ + // @codingStandardsIgnoreStart + function getName() { + return $this->getAttribute('name'); + } + // @codingStandardsIgnoreEnd + /** + * get html for help button + * + * @return string html for help button + */ + // @codingStandardsIgnoreStart + public function getHelpButton() { + return $this->_helpbutton; + } + // @codingStandardsIgnoreEnd + /** + * Sets the value of the form element + * + * @param string $value + */ + // @codingStandardsIgnoreStart + public function setvalue($value) { + $this->updateAttributes(array('value' => $value)); + } + // @codingStandardsIgnoreEnd + /** + * Gets the value of the form element + */ + public function getvalue() { + return $this->getAttribute('value'); + } + + /** + * Returns the html string to display this element. + * + * @return string + */ + public function tohtml() { + global $PAGE, $OUTPUT; + + $icon = new pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', ['class' => 'loadingicon']); + $context = (object) [ + 'icon' => $icon->export_for_template($OUTPUT), + 'name' => $this->getAttribute('name'), + 'id' => $this->getAttribute('id'), + 'value' => $this->getAttribute('value'), + "readonly" => false, + 'haspreviewconfig' => false, + ]; + $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->getAttribute('id'), null)); + return $OUTPUT->render_from_template('core_admin/setting_configcolourpicker', $context); + } + + /** + * Function to export the renderer data in a format that is suitable for a mustache template. + * + * @param \renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return \stdClass|array + */ + public function export_for_template(renderer_base $output) { + $context['html'] = $this->toHtml(); + $context['id'] = $this->getAttribute('id'); + return $context; + } +} + +/** + * Colour picker validation rule + * + * @package theme_boost_union + * @copyright 2023 Mario Wehr + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_boost_union_colourpicker_rule extends HTML_QuickForm_Rule { + + /** + * Validates the colour that was entered by the user + * + * @param string $value Value to check + * @param int|string|array $options Not used yet + * @return bool true if value is not empty + */ + public function validate($value, $options = null) { + + // List of valid HTML colour names. + $colornames = array( + 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', + 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', + 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', + 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', + 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', + 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', + 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', + 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', + 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', + 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', + 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', + 'ghostwhite', 'gold', 'goldenrod', 'gray', 'grey', 'green', + 'greenyellow', 'honeydew', 'hotpink', 'indianred', 'indigo', + 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', + 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', + 'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen', + 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', + 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', + 'lime', 'limegreen', 'linen', 'magenta', 'maroon', + 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', + 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', + 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', + 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', + 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', + 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', + 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'red', + 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', + 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', + 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', + 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', + 'whitesmoke', 'yellow', 'yellowgreen' + ); + + if (preg_match('/^#?([[:xdigit:]]{3}){1,2}$/', $value)) { + if (strpos($value, '#') !== 0) { + $value = '#'.$value; + } + return $value; + } else if (in_array(strtolower($value), $colornames)) { + return $value; + } else if (preg_match('/rgb\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\)/i', $value)) { + return $value; + } else if (preg_match('/rgba\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\, ?\d(\.\d)?\)/i', $value)) { + return $value; + } else if (preg_match('/hsl\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\)/i', $value)) { + return $value; + } else if (preg_match('/hsla\(\d{0,3}\, ?\d{0,3}%,\d{0,3}%\, ?\d(\.\d)?\)/i', $value)) { + return $value; + } else if (($value == 'transparent') || ($value == 'currentColor') || ($value == 'inherit')) { + return $value; + } else { + return false; + } + } +} diff --git a/classes/form/flavour_edit_form.php b/classes/form/flavour_edit_form.php index 78d2acfade5..915349651e2 100644 --- a/classes/form/flavour_edit_form.php +++ b/classes/form/flavour_edit_form.php @@ -51,6 +51,8 @@ class flavour_edit_form extends \moodleform { * @throws \coding_exception */ public function definition() { + global $CFG, $OUTPUT; + // Get an easier handler for the form. $mform = $this->_form; @@ -120,6 +122,96 @@ public function definition() { ]); $mform->addHelpButton('flavours_look_backgroundimage', 'flavoursbackgroundimage', 'theme_boost_union'); + // Create brand colors heading. + $context = new \stdClass(); + $context->title = get_string('brandcolorsheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'. $OUTPUT->render_from_template('core_admin/setting_heading', $context), '
' + ); + + // Register custom colourpicker. + \MoodleQuickForm::registerElementType('boost_union_colourpicker', + $CFG->dirroot . '/theme/boost_union/classes/form/colourpicker_form_element.php', + 'theme_boost_union_colourpicker_form_element'); + // Register validation rule for colourpicker. + \MoodleQuickForm::registerRule('theme_boost_union_colourpicker_rule', + null, + 'theme_boost_union_colourpicker_rule', + $CFG->dirroot . '/theme/boost_union/classes/form/colourpicker_form_element.php'); + + // Add brandcolour as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'brandcolor', + get_string('brandcolor', 'theme_boost'), + [ + 'id' => 'colourpicker_brandcolour', + ]); + $mform->setDefault('brandcolor', ''); + $mform->addHelpButton('brandcolor', 'flavoursbrandcolour', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('brandcolor', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Create Bootstrap colors heading. + $context = new \stdClass(); + $context->title = get_string('bootstrapcolorsheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'. $OUTPUT->render_from_template('core_admin/setting_heading', $context), '
' + ); + + // Add Bootstrap color for 'success' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolorsuccess', + get_string('bootstrapcolorsuccesssetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bootstrapcolorsuccess', + ]); + $mform->setDefault('bootstrapcolorsuccess', ''); + $mform->addHelpButton('bootstrapcolorsuccess', 'flavoursbootstrapcolorsuccess', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolorsuccess', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Add Bootstrap color for 'info' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolorinfo', + get_string('bootstrapcolorinfosetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bootstrapcolorinfo', + ]); + $mform->setDefault('bootstrapcolorinfo', ''); + $mform->addHelpButton('bootstrapcolorinfo', 'flavoursbootstrapcolorinfo', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolorinfo', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Add Bootstrap color for 'warning' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolorwarning', + get_string('bootstrapcolorwarningsetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bootstrapcolorwarning', + ]); + $mform->addHelpButton('bootstrapcolorwarning', 'flavoursbootstrapcolorwarning', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolorwarning', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Add Bootstrap color for 'danger' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolordanger', + get_string('bootstrapcolordangersetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bbootstrapcolordanger', + ]); + $mform->setDefault('bootstrapcolordanger', ''); + $mform->addHelpButton('bootstrapcolordanger', 'flavoursbootstrapcolordanger', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolordanger', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + // Add custom css as textarea element. // Note: In the current state of implementation, this setting only allows the usage of custom CSS, not SCSS. // It will be appended to the stack of CSS code which is shipped to the browser. diff --git a/db/install.xml b/db/install.xml index ddb4c48b1e4..1bd8d97a4cb 100644 --- a/db/install.xml +++ b/db/install.xml @@ -21,6 +21,11 @@ + + + + + diff --git a/db/upgrade.php b/db/upgrade.php index 0a82cd9e16c..4047df6c76d 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -136,5 +136,38 @@ function xmldb_theme_boost_union_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2022080922, 'theme', 'boost_union'); } + /*if ($oldversion < 0000000) { + + $table = new xmldb_table('theme_boost_union_flavours'); + + $field = new xmldb_field('brandcolor', XMLDB_TYPE_CHAR, '32', null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('bootstrapcolorsuccess', XMLDB_TYPE_CHAR, '32', null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('bootstrapcolorwarning', XMLDB_TYPE_CHAR, '32', null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('bootstrapcolorwarning', XMLDB_TYPE_CHAR, '32', null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('bootstrapcolordanger', XMLDB_TYPE_CHAR, '32', null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Boost_union savepoint reached. + upgrade_plugin_savepoint(true, 00000000, 'theme', 'boost_union'); + }*/ + return true; } diff --git a/flavours/css.php b/flavours/css.php new file mode 100644 index 00000000000..ea8fa933b19 --- /dev/null +++ b/flavours/css.php @@ -0,0 +1,354 @@ +. + +/** + * This file is responsible for serving the one huge CSS of each theme. + * + * @package core + * @copyright 2009 Petr Skoda (skodak) {@link http://skodak.org} modified by 2023 Mario Wehr for bost union theme + * @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'); +require_once($CFG->dirroot.'/lib/csslib.php'); + +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 2) { + css_send_css_not_found(); + } + + if (strpos($slashargument, '_s/') === 0) { + // Can't use SVG. + $slashargument = substr($slashargument, 3); + $usesvg = false; + } else { + $usesvg = true; + } + + list($themename, $rev, $flavourid, $type) = explode('/', $slashargument, 4); + $themename = min_clean_param($themename, 'SAFEDIR'); + $rev = min_clean_param($rev, 'RAW'); + $flavourid = min_clean_param($flavourid, 'SAFEDIR'); + $typeid = min_clean_param($type, 'SAFEDIR'); + +} else { + $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); + $rev = min_optional_param('rev', 0, 'RAW'); + $type = min_optional_param('type', 'all', 'SAFEDIR'); + $usesvg = (bool)min_optional_param('svg', '1', 'INT'); +} + +// Check if we received a theme sub revision which allows us +// to handle local caching on a per theme basis. +$values = explode('_', $rev); +$rev = min_clean_param(array_shift($values), 'INT'); +$themesubrev = array_shift($values); + +if (!is_null($themesubrev)) { + $themesubrev = min_clean_param($themesubrev, 'INT'); +} + +// Check that type fits into the expected values. +if (!in_array($type, ['all', 'all-rtl', 'editor', 'editor-rtl'])) { + css_send_css_not_found(); +} + +// @codingStandardsIgnoreStart +if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { + // The theme exists in standard location - ok. +} else if (!empty($CFG->themedir) && file_exists("$CFG->themedir/$themename/config.php")) { + // Alternative theme location contains this theme - ok. + // @codingStandardsIgnoreEnd +} else { + header('HTTP/1.0 404 not found'); + die('Theme was not found, sorry.'); +} + +$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; +$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); +$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg); + +if (file_exists($candidatesheet)) { + 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 counter. + css_send_unmodified(filemtime($candidatesheet), $etag); + } + css_send_cached_css($candidatesheet, $etag); +} + +// 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); +$theme->force_svg_use($usesvg); +$theme->set_rtl_mode(substr($type, -4) === '-rtl'); + +$themerev = theme_get_revision(); +$currentthemesubrev = theme_get_sub_revision_for_theme($themename); + +$cache = true; +// If the client is requesting a revision that doesn't match both +// the global theme revision and the theme specific revision then +// tell the browser not to cache this style sheet because it's +// likely being regenerated. +if ($themerev <= 0 || $themerev != $rev || $themesubrev != $currentthemesubrev) { + $rev = $themerev; + $themesubrev = $currentthemesubrev; + $cache = false; + + $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; + $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); + $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg); +} + +make_localcache_directory('theme', false); + +if ($type === 'editor' || $type === 'editor-rtl') { + $csscontent = $theme->get_css_content_editor(); + + if ($cache) { + css_store_css($theme, $candidatesheet, $csscontent); + css_send_cached_css($candidatesheet, $etag); + } else { + css_send_uncached_css($csscontent); + } + +} + +if (($fallbacksheet = theme_styles_fallback_content($theme)) && !$theme->has_css_cached_content()) { + // The theme is not yet available and a fallback is available. + // Return the fallback immediately, specifying the Content-Length, then generate in the background. + $css = file_get_contents($fallbacksheet); + css_send_temporary_css($css); + + // The fallback content has now been sent. + // There will be an attempt to generate the content, but it should not be served. + // The Content-Length above means that the client will disregard it anyway. + $sendaftergeneration = false; + + // There may be another client currently holding a lock and generating the stylesheet. + // Use a very low lock timeout as the connection will be ended immediately afterwards. + $locktimeout = 1; +} else { + // There is no fallback content to be issued here, therefore the generated content must be output. + $sendaftergeneration = true; + + // Use a realistic lock timeout as the intention is to avoid lock contention. + $locktimeout = rand(90, 120); +} + +// Attempt to fetch the lock. +$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content'); +$lock = $lockfactory->get_lock($themename, $locktimeout); + +if ($sendaftergeneration || $lock) { + // Either the lock was successful, or the lock was unsuccessful but the content *must* be sent. + + // The content does not exist locally. + // Generate and save it. + $candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid); + + if ($lock) { + $lock->release(); + } + + if ($sendaftergeneration) { + if (!$cache) { + // Do not pollute browser caches if invalid revision requested, + // let's ignore legacy IE breakage here too. + css_send_uncached_css(file_get_contents($candidatesheet)); + + } else { + // Real browsers - this is the expected result! + css_send_cached_css($candidatesheet, $etag); + } + } +} + +/** + * Generate the theme CSS and store it. + * + * @param theme_config $theme The theme to be generated + * @param int $rev The theme revision + * @param int $themesubrev The theme sub-revision + * @param string $candidatedir The directory that it should be stored in + * @return string The path that the primary CSS was written to + */ +function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid) { + global $CFG; + require_once("{$CFG->libdir}/filelib.php"); + + // Generate the content first. + if (!$csscontent = theme_boost_union_get_css_cached_content($theme, $flavourid)) { + $csscontent = $theme->get_css_content(); + theme_boost_union_set_css_content_cache($theme, $csscontent); + } + + if ($theme->get_rtl_mode()) { + $type = "all-rtl"; + } else { + $type = "all"; + } + + // Determine the candidatesheet path. + $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $flavourid, $themesubrev, $theme->use_svg_icons()); + + // Store the CSS. + css_store_css($theme, $candidatesheet, $csscontent); + + // Store the fallback CSS in the temp directory. + // This file is used as a fallback when waiting for a theme to compile and is not versioned in any way. + $fallbacksheet = make_temp_directory("theme/{$theme->name}") + . "/" + . theme_styles_get_filename($type, $flavourid, $theme->use_svg_icons()); + css_store_css($theme, $fallbacksheet, $csscontent); + + // Delete older revisions from localcache. ToDo check if we can leave it cause /theme/styles.php should do it. + /*$themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR); + foreach ($themecachedirs as $localcachedir) { + $cachedrev = []; + preg_match("/\/theme\/([0-9]+)$/", $localcachedir, $cachedrev); + $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0; + if ($cachedrev > 0 && $cachedrev < $rev) { + fulldelete($localcachedir); + } + }*/ + + // Delete older theme subrevision CSS from localcache. + $subrevfiles = glob("{$CFG->localcachedir}/theme/{$rev}/{$theme->name}/css/*.css"); + foreach ($subrevfiles as $subrevfile) { + $cachedsubrev = []; + preg_match("/_([0-9]+)_([0-9]+)\.([0-9]+\.)?css$/", $subrevfile, $cachedsubrev); + $cachedsubrev = isset($cachedsubrev[1]) ? intval($cachedsubrev[1]) : 0; + if ($cachedsubrev > 0 && $cachedsubrev < $themesubrev) { + fulldelete($subrevfile); + } + } + + return $candidatesheet; +} + +/** + * Fetch the preferred fallback content location if available. + * + * @param theme_config $theme The theme to be generated + * @return string The path to the fallback sheet on disk + */ +function theme_styles_fallback_content($theme) { + global $CFG; + + if (!$theme->usefallback) { + // This theme does not support fallbacks. + return false; + } + + $type = $theme->get_rtl_mode() ? 'all-rtl' : 'all'; + $filename = theme_styles_get_filename($type); + + $fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}"; + if (file_exists($fallbacksheet)) { + return $fallbacksheet; + } + + return false; +} + +/** + * Get the filename for the specified configuration. + * + * @param string $type The requested sheet type + * @param int $themesubrev The theme sub-revision + * @param bool $usesvg Whether SVGs are allowed + * @return string The filename for this sheet + */ +function theme_styles_get_filename($type, $themesubrev = 0, $flavourid = 0, $usesvg = true) { + $filename = $type; + $filename .= ($themesubrev > 0) ? "_{$themesubrev}" : ''; + $filename .= ($flavourid > 0) ? "_{$flavourid}" : ''; + $filename .= $usesvg ? '' : '-nosvg'; + + return "{$filename}.css"; +} + +/** + * Determine the correct etag for the specified configuration. + * + * @param string $themename The name of the theme + * @param int $rev The revision number + * @param string $type The requested sheet type + * @param int $themesubrev The theme sub-revision + * @param bool $usesvg Whether SVGs are allowed + * @return string The etag to use for this request + */ +function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg) { + $etag = [$rev, $themename, $type, $themesubrev]; + + if (!$usesvg) { + $etag[] = 'nosvg'; + } + + return sha1(implode('/', $etag)); +} + +/** + * Return cached post processed CSS content. + * + * @return bool|string The cached css content or false if not found. + */ +function theme_boost_union_get_css_cached_content($theme, $flavourid) { + $key = theme_boost_union_get_css_cache_key($theme, $flavourid); + $cache = cache::make('core', 'postprocessedcss'); + + return $cache->get($key); +} + +/** + * Generate the css content cache key. + * + * @return string The post processed css cache key. + */ +function theme_boost_union_get_css_cache_key($theme, $flavourid) { + $nosvg = (!$theme->use_svg_icons()) ? 'nosvg_' : ''; + $rtlmode = ($theme->get_rtl_mode() == true) ? 'rtl' : 'ltr'; + + return $nosvg . $theme->name . '_' . $flavourid . '_' . $rtlmode; +} + +/** + * Set post processed CSS content cache. + * + * @param string $csscontent The post processed CSS content. + * @return bool True if the content was successfully cached. + */ +function theme_boost_union_set_css_content_cache($theme, $csscontent) { + + $cache = cache::make('core', 'postprocessedcss'); + $key = $theme->get_css_cache_key(); + + return $cache->set($key, $csscontent); +} diff --git a/flavours/flavourslib.php b/flavours/flavourslib.php index b8e5f94e3f9..e9fd2c4a357 100644 --- a/flavours/flavourslib.php +++ b/flavours/flavourslib.php @@ -313,3 +313,34 @@ function theme_boost_union_flavour_exists_for_cohort($cohortid) { // We didn't find any matching cohort, return false. return false; } + +/** + * Helper function do get a config key from flavour item. + * + * @param $flavourid + * @param $configkey + * + * @return false + */ +function theme_boost_union_get_flavour_config_item_for_id($flavourid, $configkey) { + global $DB; + + $cache = cache::make('theme_boost_union', 'flavours'); + + $flavouridkey = 'flavour_' . $flavourid; + // Get the cached flavour for the current user flavour id. + $flavour = $cache->get($flavouridkey); + + // If we got a cached flavour. + if ($flavour == false) { + $flavour = $DB->get_record('theme_boost_union_flavours', ['id' => $flavourid]); + } + + if ($flavour !== false ) { + $cache->set($flavouridkey, $flavour); + if (isset($flavour->{$configkey})) { // ...isset returns true only if property exits and value != null;. + return $flavour->{$configkey}; + } + } + return false; +} diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php index 14e364fb397..fb46ee79cff 100644 --- a/lang/en/theme_boost_union.php +++ b/lang/en/theme_boost_union.php @@ -634,6 +634,16 @@ $string['flavoursbackgroundimage'] = 'Background image'; $string['flavoursbackgroundimage_help'] = 'With this setting, the flavour will override the background image which is configured in Boost Union\'s look settings.'; $string['flavoursbacktooverview'] = 'Back to flavour overview'; +$string['flavoursbootstrapcolordanger'] = 'Bootstrap color for "Danger"'; +$string['flavoursbootstrapcolordanger_help'] = 'With this setting, the flavour will override the Bootstrap "danger" color which is configured in Boost Union\'s look settings.'; +$string['flavoursbootstrapcolorinfo'] = 'Bootstrap color for "Info"'; +$string['flavoursbootstrapcolorinfo_help'] = 'With this setting, the flavour will override the Bootstrap info "color" which is configured in Boost Union\'s look settings.'; +$string['flavoursbootstrapcolorsuccess'] = 'Bootstrap color for "Success"'; +$string['flavoursbootstrapcolorsuccess_help'] = 'With this setting, the flavour will override the Bootstrap "success" color which is configured in Boost Union\'s look settings.'; +$string['flavoursbootstrapcolorwarning'] = 'Bootstrap color for "Warning"'; +$string['flavoursbootstrapcolorwarning_help'] = 'With this setting, the flavour will override the Bootstrap "warning" color which is configured in Boost Union\'s look settings.'; +$string['flavoursbrandcolour'] = 'Brand colour '; +$string['flavoursbrandcolour_help'] = 'With this setting, the flavour will override the brand colour which is configured in Boost Union\'s look settings.'; $string['flavourscreateflavour'] = 'Create flavour'; $string['flavourscustomcss'] = 'Custom CSS'; $string['flavourscustomcss_help'] = 'With this setting, you can write custom CSS for the flavour. It will be appended to the stack of CSS code which is shipped to the browser as soon as the flavour applies. Please note that in the current state of implementation, this setting only allows the usage of custom CSS, not SCSS.'; diff --git a/lib.php b/lib.php index 7ed82d47420..c3ab75f7109 100644 --- a/lib.php +++ b/lib.php @@ -124,10 +124,10 @@ function theme_boost_union_get_main_scss_content($theme) { * Get SCSS to prepend. * * @param theme_config $theme The theme config object. - * @return array + * @return string */ function theme_boost_union_get_pre_scss($theme) { - global $CFG; + global $CFG, $flavourid; // Require local library. require_once($CFG->dirroot . '/theme/boost_union/locallib.php'); @@ -149,7 +149,20 @@ function theme_boost_union_get_pre_scss($theme) { // Prepend variables first. foreach ($configurable as $configkey => $targets) { - $value = get_config('theme_boost_union', $configkey); + $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null; + // Check if flavour is active. + if (isset($flavourid)) { + // Require flavours library. + require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + // Get flavour config for id. + $flavourvalue = theme_boost_union_get_flavour_config_item_for_id($flavourid, $configkey); + // Check override value. + if (!($flavourvalue)) { + continue; + } + // Override em. + $value = $flavourvalue; + } if (!($value)) { continue; } @@ -219,8 +232,8 @@ function theme_boost_union_get_pre_scss($theme) { $scss .= '$blockregionoutsiderightwidth: '.$blockregionoutsiderightwidth.";\n"; // Prepend pre-scss. - if (get_config('theme_boost_union', 'scsspre')) { - $scss .= get_config('theme_boost_union', 'scsspre'); + if (!empty($theme->settings->scsspre)) { + $scss .= $theme->settings->scsspre; } return $scss; @@ -334,7 +347,7 @@ function theme_boost_union_get_extra_scss($theme) { } // If the default purpose was not 'other' and now it is, make the icon black. if ($theme->settings->{$configname} == MOD_PURPOSE_OTHER) { - $content .= '.activityicon, .icon { filter: none; }'; + $content .= '.activityicon { filter: none; }'; } $content .= '}'; } @@ -512,8 +525,38 @@ function theme_boost_union_before_standard_html_head() { theme_boost_union_add_fontawesome_to_page(); // Add the flavour CSS to the page. - theme_boost_union_add_flavourcss_to_page(); + theme_boost_union_add_flavourcss_to_page(); // Todo remove em when flavour pre/post scss goes prod. // Return an empty string to keep the caller happy. return $html; } + +function theme_boost_union_alter_css_urls(&$urls) { + global $CFG; + + // Require flavours library. + require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + + // If any flavour applies to this page. + $flavour = theme_boost_union_get_flavour_which_applies(); + if ($flavour != null) { + if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) { + // No CSS switch during behat runs, or it will take ages to run a scenario. + return; + } + + foreach (array_keys($urls) as $i) { + if ($urls[$i] instanceof moodle_url) { + $pathstyles = preg_quote($CFG->wwwroot . '/theme/styles.php', '|'); + if (preg_match("|^$pathstyles(/_s)?(.*)$|", $urls[$i]->out(false), $matches)) { + if (!empty($CFG->slasharguments)) { + $parts = explode('/', $matches[2]); + $parts[3] = $flavour->id . '/' . $parts[3]; + $urls[$i] = new moodle_url('/theme/boost_union/flavours/css.php'); + $urls[$i]->set_slashargument($matches[1] . join('/', $parts)); + } + } + } + } + } +}