diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php
index 3b2dcb1237971..cfcba51a64042 100644
--- a/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php
+++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php
@@ -198,16 +198,19 @@ private function get_balanced_tag_bookmarks() {
public function skip_to_tag_closer(): bool {
$depth = 1;
$tag_name = $this->get_tag();
- while ( $depth > 0 && $this->next_tag(
- array(
- 'tag_name' => $tag_name,
- 'tag_closers' => 'visit',
- )
- ) ) {
- if ( $this->has_self_closing_flag() ) {
- continue;
+
+ while ( $depth > 0 && $this->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
+ if ( ! $this->is_tag_closer() && $this->get_attribute_names_with_prefix( 'data-wp-' ) ) {
+ /* translators: 1: SVG or MATH HTML tag. */
+ $message = sprintf( __( 'Interactivity directives were detected inside an incompatible %1$s tag. These directives will be ignored in the server side render.' ), $tag_name );
+ _doing_it_wrong( __METHOD__, $message, '6.6.0' );
+ }
+ if ( $this->get_tag() === $tag_name ) {
+ if ( $this->has_self_closing_flag() ) {
+ continue;
+ }
+ $depth += $this->is_tag_closer() ? -1 : 1;
}
- $depth += $this->is_tag_closer() ? -1 : 1;
}
return 0 === $depth;
diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php
index ac9a48982a5c5..c15bb7ef76b54 100644
--- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php
+++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php
@@ -272,6 +272,7 @@ public function process_directives( string $html ): string {
* it returns null if the HTML contains unbalanced tags.
*
* @since 6.5.0
+ * @since 6.6.0 The function displays a warning message when the HTML contains unbalanced tags or a directive appears in a MATH or SVG tag.
*
* @param string $html The HTML content to process.
* @param array $context_stack The reference to the array used to keep track of contexts during processing.
@@ -295,6 +296,11 @@ private function process_directives_args( string $html, array &$context_stack, a
* We still process the rest of the HTML.
*/
if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
+ if ( $p->get_attribute_names_with_prefix( 'data-wp-' ) ) {
+ /* translators: 1: SVG or MATH HTML tag, 2: Namespace of the interactive block. */
+ $message = sprintf( __( 'Interactivity directives were detected on an incompatible %1$s tag when processing "%2$s". These directives will be ignored in the server side render.' ), $tag_name, end( $namespace_stack ) );
+ _doing_it_wrong( __METHOD__, $message, '6.6.0' );
+ }
$p->skip_to_tag_closer();
continue;
}
@@ -382,13 +388,21 @@ private function process_directives_args( string $html, array &$context_stack, a
}
}
}
-
/*
* It returns null if the HTML is unbalanced because unbalanced HTML is
* not safe to process. In that case, the Interactivity API runtime will
- * update the HTML on the client side during the hydration.
+ * update the HTML on the client side during the hydration. It will also
+ * display a notice to the developer to inform them about the issue.
*/
- return $unbalanced || 0 < count( $tag_stack ) ? null : $p->get_updated_html();
+ if ( $unbalanced || 0 < count( $tag_stack ) ) {
+ $tag_errored = 0 < count( $tag_stack ) ? end( $tag_stack )[0] : $tag_name;
+ /* translators: %1s: Namespace processed, %2s: The tag that caused the error; could be any HTML tag. */
+ $message = sprintf( __( 'Interactivity directives failed to process in "%1$s" due to a missing "%2$s" end tag.' ), end( $namespace_stack ), $tag_errored );
+ _doing_it_wrong( __METHOD__, $message, '6.6.0' );
+ return null;
+ }
+
+ return $p->get_updated_html();
}
/**
@@ -396,17 +410,21 @@ private function process_directives_args( string $html, array &$context_stack, a
* store namespace, state and context.
*
* @since 6.5.0
+ * @since 6.6.0 The function now adds a warning when the namespace is null, falsy, or the directive value is empty.
*
* @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute.
* @param string $default_namespace The default namespace to use if none is explicitly defined in the directive
* value.
* @param array|false $context The current context for evaluating the directive or false if there is no
* context.
- * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist.
+ * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist or the namespace is falsy.
*/
private function evaluate( $directive_value, string $default_namespace, $context = false ) {
list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace );
- if ( empty( $path ) ) {
+ if ( ! $ns || ! $path ) {
+ /* translators: %s: The directive value referenced. */
+ $message = sprintf( __( 'Namespace or reference path cannot be empty. Directive value referenced: %s' ), $directive_value );
+ _doing_it_wrong( __METHOD__, $message, '6.6.0' );
return null;
}
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php
index ef1b79326d949..0a6ffcb2a5da0 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php
@@ -154,6 +154,7 @@ public function test_wp_bind_doesnt_do_anything_on_non_existent_references() {
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_bind_ignores_empty_value() {
$html = '
Text
';
@@ -167,6 +168,7 @@ public function test_wp_bind_ignores_empty_value() {
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_bind_ignores_without_value() {
$html = 'Text
';
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php
index 95fdaed6f7504..b9cf16952be91 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php
@@ -237,6 +237,7 @@ public function test_wp_class_doesnt_change_class_attribute_with_empty_directive
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_class_doesnt_change_class_attribute_with_empty_value() {
$html = 'Text
';
@@ -251,6 +252,7 @@ public function test_wp_class_doesnt_change_class_attribute_with_empty_value() {
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_class_doesnt_change_class_attribute_without_value() {
$html = 'Text
';
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php
index 469e1d6e1418a..93e2528126215 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php
@@ -317,6 +317,7 @@ public function test_wp_context_works_with_multiple_directives() {
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_context_directive_doesnt_work_without_any_namespace() {
$html = '
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php
index 42edd9f1b3838..fa166c1799d33 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php
@@ -580,6 +580,8 @@ public function test_wp_each_nested_template_tags_using_previous_item_as_list()
* @ticket 60356
*
* @covers ::process_directives
+ *
+ * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
*/
public function test_wp_each_unbalanced_tags() {
$original = '' .
@@ -598,6 +600,8 @@ public function test_wp_each_unbalanced_tags() {
* @ticket 60356
*
* @covers ::process_directives
+ *
+ * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
*/
public function test_wp_each_unbalanced_tags_in_nested_template_tags() {
$this->interactivity->state( 'myPlugin', array( 'list2' => array( 3, 4 ) ) );
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php
index d3ac196eae484..3b645b0dd4df3 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php
@@ -365,6 +365,7 @@ public function test_wp_style_doesnt_change_style_attribute_with_empty_directive
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_style_doesnt_change_style_attribute_with_empty_value() {
$html = 'Text
';
@@ -379,6 +380,7 @@ public function test_wp_style_doesnt_change_style_attribute_with_empty_value() {
* @ticket 60356
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
*/
public function test_wp_style_doesnt_change_style_attribute_without_value() {
$html = 'Text
';
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php
index 3c2050cab38ed..5b50978980b8d 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php
@@ -649,7 +649,10 @@ public function test_process_directives_process_the_directives_in_the_correct_or
*
* @dataProvider data_html_with_unbalanced_tags
*
+ * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
+ *
* @param string $html HTML containing unbalanced tags and also a directive.
+ *
*/
public function test_process_directives_doesnt_change_html_if_contains_unbalanced_tags( $html ) {
$this->interactivity->state( 'myPlugin', array( 'id' => 'some-id' ) );
@@ -696,22 +699,17 @@ public function test_process_directives_changes_html_if_contains_svgs() {
);
$html = '
';
$processed_html = $this->interactivity->process_directives( $html );
$p = new WP_HTML_Tag_Processor( $processed_html );
- $p->next_tag( 'svg' );
- $this->assertNull( $p->get_attribute( 'width' ) );
$p->next_tag( 'div' );
$this->assertEquals( 'some-id', $p->get_attribute( 'id' ) );
- $p->next_tag( 'div' );
- $this->assertEquals( '100', $p->get_attribute( 'id' ) );
}
/**
@@ -721,6 +719,7 @@ public function test_process_directives_changes_html_if_contains_svgs() {
* @ticket 60517
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer
*/
public function test_process_directives_does_not_change_inner_html_in_svgs() {
$this->interactivity->state(
@@ -750,6 +749,7 @@ public function test_process_directives_does_not_change_inner_html_in_svgs() {
* @ticket 60517
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
*/
public function test_process_directives_change_html_if_contains_math() {
$this->interactivity->state(
@@ -784,6 +784,8 @@ public function test_process_directives_change_html_if_contains_math() {
* @ticket 60517
*
* @covers ::process_directives
+ * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
+ * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer
*/
public function test_process_directives_does_not_change_inner_html_in_math() {
$this->interactivity->state(
@@ -814,7 +816,7 @@ public function test_process_directives_does_not_change_inner_html_in_math() {
* @param string $directive_value The directive attribute value to evaluate.
* @return mixed The result of the evaluate method.
*/
- private function evaluate( $directive_value ) {
+ private function evaluate( $directive_value, $default_namespace = 'myPlugin' ) {
$generate_state = function ( $name ) {
return array(
'key' => $name,
@@ -829,7 +831,7 @@ private function evaluate( $directive_value ) {
);
$evaluate = new ReflectionMethod( $this->interactivity, 'evaluate' );
$evaluate->setAccessible( true );
- return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, 'myPlugin', $context ) );
+ return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, $default_namespace, $context ) );
}
/**
@@ -923,6 +925,25 @@ public function test_evaluate_nested_value() {
$this->assertEquals( 'otherPlugin-context-nested', $result );
}
+ /**
+ * Tests the `evaluate` method for non valid namespace values.
+ *
+ * @ticket 61044
+ *
+ * @covers ::evaluate
+ * @expectedIncorrectUsage WP_Interactivity_API::evaluate
+ */
+ public function test_evaluate_unvalid_namespaces() {
+ $result = $this->evaluate( 'path', 'null' );
+ $this->assertNull( $result );
+
+ $result = $this->evaluate( 'path', '' );
+ $this->assertNull( $result );
+
+ $result = $this->evaluate( 'path', '{}' );
+ $this->assertNull( $result );
+ }
+
/**
* Tests the `kebab_to_camel_case` method.
*
diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPIWPText.php
similarity index 97%
rename from tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php
rename to tests/phpunit/tests/interactivity-api/wpInteractivityAPIWPText.php
index 2fa9363ac93f4..b24c3678fb79a 100644
--- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php
+++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPIWPText.php
@@ -12,7 +12,7 @@
*
* @group interactivity-api
*/
-class Tests_WP_Interactivity_API_WP_Text extends WP_UnitTestCase {
+class Tests_Interactivity_API_WpInteractivityAPIWPText extends WP_UnitTestCase {
/**
* Instance of WP_Interactivity_API.
*
@@ -117,6 +117,7 @@ public function test_wp_text_sets_inner_content_with_nested_tags() {
* @ticket 60356
*
* @covers ::process_directives
+ *
*/
public function test_wp_text_sets_inner_content_even_with_unbalanced_but_different_tags_inside_content() {
$html = 'Text
';
@@ -131,6 +132,8 @@ public function test_wp_text_sets_inner_content_even_with_unbalanced_but_differe
* @ticket 60356
*
* @covers ::process_directives
+ *
+ * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
*/
public function test_wp_text_fails_with_unbalanced_and_same_tags_inside_content() {
$html = '