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:

TabCategoryPropertyDescriptionTypeDependencyTenant customisable
Brandbrandformbrand_field_logoalttextName of the site to function as a text alternative to the logo image.text-Yes
Colours






colours






color-statePrimary brand colour to set the colour for interactive elements.value-Yes
formcolours_field_useoverridesSwitch indicating that specific colour properties will be enabled or disabled depending of this value.boolean-Yes
color-primaryAn additional accent colour to set the highlight colour of non-interactive elements.value-Yes
nav-bg-colorBackground colour of the main navigation.valueformcolours_field_useoverridesYes
nav-text-colorText colour of the main navigation.valueformcolours_field_useoverridesYes
color-textMain text colour of the site.valueformcolours_field_useoverridesYes
footer-bg-colorBackground colour of the footer.valueformcolours_field_useoverridesYes
footer-text-colorText colour of the footer.valueformcolours_field_useoverridesYes
Imagesimagesformimages_field_displayloginSwitch indicating that the login image will be enabled/disabled.boolean-Yes
formimages_field_loginalttextText alternative that conveys the content and function of the login image.text-Yes
Customcustomformcustom_field_customfooterText that will be visible in the footer for the currently selected theme.text-Yes
formcustom_field_customcssCSS 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:

TabUI KeyDescriptionDependencyTenant customisable
BrandsitelogoImage to be used as the site’s logo.-Yes
sitefaviconBrowser tab icon that will appear next to your site's title in the browser.-Yes
Images




siteloginLogin page default image.-Yes
learncourseCourse default image.-Yes
learnprogramProgram default image.'programs' advanced featureYes
learncertCertification default image.'certifications' advanced featureYes
engageresourceResource default image.'engage_resources' advanced featureYes
engageworkspaceWorkspace default image.'container_workspace' advanced featureYes

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:

  1. server/totara/tui/classes/controllers/theme_settings.php

  2. 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 

This is a temporary solution and a longer term solution is in the works (Partners can see TL-30019 in the public tracker for more information). 

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:

  1. plugin = 'theme_' + theme name
  2. 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
  3. value = JSON representation of the settings specifically related to this tenant

Examples

An example of the settings being applied.

This record means that there are currently no settings altered for the site and the defaults will apply.

An example of the settings being applied.

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:

  1. 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"
  2. 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"
  3. 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:

  1. category (string): The name of the category that you want the property to be extracted from
  2. property (string): The name of the property you want the value of
  3. 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:

  1. category (string): The name of the category that you want the property to be extracted from
  2. property (string): The name of the property you want to evaluate
  3. 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:

  1. client/component/tui/src/pages/ThemeSettings.vue
  2. 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 $PAGE  variable.

This declaration should be used with great caution - the $PAGE  variable is only correctly referred to in requests where the page is being served. For any service request it should be strongly avoided. If theme_config is needed in a service request then consider passing the theme with the request in order to set up the theme_config object correctly using the same theme the page is using.

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 \core\theme\file\theme_file  object, i.e logo_image using the theme_config  instance created earlier.


$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 $USER  global variable:

$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:

  1. Tenant (skipped during initial install)
  2. Site (skipped during initial install)
  3. Theme default




1. Tenant?

If the tenant ID is set within the theme_file  then it means that we are looking for a file belonging to the tenant.


2. Enabled?

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 theme_file  is enabled.

Code snippet from core\theme\settings::is_tenant_branding_enabled:

/**
 * Check if tenant branding is enabled.
 *
 * @return bool
 */
public function is_tenant_branding_enabled(): bool {
    return $this->is_enabled('tenant', 'formtenant_field_tenant', false);
}


3. Get item ID

Item ID is based on the record ID in config_plugins. Refer to 'How settings are saved' heading above for more information.

Code snippet from core\theme\settings::get_item_id:

/**
 * 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;
}


4. Fall back to site

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.