Type-safe HTML
In Totara, content strings generally fall into two categories: HTML or plain text. If both are simple strings, distinguishing between them relies solely on documentation and conventions.
Handling HTML safely is crucial, as passing plain text to an API expecting HTML can introduce security risks such as XSS.
To mitigate this, Totara 20 introduces a type-safe API that encapsulates trusted HTML within a wrapper class, enforcing safety at the type system level.
This provides two key benefits:
Code working with HTML is able to require HTML knowingly be provided.
APIs can safely accept both HTML and plain text.
Type safety in PHP
In PHP, trusted HTML is represented by the \core\trusted_html
class. New code that accepts HTML should be declaring parameters of this type instead of string
.
You can create instances of \core\trusted_html
class using the following methods:
trusted_html::trust($html)
: Mark the provided HTML as safe (use sparingly)trusted_html::clean($html)
: Clean the provided HTMLtrusted_html::from_plain_text($html)
: Convert the provided text to HTML by escaping ittrusted_html::join($trusted)
: Join multiple existingtrusted_html
instances
Use trusted_html::trust
cautiously. Calling this method asserts that the provided HTML is already safe, making it a potential security risk. Instead, whenever possible, obtain and pass content in a trusted form from the outset.
To facilitate safer HTML handling, \core\output\html
can be used to generate HTML for output. Unlike html_writer
, it never treats a provided string as HTML unless explicitly wrapped in trusted_html
.
Example usage
// Example function that takes trusted_html
function output_content(trusted_html $content): void {
echo $content->html();
}
// Build trusted_html using the safe \core\output\html builder
$doc = html::el('div', content: [
// plain text is escaped for output
$course->name . ' ',
// as html::el returns a trusted_html instance, it is incorporated as-is
html::el('a', ['href' => '...'], 'Edit'),
]);
// This will succeed:
output_content($doc);
// <div>Potatoes & Gravy <a href="...">Edit</a></div>
// This will throw a type error:
output_content($user->name);
// Will work when updated to this:
output_content(trusted_html::from_plain_text($user->name));
Where to use it
Generally, existing APIs cannot be updated to require trusted_html
as that would be a breaking change. Instead, a new API is typically introduced, replacing the old one which may then be deprecated.
string|trusted_html
should be used for cases where a method accepts either plain text or HTML. Do not accept both a simple HTML string and trusted_html
within the same method or parameter, as this may mislead consumers of the API.
Interoperability with existing HTML handling
trusted_html
implements Stringable
, so it may be passed to existing methods that expect an HTML string, or the HTML string value can be fetched with ->html()
.
For existing APIs that return HTML as a string (such as format_string
), you will need to wrap the value with trusted_html::trust
until there are versions that return HTML natively.
Safe HTML on the frontend
In the Tui frontend, we use objects with a particular shape to represent HTML. These should never be created manually, but JSON encoding a trusted_html
object on the server will produce this, as will trustHtml()
from tui/content
(also available as $trustHtml
in Vue templates).
Within Vue, we have a v-content
directive that accepts either a string or a Trusted HTML object. Strings will be rendered as plain text. This should be used instead of v-html
.
In this way, you can safely create components that accept both plain text and HTML as a prop. As trusted_html
on the server side is encoded to JSON in the required shape, it can also be passed as a prop from the server with no extra work.
Example:
<!-- Notice.vue -->
<script>
defineProps({
message: [String, Object],
});
</script>
<template>
<div v-content="message" />
</template>
<!-- Example.vue -->
<template>
<div>
<Notice v-content="'Potatoes & Gravy'">
<Notice v-content="$trustHtml('Thirsty for <strong>coffee</strong>?')">
</div>
</template>
Result:
<div>
<div>Potatoes & Gravy</div>
<div>Thirsty for <strong>coffee</strong>?</div>
</div>
Or, passing from the server:
new component('mod_example/ui/Notice', [
'message' => trusted_html::trust('<strong>Hello</strong>')
]);
Currently, passing via GraphQL has no special behaviour – it is still just a string.