Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -479,31 +479,22 @@ function attribute_has_directive( $value ) {
}

/**
* Whether any tag in the HTML carries an Interactivity API `data-wp-*` attribute.
* Whether the HTML carries an Interactivity API `data-wp-*` marker.
*
* The sanitised markup is scanned too: a raw-text element hides its content from the tokenizer, and KSES
* then deletes that wrapper on save and keeps the tags it held.
* A pattern may not carry one at all, in a tag or anywhere else, so the marker itself is the test.
*
* Reading it as markup instead would be narrower than the rule. `WP_HTML_Tag_Processor` does not
* descend into a raw-text or RCDATA element, so a tag inside one is invisible to it, and scanning the
* `wp_kses_post()` form as well only helps where KSES removes the element that did the hiding. It does
* that for `<script>` and `<style>`, and not for `<title>` or `<textarea>`, which it keeps: those hold
* their contents as text, so neither pass ever reads the tag. A substring test has no such blind spot.
*
* @param string $html The HTML to scan.
*
* @return bool Whether a directive is present.
*/
function content_has_block_directives( $html ) {
// Directives are rare; don't sanitise or tokenize the whole document when the marker can't be present.
if ( false === stripos( $html, 'data-wp-' ) ) {
return false;
}

foreach ( array( $html, wp_kses_post( $html ) ) as $markup ) {
$tags = new \WP_HTML_Tag_Processor( $markup );
while ( $tags->next_tag() ) {
if ( $tags->get_attribute_names_with_prefix( 'data-wp-' ) ) {
return true;
}
}
}

return false;
return false !== stripos( $html, 'data-wp-' );
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ public function data_invalid_content() {
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:image -->\n<figure class=\"wp-block-image\"><img data-wp-bind--src=\"context.href\" alt=\"\"/></figure>\n<!-- /wp:image -->" ),

/*
* A raw-text element hides its content from the tokenizer, but KSES deletes the wrapper on save
* and leaves what it held as live markup. Every element the tokenizer skips carries the same payload.
* The rows below put the directive somewhere a check that read the content as markup would not
* have found it. An element whose contents are text -- raw-text or RCDATA -- hides the tag from
* `WP_HTML_Tag_Processor`, and nesting or leaving one unclosed hides it further.
*/
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:paragraph -->\n<p>Three.<style><span data-wp-interactive=\"wporg/patterns\" data-wp-init=\"actions.go\">x</span></style></p>\n<!-- /wp:paragraph -->" ),
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:paragraph -->\n<p>Three.<xmp><span data-wp-interactive=\"wporg/patterns\" data-wp-init=\"actions.go\">x</span></xmp></p>\n<!-- /wp:paragraph -->" ),
Expand All @@ -176,10 +177,20 @@ public function data_invalid_content() {
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:paragraph -->\n<p>Three.<script><span data-wp-interactive=\"wporg/patterns\" data-wp-init=\"actions.go\">x</span></p>\n<!-- /wp:paragraph -->" ),
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:paragraph -->\n<p>Three.<style><style><style><span data-wp-interactive=\"wporg/patterns\" data-wp-init=\"actions.go\">x</span></style></p>\n<!-- /wp:paragraph -->" ),

// `<svg>` is foreign content rather than a raw-text element, so the nested `<script>` hides the anchor too.
// The same `<script>`, inside `<svg>`, closed and unclosed.
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:html -->\n<svg><script><a href=\"#\" data-wp-interactive=\"core/query\" data-wp-context='{\"url\":\"javascript:alert(1)\"}' data-wp-bind--href=\"context.url\">x</a></svg>\n<!-- /wp:html -->" ),
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:html -->\n<svg><script><a href=\"#\" data-wp-interactive=\"core/query\" data-wp-bind--href=\"context.url\">x</a></script></svg>\n<!-- /wp:html -->" ),

/*
* `<title>` and `<textarea>` hold their contents as text like `<script>` does, but KSES keeps
* both elements, so sanitising the markup first does not expose the tag the way it does for a
* wrapper KSES removes. Nothing that reads these as markup sees the directive at all.
*/
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:html -->\n<title><a href=\"#\" data-wp-interactive=\"wporg/patterns\" data-wp-bind--href=\"context.url\">x</a></title>\n<!-- /wp:html -->" ),
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:html -->\n<textarea><a href=\"#\" data-wp-interactive=\"wporg/patterns\" data-wp-bind--href=\"context.url\">x</a></textarea>\n<!-- /wp:html -->" ),
// The marker in ordinary text, with no tag anywhere near it.
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:paragraph -->\n<p>Bind it with data-wp-bind--href.</p>\n<!-- /wp:paragraph -->" ),

// A block delimiter is a comment, so the directive in its attribute JSON is not a tag either.
array( 'rest_pattern_interactivity_directive', "$two_paragraphs\n\n<!-- wp:categories {\"displayAsDropdown\":true,\"showLabel\":true,\"label\":\"<span data-wp-interactive=\\u0022wporg/patterns\\u0022 data-wp-init=\\u0022actions.go\\u0022>x</span>\"} /-->" ),
// The same attribute with its angle brackets JSON-escaped, so the stored delimiter holds no markup.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ public function data_disallowed_content(): array {
return array(
'directive in inner HTML' => array( "<!-- wp:paragraph -->\n<p><span data-wp-interactive=\"x\" data-wp-init=\"actions.go\">t</span></p>\n<!-- /wp:paragraph -->" ),
'directive in a raw-text el' => array( "<!-- wp:paragraph -->\n<p><style><span data-wp-interactive=\"x\" data-wp-init=\"actions.go\">t</span></style></p>\n<!-- /wp:paragraph -->" ),
// `<svg>` opens foreign content, so the tokenizer reads the nested `<script>` as text while KSES keeps what it held.
// The same `<script>`, inside `<svg>`.
'directive in foreign content' => array( "<!-- wp:html -->\n<svg><script><a href=\"#\" data-wp-interactive=\"x\" data-wp-bind--href=\"context.url\">t</a></svg>\n<!-- /wp:html -->" ),
// `<title>` holds its contents as text and KSES keeps the element, so nothing reading this as markup sees the tag.
'directive in RCDATA' => array( "<!-- wp:html -->\n<title><a href=\"#\" data-wp-interactive=\"x\" data-wp-bind--href=\"context.url\">t</a></title>\n<!-- /wp:html -->" ),
'directive in an attribute' => array( '<!-- wp:heading {"placeholder":"<span data-wp-interactive="x" data-wp-init="actions.go">t</span>"} -->' . "\n<h2>t</h2>\n<!-- /wp:heading -->" ),
'disallowed block' => array( '<!-- wp:shortcode -->[gallery]<!-- /wp:shortcode -->' ),
);
Expand Down