Skip to content

Commit b260222

Browse files
committed
Interactivity API: Document the trusted HTML contract and safe usage.
1 parent 3299ff3 commit b260222

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

‎src/wp-includes/interactivity-api/class-wp-interactivity-dangerous-html.php‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
*
55
* @package WordPress
66
* @subpackage Interactivity API
7+
* @since 7.2.0
78
*/
89

910
/**
1011
* Opaque identity token for HTML registered by wp_interactivity_as_dangerous_html().
12+
*
13+
* Only that function creates instances that render; an instance made any other
14+
* way, such as with `new`, is not recognized. See it for how to use the token and
15+
* supply HTML safely.
16+
*
17+
* @since 7.2.0
18+
*
19+
* @see wp_interactivity_as_dangerous_html()
1120
*/
1221
final class WP_Interactivity_Dangerous_HTML {}

‎src/wp-includes/interactivity-api/interactivity-api.php‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@ function wp_interactivity(): WP_Interactivity_API {
3131
* Processes the interactivity directives contained within the HTML content
3232
* and updates the markup accordingly.
3333
*
34+
* To render trusted HTML with a `data-wp-html` directive, see
35+
* wp_interactivity_as_dangerous_html() for how to supply it safely and for what
36+
* the directive does when it declines to render.
37+
*
3438
* @since 6.5.0
3539
*
40+
* @see wp_interactivity_as_dangerous_html()
41+
*
3642
* @param string $html The HTML content to process.
3743
* @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags.
3844
*/
@@ -142,6 +148,129 @@ function wp_interactivity_get_element(): ?array {
142148
/**
143149
* Creates an opaque token for verbatim server-rendered HTML.
144150
*
151+
* During server directive processing, a `data-wp-html` directive whose
152+
* reference resolves to the returned token replaces the content of its element
153+
* with the registered HTML, unchanged. This function is part of Core; no plugin
154+
* is needed to use it.
155+
*
156+
* Token identity:
157+
*
158+
* The token is the only value `data-wp-html` renders, and it is recognized by
159+
* its original object identity within the current request. The token holds no
160+
* HTML and has no declared public methods, and writing properties to it does
161+
* not replace the HTML registered for it. A string, even a sanitized one, is
162+
* not a token. Neither is a clone, an object created with `new` or by
163+
* unserializing, or a value decoded from context or JSON: none of them acquire
164+
* trust. A token stored in state, or returned by a derived state getter, can be
165+
* rendered any number of times.
166+
*
167+
* Safety:
168+
*
169+
* Neither this function nor the rendering of `data-wp-html` sanitizes or
170+
* validates the HTML. Passing untrusted input creates a cross-site scripting
171+
* (XSS) vulnerability, and passing malformed HTML can break the page. HTML that
172+
* contains tags such as `body` can change the page's own root elements.
173+
* Sanitize the HTML where the string is produced, for example with
174+
* wp_kses_post().
175+
*
176+
* Never pass a value read from context to this function, because any ancestor
177+
* block can override context. Use context only to select, by ID, which value
178+
* held in state to show, and take the HTML itself from state. This is guidance
179+
* for the caller: nothing checks at runtime where the string came from.
180+
*
181+
* Rendering and declines:
182+
*
183+
* The reference is evaluated with the namespace and context of the element that
184+
* carries `data-wp-html`. When it resolves to a token, only the content of that
185+
* element is replaced. The element and its attributes, including
186+
* `data-wp-html`, are kept, its other directives are processed as usual, and
187+
* processing continues with the elements that follow. Directives inside the
188+
* inserted HTML are not processed, and the inserted HTML is not checked for
189+
* balanced tags. Inside a `template` with `data-wp-each`, the directive renders
190+
* once per item, with that item in context, and the rendered items keep their
191+
* usual `data-wp-each-child` markings. The directive does not accept an HTML
192+
* string directly.
193+
*
194+
* When the directive declines to render, the element keeps its content as a
195+
* fallback and is processed as it would be without `data-wp-html`. Only a
196+
* non-empty `data-wp-html` entry without a suffix or a unique ID acts; any other
197+
* `data-wp-html` entry is ignored without a notice. A `null` value, such as for
198+
* a reference that is not defined, also keeps the fallback without a notice,
199+
* unless the element is ineligible or combined as described below. These
200+
* declines raise a `_doing_it_wrong()` notice, each for a distinct reason:
201+
*
202+
* - The value is not a token created by this function.
203+
* - The element cannot hold content: a void element, or `script`, `style`,
204+
* `textarea`, `title`, `iframe`, `noembed`, `noframes` or `xmp`.
205+
* - The element also has any `data-wp-text` entry, or is a `template` with any
206+
* `data-wp-each` entry. This takes precedence over the other reasons and
207+
* applies even if the `data-wp-html` entry is empty or has a suffix or a
208+
* unique ID. `data-wp-text` and `data-wp-each` then run as usual.
209+
*
210+
* A notice names the reason, the element's tag and, where available, the
211+
* directive's reference. It never includes the evaluated data. Errors raised
212+
* while evaluating the reference, such as in a derived state getter, have their
213+
* own notices. Directives inside `svg` and `math` elements are still not
214+
* processed.
215+
*
216+
* The client boundary:
217+
*
218+
* The directive does not write tokens or HTML to the data the Interactivity API
219+
* prints, or to context attributes, and `data-wp-html` stays on the element.
220+
* Strings held in state are still printed, so HTML kept in state, as in the
221+
* example below, is visible to the client. A token placed in state serializes
222+
* as an empty object: it carries neither its registered HTML nor its trust.
223+
* Evaluating a derived state getter can record the path of that getter in the
224+
* printed data, even if the directive then declines to render.
225+
*
226+
* The server never gives the client trust: the client has to create its own
227+
* token, with `asDangerousHTML()` from `@wordpress/interactivity`, in a client
228+
* that supports `data-wp-html`. The server rendering and that client support are
229+
* released in coordination. On such a client, the server-rendered content is
230+
* expected to stay in place until the client value resolves to a client token,
231+
* and each item of a `data-wp-each` list is expected to be reused only when the
232+
* client list has the same items in the same number and order. Items that the
233+
* client mounts afresh start from the template's content. These behaviors
234+
* belong to the client and are not guaranteed by the server processing.
235+
*
236+
* Example:
237+
*
238+
* wp_interactivity_state(
239+
* 'myplugin',
240+
* array(
241+
* // The HTML is sanitized where it is produced, and kept in state by ID.
242+
* 'htmlById' => array(
243+
* 'intro' => wp_kses_post( '<p>Welcome to <em>my</em> site.</p>' ),
244+
* 'outro' => wp_kses_post( '<p>Thanks for reading.</p>' ),
245+
* ),
246+
* 'sectionIds' => array( 'intro', 'outro' ),
247+
* // The context only selects the ID. The token is created here, from state.
248+
* 'sectionHtml' => static function () {
249+
* $id = wp_interactivity_get_context()['sectionId'] ?? null;
250+
* $state = wp_interactivity_state();
251+
* $html = is_string( $id ) ? ( $state['htmlById'][ $id ] ?? null ) : null;
252+
* return is_string( $html ) ? wp_interactivity_as_dangerous_html( $html ) : null;
253+
* },
254+
* )
255+
* );
256+
*
257+
* <div data-wp-interactive="myplugin">
258+
* <template data-wp-each--section-id="state.sectionIds">
259+
* <section data-wp-html="state.sectionHtml">Fallback content</section>
260+
* </template>
261+
* </div>
262+
*
263+
* Each rendered `section` shows the HTML for its ID. An ID that has no HTML
264+
* returns `null`, so its `section` keeps "Fallback content" without a notice.
265+
*
266+
* @since 7.2.0
267+
*
268+
* @see WP_Interactivity_Dangerous_HTML
269+
* @see wp_interactivity_state()
270+
* @see wp_interactivity_get_context()
271+
* @see wp_interactivity_process_directives()
272+
* @see wp_kses_post()
273+
*
145274
* @param string $html HTML to register.
146275
* @return WP_Interactivity_Dangerous_HTML The registered token.
147276
*/

0 commit comments

Comments
 (0)