Back-end Infrastructure
With the release of Totara 13, for Site Administrators and Tenant Domain Managers, we introduced a new way of managing theme settings. These settings are tenant aware and can be customized on an individual tenant level.
The list of supported theme settings categories, that can be customised, are as follows:
Tab | Category | Property | Description | Type | Dependency | Tenant customisable |
---|---|---|---|---|---|---|
Brand | brand | formbrand_field_logoalttext | Name of the site to function as a text alternative to the logo image. | text | - | Yes |
Colours | colours | color-state | Primary brand colour to set the colour for interactive elements. | value | - | Yes |
formcolours_field_useoverrides | Switch indicating that specific colour properties will be enabled or disabled depending of this value. | boolean | - | Yes | ||
color-primary | An additional accent colour to set the highlight colour of non-interactive elements. | value | - | Yes | ||
nav-bg-color | Background colour of the main navigation. | value | formcolours_field_useoverrides | Yes | ||
nav-text-color | Text colour of the main navigation. | value | formcolours_field_useoverrides | Yes | ||
color-text | Main text colour of the site. | value | formcolours_field_useoverrides | Yes | ||
footer-bg-color | Background colour of the footer. | value | formcolours_field_useoverrides | Yes | ||
footer-text-color | Text colour of the footer. | value | formcolours_field_useoverrides | Yes | ||
Images | images | formimages_field_displaylogin | Switch indicating that the login image will be enabled/disabled. | boolean | - | Yes |
formimages_field_loginalttext | Text alternative that conveys the content and function of the login image. | text | - | Yes | ||
Custom | custom | formcustom_field_customfooter | Text that will be visible in the footer for the currently selected theme. | text | - | Yes |
formcustom_field_customcss | CSS that will be added after all other styles on every page. | text | - | No |
The list of supported theme files, that can be customised, are as follows:
Tab | UI Key | Description | Dependency | Tenant customisable |
---|---|---|---|---|
Brand | sitelogo | Image to be used as the site’s logo. | - | Yes |
sitefavicon | Browser tab icon that will appear next to your site's title in the browser. | - | Yes | |
Images | sitelogin | Login page default image. | - | Yes |
learncourse | Course default image. | - | Yes | |
learnprogram | Program default image. | 'programs' advanced feature | Yes | |
learncert | Certification default image. | 'certifications' advanced feature | Yes | |
engageresource | Resource default image. | 'engage_resources' advanced feature | Yes | |
engageworkspace | Workspace default image. | 'container_workspace' advanced feature | Yes |
Accessing theme settings
To access the theme settings for our Ventura theme, the following page exists:
server/theme/ventura/index.php
Â
This page uses the TUI framework controllers, namely:
server/totara/tui/classes/controllers/theme_settings.php
server/totara/tui/classes/controllers/theme_tenants.php
Â
The theme settings controller will reroute to the theme_tenants controller should multitenancy be enabled or if the settings is being access by a Tenant Domain Manager.
The settings page is set up via an external admin page defined in server/theme/ventura/settings.php
Â
Main settings class
Refer to server/lib/classes/theme/settings.php
 for more.
How settings are savedÂ
Settings from the user interface (see Creating custom themes) are defined in categories and properties where the properties are key/value pairs.
The back-end was designed to be very dynamic in such a way to allow the front-end components to dictate what the categories and properties are that we need to save. These categories and their corresponding properties are defined in a JSON string which makes it very dynamic to save just about any setting.
The back-end does some basic validation based on the type of property, see server/lib/classes/theme/validation/property/property_validator.php
 for more information.
The categories are normally named after the tabs in the UI, meaning if the name of a category is "brand" the name of the tab will also be "brand" (see the How settings are used in the front-end section of this document).
All settings are saved to the config_plugins
 table in the following format:
- plugin = 'theme_' + theme name
- name = "tenant_{$tenant_id}_settings", where
tenant_id
 is 0 for site settings, or the value in the ID column of the tenant table if these settings relates to a tenant - value = JSON representation of the settings specifically related to this tenant
Examples
This record means that there are currently no settings altered for the site and the defaults will apply.
This record means that there are some overrides for the site and the values will be in the following format:
[ { "name":"category_1", "properties":[ { "name":"property_1", "type":"boolean", "value":"false", "selectors":[ "selector_name" ] }, { "name":"property_2", "type":"value", "value":"#ff0000", "selectors":[] } ] }, { "name":"category_2", "properties":[ { "name":"property_1" "type":"boolean", "value":"false", "selectors":[] } ] } ]
How settings are retrieved
When theme settings are loaded, the categories are prioritized in the following order:
- Tenant categories: All the settings that have been saved for a specific tenant and will be stored in the
config_plugins
 table with a name of "tenant_{$tenant_id}_settings" - Site categories: All the settings that have been saved for the site and will be stored in the
