@@ -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