Make WordPress Core

Changeset 58321


Ignore:
Timestamp:
06/04/2024 07:16:48 AM (7 weeks ago)
Author:
gziolo
Message:

Interactivity API: Print debug warning when server directives processing encounters errors

Aims to improve the developer experience of the Interactivity API server directives processing.

Props cbravobernal, jonsurrell, westonruter, darerodz, czapla, gziolo.
Fixes #61044.

Location:
trunk
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php

    r58233 r58321  
    199199        $depth    = 1;
    200200        $tag_name = $this->get_tag();
    201         while ( $depth > 0 && $this->next_tag(
    202             array(
    203                 'tag_name'    => $tag_name,
    204                 'tag_closers' => 'visit',
    205             )
    206         ) ) {
    207             if ( $this->has_self_closing_flag() ) {
    208                 continue;
    209             }
    210             $depth += $this->is_tag_closer() ? -1 : 1;
     201
     202        while ( $depth > 0 && $this->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
     203            if ( ! $this->is_tag_closer() && $this->get_attribute_names_with_prefix( 'data-wp-' ) ) {
     204                /* translators: 1: SVG or MATH HTML tag. */
     205                $message = sprintf( __( 'Interactivity directives were detected inside an incompatible %1$s tag. These directives will be ignored in the server side render.' ), $tag_name );
     206                _doing_it_wrong( __METHOD__, $message, '6.6.0' );
     207            }
     208            if ( $this->get_tag() === $tag_name ) {
     209                if ( $this->has_self_closing_flag() ) {
     210                    continue;
     211                }
     212                $depth += $this->is_tag_closer() ? -1 : 1;
     213            }
    211214        }
    212215
  • trunk/src/wp-includes/interactivity-api/class-wp-interactivity-api.php

    r58320 r58321  
    273273     *
    274274     * @since 6.5.0
     275     * @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.
    275276     *
    276277     * @param string $html            The HTML content to process.
     
    296297             */
    297298            if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
     299                if ( $p->get_attribute_names_with_prefix( 'data-wp-' ) ) {
     300                    /* translators: 1: SVG or MATH HTML tag, 2: Namespace of the interactive block. */
     301                    $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 ) );
     302                    _doing_it_wrong( __METHOD__, $message, '6.6.0' );
     303                }
    298304                $p->skip_to_tag_closer();
    299305                continue;
     
    383389            }
    384390        }
    385 
    386391        /*
    387392         * It returns null if the HTML is unbalanced because unbalanced HTML is
    388393         * not safe to process. In that case, the Interactivity API runtime will
    389          * update the HTML on the client side during the hydration.
     394         * update the HTML on the client side during the hydration. It will also
     395         * display a notice to the developer to inform them about the issue.
    390396         */
    391         return $unbalanced || 0 < count( $tag_stack ) ? null : $p->get_updated_html();
     397        if ( $unbalanced || 0 < count( $tag_stack ) ) {
     398            $tag_errored = 0 < count( $tag_stack ) ? end( $tag_stack )[0] : $tag_name;
     399            /* translators: %1s: Namespace processed, %2s: The tag that caused the error; could be any HTML tag.  */
     400            $message = sprintf( __( 'Interactivity directives failed to process in "%1$s" due to a missing "%2$s" end tag.' ), end( $namespace_stack ), $tag_errored );
     401            _doing_it_wrong( __METHOD__, $message, '6.6.0' );
     402            return null;
     403        }
     404
     405        return $p->get_updated_html();
    392406    }
    393407
     
    397411     *
    398412     * @since 6.5.0
     413     * @since 6.6.0 The function now adds a warning when the namespace is null, falsy, or the directive value is empty.
    399414     *
    400415     * @param string|true $directive_value   The directive attribute value string or `true` when it's a boolean attribute.
     
    403418     * @param array|false $context           The current context for evaluating the directive or false if there is no
    404419     *                                       context.
    405      * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist.
     420     * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist or the namespace is falsy.
    406421     */
    407422    private function evaluate( $directive_value, string $default_namespace, $context = false ) {
    408423        list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace );
    409         if ( empty( $path ) ) {
     424        if ( ! $ns || ! $path ) {
     425            /* translators: %s: The directive value referenced. */
     426            $message = sprintf( __( 'Namespace or reference path cannot be empty. Directive value referenced: %s' ), $directive_value );
     427            _doing_it_wrong( __METHOD__, $message, '6.6.0' );
    410428            return null;
    411429        }
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php

    r57836 r58321  
    155155     *
    156156     * @covers ::process_directives
     157     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    157158     */
    158159    public function test_wp_bind_ignores_empty_value() {
     
    168169     *
    169170     * @covers ::process_directives
     171     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    170172     */
    171173    public function test_wp_bind_ignores_without_value() {
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php

    r57836 r58321  
    238238     *
    239239     * @covers ::process_directives
     240     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    240241     */
    241242    public function test_wp_class_doesnt_change_class_attribute_with_empty_value() {
     
    252253     *
    253254     * @covers ::process_directives
     255     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    254256     */
    255257    public function test_wp_class_doesnt_change_class_attribute_without_value() {
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php

    r57836 r58321  
    318318     *
    319319     * @covers ::process_directives
     320     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    320321     */
    321322    public function test_wp_context_directive_doesnt_work_without_any_namespace() {
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php

    r57836 r58321  
    581581     *
    582582     * @covers ::process_directives
     583     *
     584     * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
    583585     */
    584586    public function test_wp_each_unbalanced_tags() {
     
    599601     *
    600602     * @covers ::process_directives
     603     *
     604     * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
    601605     */
    602606    public function test_wp_each_unbalanced_tags_in_nested_template_tags() {
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php

    r57836 r58321  
    366366     *
    367367     * @covers ::process_directives
     368     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    368369     */
    369370    public function test_wp_style_doesnt_change_style_attribute_with_empty_value() {
     
    380381     *
    381382     * @covers ::process_directives
     383     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
    382384     */
    383385    public function test_wp_style_doesnt_change_style_attribute_without_value() {
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php

    r57836 r58321  
    1313 * @group interactivity-api
    1414 */
    15 class Tests_WP_Interactivity_API_WP_Text extends WP_UnitTestCase {
     15class Tests_Interactivity_API_WpInteractivityAPIWPText extends WP_UnitTestCase {
    1616    /**
    1717     * Instance of WP_Interactivity_API.
     
    132132     *
    133133     * @covers ::process_directives
     134     *
     135     * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
    134136     */
    135137    public function test_wp_text_fails_with_unbalanced_and_same_tags_inside_content() {
  • trunk/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php

    r58320 r58321  
    650650     * @dataProvider data_html_with_unbalanced_tags
    651651     *
     652     * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
     653     *
    652654     * @param string $html HTML containing unbalanced tags and also a directive.
    653655     */
     
    697699        $html           = '
    698700            <header>
    699                 <svg height="100" data-wp-bind--width="myPlugin::state.width">
     701                <svg height="100">
    700702                    <title>Red Circle</title>
    701703                    <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
    702704                </svg>
    703705                <div data-wp-bind--id="myPlugin::state.id"></div>
    704                 <div data-wp-bind--id="myPlugin::state.width"></div>
    705706            </header>
    706707        ';
    707708        $processed_html = $this->interactivity->process_directives( $html );
    708709        $p              = new WP_HTML_Tag_Processor( $processed_html );
    709         $p->next_tag( 'svg' );
    710         $this->assertNull( $p->get_attribute( 'width' ) );
    711710        $p->next_tag( 'div' );
    712711        $this->assertEquals( 'some-id', $p->get_attribute( 'id' ) );
    713         $p->next_tag( 'div' );
    714         $this->assertEquals( '100', $p->get_attribute( 'id' ) );
    715712    }
    716713
     
    722719     *
    723720     * @covers ::process_directives
     721     * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer
    724722     */
    725723    public function test_process_directives_does_not_change_inner_html_in_svgs() {
     
    751749     *
    752750     * @covers ::process_directives
     751     * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
    753752     */
    754753    public function test_process_directives_change_html_if_contains_math() {
     
    785784     *
    786785     * @covers ::process_directives
     786     * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args
     787     * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer
    787788     */
    788789    public function test_process_directives_does_not_change_inner_html_in_math() {
     
    812813     * Invokes the private `evaluate` method of WP_Interactivity_API class.
    813814     *
    814      * @param string $directive_value The directive attribute value to evaluate.
     815     * @param string $directive_value   The directive attribute value to evaluate.
     816     * @param string $default_namespace The default namespace used with directives.
    815817     * @return mixed The result of the evaluate method.
    816818     */
    817     private function evaluate( $directive_value ) {
     819    private function evaluate( $directive_value, $default_namespace = 'myPlugin' ) {
    818820        $generate_state = function ( $name ) {
    819821            $obj       = new stdClass();
     
    848850        $evaluate = new ReflectionMethod( $this->interactivity, 'evaluate' );
    849851        $evaluate->setAccessible( true );
    850         return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, 'myPlugin', $context ) );
     852        return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, $default_namespace, $context ) );
    851853    }
    852854
     
    946948        $result = $this->evaluate( 'otherPlugin::context.nested.key' );
    947949        $this->assertEquals( 'otherPlugin-context-nested', $result );
     950    }
     951
     952    /**
     953     * Tests the `evaluate` method for non valid namespace values.
     954     *
     955     * @ticket 61044
     956     *
     957     * @covers ::evaluate
     958     * @expectedIncorrectUsage WP_Interactivity_API::evaluate
     959     */
     960    public function test_evaluate_unvalid_namespaces() {
     961        $result = $this->evaluate( 'path', 'null' );
     962        $this->assertNull( $result );
     963
     964        $result = $this->evaluate( 'path', '' );
     965        $this->assertNull( $result );
     966
     967        $result = $this->evaluate( 'path', '{}' );
     968        $this->assertNull( $result );
    948969    }
    949970
Note: See TracChangeset for help on using the changeset viewer.