config_plugins
table with a name of "tenant_0_settings" - Default categories: Any default values you want certain settings to have
Retrieving the value of a single property
core\theme\settings
class has a function get_property
that can be used to extract the value of a specific property from a specific category.
The function takes three arguments:
- category (string): The name of the category that you want the property to be extracted from
- property (string): The name of the property you want the value of
- categories (array, optional): If you have a set of categories that you want to search through instead of fetching the categories from the
get_categories
 function
Example
To get the alternative text for the login image you can do as follows (see server/lib/classes/theme/file/login_image.php
 function get_alt_text
) :
/** * Get custom alternative text. * * @return string */ public function get_alt_text(): string { $settings = new \core\theme\settings($this->theme_config, $this->tenant_id); $property = $settings->get_property('images', 'formimages_field_loginalttext'); if (!empty($property)) { return $property['value']; } return get_string('totaralogin', 'totara_core'); }
Checking if a setting is enabled
If we want to see if a switch has been enabled core\theme\settings
also provides a function is_enabled
.
The function takes three arguments:
- category (string): The name of the category that you want the property to be extracted from
- property (string): The name of the property you want to evaluate
- default (bool): If the property is not found or not boolean then it returns this default
Example
To check if the login image has been enabled in theme settings you can do as follows (see server/lib/classes/theme/file/login_image.php
function is_available
):
/** * @inheritDoc */ public function is_available(): bool { // Check if feature is disabled. if (!$this->is_enabled()) { return false; } // Check if setting is enabled. $settings = new \core\theme\settings($this->theme_config, 0); return $settings->is_enabled('images', 'formimages_field_displaylogin', true); }
How settings are used in front-endÂ
The theme settings entry point is the TUI front-end page components:
client/component/tui/src/pages/ThemeSettings.vue
client/component/tui/src/pages/ThemeTenants.vue
Â
ThemeTenants displays a table from which a user needs to select either the site settings or a tenant to be routed to the ThemeSettings page component which is the main component for integrating into the theme settings API.
There are sub components defined in client/component/tui/src/components/theme_settings
that are, at this point, all the different tab components. For example the "brand" tab is:
client/component/tui/src/components/theme_settings/SettingsFormBrand.vue
 Â
The ThemeSettings
 component makes use of the server/lib/webapi/ajax/get_theme_settings.graphql
 GraphQL query to fetch the theme settings for a specific theme.
Each sub component is linked to a specific category (see How settings are saved above).
How custom CSS is appended to stylesheets
As part of the styles mediation, any custom CSS that was saved for the site or specific tenant will be appended to the generated CSS.
Refer to function core\theme\settings::get_css_variables
(defined in server/lib/classes/theme/settings.php
) for more information.
In our list of supported theme setting categories we only identified the formcustom_field_customcss
property of the custom
 category as being a CSS category property, but any category can specify properties that need to be appended to the generated CSS.
For more information on how to specify additional category properties for CSS inclusion, refer to the theme_settings_css_categories_hook
 hook in this document.
Hooks
Controlling what settings can be updated for a tenant
In the main theme settings class we set up a default list of settings that can be updated for a tenant:
$default_tenant_can_customize = [ 'brand' => '*', 'colours' => '*', 'images' => [ 'sitelogin', 'formimages_field_displaylogin', 'formimages_field_loginalttext', ], 'custom' => ['formcustom_field_customfooter'], 'tenant' => '*', ]; $this->tenant_settings_hook = new tenant_customizable_theme_settings_hook($default_tenant_can_customize); $this->tenant_settings_hook->execute();
If there is a category or property that you want to allow to be updated by a tenant then the server/lib/classes/hook/tenant_customizable_theme_settings.php
hook can be used to extend this list of categories and/or properties.
The array entries must have the following format:
- key: Name of the category. This name is usually the name given to a tab like 'brand' or 'images'. In most situations it matches the name given to the tab in the corresponding Vue file.
- value: Asterix (*) or an array of setting names. An asterix (*) indicates all settings and if an array is specified then only those settings mentioned in the array will be available to be updated for a tenant.
Example
$settings = [ 'category_1' => '*', // All settings in the 'category_a' category will be available for a tenant. 'category_2' => ['c2_field_1', 'c2_field_2', 'c2_field_3'], // All other fields, not mentioned in this list, in the 'category_b' category will not be available for a tenant. 'category_3' => ['c3_field_1'], // Only 'c3_field_1' in the 'category_3' category will be available for a tenant. ];
If we examine the example theme's watcher then we notice that in the following code snippet we have a category named 'example_file' and we allow all fields in that category to be customized for a tenant:
public static function customize_tenant_category_settings(tenant_customizable_theme_settings_hook $hook) { $settings = $hook->get_customizable_settings(); // Add our example file tab. $settings['example_file'] = '*'; $hook->set_customizable_settings($settings); }
The category 'example_file' matches the name we gave the tab we introduced in the ThemeSettings.vue override, see line 18 below:
<!-- Lets add a new tab with a file example, the file will refer to the example file we created under the theme directory. --> <Tab v-if="embeddedFormData.formFieldData.example_file" :id="'themesettings-tab-5'" :name="$str('tab_example_file', 'theme_example')" :always-render="true" :disabled="!customCSSEnabled" > <SettingsFormExampleFile :saved-form-field-data="embeddedFormData.formFieldData.example_file" :file-form-field-data="embeddedFormData.fileData" :is-saving="isSaving" :selected-tenant-id="selectedTenantId" :customizable-tenant-settings=" customizableTenantCategorySettings('example_file') " @mounted="setInitialTenantCategoryValues" @submit="submit" /> </Tab>
Controlling what properties should be added to the stylesheetsÂ
In the main theme settings class we set up a default list of categories and properties that define CSS variables that needs to be included in the stylesheets (see function get_categories_with_css_settings
):
$default_css_settings_categories = [ 'colours' => '*', 'custom' => [ 'formcustom_field_customcss' => ['transform' => false], ], ]; $css_categories_hook = new theme_settings_css_categories_hook($default_css_settings_categories); $css_categories_hook->execute();
If there is a category or property that defines a CSS variable/value and you want that value to be included in the stylesheet then you can implement a watcher for the hook theme_settings_css_categories_hook
 and extend the default list of categories/properties.
Category array entries must have the following format:
key => value
Â
Where key
is the name of category and value
can be one of the following options:
- '*' - indicates that all properties in the category are treated as CSS variables
- [] - an array of properties that are CSS variables that need to be included in the stylesheet
Example
$css_settings_categories = [ 'category_1' => '*', 'category_2' => [ 'property_name_1' => ['transform' => false], 'property_name_2' => [], ], ];
Note that the properties are also key => value
 pairs where the keys are the names of the properties and the values are and array of settings that the property has.
The settings that properties can define are as follows:
transform:
Indicates that this property needs to be transformed into aÂ'--name: value;'
 pair. Default is true.
This hook is implemented in server/lib/classes/hook/theme_settings_css_categories.php
Â
Theme helper
Refer to server/lib/classes/theme/file/helper.php
.
The theme helper class is responsible for providing functionality that is outside the main scope of the theme settings class.
The functionality in the helper class includes:
- Assisting the GraphQL resolver to format the output of theme settings
- Determine pre-login tenant ID
- Load theme config
Validators
Refer to server/lib/classes/theme/validation/property/property_validator.php
Â
For some category properties we can validate the values being set based on their type. These properties are validated each time theme settings categories are updated.
We currently support the following property type validators:
- Boolean:Â
server/lib/classes/theme/validation/property/boolean_validator.php
Â
Theme filesÂ
Refer to server/lib/classes/theme/file/theme_file.php
Â
For each file associated with a theme we need to have a corresponding class that is responsible for the following:
- What type it associates with, see
server/lib/classes/files/type/file_type.php
Â- This in turns provide a list of valid extensions
- Determining context in which the user is trying to access the file
- Assessing user's capability based on the point above - see
core\theme\settings::can_manage
 for more information
We currently support the following list of theme files:
- Login image:Â
server/lib/classes/theme/file/login_image.php
 - Favicon image:Â
server/lib/classes/theme/file/favicon_image.php
 - Logo image:Â
server/lib/classes/theme/file/logo_image.php
 - Course image:Â
server/course/classes/theme/file/course_image.php
 - Survey image:Â
server/totara/engage/resources/survey/classes/theme/file/survey_image.php
 - Article image:Â
server/totara/engage/resources/article/classes/theme/file/article_image.php
 - Program image:Â
server/totara/program/classes/theme/file/program_image.php
 - Certification image:Â
server/totara/certification/classes/theme/file/certification_image.php
 - Workspace image:Â
server/container/type/workspace/classes/theme/file/workspace_image.php
Â
Function get_classes
in class core\theme\file\helper
(see server/lib/classes/theme/file/helper.php
) is responsible for finding all the theme file classes.
All theme files need to have the following criteria in order to be picked up by the helper function above:
- Have
theme\file
in its namespace - Extend
core\theme\file\theme_file
Â
 core\theme\file\theme_file
specifies a few abstract methods that each theme file derived class needs to implement.
Example usage
global $PAGE; $theme_config = $PAGE->theme; $logo_image = new \core\theme\file\logo_image($theme_config); $logo_image->set_tenant_id($tenant_id); $url = $logo_image->get_current_or_default_url(); $url = $url->out();
Example usage explanation
Line | Detail |
---|---|
global $PAGE; $theme_config = $PAGE->theme; | This declares the globally defined This declaration should be used with great caution - the In the front-end the theme name is available in the config javascript utility: import { config } from 'tui/config'; let theme = config.theme.name; In the back-end resolver you would then set up theme config as follows: $theme_config = \theme_config::load($theme_name_parameter); |
$logo_image = new \core\theme\file\logo_image($theme_config); | This line instantiates a |
$logo_image->set_tenant_id($tenant_id); | If we want the logo image associated with a specific tenant then we can set the tenant ID using this method. The tenant ID can be extracted from the $tenant_id = !empty($USER->tenantid) ? $USER->tenantid : 0; |
$url = $logo_image->get_current_or_default_url(); | This goes through a few steps to determine if we have an image that is overriding the default theme image. The URL is fetched based on the following order of precedence:
If the tenant ID is set within the Tenant settings can be enabled or disabled in theme settings for a specific tenant so this checks if the specific tenant that we are referring to within the Code snippet from /** * Check if tenant branding is enabled. * * @return bool */ public function is_tenant_branding_enabled(): bool { return $this->is_enabled('tenant', 'formtenant_field_tenant', false); } Item ID is based on the record ID in Code snippet from /** * Get item ID of the theme plugin. * * @param int|null $tenant_id * @param string|null $theme * * @return int */ public function get_item_id(?int $tenant_id = null, ?string $theme = null): int { global $DB; $id = $tenant_id ?? $this->get_tenant_id(); $plugin = "theme_" . ($theme ?? $this->get_theme_config()->name); $name = "tenant_{$id}_settings"; // Always make sure that there is a record representing this config. if (!get_config($plugin, $name)) { set_config($name, '{}', $plugin); } // To keep this settings unique per theme we need to get a // unique ID and the plugin ID is as good as any. $this->item_id = $DB->get_field( 'config_plugins', 'id', [ 'plugin' => $plugin, 'name' => $name, ] ); return $this->item_id; } This basically sets the tenant ID to 0, meaning that we are not currently looking for a file specific to a tenant, but rather the site branded file. |
Theme file helper
Refer to server/lib/classes/theme/file/helper.php
 .
The theme file helper class is responsible for providing functionality that is outside the main scope of the theme file class.
The functionality in the theme file helper class includes:
- Getting all
theme_file
 derived classes - Getting a
theme_file
 object for a specific component
theme_config
Refer to server/lib/outputlib.php
.
image_url
The theme_config::image_url
 has been extended to check for any image that is overridden by theme settings.
/** * Return the direct URL for an image from the pix folder. * * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template. * * @param string $imagename the name of the icon. * @param string $component specification of one plugin like in get_string() * @param bool|null $use_override If true, check for any theme file override. * * @return moodle_url */ public function image_url($imagename, $component, ?bool $use_override = true) { ... // If this is a theme file then see if an override exists. if ($use_override) { $url = $this->get_overridden_image_url($params['component'], $imagename); if (!empty($url)) { return $url; } } ... }
For any $OUTPUT->image_url
 call, the above function will determine if there is an override in theme settings. The override is determined by calling the theme_file::get_id
 function and if that matches {$component}/{$imagename}
from $OUTPUT->image_url
then theme_file
 will check for an override in theme settings (refer to the Theme file section above).
The $use_override
 parameter can be set to false to explicitly skip the override check and use the default theme image.