From c18163c00a548360394779e135f60478baa72cb4 Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sun, 8 Nov 2026 15:32:02 +0100 Subject: [PATCH] Document trusted HTML values and helper support --- docs/en/appendices/6-0-migration-guide.md | 9 ++ .../global-constants-and-functions.md | 27 +++++ docs/en/views.md | 99 +++++++++++++++++++ docs/en/views/helpers/form.md | 84 ++++++++++++---- docs/en/views/helpers/html.md | 38 +++++-- 5 files changed, 231 insertions(+), 26 deletions(-) diff --git a/docs/en/appendices/6-0-migration-guide.md b/docs/en/appendices/6-0-migration-guide.md index 482b2039a8..5b62be5c74 100644 --- a/docs/en/appendices/6-0-migration-guide.md +++ b/docs/en/appendices/6-0-migration-guide.md @@ -295,3 +295,12 @@ still uses other Diactoros classes or functions, declare classes and actions using PHP attributes such as `#[Get]`, `#[Post]`, `#[Scope]`, `#[Resource]`, and more. See [Attribute Routing](../development/attribute-routing) for full documentation. + +### View + +- `HtmlString` and the `Cake\View\html()` function wrap trusted markup, while + `HtmlString::render()` escapes ordinary strings and preserves values + implementing the new `HtmlStringable` contract. Supported HtmlHelper and + FormHelper content arguments accept these values without disabling escaping. + See [Rendering Trusted HTML](../views#rendering-trusted-html) for usage and + escaping rules. diff --git a/docs/en/core-libraries/global-constants-and-functions.md b/docs/en/core-libraries/global-constants-and-functions.md index 61b6f930e9..cefd3cba98 100644 --- a/docs/en/core-libraries/global-constants-and-functions.md +++ b/docs/en/core-libraries/global-constants-and-functions.md @@ -183,6 +183,10 @@ distribute the code), since it's a full emulation wrapper. Convenience wrapper for `htmlspecialchars()`. +This function also escapes `Cake\View\HtmlStringable` values. To escape +ordinary strings while preserving trusted HTML, use +[`HtmlString::render()`](../views#rendering-trusted-html). + ### pluginSplit() `function` **pluginSplit(string $name, boolean $dotAppend = false, string $plugin = null)** @@ -200,6 +204,29 @@ Split the namespace from the classname. Commonly used like `list($namespace, $className) = namespaceSplit('Cake\Core\App');` +## View Functions + +### html() + +`function` Cake\\View\\**html(string $html): HtmlString** + +Wraps trusted markup in an immutable `Cake\View\HtmlString` value. Import +the namespaced function before using it: + +```php +use function Cake\View\html; + +echo $this->Html->link(html('Read more'), '/articles'); +``` + +This function marks markup as trusted and does not sanitize it. See +[Rendering Trusted HTML](../views#rendering-trusted-html) for escaping rules, +helper support, and custom implementations. + +::: info Added in version 6.0.0 +The `Cake\View\html()` function was added. +::: + ## Core Definition Constants Most of the following constants refer to paths in your application. diff --git a/docs/en/views.md b/docs/en/views.md index 7b82ce69a2..099b4be4b2 100644 --- a/docs/en/views.md +++ b/docs/en/views.md @@ -157,6 +157,105 @@ the `h()` function: bio); ?> ``` +### Rendering Trusted HTML + +When a value can contain either plain text or HTML intended to render directly, +use `Cake\View\HtmlString` to distinguish trusted markup from text that needs +escaping. `HtmlString` is an immutable value object: + +```php +use Cake\View\HtmlString; +use function Cake\View\html; + +$content = new HtmlString('Featured article'); + +// The html() convenience function creates the same value object. +$content = html('Featured article'); + +echo $content; +// Featured article +``` + +`HtmlString` marks markup as trusted; it does not sanitize it. Only wrap HTML +you control, and escape any untrusted values before including them: + +```php +$content = html('' . h($article->title) . ''); +``` + +Use `HtmlString::render()` in templates or custom renderers that accept either +a string or an `HtmlStringable` value. It escapes ordinary strings with `h()` +and renders trusted HTML unchanged: + +```php +echo HtmlString::render('News & updates'); +// News & updates + +echo HtmlString::render(html('News')); +// News +``` + +An explicit call to `h()` still escapes trusted markup: + +```php +echo h(html('News')); +// <strong>News</strong> +``` + +Casting an `HtmlString` to a string returns its original markup. It also +implements `JsonSerializable`, so `json_encode()` serializes it as a JSON string. + +#### Using Trusted HTML with Helpers + +The [HtmlHelper](views/helpers/html#creating-links) and +[FormHelper](views/helpers/form#rendering-trusted-html) accept trusted HTML in +supported content arguments without requiring `'escape' => false`: + +```php +echo $this->Html->link(html('Read more'), '/articles'); +// Read more + +echo $this->Form->button(html('Save')); +// +``` + +Helper methods still return strings. To pass their output as trusted content +to another helper, wrap it with `html()`. Attribute values continue to be +escaped according to the helper's attribute escaping options. + +#### Implementing HtmlStringable + +Custom components and plugins can implement `Cake\View\HtmlStringable` +instead of returning an `HtmlString`. The interface extends `Stringable` and +requires a `__toString(): string` method that returns trusted HTML: + +```php +use Cake\View\HtmlStringable; + +final class Badge implements HtmlStringable +{ + public function __construct(private string $label) + { + } + + public function __toString(): string + { + return '' . h($this->label) . ''; + } +} + +echo HtmlString::render(new Badge('News & updates')); +// News & updates +``` + +Implementations are responsible for escaping any untrusted values in their +markup. The contract applies to HTML content, and does not bypass attribute +escaping or explicit calls to `h()`. + +::: info Added in version 6.0.0 +`HtmlString`, `HtmlStringable`, and `Cake\View\html()` were added. +::: + ### Setting View Variables `method` Cake\\View\\View::**set**(array|string $name, mixed $value = null): static diff --git a/docs/en/views/helpers/form.md b/docs/en/views/helpers/form.md index 43334f1b94..8d7132a0f4 100644 --- a/docs/en/views/helpers/form.md +++ b/docs/en/views/helpers/form.md @@ -13,6 +13,54 @@ re-population and layout. The FormHelper is also flexible - it will do almost everything for you using conventions, or you can use specific methods to get only what you need. +## Rendering Trusted HTML + +FormHelper accepts [HtmlString](../../views#rendering-trusted-html) and other +`Cake\View\HtmlStringable` values in the following content positions: + +- Captions for `button()` and `postButton()`, and content for `postLink()` and + `deleteLink()`. +- Label text for `label()`, the `label` option of `control()`, and the `text` + key of a label options array. +- Option labels for `radio()` and `multiCheckbox()`. +- Explicit error messages passed to `error()` or the `error` option of + `control()`, including individual messages in an array of overrides. + +These values render without escaping their markup, while ordinary strings +continue to follow the existing escaping options: + +```php +use function Cake\View\html; + +echo $this->Form->button(html('Save')); + +echo $this->Form->control('name', [ + 'label' => html('Your name'), +]); + +echo $this->Form->radio('priority', [ + ['value' => 'high', 'text' => html('High')], + ['value' => 'normal', 'text' => 'Normal'], +]); + +// Override one message while retaining other validation messages. +echo $this->Form->error('name', [ + '_empty' => html('Please enter your name.'), +]); +``` + +Only explicit error overrides are treated as trusted HTML. Messages obtained +from the form context, including fallback messages without an override, remain +escaped by default even if they implement `HtmlStringable`. + +Wrapping a value does not bypass escaping in attributes, input values, +textarea contents, or select option text. Only wrap markup you control; +`HtmlString` does not sanitize HTML. + +::: info Added in version 6.0.0 +Support for `HtmlStringable` content was added. +::: + ## Starting a Form `method` Cake\\View\\Helper\\FormHelper::**create**(mixed $context = null, array $options = []): string @@ -1833,10 +1881,10 @@ If it was 2009, you would get the following: ## Creating Labels -`method` Cake\\View\\Helper\\FormHelper::**label**(string $fieldName, string $text, array $options): string +`method` Cake\\View\\Helper\\FormHelper::**label**(string $fieldName, HtmlStringable|string|null $text = null, array $options = []): string - `$fieldName` - A field name in the form `'Modelname.fieldname'`. -- `$text` - An optional string providing the label caption text. +- `$text` - An optional string or `HtmlStringable` value providing the label caption. - `$options` - Optional. Array containing any of the [General Control Options](#general-control-options) as well as any valid HTML attributes. @@ -1882,7 +1930,7 @@ field errors and when necessary display customized error messages. `method` Cake\\View\\Helper\\FormHelper::**error**(string $fieldName, mixed $text, array $options): string - `$fieldName` - A field name in the form `'Modelname.fieldname'`. -- `$text` - Optional. A string or array providing the error message(s). If an +- `$text` - Optional. A string, `HtmlStringable` value, or array providing the error message(s). If an array, then it should be a hash of key names => messages. Defaults to `null`. - `$options` - An optional array that can only contain a boolean with the key @@ -2053,15 +2101,15 @@ more complex button content you should use `button()`. ### Creating Button Elements -`method` Cake\\View\\Helper\\FormHelper::**button**(string $title, array $options = []): string +`method` Cake\\View\\Helper\\FormHelper::**button**(HtmlStringable|string $content, array $options = []): string -- `$title` - Mandatory string providing the button's text caption. +- `$content` - Mandatory string or `HtmlStringable` value providing the button's caption. - `$options` - An optional array including any of the [General Control Options](#general-control-options), or of the specific button options (see below) as well as any valid HTML attributes. -Creates an HTML button with the specified title and a default type -of `'button'`. +Creates an HTML button with the specified content and a default type +of `'submit'`. **Options for Button** @@ -2072,9 +2120,9 @@ of `'button'`. as `submit()` does. This is the default type. 2. `'reset'` - Creates a form reset button. 3. `'button'` - Creates a standard push button. -- `escapeTitle` - Boolean. If set to `true` it will HTML encode - the value provided inside `$title`. Defaults to `true`. - `escape` - Boolean. If set to `true` it will HTML encode + ordinary strings provided inside `$content`. Defaults to `true`. +- `escapeAttributes` - Boolean. If set to `true` it will HTML encode all the HTML attributes generated for the button. Defaults to `true`. - `confirm` - The confirmation message to display on click. Defaults to `null`. @@ -2097,13 +2145,13 @@ Will output: ``` -Example use of the `'escapeTitle'` option: +Example use of the `'escape'` option: ```php // Will render unescaped HTML. echo $this->Form->button('Submit Form', [ 'type' => 'submit', - 'escapeTitle' => false, + 'escape' => false, ]); ``` @@ -2165,10 +2213,10 @@ template option on `FormHelper`. ### Creating POST Buttons -`method` Cake\\View\\Helper\\FormHelper::**postButton**(string $title, mixed $url, array $options = []): string +`method` Cake\\View\\Helper\\FormHelper::**postButton**(HtmlStringable|string $content, array|string $url, array $options = []): string -- `$title` - Mandatory string providing the button's text caption. By default - not HTML encoded. +- `$content` - Mandatory string or `HtmlStringable` value providing the button's + caption. Ordinary strings are HTML encoded by default. - `$url` - The URL of the form provided as a string or as array. - `$options` - An optional array including any of the [General Control Options](#general-control-options), or of the specific options (see below) as well @@ -2218,9 +2266,9 @@ inside opened forms. ### Creating POST Links -`method` Cake\\View\\Helper\\FormHelper::**postLink**(string $title, array|string|null $url = null, array $options = []): string +`method` Cake\\View\\Helper\\FormHelper::**postLink**(HtmlStringable|string $content, array|string|null $url = null, array $options = []): string -- `$title` - Mandatory string providing the text to be wrapped in `` +- `$content` - Mandatory string or `HtmlStringable` value to be wrapped in `` tags. - `$url` - Optional. String or array which contains the URL of the form (Cake-relative or external URL starting with `http://`). @@ -2269,9 +2317,9 @@ use `Cake\View\Helper\FormHelper::button()` or ### Creating DELETE Links -`method` Cake\\View\\Helper\\FormHelper::**deleteLink**(string $title, array|string|null $url = null, array $options = []): string +`method` Cake\\View\\Helper\\FormHelper::**deleteLink**(HtmlStringable|string $content, array|string|null $url = null, array $options = []): string -- `$title` - Mandatory string providing the text to be wrapped in `` +- `$content` - Mandatory string or `HtmlStringable` value to be wrapped in `` tags. - `$url` - Optional. String or array which contains the URL of the form (Cake-relative or external URL starting with `http://`). diff --git a/docs/en/views/helpers/html.md b/docs/en/views/helpers/html.md index 959620cee1..42e80e0fca 100644 --- a/docs/en/views/helpers/html.md +++ b/docs/en/views/helpers/html.md @@ -313,11 +313,11 @@ Will output: ### Creating Links -`method` Cake\\View\\Helper\\HtmlHelper::**link**(array|string $title, array|string|null $url = null, array $options = []): string +`method` Cake\\View\\Helper\\HtmlHelper::**link**(HtmlStringable|array|string $content, array|string|null $url = null, array $options = []): string General purpose method for creating HTML links. Use `$options` to specify attributes for the element and whether or not the -`$title` should be escaped. +`$content` should be escaped. ```php echo $this->Html->link( @@ -387,7 +387,7 @@ Will output: View image ``` -HTML special characters in `$title` will be converted to HTML +HTML special characters in string `$content` will be converted to HTML entities. To disable this conversion, set the escape option to `false` in the `$options` array. @@ -407,15 +407,14 @@ Will output: ``` -Setting `escape` to `false` will also disable escaping of attributes of the -link. You can use the option `escapeTitle` to disable just -escaping of title and not the attributes. +The `escape` option controls the link content, while `escapeAttributes` +controls attribute escaping independently. Attributes are escaped by default: ```php echo $this->Html->link( $this->Html->image('recipes/6.jpg', ['alt' => 'Brownies']), 'recipes/view/6', - ['escapeTitle' => false, 'title' => 'hi "howdy"'], + ['escape' => false, 'title' => 'hi "howdy"'], ); ``` @@ -427,12 +426,35 @@ Will output: ``` +You can also wrap trusted content in an [HtmlString](../../views#rendering-trusted-html) +to render it without changing the escaping options: + +```php +use function Cake\View\html; + +echo $this->Html->link( + html($this->Html->image('recipes/6.jpg', ['alt' => 'Brownies'])), + 'recipes/view/6', + ['title' => 'hi "howdy"'], +); +``` + +When the content implements `Cake\View\HtmlStringable`, an explicit URL is +required. Omitting the URL throws an `InvalidArgumentException`. +`linkFromPath()` also accepts trusted HTML content. The `tag()`, `div()`, and +`para()` methods accept it as their text argument and preserve its markup even +when `'escape' => true`. Attribute escaping is unaffected. + +::: info Added in version 6.0.0 +Support for `HtmlStringable` content was added. +::: + Also check `Cake\View\Helper\UrlHelper::build()` method for more examples of different types of URLs. ### Creating Links from Route Paths -`method` Cake\\View\\Helper\\HtmlHelper::**linkFromPath**(string $title, string $path, array $params = [], array $options = []): string +`method` Cake\\View\\Helper\\HtmlHelper::**linkFromPath**(HtmlStringable|string $content, string $path, array $params = [], array $options = []): string If you want to use route path strings, you can do that using this method: