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 f6764736f9e8a..8283349868d64 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -579,6 +579,36 @@ private function evaluate( $directive_value ) { $path_segments = explode( '.', $path ); $current = $store; foreach ( $path_segments as $path_segment ) { + /* + * Special case for numeric arrays and strings. Add length + * property mimicking JavaScript behavior. + * + * @since 6.8.0 + */ + if ( 'length' === $path_segment ) { + if ( is_array( $current ) && array_is_list( $current ) ) { + $current = count( $current ); + break; + } + + if ( is_string( $current ) ) { + /* + * Differences in encoding between PHP strings and + * JavaScript mean that it's complicated to calculate + * the string length JavaScript would see from PHP. + * `strlen` is a reasonable approximation. + * + * Users that desire a more precise length likely have + * more precise needs than "bytelength" and should + * implement their own length calculation in derived + * state taking into account encoding and their desired + * output (codepoints, graphemes, bytes, etc.). + */ + $current = strlen( $current ); + break; + } + } + if ( ( is_array( $current ) || $current instanceof ArrayAccess ) && isset( $current[ $path_segment ] ) ) { $current = $current[ $path_segment ]; } elseif ( is_object( $current ) && isset( $current->$path_segment ) ) { diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index c63c64c6888e6..bcc4ba6bdf149 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -1541,4 +1541,48 @@ public function test_get_element_outside_of_directive_processing() { $element = $this->interactivity->get_element(); $this->assertNull( $element ); } + + /** + * Verify behavior of .length directive access. + * + * @ticket 62582 + * + * @covers ::process_directives + * + * @dataProvider data_length_directives + * + * @param mixed $value The property value. + * @param string $expected The expected property length as a string, + * or "" if no length is expected. + */ + public function test_process_directives_string_array_length( $value, string $expected ) { + $this->interactivity->state( + 'myPlugin', + array( 'prop' => $value ) + ); + $html = '
'; + $processed_html = $this->interactivity->process_directives( $html ); + $processor = new WP_HTML_Tag_Processor( $processed_html ); + $processor->next_tag( 'DIV' ); + $processor->next_token(); + $this->assertSame( $expected, $processor->get_modifiable_text() ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_length_directives(): array { + return array( + 'numeric array' => array( array( 'a', 'b', 'c' ), '3' ), + 'empty array' => array( array(), '0' ), + 'string' => array( 'abc', '3' ), + 'empty string' => array( '', '0' ), + + // Failure cases resulting in empty string. + 'non-numeric array' => array( array( 'a' => 'a' ), '' ), + 'object' => array( new stdClass(), '' ), + ); + } }