From e0ed30f1c802cda3c709b621746b420e300ecede Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Wed, 24 Jul 2024 10:57:27 +0000 Subject: [PATCH 001/106] Block Bindings: Adds sources in the editor settings to consume them in the client Adds a new property `blockBindingsSources` to the editor settings to expose the block bindings sources registered in the server. Props santosguillamot, cbravobernal, gziolo, artemiosans. Fixes #61641. git-svn-id: https://develop.svn.wordpress.org/trunk@58798 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-editor.php | 17 ++++++++++ .../wpBlockBindingsRegistry.php | 5 +++ tests/phpunit/tests/blocks/editor.php | 34 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index 00633d4d7edd9..e8900d1ccdc1a 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -648,6 +648,23 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex $editor_settings['postContentAttributes'] = $post_content_block_attributes; } + // Expose block bindings sources in the editor settings. + $registered_block_bindings_sources = get_all_registered_block_bindings_sources(); + if ( ! empty( $registered_block_bindings_sources ) ) { + // Initialize array. + $editor_settings['blockBindingsSources'] = array(); + foreach ( $registered_block_bindings_sources as $source_name => $source_properties ) { + // Add source with the label to editor settings. + $editor_settings['blockBindingsSources'][ $source_name ] = array( + 'label' => $source_properties->label, + ); + // Add `usesContext` property if exists. + if ( ! empty( $source_properties->uses_context ) ) { + $editor_settings['blockBindingsSources'][ $source_name ]['usesContext'] = $source_properties->uses_context; + } + } + } + /** * Filters the settings to pass to the block editor for all editor type. * diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php index fc5b91a9d702a..1d5c4b1947467 100644 --- a/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php @@ -311,6 +311,9 @@ public function test_get_registered() { $expected = new WP_Block_Bindings_Source( $source_two_name, $source_two_properties ); $result = $this->registry->get_registered( 'test/source-two' ); + $this->registry->unregister( 'test/source-one' ); + $this->registry->unregister( 'test/source-two' ); + $this->registry->unregister( 'test/source-three' ); $this->assertEquals( $expected, @@ -380,6 +383,8 @@ public function test_merging_uses_context_from_multiple_sources() { ); $new_uses_context = $block_registry->get_registered( 'core/paragraph' )->uses_context; + unregister_block_bindings_source( 'test/source-one' ); + unregister_block_bindings_source( 'test/source-two' ); // Checks that the resulting `uses_context` contains the values from both sources. $this->assertContains( 'commonContext', $new_uses_context ); $this->assertContains( 'sourceOneContext', $new_uses_context ); diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 0682839605da9..b10dbb93dae01 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -720,4 +720,38 @@ public function data_block_editor_rest_api_preload_adds_missing_leading_slash() ), ); } + + /** + * @ticket 61641 + */ + public function test_get_block_editor_settings_block_bindings_sources() { + $block_editor_context = new WP_Block_Editor_Context(); + register_block_bindings_source( + 'test/source-one', + array( + 'label' => 'Source One', + 'get_value_callback' => function () {}, + 'uses_context' => array( 'postId' ), + ) + ); + register_block_bindings_source( + 'test/source-two', + array( + 'label' => 'Source Two', + 'get_value_callback' => function () {}, + ) + ); + $settings = get_block_editor_settings( array(), $block_editor_context ); + $exposed_sources = $settings['blockBindingsSources']; + unregister_block_bindings_source( 'test/source-one' ); + unregister_block_bindings_source( 'test/source-two' ); + // It is expected to have 4 sources: the 2 registered sources in the test, and the 2 core sources. + $this->assertCount( 4, $exposed_sources ); + $source_one = $exposed_sources['test/source-one']; + $this->assertSame( 'Source One', $source_one['label'] ); + $this->assertSameSets( array( 'postId' ), $source_one['usesContext'] ); + $source_two = $exposed_sources['test/source-two']; + $this->assertSame( 'Source Two', $source_two['label'] ); + $this->assertArrayNotHasKey( 'usesContext', $source_two ); + } } From ef71a9ed3917d11cf0daec94bc8f3201959af1b8 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 24 Jul 2024 11:29:58 +0000 Subject: [PATCH 002/106] Tests: Removes the obsolete Block Binding unregistration Follow-up to [58798]. Props santosguillamot, cbravobernal, gziolo. See #61641. git-svn-id: https://develop.svn.wordpress.org/trunk@58799 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php index 1d5c4b1947467..e4aa415e9af96 100644 --- a/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php @@ -311,9 +311,6 @@ public function test_get_registered() { $expected = new WP_Block_Bindings_Source( $source_two_name, $source_two_properties ); $result = $this->registry->get_registered( 'test/source-two' ); - $this->registry->unregister( 'test/source-one' ); - $this->registry->unregister( 'test/source-two' ); - $this->registry->unregister( 'test/source-three' ); $this->assertEquals( $expected, From caa38c4faa7942836385c0655504816258330005 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Wed, 24 Jul 2024 14:05:18 +0000 Subject: [PATCH 003/106] External Libraries: Update the `regenerator-runtime` library. This updates the `regenerator-runtime` library to version `0.14.1`. This library has not been used by Core itself in quite a while and only maintained as a courtesy. Any projects relying on `regenerator-runtime` should reevaluate their usage. Props manooweb. Fixes #60515. git-svn-id: https://develop.svn.wordpress.org/trunk@58800 602fd350-edb4-49c9-b593-d223f7449a82 --- package-lock.json | 14 +++++++------- package.json | 2 +- src/wp-includes/script-loader.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17e20b756c8c2..4906648085f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,7 +96,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-is": "18.3.1", - "regenerator-runtime": "0.14.0", + "regenerator-runtime": "0.14.1", "underscore": "1.13.6", "whatwg-fetch": "3.6.17", "wicg-inert": "3.1.2" @@ -28768,9 +28768,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -55632,9 +55632,9 @@ } }, "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regenerator-transform": { "version": "0.15.1", diff --git a/package.json b/package.json index 6f3fce17b19f0..06fcc6829a8eb 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-is": "18.3.1", - "regenerator-runtime": "0.14.0", + "regenerator-runtime": "0.14.1", "underscore": "1.13.6", "whatwg-fetch": "3.6.17", "wicg-inert": "3.1.2" diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index b7da065ad1796..b73f12b9cc37b 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -111,7 +111,7 @@ function wp_default_packages_vendor( $scripts ) { 'react' => '18.3.1', 'react-dom' => '18.3.1', 'react-jsx-runtime' => '18.3.1', - 'regenerator-runtime' => '0.14.0', + 'regenerator-runtime' => '0.14.1', 'moment' => '2.29.4', 'lodash' => '4.17.21', 'wp-polyfill-fetch' => '3.6.17', From 5a76260c3cd22d4688b91620ad7a00a42300c707 Mon Sep 17 00:00:00 2001 From: bernhard-reiter Date: Wed, 24 Jul 2024 14:09:58 +0000 Subject: [PATCH 004/106] block.json: Allow passing PHP filename as `variations` field. Previously, the `variations` field in a block.json file could be used to provide a static list of the block's variations (i.e., an array). Alternatively, the block's `variation_callback` could be set during server-side block registration to point to a PHP function to generate those variations. This changeset makes it so that the block.json `variations` field can be alternatively set to a string, which will be interpreted as the filename of a PHP file that generates the variations. It is loosely modeled after [54132], which introduced the `render` field for `block.json`, as a way to point to a PHP file instead of providing a `render_callback`. Props bernhard-reiter, gziolo. Fixes #61280. git-svn-id: https://develop.svn.wordpress.org/trunk@58801 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/blocks.php | 29 +++++++++++++++++ .../phpunit/data/blocks/notice/variations.php | 10 ++++++ tests/phpunit/tests/blocks/register.php | 31 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 tests/phpunit/data/blocks/notice/variations.php diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 3b1fc25d48824..9b24b989d4024 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -385,6 +385,7 @@ function get_block_metadata_i18n_schema() { * @since 6.3.0 Added `selectors` field. * @since 6.4.0 Added support for `blockHooks` field. * @since 6.5.0 Added support for `allowedBlocks`, `viewScriptModule`, and `viewStyle` fields. + * @since 6.7.0 Allow PHP filename as `variations` argument. * * @param string $file_or_folder Path to the JSON file with metadata definition for * the block or path to the folder where the `block.json` file is located. @@ -522,6 +523,34 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { } } + // If `variations` is a string, it's the name of a PHP file that + // generates the variations. + if ( ! empty( $metadata['variations'] ) && is_string( $metadata['variations'] ) ) { + $variations_path = wp_normalize_path( + realpath( + dirname( $metadata['file'] ) . '/' . + remove_block_asset_path_prefix( $metadata['variations'] ) + ) + ); + if ( $variations_path ) { + /** + * Generates the list of block variations. + * + * @since 6.7.0 + * + * @return string Returns the list of block variations. + */ + $settings['variation_callback'] = static function () use ( $variations_path ) { + $variations = require $variations_path; + return $variations; + }; + // The block instance's `variations` field is only allowed to be an array + // (of known block variations). We unset it so that the block instance will + // provide a getter that returns the result of the `variation_callback` instead. + unset( $settings['variations'] ); + } + } + $settings = array_merge( $settings, $args ); $script_fields = array( diff --git a/tests/phpunit/data/blocks/notice/variations.php b/tests/phpunit/data/blocks/notice/variations.php new file mode 100644 index 0000000000000..bed66d9544176 --- /dev/null +++ b/tests/phpunit/data/blocks/notice/variations.php @@ -0,0 +1,10 @@ + 'warning', + 'title' => 'warning', + 'description' => 'Shows warning.', + 'keywords' => array( 'warning' ), + ), +); diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php index 1dbc688bb16cf..7e0c391e1f226 100644 --- a/tests/phpunit/tests/blocks/register.php +++ b/tests/phpunit/tests/blocks/register.php @@ -957,6 +957,37 @@ public function data_register_block_registers_with_args_override_returns_false_w ); } + /** + * Tests registering a block with variations from a PHP file. + * + * @ticket 61280 + * + * @covers ::register_block_type_from_metadata + */ + public function test_register_block_type_from_metadata_with_variations_php_file() { + $filter_metadata_registration = static function ( $metadata ) { + $metadata['variations'] = 'file:./variations.php'; + return $metadata; + }; + + add_filter( 'block_type_metadata', $filter_metadata_registration, 10, 2 ); + $result = register_block_type_from_metadata( + DIR_TESTDATA . '/blocks/notice' + ); + remove_filter( 'block_type_metadata', $filter_metadata_registration ); + + $this->assertInstanceOf( 'WP_Block_Type', $result, 'The block was not registered' ); + + $this->assertIsCallable( $result->variation_callback, 'The variation callback hasn\'t been set' ); + $expected_variations = require DIR_TESTDATA . '/blocks/notice/variations.php'; + $this->assertSame( + $expected_variations, + call_user_func( $result->variation_callback ), + 'The variation callback hasn\'t been set correctly' + ); + $this->assertSame( $expected_variations, $result->variations, 'The block variations are incorrect' ); + } + /** * Tests that the function returns the registered block when the `block.json` * is found in the fixtures directory. From edcd7d285f89c4633b6c20fd09b709c0a649f4d5 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 24 Jul 2024 15:08:08 +0000 Subject: [PATCH 005/106] Tests: Use more specific assertions in `get_comment_author()` tests. Follow-up to [58335]. See #61530. git-svn-id: https://develop.svn.wordpress.org/trunk@58803 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/comment/getCommentAuthor.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/comment/getCommentAuthor.php b/tests/phpunit/tests/comment/getCommentAuthor.php index 93570b51d16dd..61e8c213b54b9 100644 --- a/tests/phpunit/tests/comment/getCommentAuthor.php +++ b/tests/phpunit/tests/comment/getCommentAuthor.php @@ -22,7 +22,7 @@ public static function set_up_before_class() { public function get_comment_author_filter( $comment_author, $comment_id, $comment ) { $this->assertSame( $comment_id, self::$comment->comment_ID, 'Comment IDs do not match.' ); - $this->assertTrue( is_string( $comment_id ), '$comment_id parameter is not a string.' ); + $this->assertIsString( $comment_id, '$comment_id parameter is not a string.' ); return $comment_author; } @@ -41,7 +41,7 @@ public function test_comment_author_passes_correct_comment_id_for_int() { public function get_comment_author_filter_non_existent_id( $comment_author, $comment_id, $comment ) { $this->assertSame( $comment_id, (string) self::$non_existent_comment_id, 'Comment IDs do not match.' ); - $this->assertTrue( is_string( $comment_id ), '$comment_id parameter is not a string.' ); + $this->assertIsString( $comment_id, '$comment_id parameter is not a string.' ); return $comment_author; } @@ -71,7 +71,9 @@ public function test_should_return_author_when_given_object_without_comment_id( $user = self::factory()->user->create_and_get( $user_data ); $comment_props->user_id = $user->ID; } + $comment = new WP_Comment( $comment_props ); + $this->assertSame( $expected, get_comment_author( $comment ) ); } From 881ac87847e69baa63bbc62668837b4d66580d20 Mon Sep 17 00:00:00 2001 From: Tonya Mork Date: Wed, 24 Jul 2024 16:21:59 +0000 Subject: [PATCH 006/106] Customize: Sanitize autofocus URL parameter as an array. [58069] introduced calling `sanitize_text_field()` with `$_REQUEST['autofocus']` (which is an array) and setting its default to a `string`. This fix restores the `array` data type for `autofocus`. The fix also relocates the unsplash for `url`, `return`, and `autofocus` before sanitizing. Follow-up to [58069], [34269], [29026], [21028]. Props jamesros161, swissspidy, dlh, audrasjb, hellofromTonya, ironprogrammer. Fixes #61561. git-svn-id: https://develop.svn.wordpress.org/trunk@58804 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/customize.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 2a53480feec9c..40857031a7ef8 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -84,18 +84,20 @@ } } -$url = ! empty( $_REQUEST['url'] ) ? sanitize_text_field( $_REQUEST['url'] ) : ''; -$return = ! empty( $_REQUEST['return'] ) ? sanitize_text_field( $_REQUEST['return'] ) : ''; -$autofocus = ! empty( $_REQUEST['autofocus'] ) ? sanitize_text_field( $_REQUEST['autofocus'] ) : ''; +$url = ! empty( $_REQUEST['url'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['url'] ) ) : ''; +$return = ! empty( $_REQUEST['return'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['return'] ) ) : ''; +$autofocus = ! empty( $_REQUEST['autofocus'] ) && is_array( $_REQUEST['autofocus'] ) + ? array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['autofocus'] ) ) + : array(); if ( ! empty( $url ) ) { - $wp_customize->set_preview_url( wp_unslash( $url ) ); + $wp_customize->set_preview_url( $url ); } if ( ! empty( $return ) ) { - $wp_customize->set_return_url( wp_unslash( $return ) ); + $wp_customize->set_return_url( $return ); } -if ( ! empty( $autofocus ) && is_array( $autofocus ) ) { - $wp_customize->set_autofocus( wp_unslash( $autofocus ) ); +if ( ! empty( $autofocus ) ) { + $wp_customize->set_autofocus( $autofocus ); } $registered = $wp_scripts->registered; From 5351cd20f3d5eb890cb7b53270d01b626ce02f7d Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Wed, 24 Jul 2024 18:39:46 +0000 Subject: [PATCH 007/106] HTML API: Add TABLE support in HTML Processor. As part of work to add more spec support to the HTML API, this patch adds support for various table-related insertion modes. This includes support for tables, table rows, table cells, table column groups, etc... Developed in https://github.com/wordpress/wordpress-develop/pull/6040 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58806 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-open-elements.php | 74 +++ .../html-api/class-wp-html-processor.php | 587 +++++++++++++++++- .../html-api/wpHtmlProcessorBreadcrumbs.php | 9 - .../html-api/wpHtmlProcessorSemanticRules.php | 24 + 4 files changed, 677 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index d59bd32140582..1162a267f9c9b 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -720,6 +720,80 @@ public function after_element_pop( WP_HTML_Token $item ): void { } } + /** + * Clear the stack back to a table context. + * + * > When the steps above require the UA to clear the stack back to a table context, it means + * > that the UA must, while the current node is not a table, template, or html element, pop + * > elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-context + * + * @since 6.7.0 + */ + public function clear_to_table_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TABLE' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Clear the stack back to a table body context. + * + * > When the steps above require the UA to clear the stack back to a table body context, it + * > means that the UA must, while the current node is not a tbody, tfoot, thead, template, or + * > html element, pop elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-body-context + * + * @since 6.7.0 + */ + public function clear_to_table_body_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TBODY' === $item->node_name || + 'TFOOT' === $item->node_name || + 'THEAD' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Clear the stack back to a table row context. + * + * > When the steps above require the UA to clear the stack back to a table row context, it + * > means that the UA must, while the current node is not a tr, template, or html element, pop + * > elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-row-context + * + * @since 6.7.0 + */ + public function clear_to_table_row_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TR' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + /** * Wakeup magic method. * diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index f9073492d86ac..975f21a0f0d77 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1786,6 +1786,10 @@ private function step_in_body(): bool { * > A start tag whose tag name is "table" */ case '+TABLE': + /* + * > If the Document is not set to quirks mode, and the stack of open elements + * > has a p element in button scope, then close a p element. + */ if ( WP_HTML_Processor_State::QUIRKS_MODE !== $this->state->document_mode && $this->state->stack_of_open_elements->has_p_in_button_scope() @@ -2117,7 +2121,7 @@ private function step_in_body(): bool { * This internal function performs the 'in table' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -2127,7 +2131,245 @@ private function step_in_body(): bool { * @return bool Whether an element was found. */ private function step_in_table(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token, if the current node is table, + * > tbody, template, tfoot, thead, or tr element + */ + case '#text': + $current_node = $this->state->stack_of_open_elements->current_node(); + $current_node_name = $current_node ? $current_node->node_name : null; + if ( + $current_node_name && ( + 'TABLE' === $current_node_name || + 'TBODY' === $current_node_name || + 'TEMPLATE' === $current_node_name || + 'TFOOT' === $current_node_name || + 'THEAD' === $current_node_name || + 'TR' === $current_node_name + ) + ) { + $text = $this->get_modifiable_text(); + /* + * If the text is empty after processing HTML entities and stripping + * U+0000 NULL bytes then ignore the token. + */ + if ( '' === $text ) { + return $this->step(); + } + + /* + * This follows the rules for "in table text" insertion mode. + * + * Whitespace-only text nodes are inserted in-place. Otherwise + * foster parenting is enabled and the nodes would be + * inserted out-of-place. + * + * > If any of the tokens in the pending table character tokens + * > list are character tokens that are not ASCII whitespace, + * > then this is a parse error: reprocess the character tokens + * > in the pending table character tokens list using the rules + * > given in the "anything else" entry in the "in table" + * > insertion mode. + * > + * > Otherwise, insert the characters given by the pending table + * > character tokens list. + * + * @see https://html.spec.whatwg.org/#parsing-main-intabletext + */ + if ( strlen( $text ) === strspn( $text, " \t\f\r\n" ) ) { + $this->insert_html_element( $this->state->current_token ); + return true; + } + + // Non-whitespace would trigger fostering, unsupported at this time. + $this->bail( 'Foster parenting is not supported.' ); + break; + } + break; + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "caption" + */ + case '+CAPTION': + $this->state->stack_of_open_elements->clear_to_table_context(); + $this->state->active_formatting_elements->insert_marker(); + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION; + return true; + + /* + * > A start tag whose tag name is "colgroup" + */ + case '+COLGROUP': + $this->state->stack_of_open_elements->clear_to_table_context(); + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; + return true; + + /* + * > A start tag whose tag name is "col" + */ + case '+COL': + $this->state->stack_of_open_elements->clear_to_table_context(); + + /* + * > Insert an HTML element for a "colgroup" start tag token with no attributes, + * > then switch the insertion mode to "in column group". + */ + $this->insert_virtual_node( 'COLGROUP' ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > A start tag whose tag name is one of: "tbody", "tfoot", "thead" + */ + case '+TBODY': + case '+TFOOT': + case '+THEAD': + $this->state->stack_of_open_elements->clear_to_table_context(); + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + return true; + + /* + * > A start tag whose tag name is one of: "td", "th", "tr" + */ + case '+TD': + case '+TH': + case '+TR': + $this->state->stack_of_open_elements->clear_to_table_context(); + /* + * > Insert an HTML element for a "tbody" start tag token with no attributes, + * > then switch the insertion mode to "in table body". + */ + $this->insert_virtual_node( 'TBODY' ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > A start tag whose tag name is "table" + * + * This tag in the IN TABLE insertion mode is a parse error. + */ + case '+TABLE': + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TABLE' ) ) { + return $this->step(); + } + + $this->state->stack_of_open_elements->pop_until( 'TABLE' ); + $this->reset_insertion_mode(); + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is "table" + */ + case '-TABLE': + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TABLE' ) ) { + // @todo Indicate a parse error once it's possible. + return $this->step(); + } + + $this->state->stack_of_open_elements->pop_until( 'TABLE' ); + $this->reset_insertion_mode(); + return true; + + /* + * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" + */ + case '-BODY': + case '-CAPTION': + case '-COL': + case '-COLGROUP': + case '-HTML': + case '-TBODY': + case '-TD': + case '-TFOOT': + case '-TH': + case '-THEAD': + case '-TR': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is one of: "style", "script", "template" + * > An end tag whose tag name is "template" + */ + case '+STYLE': + case '+SCRIPT': + case '+TEMPLATE': + case '-TEMPLATE': + /* + * > Process the token using the rules for the "in head" insertion mode. + */ + return $this->step_in_head(); + + /* + * > A start tag whose tag name is "input" + * + * > If the token does not have an attribute with the name "type", or if it does, but + * > that attribute's value is not an ASCII case-insensitive match for the string + * > "hidden", then: act as described in the "anything else" entry below. + */ + case '+INPUT': + $type_attribute = $this->get_attribute( 'type' ); + if ( ! is_string( $type_attribute ) || 'hidden' !== strtolower( $type_attribute ) ) { + goto anything_else; + } + // @todo Indicate a parse error once it's possible. + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A start tag whose tag name is "form" + * + * This tag in the IN TABLE insertion mode is a parse error. + */ + case '+FORM': + if ( + $this->state->stack_of_open_elements->has_element_in_scope( 'TEMPLATE' ) || + isset( $this->state->form_element ) + ) { + return $this->step(); + } + + // This FORM is special because it immediately closes and cannot have other children. + $this->insert_html_element( $this->state->current_token ); + $this->state->form_element = $this->state->current_token; + $this->state->stack_of_open_elements->pop(); + return true; + } + + /* + * > Anything else + * > Parse error. Enable foster parenting, process the token using the rules for the + * > "in body" insertion mode, and then disable foster parenting. + * + * @todo Indicate a parse error once it's possible. + */ + anything_else: + $this->bail( 'Foster parenting is not supported.' ); } /** @@ -2193,7 +2435,7 @@ private function step_in_column_group(): bool { * This internal function performs the 'in table body' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -2203,7 +2445,97 @@ private function step_in_column_group(): bool { * @return bool Whether an element was found. */ private function step_in_table_body(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY . ' state.' ); + $tag_name = $this->get_tag(); + $op_sigil = $this->is_tag_closer() ? '-' : '+'; + $op = "{$op_sigil}{$tag_name}"; + + switch ( $op ) { + /* + * > A start tag whose tag name is "tr" + */ + case '+TR': + $this->state->stack_of_open_elements->clear_to_table_body_context(); + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; + return true; + + /* + * > A start tag whose tag name is one of: "th", "td" + */ + case '+TH': + case '+TD': + // @todo Indicate a parse error once it's possible. + $this->state->stack_of_open_elements->clear_to_table_body_context(); + $this->insert_virtual_node( 'TR' ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is one of: "tbody", "tfoot", "thead" + */ + case '-TBODY': + case '-TFOOT': + case '-THEAD': + /* + * @todo This needs to check if the element in scope is an HTML element, meaning that + * when SVG and MathML support is added, this needs to differentiate between an + * HTML element of the given name, such as `
`, and a foreign element of + * the same given name. + */ + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { + // Parse error: ignore the token. + return $this->step(); + } + + $this->state->stack_of_open_elements->clear_to_table_body_context(); + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + return true; + + /* + * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead" + * > An end tag whose tag name is "table" + */ + case '+CAPTION': + case '+COL': + case '+COLGROUP': + case '+TBODY': + case '+TFOOT': + case '+THEAD': + case '-TABLE': + if ( + ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TBODY' ) && + ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'THEAD' ) && + ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TFOOT' ) + ) { + // Parse error: ignore the token. + return $this->step(); + } + $this->state->stack_of_open_elements->clear_to_table_body_context(); + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th", "tr" + */ + case '-BODY': + case '-CAPTION': + case '-COL': + case '-COLGROUP': + case '-HTML': + case '-TD': + case '-TH': + case '-TR': + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else + * > Process the token using the rules for the "in table" insertion mode. + */ + return $this->step_in_table(); } /** @@ -2212,7 +2544,7 @@ private function step_in_table_body(): bool { * This internal function performs the 'in row' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -2222,7 +2554,104 @@ private function step_in_table_body(): bool { * @return bool Whether an element was found. */ private function step_in_row(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_ROW . ' state.' ); + $tag_name = $this->get_tag(); + $op_sigil = $this->is_tag_closer() ? '-' : '+'; + $op = "{$op_sigil}{$tag_name}"; + + switch ( $op ) { + /* + * > A start tag whose tag name is one of: "th", "td" + */ + case '+TH': + case '+TD': + $this->state->stack_of_open_elements->clear_to_table_row_context(); + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL; + $this->state->active_formatting_elements->insert_marker(); + return true; + + /* + * > An end tag whose tag name is "tr" + */ + case '-TR': + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) { + // Parse error: ignore the token. + return $this->step(); + } + + $this->state->stack_of_open_elements->clear_to_table_row_context(); + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + return true; + + /* + * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr" + * > An end tag whose tag name is "table" + */ + case '+CAPTION': + case '+COL': + case '+COLGROUP': + case '+TBODY': + case '+TFOOT': + case '+THEAD': + case '+TR': + case '-TABLE': + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) { + // Parse error: ignore the token. + return $this->step(); + } + + $this->state->stack_of_open_elements->clear_to_table_row_context(); + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is one of: "tbody", "tfoot", "thead" + */ + case '-TBODY': + case '-TFOOT': + case '-THEAD': + /* + * @todo This needs to check if the element in scope is an HTML element, meaning that + * when SVG and MathML support is added, this needs to differentiate between an + * HTML element of the given name, such as `
`, and a foreign element of + * the same given name. + */ + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { + // Parse error: ignore the token. + return $this->step(); + } + + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) { + // Ignore the token. + return $this->step(); + } + + $this->state->stack_of_open_elements->clear_to_table_row_context(); + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th" + */ + case '-BODY': + case '-CAPTION': + case '-COL': + case '-COLGROUP': + case '-HTML': + case '-TD': + case '-TH': + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else + * > Process the token using the rules for the "in table" insertion mode. + */ + return $this->step_in_table(); } /** @@ -2231,7 +2660,7 @@ private function step_in_row(): bool { * This internal function performs the 'in cell' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -2241,7 +2670,104 @@ private function step_in_row(): bool { * @return bool Whether an element was found. */ private function step_in_cell(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_CELL . ' state.' ); + $tag_name = $this->get_tag(); + $op_sigil = $this->is_tag_closer() ? '-' : '+'; + $op = "{$op_sigil}{$tag_name}"; + + switch ( $op ) { + /* + * > An end tag whose tag name is one of: "td", "th" + */ + case '-TD': + case '-TH': + /* + * @todo This needs to check if the element in scope is an HTML element, meaning that + * when SVG and MathML support is added, this needs to differentiate between an + * HTML element of the given name, such as `
`, and a foreign element of + * the same given name. + */ + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { + // Parse error: ignore the token. + return $this->step(); + } + + $this->generate_implied_end_tags(); + + /* + * @todo This needs to check if the current node is an HTML element, meaning that + * when SVG and MathML support is added, this needs to differentiate between an + * HTML element of the given name, such as `
`, and a foreign element of + * the same given name. + */ + if ( ! $this->state->stack_of_open_elements->current_node_is( $tag_name ) ) { + // @todo Indicate a parse error once it's possible. + } + + $this->state->stack_of_open_elements->pop_until( $tag_name ); + $this->state->active_formatting_elements->clear_up_to_last_marker(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; + return true; + + /* + * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", + * > "tfoot", "th", "thead", "tr" + */ + case '+CAPTION': + case '+COL': + case '+COLGROUP': + case '+TBODY': + case '+TD': + case '+TFOOT': + case '+TH': + case '+THEAD': + case '+TR': + /* + * > Assert: The stack of open elements has a td or th element in table scope. + * + * Nothing to do here, except to verify in tests that this never appears. + */ + + $this->close_cell(); + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html" + */ + case '-BODY': + case '-CAPTION': + case '-COL': + case '-COLGROUP': + case '-HTML': + // Parse error: ignore the token. + return $this->step(); + + /* + * > An end tag whose tag name is one of: "table", "tbody", "tfoot", "thead", "tr" + */ + case '-TABLE': + case '-TBODY': + case '-TFOOT': + case '-THEAD': + case '-TR': + /* + * @todo This needs to check if the element in scope is an HTML element, meaning that + * when SVG and MathML support is added, this needs to differentiate between an + * HTML element of the given name, such as `
`, and a foreign element of + * the same given name. + */ + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { + // Parse error: ignore the token. + return $this->step(); + } + $this->close_cell(); + return $this->step( self::REPROCESS_CURRENT_NODE ); + } + + /* + * > Anything else + * > Process the token using the rules for the "in body" insertion mode. + */ + return $this->step_in_body(); } /** @@ -3576,6 +4102,33 @@ private function run_adoption_agency_algorithm(): void { $this->bail( 'Cannot run adoption agency when looping required.' ); } + /** + * Runs the "close the cell" algorithm. + * + * > Where the steps above say to close the cell, they mean to run the following algorithm: + * > 1. Generate implied end tags. + * > 2. If the current node is not now a td element or a th element, then this is a parse error. + * > 3. Pop elements from the stack of open elements stack until a td element or a th element has been popped from the stack. + * > 4. Clear the list of active formatting elements up to the last marker. + * > 5. Switch the insertion mode to "in row". + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#close-the-cell + * + * @since 6.7.0 + */ + private function close_cell(): void { + $this->generate_implied_end_tags(); + // @todo Parse error if the current node is a "td" or "th" element. + foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) { + $this->state->stack_of_open_elements->pop(); + if ( 'TD' === $element->node_name || 'TH' === $element->node_name ) { + break; + } + } + $this->state->active_formatting_elements->clear_up_to_last_marker(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; + } + /** * Inserts an HTML element on the stack of open elements. * @@ -3589,6 +4142,24 @@ private function insert_html_element( WP_HTML_Token $token ): void { $this->state->stack_of_open_elements->push( $token ); } + /** + * Inserts a virtual element on the stack of open elements. + * + * @since 6.7.0 + * + * @param string $token_name Name of token to create and insert into the stack of open elements. + * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node. + * Defaults to auto-creating a bookmark name. + */ + private function insert_virtual_node( $token_name, $bookmark_name = null ): void { + $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; + $name = $bookmark_name ?? $this->bookmark_token(); + + $this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 ); + + $this->insert_html_element( new WP_HTML_Token( $name, $token_name, false ) ); + } + /* * HTML Specification Helpers */ diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index e9c7362c179a2..7dd94747fd8e8 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -181,9 +181,6 @@ public static function data_unsupported_elements() { 'BASE', 'BGSOUND', // Deprecated; self-closing if self-closing flag provided, otherwise normal. 'BODY', - 'CAPTION', - 'COL', - 'COLGROUP', 'FRAME', 'FRAMESET', 'HEAD', @@ -198,15 +195,9 @@ public static function data_unsupported_elements() { 'SCRIPT', 'STYLE', 'SVG', - 'TBODY', - 'TD', 'TEMPLATE', 'TEXTAREA', - 'TFOOT', - 'TH', - 'THEAD', 'TITLE', - 'TR', 'XMP', // Deprecated, use PRE instead. ); diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php index a64872bed2f1d..ffc99ad58fd8e 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php @@ -424,4 +424,28 @@ public function test_br_end_tag_unsupported() { $this->assertFalse( $processor->is_tag_closer(), 'Should have treated the tag as an opening tag.' ); $this->assertNull( $processor->get_attribute_names_with_prefix( '' ), 'Should have ignored any attributes on the tag.' ); } + + /******************************************************************* + * RULES FOR "IN TABLE" MODE + *******************************************************************/ + + /** + * Ensure that form elements in tables (but not cells) are immediately popped off the stack. + * + * @ticket 61576 + */ + public function test_table_form_element_immediately_popped() { + $processor = WP_HTML_Processor::create_fragment( '' ); + + // There should be a FORM opener and a (virtual) FORM closer. + $this->assertTrue( $processor->next_tag( 'FORM' ) ); + $this->assertTrue( $processor->next_token() ); + $this->assertSame( 'FORM', $processor->get_token_name() ); + $this->assertTrue( $processor->is_tag_closer() ); + + // Followed by the comment token. + $this->assertTrue( $processor->next_token() ); + $this->assertSame( '#comment', $processor->get_token_name() ); + $this->assertsame( array( 'HTML', 'BODY', 'TABLE', '#comment' ), $processor->get_breadcrumbs() ); + } } From 4acefacbf6f7c956f1cf30e406d3c9c04bc855f9 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 24 Jul 2024 21:40:59 +0000 Subject: [PATCH 008/106] Taxonomy: Ensure `get_edit_term_link()` produces the correct result when called without taxonomy. This fixes an oversight missed in [36646]. Props debarghyabanerjee. Fixes #61726. See #35922. git-svn-id: https://develop.svn.wordpress.org/trunk@58807 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/link-template.php | 2 +- tests/phpunit/tests/link/getEditTermLink.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index dde12c16168be..37e8684bf0d8e 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -1092,7 +1092,7 @@ function get_edit_term_link( $term, $taxonomy = '', $object_type = '' ) { } $args = array( - 'taxonomy' => $taxonomy, + 'taxonomy' => $tax->name, 'tag_ID' => $term_id, ); diff --git a/tests/phpunit/tests/link/getEditTermLink.php b/tests/phpunit/tests/link/getEditTermLink.php index d0303f1b1a403..e86cd78449f3d 100644 --- a/tests/phpunit/tests/link/getEditTermLink.php +++ b/tests/phpunit/tests/link/getEditTermLink.php @@ -236,4 +236,17 @@ public function data_get_edit_term_link() { ), ); } + + /** + * Checks that `get_edit_term_link()` produces the correct URL when called without taxonomy. + * + * @ticket 61726 + */ + public function test_get_edit_term_link_without_taxonomy() { + $term = $this->get_term( 'wptests_tax', true ); + + $actual = get_edit_term_link( $term ); + $expected = sprintf( admin_url( 'term.php?taxonomy=wptests_tax&tag_ID=%d&post_type=post' ), $term ); + $this->assertSame( $expected, $actual ); + } } From 5ea8ba0bf041e3148548dc9d2dc1a1d16eb3674a Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 25 Jul 2024 22:36:13 +0000 Subject: [PATCH 009/106] Comments: Only type cast a scalar `$comment_id` in `get_comment_author_link()`. This aims to resolve a fatal error when the incoming `$comment_id` is an instance of `WP_Comment` (or any object) without a `comment_ID` property defined, or if it's empty: {{{ Object of class WP_Comment could not be converted to string }}} This commit mirrors the changes previously made for a similar code fragment in `get_comment_author()`. Includes: * Unit tests to demonstrate the fatal error and validate the fix. * Changing the default value for a non-existent comment ID in `get_comment_author()` from an empty string to zero as a numeric string, for consistency with `get_comment_ID()`. Follow-up to [52818], [55289], [58335], [58755]. Props narenin, mukesh27, iflairwebtechnologies, umeshsinghin, SergeyBiryukov. Fixes #61715. git-svn-id: https://develop.svn.wordpress.org/trunk@58809 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/comment-template.php | 10 +- .../tests/comment/getCommentAuthorLink.php | 116 ++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 tests/phpunit/tests/comment/getCommentAuthorLink.php diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index fed6568af33d5..ccc57ed8ee085 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -29,7 +29,7 @@ function get_comment_author( $comment_id = 0 ) { } elseif ( is_scalar( $comment_id ) ) { $comment_id = (string) $comment_id; } else { - $comment_id = ''; + $comment_id = '0'; } if ( empty( $comment->comment_author ) ) { @@ -233,7 +233,13 @@ function get_comment_author_email_link( $link_text = '', $before = '', $after = function get_comment_author_link( $comment_id = 0 ) { $comment = get_comment( $comment_id ); - $comment_id = ! empty( $comment->comment_ID ) ? $comment->comment_ID : (string) $comment_id; + if ( ! empty( $comment->comment_ID ) ) { + $comment_id = $comment->comment_ID; + } elseif ( is_scalar( $comment_id ) ) { + $comment_id = (string) $comment_id; + } else { + $comment_id = '0'; + } $comment_author_url = get_comment_author_url( $comment ); $comment_author = get_comment_author( $comment ); diff --git a/tests/phpunit/tests/comment/getCommentAuthorLink.php b/tests/phpunit/tests/comment/getCommentAuthorLink.php new file mode 100644 index 0000000000000..c3d1033d837b5 --- /dev/null +++ b/tests/phpunit/tests/comment/getCommentAuthorLink.php @@ -0,0 +1,116 @@ +comment->create_and_get( + array( + 'comment_post_ID' => 0, + ) + ); + } + + public function get_comment_author_link_filter( $comment_author_link, $comment_author, $comment_id ) { + $this->assertSame( $comment_id, self::$comment->comment_ID, 'Comment IDs do not match.' ); + $this->assertIsString( $comment_id, '$comment_id parameter is not a string.' ); + + return $comment_author_link; + } + + public function test_comment_author_link_passes_correct_comment_id_for_comment_object() { + add_filter( 'get_comment_author_link', array( $this, 'get_comment_author_link_filter' ), 99, 3 ); + + get_comment_author_link( self::$comment ); + } + + public function test_comment_author_link_passes_correct_comment_id_for_int() { + add_filter( 'get_comment_author_link', array( $this, 'get_comment_author_link_filter' ), 99, 3 ); + + get_comment_author_link( (int) self::$comment->comment_ID ); + } + + public function get_comment_author_link_filter_non_existent_id( $comment_author_link, $comment_author, $comment_id ) { + $this->assertSame( $comment_id, (string) self::$non_existent_comment_id, 'Comment IDs do not match.' ); + $this->assertIsString( $comment_id, '$comment_id parameter is not a string.' ); + + return $comment_author_link; + } + + /** + * @ticket 60475 + */ + public function test_comment_author_link_passes_correct_comment_id_for_non_existent_comment() { + add_filter( 'get_comment_author_link', array( $this, 'get_comment_author_link_filter_non_existent_id' ), 99, 3 ); + + self::$non_existent_comment_id = self::$comment->comment_ID + 1; + + get_comment_author_link( self::$non_existent_comment_id ); // Non-existent comment ID. + } + + /** + * @ticket 61681 + * @ticket 61715 + * + * @dataProvider data_should_return_author_when_given_object_without_comment_id + * + * @param stdClass $comment_props Comment properties test data. + * @param string $expected The expected result. + * @param array $user_data Optional. User data for creating an author. Default empty array. + */ + public function test_should_return_author_when_given_object_without_comment_id( $comment_props, $expected, $user_data = array() ) { + if ( ! empty( $comment_props->user_id ) ) { + $user = self::factory()->user->create_and_get( $user_data ); + $comment_props->user_id = $user->ID; + } + + $comment = new WP_Comment( $comment_props ); + + $this->assertSame( $expected, get_comment_author_link( $comment ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_return_author_when_given_object_without_comment_id() { + return array( + 'with no author' => array( + 'comment_props' => new stdClass(), + 'expected' => 'Anonymous', + ), + 'with author name' => array( + 'comment_props' => (object) array( + 'comment_author' => 'tester1', + ), + 'expected' => 'tester1', + ), + 'with author name, empty ID' => array( + 'comment_props' => (object) array( + 'comment_author' => 'tester2', + 'comment_ID' => '', + ), + 'expected' => 'tester2', + ), + 'with author ID' => array( + 'comment_props' => (object) array( + 'user_id' => 1, // Populates in the test with an actual user ID. + ), + 'expected' => 'Tester3', + 'user_data' => array( + 'display_name' => 'Tester3', + ), + ), + ); + } +} From 4de5282f235f02605ffd86356c23d19c0a6b5ee7 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 25 Jul 2024 22:38:01 +0000 Subject: [PATCH 010/106] General: Update English Gravatar links. Replace links to en.gravatar.com with links to gravatar.com as the english site now uses the base domain. This avoids an unnecessary redirect for english language sites. The links remain translatable for non-english versions of WordPress. Props narenin, knutsp. Fixes #61424. git-svn-id: https://develop.svn.wordpress.org/trunk@58810 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/upgrade.php | 3 ++- src/wp-admin/user-edit.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 715f5e2bdbc70..a7f29b294aa92 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -278,7 +278,8 @@ function wp_install_defaults( $user_id ) { To get started with moderating, editing, and deleting comments, please visit the Comments screen in the dashboard. Commenter avatars come from Gravatar.' ), - esc_url( __( 'https://en.gravatar.com/' ) ) + /* translators: The localized Gravatar URL. */ + esc_url( __( 'https://gravatar.com/' ) ) ); $wpdb->insert( $wpdb->comments, diff --git a/src/wp-admin/user-edit.php b/src/wp-admin/user-edit.php index f66a54d85fbb6..07810adc7a841 100644 --- a/src/wp-admin/user-edit.php +++ b/src/wp-admin/user-edit.php @@ -628,7 +628,8 @@ $description = sprintf( /* translators: %s: Gravatar URL. */ __( 'You can change your profile picture on Gravatar.' ), - __( 'https://en.gravatar.com/' ) + /* translators: The localized Gravatar URL. */ + __( 'https://gravatar.com/' ) ); } else { $description = ''; From b38541de39c5e3ccb75ff76b49ab930846c878b6 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 25 Jul 2024 23:02:47 +0000 Subject: [PATCH 011/106] Options, Meta APIs: Prevent Single Site installs using network notoptions cache. Modifies the caching of `notoptions` in `delete_network_option()` to ensure that the network cache is bypassed on single site installs. On single site installs the incorrect caching was causing the `notoptions` cache to remain populated once a deleted option was subsequently added or updated. Follow up to [58782]. Props bjorsch, pbearne. Fixes #61730. See #61484. git-svn-id: https://develop.svn.wordpress.org/trunk@58811 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/option.php | 20 ++-- tests/phpunit/tests/option/networkOption.php | 110 +++++++++++++++++++ 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 125f25d40d157..537f7aafd0aba 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -2264,6 +2264,17 @@ function delete_network_option( $network_id, $option ) { 'site_id' => $network_id, ) ); + + if ( $result ) { + $notoptions_key = "$network_id:notoptions"; + $notoptions = wp_cache_get( $notoptions_key, 'site-options' ); + + if ( ! is_array( $notoptions ) ) { + $notoptions = array(); + } + $notoptions[ $option ] = true; + wp_cache_set( $notoptions_key, $notoptions, 'site-options' ); + } } if ( $result ) { @@ -2293,15 +2304,6 @@ function delete_network_option( $network_id, $option ) { */ do_action( 'delete_site_option', $option, $network_id ); - $notoptions_key = "$network_id:notoptions"; - $notoptions = wp_cache_get( $notoptions_key, 'site-options' ); - - if ( ! is_array( $notoptions ) ) { - $notoptions = array(); - } - $notoptions[ $option ] = true; - wp_cache_set( $notoptions_key, $notoptions, 'site-options' ); - return true; } diff --git a/tests/phpunit/tests/option/networkOption.php b/tests/phpunit/tests/option/networkOption.php index 3c89d5d0b408a..a4247d4926cec 100644 --- a/tests/phpunit/tests/option/networkOption.php +++ b/tests/phpunit/tests/option/networkOption.php @@ -60,6 +60,7 @@ public function test_delete_network_option_on_only_one_network() { * Tests that calling delete_network_option() updates nooptions when option deleted. * * @ticket 61484 + * @ticket 61730 * * @covers ::delete_network_option */ @@ -73,6 +74,11 @@ public function test_check_delete_network_option_updates_notoptions() { $this->assertIsArray( $notoptions, 'The notoptions cache is expected to be an array.' ); $this->assertTrue( $notoptions['foo'], 'The deleted options is expected to be in notoptions.' ); + if ( ! is_multisite() ) { + $network_notoptions = wp_cache_get( '1:notoptions', 'site-options' ); + $this->assertTrue( empty( $network_notoptions['foo'] ), 'The deleted option is not expected to be in network notoptions on a non-multisite.' ); + } + $before = get_num_queries(); get_network_option( 1, 'foo' ); $queries = get_num_queries() - $before; @@ -302,4 +308,108 @@ public function test_add_network_option_clears_the_notoptions_cache() { $updated_notoptions = wp_cache_get( $cache_key, $cache_group ); $this->assertArrayNotHasKey( $option_name, $updated_notoptions, 'The "foobar" option should not be in the notoptions cache after updating it.' ); } + + /** + * Test adding a previously known notoption returns the correct value. + * + * @ticket 61730 + * + * @covers ::add_network_option + * @covers ::delete_network_option + */ + public function test_adding_previous_notoption_returns_correct_value() { + $option_name = 'ticket_61730_option_to_be_created'; + + add_network_option( 1, $option_name, 'baz' ); + delete_network_option( 1, $option_name ); + + $this->assertFalse( get_network_option( 1, $option_name ), 'The option should not be found.' ); + + add_network_option( 1, $option_name, 'foo' ); + $this->assertSame( 'foo', get_network_option( 1, $option_name ), 'The option should return the newly set value.' ); + } + + /** + * Test `get_network_option()` does not use network notoptions cache for single sites. + * + * @ticket 61730 + * + * @group ms-excluded + * + * @covers ::get_network_option + */ + public function test_get_network_option_does_not_use_network_notoptions_cache_for_single_sites() { + get_network_option( 1, 'ticket_61730_notoption' ); + + $network_notoptions_cache = wp_cache_get( '1:notoptions', 'site-options' ); + $single_site_notoptions_cache = wp_cache_get( 'notoptions', 'options' ); + + $this->assertEmpty( $network_notoptions_cache, 'Network notoptions cache should not be set for single site installs.' ); + $this->assertIsArray( $single_site_notoptions_cache, 'Single site notoptions cache should be set.' ); + $this->assertArrayHasKey( 'ticket_61730_notoption', $single_site_notoptions_cache, 'The option should be in the notoptions cache.' ); + } + + /** + * Test `delete_network_option()` does not use network notoptions cache for single sites. + * + * @ticket 61730 + * @ticket 61484 + * + * @group ms-excluded + * + * @covers ::delete_network_option + */ + public function test_delete_network_option_does_not_use_network_notoptions_cache_for_single_sites() { + add_network_option( 1, 'ticket_61730_notoption', 'value' ); + delete_network_option( 1, 'ticket_61730_notoption' ); + + $network_notoptions_cache = wp_cache_get( '1:notoptions', 'site-options' ); + $single_site_notoptions_cache = wp_cache_get( 'notoptions', 'options' ); + + $this->assertEmpty( $network_notoptions_cache, 'Network notoptions cache should not be set for single site installs.' ); + $this->assertIsArray( $single_site_notoptions_cache, 'Single site notoptions cache should be set.' ); + $this->assertArrayHasKey( 'ticket_61730_notoption', $single_site_notoptions_cache, 'The option should be in the notoptions cache.' ); + } + + /** + * Test `get_network_option()` does not use single site notoptions cache for networks. + * + * @ticket 61730 + * + * @group ms-required + * + * @covers ::get_network_option + */ + public function test_get_network_option_does_not_use_single_site_notoptions_cache_for_networks() { + get_network_option( 1, 'ticket_61730_notoption' ); + + $network_notoptions_cache = wp_cache_get( '1:notoptions', 'site-options' ); + $single_site_notoptions_cache = wp_cache_get( 'notoptions', 'options' ); + + $this->assertEmpty( $single_site_notoptions_cache, 'Single site notoptions cache should not be set for multisite installs.' ); + $this->assertIsArray( $network_notoptions_cache, 'Multisite notoptions cache should be set.' ); + $this->assertArrayHasKey( 'ticket_61730_notoption', $network_notoptions_cache, 'The option should be in the notoptions cache.' ); + } + + /** + * Test `delete_network_option()` does not use single site notoptions cache for networks. + * + * @ticket 61730 + * @ticket 61484 + * + * @group ms-required + * + * @covers ::delete_network_option + */ + public function test_delete_network_option_does_not_use_single_site_notoptions_cache_for_networks() { + add_network_option( 1, 'ticket_61730_notoption', 'value' ); + delete_network_option( 1, 'ticket_61730_notoption' ); + + $network_notoptions_cache = wp_cache_get( '1:notoptions', 'site-options' ); + $single_site_notoptions_cache = wp_cache_get( 'notoptions', 'options' ); + + $this->assertEmpty( $single_site_notoptions_cache, 'Single site notoptions cache should not be set for multisite installs.' ); + $this->assertIsArray( $network_notoptions_cache, 'Multisite notoptions cache should be set.' ); + $this->assertArrayHasKey( 'ticket_61730_notoption', $network_notoptions_cache, 'The option should be in the notoptions cache.' ); + } } From ed59f778ba7e275c7f34f3f8dd5795abcb07ca91 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 26 Jul 2024 07:54:26 +0000 Subject: [PATCH 012/106] General: Cast `$max_depth` and `$depth` to an integer in the `Walker` class. This ensures that the arguments are correctly interpreted when passed as a query string, i.e. when `wp_parse_args()` is involved. For example, `wp_list_pages( 'depth=0' )` should display a list of all pages to the maximum depth. Follow-up to [57848]. Props freibergergarcia, peterwilsoncc, ahortin. Fixes #61749. git-svn-id: https://develop.svn.wordpress.org/trunk@58812 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-walker.php | 13 +++++++-- tests/phpunit/tests/post/wpListPages.php | 36 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-walker.php b/src/wp-includes/class-wp-walker.php index ff5eac2f50040..df67921c2746c 100644 --- a/src/wp-includes/class-wp-walker.php +++ b/src/wp-includes/class-wp-walker.php @@ -135,6 +135,9 @@ public function display_element( $element, &$children_elements, $max_depth, $dep return; } + $max_depth = (int) $max_depth; + $depth = (int) $depth; + $id_field = $this->db_fields['id']; $id = $element->$id_field; @@ -191,6 +194,8 @@ public function display_element( $element, &$children_elements, $max_depth, $dep public function walk( $elements, $max_depth, ...$args ) { $output = ''; + $max_depth = (int) $max_depth; + // Invalid parameter or nothing to walk. if ( $max_depth < -1 || empty( $elements ) ) { return $output; @@ -285,12 +290,14 @@ public function walk( $elements, $max_depth, ...$args ) { * @return string XHTML of the specified page of elements. */ public function paged_walk( $elements, $max_depth, $page_num, $per_page, ...$args ) { + $output = ''; + + $max_depth = (int) $max_depth; + if ( empty( $elements ) || $max_depth < -1 ) { - return ''; + return $output; } - $output = ''; - $parent_field = $this->db_fields['parent']; $count = -1; diff --git a/tests/phpunit/tests/post/wpListPages.php b/tests/phpunit/tests/post/wpListPages.php index 38ba729cee20b..f059591219b1a 100644 --- a/tests/phpunit/tests/post/wpListPages.php +++ b/tests/phpunit/tests/post/wpListPages.php @@ -160,6 +160,42 @@ public function test_wp_list_pages_depth() { $this->assertSameIgnoreEOL( $expected, wp_list_pages( $args ) ); } + /** + * @ticket 61749 + */ + public function test_wp_list_pages_depth_equals_zero() { + $expected = ''; + + // Execute wp_list_pages() with a string to force calling wp_parse_args(). + ob_start(); + wp_list_pages( 'depth=0' ); + $output = ob_get_clean(); + + // If depth equals 0, all levels should be displayed. + $this->assertSameIgnoreEOL( $expected, $output ); + } + public function test_wp_list_pages_show_date() { $args = array( 'echo' => false, From c41304d73c32c968056abd2b1ffa19e83e9833a9 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sat, 27 Jul 2024 00:25:44 +0000 Subject: [PATCH 013/106] General: Introduce `wp_get_wp_version()` to get unmodified version. Introduces `wp_get_wp_version()` to get an unmodified value of `$wp_version` from `wp-includes/version.php`. Some plugins modify the global in an attempt to improve security through obscurity. This practice can cause errors in WordPress so the ability to get an unmodified version is needed. Replaces instances within the code base in which `version.php` was required in order to get an unmodified value. `script-loader.php` is intentionally excluded from the replacements as the function is not always available to the file. Props debarghyabanerjee, afragen, costdev. See #61627. git-svn-id: https://develop.svn.wordpress.org/trunk@58813 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/credits.php | 5 +-- src/wp-admin/includes/dashboard.php | 5 +-- src/wp-admin/includes/import.php | 9 ++--- src/wp-admin/includes/plugin-install.php | 7 ++-- src/wp-admin/includes/theme.php | 7 ++-- src/wp-admin/includes/translation-install.php | 10 ++---- src/wp-admin/includes/update.php | 9 ++--- src/wp-includes/functions.php | 18 ++++++++++ ...s-wp-rest-pattern-directory-controller.php | 9 +---- src/wp-includes/script-loader.php | 16 +++++++-- src/wp-includes/update.php | 27 +++++---------- .../tests/functions/wpGetWpVersion.php | 34 +++++++++++++++++++ 12 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 tests/phpunit/tests/functions/wpGetWpVersion.php diff --git a/src/wp-admin/includes/credits.php b/src/wp-admin/includes/credits.php index 907ee93c0bae3..1ad2a37c2bb7e 100644 --- a/src/wp-admin/includes/credits.php +++ b/src/wp-admin/includes/credits.php @@ -19,10 +19,7 @@ */ function wp_credits( $version = '', $locale = '' ) { if ( ! $version ) { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - - $version = $wp_version; + $version = wp_get_wp_version(); } if ( ! $locale ) { diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index 2aeaa24e461b4..022a00174c8a0 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -1819,13 +1819,10 @@ function wp_check_browser_version() { $response = get_site_transient( 'browser_' . $key ); if ( false === $response ) { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - $url = 'http://api.wordpress.org/core/browse-happy/1.1/'; $options = array( 'body' => array( 'useragent' => $_SERVER['HTTP_USER_AGENT'] ), - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ), ); if ( wp_http_supports( array( 'ssl' ) ) ) { diff --git a/src/wp-admin/includes/import.php b/src/wp-admin/includes/import.php index 87ee00e5f7573..530045894c745 100644 --- a/src/wp-admin/includes/import.php +++ b/src/wp-admin/includes/import.php @@ -136,22 +136,19 @@ function wp_import_handle_upload() { * @return array Importers with metadata for each. */ function wp_get_popular_importers() { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - $locale = get_user_locale(); - $cache_key = 'popular_importers_' . md5( $locale . $wp_version ); + $cache_key = 'popular_importers_' . md5( $locale . wp_get_wp_version() ); $popular_importers = get_site_transient( $cache_key ); if ( ! $popular_importers ) { $url = add_query_arg( array( 'locale' => $locale, - 'version' => $wp_version, + 'version' => wp_get_wp_version(), ), 'http://api.wordpress.org/core/importers/1.1/' ); - $options = array( 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ) ); + $options = array( 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ) ); if ( wp_http_supports( array( 'ssl' ) ) ) { $url = set_url_scheme( $url, 'https' ); diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index 59537e21df962..38c4b50e7d049 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -100,9 +100,6 @@ * for more information on the make-up of possible return values depending on the value of `$action`. */ function plugins_api( $action, $args = array() ) { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - if ( is_array( $args ) ) { $args = (object) $args; } @@ -118,7 +115,7 @@ function plugins_api( $action, $args = array() ) { } if ( ! isset( $args->wp_version ) ) { - $args->wp_version = substr( $wp_version, 0, 3 ); // x.y + $args->wp_version = substr( wp_get_wp_version(), 0, 3 ); // x.y } /** @@ -168,7 +165,7 @@ function plugins_api( $action, $args = array() ) { $http_args = array( 'timeout' => 15, - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ), ); $request = wp_remote_get( $url, $http_args ); diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php index 64ef380fc45fe..8fc9c00f149db 100644 --- a/src/wp-admin/includes/theme.php +++ b/src/wp-admin/includes/theme.php @@ -493,9 +493,6 @@ function get_theme_feature_list( $api = true ) { * for more information on the make-up of possible return objects depending on the value of `$action`. */ function themes_api( $action, $args = array() ) { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - if ( is_array( $args ) ) { $args = (object) $args; } @@ -511,7 +508,7 @@ function themes_api( $action, $args = array() ) { } if ( ! isset( $args->wp_version ) ) { - $args->wp_version = substr( $wp_version, 0, 3 ); // x.y + $args->wp_version = substr( wp_get_wp_version(), 0, 3 ); // x.y } /** @@ -562,7 +559,7 @@ function themes_api( $action, $args = array() ) { $http_args = array( 'timeout' => 15, - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ), ); $request = wp_remote_get( $url, $http_args ); diff --git a/src/wp-admin/includes/translation-install.php b/src/wp-admin/includes/translation-install.php index ef9fcc6065d09..f18bbecd9cd26 100644 --- a/src/wp-admin/includes/translation-install.php +++ b/src/wp-admin/includes/translation-install.php @@ -17,9 +17,6 @@ * @return array|WP_Error On success an associative array of translations, WP_Error on failure. */ function translations_api( $type, $args = null ) { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - if ( ! in_array( $type, array( 'plugins', 'themes', 'core' ), true ) ) { return new WP_Error( 'invalid_type', __( 'Invalid translation type.' ) ); } @@ -46,7 +43,7 @@ function translations_api( $type, $args = null ) { $options = array( 'timeout' => 3, 'body' => array( - 'wp_version' => $wp_version, + 'wp_version' => wp_get_wp_version(), 'locale' => get_locale(), 'version' => $args['version'], // Version of plugin, theme or core. ), @@ -128,10 +125,7 @@ function wp_get_available_translations() { } } - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - - $api = translations_api( 'core', array( 'version' => $wp_version ) ); + $api = translations_api( 'core', array( 'version' => wp_get_wp_version() ) ); if ( is_wp_error( $api ) || empty( $api['translations'] ) ) { return array(); diff --git a/src/wp-admin/includes/update.php b/src/wp-admin/includes/update.php index 312289f78e008..629c1be1cc639 100644 --- a/src/wp-admin/includes/update.php +++ b/src/wp-admin/includes/update.php @@ -264,10 +264,7 @@ function core_update_footer( $msg = '' ) { $cur->response = ''; } - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - - $is_development_version = preg_match( '/alpha|beta|RC/', $wp_version ); + $is_development_version = preg_match( '/alpha|beta|RC/', wp_get_wp_version() ); if ( $is_development_version ) { return sprintf( @@ -854,8 +851,6 @@ function wp_theme_update_row( $theme_key, $theme ) { * @return void|false */ function maintenance_nag() { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; global $upgrading; $nag = isset( $upgrading ); @@ -873,7 +868,7 @@ function maintenance_nag() { * This flag is cleared whenever a successful update occurs using Core_Upgrader. */ $comparison = ! empty( $failed['critical'] ) ? '>=' : '>'; - if ( isset( $failed['attempted'] ) && version_compare( $failed['attempted'], $wp_version, $comparison ) ) { + if ( isset( $failed['attempted'] ) && version_compare( $failed['attempted'], wp_get_wp_version(), $comparison ) ) { $nag = true; } } diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index e821f6f2b08be..91afbecf963e1 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -9006,3 +9006,21 @@ function wp_admin_notice( $message, $args = array() ) { echo wp_kses_post( wp_get_admin_notice( $message, $args ) ); } + +/** + * Returns the current WordPress Version. + * + * Returns an unmodified version of `$wp_version`. Some plugins modify the + * global in an attempt to improve security through obscurity. This + * practice can cause errors in WordPress so the ability to get an + * unmodified version is needed. + * + * @since 6.7.0 + * + * @return string The current WordPress Version. + */ +function wp_get_wp_version() { + require ABSPATH . WPINC . '/version.php'; + + return $wp_version; +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php index 41f37c9f1db8d..3345ea0aa18a7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php @@ -87,13 +87,6 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { - /* - * Include an unmodified `$wp_version`, so the API can craft a response that's tailored to - * it. Some plugins modify the version in a misguided attempt to improve security by - * obscuring the version, which can cause invalid requests. - */ - require ABSPATH . WPINC . '/version.php'; - $valid_query_args = array( 'offset' => true, 'order' => true, @@ -106,7 +99,7 @@ public function get_items( $request ) { $query_args = array_intersect_key( $request->get_params(), $valid_query_args ); $query_args['locale'] = get_user_locale(); - $query_args['wp-version'] = $wp_version; + $query_args['wp-version'] = wp_get_wp_version(); $query_args['pattern-categories'] = isset( $request['category'] ) ? $request['category'] : false; $query_args['pattern-keywords'] = isset( $request['keyword'] ) ? $request['keyword'] : false; diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index b73f12b9cc37b..121adc646e0ad 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -684,7 +684,13 @@ function wp_scripts_get_suffix( $type = '' ) { static $suffixes; if ( null === $suffixes ) { - // Include an unmodified $wp_version. + /* + * Include an unmodified $wp_version. + * + * Note: wp_get_wp_version() is not used here, as this file can be included + * via wp-admin/load-scripts.php or wp-admin/load-styles.php, in which case + * wp-includes/functions.php is not loaded. + */ require ABSPATH . WPINC . '/version.php'; /* @@ -1522,7 +1528,13 @@ function wp_default_scripts( $scripts ) { function wp_default_styles( $styles ) { global $editor_styles; - // Include an unmodified $wp_version. + /* + * Include an unmodified $wp_version. + * + * Note: wp_get_wp_version() is not used here, as this file can be included + * via wp-admin/load-scripts.php or wp-admin/load-styles.php, in which case + * wp-includes/functions.php is not loaded. + */ require ABSPATH . WPINC . '/version.php'; if ( ! defined( 'SCRIPT_DEBUG' ) ) { diff --git a/src/wp-includes/update.php b/src/wp-includes/update.php index d521913bb93e4..f8aa27bd8d7bf 100644 --- a/src/wp-includes/update.php +++ b/src/wp-includes/update.php @@ -31,22 +31,20 @@ function wp_version_check( $extra_stats = array(), $force_check = false ) { return; } - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; $php_version = PHP_VERSION; $current = get_site_transient( 'update_core' ); $translations = wp_get_installed_translations( 'core' ); // Invalidate the transient when $wp_version changes. - if ( is_object( $current ) && $wp_version !== $current->version_checked ) { + if ( is_object( $current ) && wp_get_wp_version() !== $current->version_checked ) { $current = false; } if ( ! is_object( $current ) ) { $current = new stdClass(); $current->updates = array(); - $current->version_checked = $wp_version; + $current->version_checked = wp_get_wp_version(); } if ( ! empty( $extra_stats ) ) { @@ -95,7 +93,7 @@ function wp_version_check( $extra_stats = array(), $force_check = false ) { $extensions = get_loaded_extensions(); sort( $extensions, SORT_STRING | SORT_FLAG_CASE ); $query = array( - 'version' => $wp_version, + 'version' => wp_get_wp_version(), 'php' => $php_version, 'locale' => $locale, 'mysql' => $mysql_version, @@ -191,7 +189,7 @@ function wp_version_check( $extra_stats = array(), $force_check = false ) { $options = array( 'timeout' => $doing_cron ? 30 : 3, - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ), 'headers' => array( 'wp_install' => $wp_install, 'wp_blog' => home_url( '/' ), @@ -266,7 +264,7 @@ function wp_version_check( $extra_stats = array(), $force_check = false ) { $updates = new stdClass(); $updates->updates = $offers; $updates->last_checked = time(); - $updates->version_checked = $wp_version; + $updates->version_checked = wp_get_wp_version(); if ( isset( $body['translations'] ) ) { $updates->translations = $body['translations']; @@ -315,9 +313,6 @@ function wp_update_plugins( $extra_stats = array() ) { return; } - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - // If running blog-side, bail unless we've not checked in the last 12 hours. if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; @@ -423,7 +418,7 @@ function wp_update_plugins( $extra_stats = array() ) { 'locale' => wp_json_encode( $locales ), 'all' => wp_json_encode( true ), ), - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ), ); if ( $extra_stats ) { @@ -590,9 +585,6 @@ function wp_update_themes( $extra_stats = array() ) { return; } - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - $installed_themes = wp_get_themes(); $translations = wp_get_installed_translations( 'themes' ); @@ -705,7 +697,7 @@ function wp_update_themes( $extra_stats = array() ) { 'translations' => wp_json_encode( $translations ), 'locale' => wp_json_encode( $locales ), ), - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), + 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ), ); if ( $extra_stats ) { @@ -989,14 +981,11 @@ function wp_get_update_data() { * @global string $wp_version The WordPress version string. */ function _maybe_update_core() { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - $current = get_site_transient( 'update_core' ); if ( isset( $current->last_checked, $current->version_checked ) && 12 * HOUR_IN_SECONDS > ( time() - $current->last_checked ) - && $current->version_checked === $wp_version + && wp_get_wp_version() === $current->version_checked ) { return; } diff --git a/tests/phpunit/tests/functions/wpGetWpVersion.php b/tests/phpunit/tests/functions/wpGetWpVersion.php new file mode 100644 index 0000000000000..d10d946e3baea --- /dev/null +++ b/tests/phpunit/tests/functions/wpGetWpVersion.php @@ -0,0 +1,34 @@ +assertSame( $GLOBALS['wp_version'], wp_get_wp_version() ); + } + + /** + * Tests that changes to the `$wp_version` global are ignored. + * + * @ticket 61627 + */ + public function test_should_ignore_changes_to_wp_version_global() { + $original_wp_version = $GLOBALS['wp_version']; + $GLOBALS['wp_version'] = 'modified_wp_version'; + $actual = wp_get_wp_version(); + $GLOBALS['wp_version'] = $original_wp_version; + + $this->assertSame( $original_wp_version, $actual ); + } +} From d497e92424a70cd999f8ca5956aa70ccb32ed976 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 27 Jul 2024 07:50:32 +0000 Subject: [PATCH 014/106] Docs: Clarify the description for `wp_strip_all_tags()`. Follow-up to [11929], [27042]. Props coffee2code, krupalpanchal, mukesh27. Fixes #61759. git-svn-id: https://develop.svn.wordpress.org/trunk@58814 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/formatting.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index cd1ee2689489e..d170728e015b5 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5499,11 +5499,11 @@ function normalize_whitespace( $str ) { } /** - * Properly strips all HTML tags including script and style + * Properly strips all HTML tags including 'script' and 'style'. * * This differs from strip_tags() because it removes the contents of * the `' )` - * will return 'something'. wp_strip_all_tags will return '' + * will return 'something'. wp_strip_all_tags() will return an empty string. * * @since 2.9.0 * From 916a9925306c4f315a22a499c3624fb923d227fd Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sat, 27 Jul 2024 09:22:35 +0000 Subject: [PATCH 015/106] Twenty Thirteen: Fixes Code block not adjusting to font size changes. This resolves the Code block not changing when the font sizes are switched. This theme uses a 14 px size for both and this uses inherit when nested inside pre tag. Props viralsampat, sabernhardt. Fixes #61697. git-svn-id: https://develop.svn.wordpress.org/trunk@58815 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentythirteen/css/editor-style.css | 4 ++++ src/wp-content/themes/twentythirteen/style.css | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/wp-content/themes/twentythirteen/css/editor-style.css b/src/wp-content/themes/twentythirteen/css/editor-style.css index 24297fd5b88b4..5da64436b3983 100644 --- a/src/wp-content/themes/twentythirteen/css/editor-style.css +++ b/src/wp-content/themes/twentythirteen/css/editor-style.css @@ -174,6 +174,10 @@ pre { word-wrap: break-word; } +pre code { + font-size: inherit; +} + blockquote, q { quotes: none; diff --git a/src/wp-content/themes/twentythirteen/style.css b/src/wp-content/themes/twentythirteen/style.css index 058f7caf1abc1..2a5b83fb30ce1 100644 --- a/src/wp-content/themes/twentythirteen/style.css +++ b/src/wp-content/themes/twentythirteen/style.css @@ -231,6 +231,10 @@ pre { word-wrap: break-word; } +pre code { + font-size: inherit; +} + blockquote, q { -webkit-hyphens: none; From a3e07a2ec026aebc520385dc781cb10936e93d97 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sat, 27 Jul 2024 09:35:19 +0000 Subject: [PATCH 016/106] Multiple themes: Fixes Code block not adjusting to font size changes. This resolves the Code block not changing when the font sizes are switched in Twenty Eleven and Twenty Twelve. The solution is the same as used for other code block adjustments for font sizes. Props viralsampat, sabernhardt. Fixes #61753. git-svn-id: https://develop.svn.wordpress.org/trunk@58816 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentyeleven/editor-style.css | 3 +++ src/wp-content/themes/twentyeleven/style.css | 3 +++ src/wp-content/themes/twentytwelve/editor-style.css | 3 +++ src/wp-content/themes/twentytwelve/style.css | 1 + 4 files changed, 10 insertions(+) diff --git a/src/wp-content/themes/twentyeleven/editor-style.css b/src/wp-content/themes/twentyeleven/editor-style.css index 77342bf691c8e..b9717cd002a90 100644 --- a/src/wp-content/themes/twentyeleven/editor-style.css +++ b/src/wp-content/themes/twentyeleven/editor-style.css @@ -119,6 +119,9 @@ pre { code, kbd, samp, var { font: 13px Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; } +pre code { + font-size: inherit; +} abbr, acronym, dfn { border-bottom: 1px dotted #666; cursor: help; diff --git a/src/wp-content/themes/twentyeleven/style.css b/src/wp-content/themes/twentyeleven/style.css index fb10237ba77f8..dca83cfe9a4af 100644 --- a/src/wp-content/themes/twentyeleven/style.css +++ b/src/wp-content/themes/twentyeleven/style.css @@ -408,6 +408,9 @@ pre { code, kbd, samp, var { font: 13px Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; } +pre code { + font-size: inherit; +} abbr, acronym, dfn { border-bottom: 1px dotted #666; cursor: help; diff --git a/src/wp-content/themes/twentytwelve/editor-style.css b/src/wp-content/themes/twentytwelve/editor-style.css index d3934a9151dfa..9d3b015485309 100644 --- a/src/wp-content/themes/twentytwelve/editor-style.css +++ b/src/wp-content/themes/twentytwelve/editor-style.css @@ -157,6 +157,9 @@ var { font-size: 0.857142857rem; line-height: 2; } +pre code { + font-size: inherit; +} abbr, acronym, dfn { diff --git a/src/wp-content/themes/twentytwelve/style.css b/src/wp-content/themes/twentytwelve/style.css index 21b797f2ecf4a..0ad16374804e3 100644 --- a/src/wp-content/themes/twentytwelve/style.css +++ b/src/wp-content/themes/twentytwelve/style.css @@ -903,6 +903,7 @@ article.sticky .featured-post { .entry-content pre code, .comment-content pre code { display: block; + font-size: inherit; } .entry-content abbr, .comment-content abbr, From 5159e54b10503da0562c1f4e2ffa3611b28bebf8 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sat, 27 Jul 2024 10:12:31 +0000 Subject: [PATCH 017/106] Twenty Twenty: Adds Plain Style into Quote block. Plan style was missing for the Quote block. This brings it in as should have been originally. Props kajalgohel, devtanbir, costdev, sabernhardt. Fixes #56011. git-svn-id: https://develop.svn.wordpress.org/trunk@58817 602fd350-edb4-49c9-b593-d223f7449a82 --- .../twentytwenty/assets/css/editor-style-block-rtl.css | 5 +++++ .../themes/twentytwenty/assets/css/editor-style-block.css | 5 +++++ src/wp-content/themes/twentytwenty/style-rtl.css | 7 +++++++ src/wp-content/themes/twentytwenty/style.css | 7 +++++++ 4 files changed, 24 insertions(+) diff --git a/src/wp-content/themes/twentytwenty/assets/css/editor-style-block-rtl.css b/src/wp-content/themes/twentytwenty/assets/css/editor-style-block-rtl.css index f228e5d0ccf25..3b94567345077 100644 --- a/src/wp-content/themes/twentytwenty/assets/css/editor-style-block-rtl.css +++ b/src/wp-content/themes/twentytwenty/assets/css/editor-style-block-rtl.css @@ -683,6 +683,11 @@ hr.wp-block-separator.is-style-dots::before { letter-spacing: inherit; } +.editor-styles-wrapper .wp-block-quote.is-style-plain { + border-width: 0; + padding: 5px 20px; +} + .editor-styles-wrapper .wp-block-quote.is-style-large { border: none; padding: 0; diff --git a/src/wp-content/themes/twentytwenty/assets/css/editor-style-block.css b/src/wp-content/themes/twentytwenty/assets/css/editor-style-block.css index 5287a99d11794..2957482e044e6 100644 --- a/src/wp-content/themes/twentytwenty/assets/css/editor-style-block.css +++ b/src/wp-content/themes/twentytwenty/assets/css/editor-style-block.css @@ -687,6 +687,11 @@ hr.wp-block-separator.is-style-dots::before { letter-spacing: inherit; } +.editor-styles-wrapper .wp-block-quote.is-style-plain { + border-width: 0; + padding: 5px 20px; +} + .editor-styles-wrapper .wp-block-quote.is-style-large { border: none; padding: 0; diff --git a/src/wp-content/themes/twentytwenty/style-rtl.css b/src/wp-content/themes/twentytwenty/style-rtl.css index 2fc1a9bfbc7e2..210e858f0d6bc 100644 --- a/src/wp-content/themes/twentytwenty/style-rtl.css +++ b/src/wp-content/themes/twentytwenty/style-rtl.css @@ -3405,6 +3405,13 @@ figure.wp-block-table.is-style-stripes { padding: 0 2rem 0 0; } +/* STYLE: PLAIN */ + +.wp-block-quote.is-style-plain { + border-width: 0; + padding: 0.5rem 2rem; +} + /* STYLE: LARGE */ .wp-block-quote.is-large, diff --git a/src/wp-content/themes/twentytwenty/style.css b/src/wp-content/themes/twentytwenty/style.css index 1a802509a4785..4113ace6efc6e 100644 --- a/src/wp-content/themes/twentytwenty/style.css +++ b/src/wp-content/themes/twentytwenty/style.css @@ -3429,6 +3429,13 @@ figure.wp-block-table.is-style-stripes { /*rtl:end:ignore*/ } +/* STYLE: PLAIN */ + +.wp-block-quote.is-style-plain { + border-width: 0; + padding: 0.5rem 2rem; +} + /* STYLE: LARGE */ .wp-block-quote.is-large, From 859263db0657a7f536fdeefe60a1225ceb6fd4f7 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sat, 27 Jul 2024 12:32:21 +0000 Subject: [PATCH 018/106] Twenty Twenty-Three: Fixes unnecessary borders for links images in Whisper variation. This fixes the Whisper variation having borders for links images. Other styles did not have this. Props colorful-tones, sabernhardt. Fixes #57368. git-svn-id: https://develop.svn.wordpress.org/trunk@58818 602fd350-edb4-49c9-b593-d223f7449a82 --- .../twentytwentythree/styles/whisper.json | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/wp-content/themes/twentytwentythree/styles/whisper.json b/src/wp-content/themes/twentytwentythree/styles/whisper.json index 3fd195eaac4c7..d1e677be4ec21 100644 --- a/src/wp-content/themes/twentytwentythree/styles/whisper.json +++ b/src/wp-content/themes/twentytwentythree/styles/whisper.json @@ -83,6 +83,20 @@ }, "styles": { "blocks": { + "core/image": { + "elements": { + "link": { + "border": { + "width": "0" + }, + ":hover": { + "color": { + "background": "transparent" + } + } + } + } + }, "core/navigation": { "color": { "text": "var(--wp--preset--color--contrast)" @@ -169,6 +183,20 @@ } } }, + "core/post-featured-image": { + "elements": { + "link": { + "border": { + "width": "0" + }, + ":hover": { + "color": { + "background": "transparent" + } + } + } + } + }, "core/post-title": { "elements": { "link": { @@ -251,6 +279,15 @@ "width": "6px 0 0 0" } }, + "core/site-logo": { + "elements": { + "link": { + "border": { + "width": "0" + } + } + } + }, "core/site-title": { "elements": { "link": { From defe4b0ec5395ecc2e56ba02b802f5b8e47223c2 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sat, 27 Jul 2024 16:41:08 +0000 Subject: [PATCH 019/106] Twenty Twenty: Calendar and Table blocks do not apply custom font size. This fixes adding a custom font size to a Calendar and Table block. This was only an issue for custom font size entering. Props nidhidhandhukiya, yurajsinj2211, ankit-k-gupta, anveshika, sabernhardt, darshitrajyaguru97, shailu25, umesh84, SergeyBiryukov. Fixes #59996, #56157. git-svn-id: https://develop.svn.wordpress.org/trunk@58819 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentytwenty/style-rtl.css | 9 +++++++++ src/wp-content/themes/twentytwenty/style.css | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/wp-content/themes/twentytwenty/style-rtl.css b/src/wp-content/themes/twentytwenty/style-rtl.css index 210e858f0d6bc..e346329449895 100644 --- a/src/wp-content/themes/twentytwenty/style-rtl.css +++ b/src/wp-content/themes/twentytwenty/style-rtl.css @@ -3041,6 +3041,15 @@ ol.wp-block-latest-comments { letter-spacing: inherit; } +/* Block: Calendar --------------------------- */ + +.wp-block-calendar[class*="-font-size"] table, +.wp-block-calendar[style*="font-size"] table, +.wp-block-calendar[class*="-font-size"] .wp-calendar-nav, +.wp-block-calendar[style*="font-size"] .wp-calendar-nav { + font-size: inherit; +} + /* Block: Columns ---------------------------- */ .wp-block-columns.alignfull, diff --git a/src/wp-content/themes/twentytwenty/style.css b/src/wp-content/themes/twentytwenty/style.css index 4113ace6efc6e..305dc375e1fa4 100644 --- a/src/wp-content/themes/twentytwenty/style.css +++ b/src/wp-content/themes/twentytwenty/style.css @@ -3061,6 +3061,15 @@ ol.wp-block-latest-comments { letter-spacing: inherit; } +/* Block: Calendar --------------------------- */ + +.wp-block-calendar[class*="-font-size"] table, +.wp-block-calendar[style*="font-size"] table, +.wp-block-calendar[class*="-font-size"] .wp-calendar-nav, +.wp-block-calendar[style*="font-size"] .wp-calendar-nav { + font-size: inherit; +} + /* Block: Columns ---------------------------- */ .wp-block-columns.alignfull, From b59bc83af44b5895b9e2d790c8ead43a1b91a281 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sat, 27 Jul 2024 23:04:51 +0000 Subject: [PATCH 020/106] Application Passwords: Open documentation link in same window. Removes the `target` to to documentation for [https://developer.wordpress.org/apis/wp-config-php/#wp-environment-type setting the environment type] for applications passwords so the tabs open in the same window. This follows [58137] which added a confirmation prompt for users navigating away from the profile edit screen if they have changed data without saving it. Props sabernhardt, joedolson. Fixes #60100. git-svn-id: https://develop.svn.wordpress.org/trunk@58820 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/user-edit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/user-edit.php b/src/wp-admin/user-edit.php index 07810adc7a841..860a0a6e07340 100644 --- a/src/wp-admin/user-edit.php +++ b/src/wp-admin/user-edit.php @@ -859,7 +859,7 @@ set the environment type accordingly to enable application passwords.' ), + __( 'If this is a development website, you can set the environment type accordingly to enable application passwords.' ), __( 'https://developer.wordpress.org/apis/wp-config-php/#wp-environment-type' ) ); ?> From a3173b65060a2dd6c4c2cdcbc6dd81ed693f4c41 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 28 Jul 2024 18:25:13 +0000 Subject: [PATCH 021/106] Feeds: Introduce `feed_links_args` and `feed_links_extra_args` filters. This allows for more flexibility in modifying how feed links are displayed by the `feed_links()` and `feed_links_extra()` functions, including, for example, a way to change the `»` separator to something else. Follow-up to [10377], [33838], [33839], [53125], [54161]. Props topdownjimmy, tw2113, williampatton. Fixes #43225. git-svn-id: https://develop.svn.wordpress.org/trunk@58821 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/general-template.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index bf70defd38701..f199000b5cd53 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -3120,6 +3120,15 @@ function feed_links( $args = array() ) { $args = wp_parse_args( $args, $defaults ); + /** + * Filters the feed links arguments. + * + * @since 6.7.0 + * + * @param array $args An array of feed links arguments. + */ + $args = apply_filters( 'feed_links_args', $args ); + /** * Filters whether to display the posts feed link. * @@ -3182,6 +3191,15 @@ function feed_links_extra( $args = array() ) { $args = wp_parse_args( $args, $defaults ); + /** + * Filters the extra feed links arguments. + * + * @since 6.7.0 + * + * @param array $args An array of extra feed links arguments. + */ + $args = apply_filters( 'feed_links_extra_args', $args ); + if ( is_singular() ) { $id = 0; $post = get_post( $id ); From c1ffe3309abbda7abecdd292ad43309f3ea59495 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 29 Jul 2024 01:57:11 +0000 Subject: [PATCH 022/106] Users: Always use HTTPS URLs for Gravatar links. Modifies gravatar image URLs to always use the HTTPS version from secure.gravatar.com. Gravatar now redirects HTTP image requests to their HTTPS equivalent, resulting in redirects for sites running over an HTTP connection (`is_ssl() === false`). Since the introduction of HTTP/2 the use of sub-domains for different hashes ([1-3].gravatar.com) now represents a performance hinderance rather than improvement. The scheme passed to `get_avatar_data()` is now ignored for the generation of Gravatar URLs but the setting retained to avoid introducing bugs for sites using either local avatars or third party providers. Props neoxx, SergeyBiryukov, sippis, peterwilsoncc, mukesh27, costdev, dd32. Fixes #37454. git-svn-id: https://develop.svn.wordpress.org/trunk@58822 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/link-template.php | 21 ++++++----- tests/phpunit/tests/avatar.php | 20 ++++++++--- .../tests/rest-api/rest-schema-setup.php | 6 ++-- tests/qunit/fixtures/wp-api-generated.js | 36 +++++++++---------- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 37e8684bf0d8e..426da57c392fa 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -4328,6 +4328,7 @@ function is_avatar_comment_type( $comment_type ) { * Retrieves default data about the avatar. * * @since 4.2.0 + * @since 6.7.0 Gravatar URLs always use HTTPS. * * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash, * user email, WP_User object, WP_Post object, or WP_Comment object. @@ -4358,6 +4359,9 @@ function is_avatar_comment_type( $comment_type ) { * - 'X' (even more mature than above) * Default is the value of the 'avatar_rating' option. * @type string $scheme URL scheme to use. See set_url_scheme() for accepted values. + * For Gravatars this setting is ignored and HTTPS is used to avoid + * unnecessary redirects. The setting is retained for systems using + * the {@see 'pre_get_avatar_data'} filter to customize avatars. * Default null. * @type array $processed_args When the function returns, the value will be the processed/sanitized $args * plus a "found_avatar" guess. Pass as a reference. Default null. @@ -4508,9 +4512,6 @@ function get_avatar_data( $id_or_email, $args = null ) { if ( $email_hash ) { $args['found_avatar'] = true; - $gravatar_server = hexdec( $email_hash[0] ) % 3; - } else { - $gravatar_server = rand( 0, 2 ); } $url_args = array( @@ -4520,15 +4521,17 @@ function get_avatar_data( $id_or_email, $args = null ) { 'r' => $args['rating'], ); - if ( is_ssl() ) { - $url = 'https://secure.gravatar.com/avatar/' . $email_hash; - } else { - $url = sprintf( 'http://%d.gravatar.com/avatar/%s', $gravatar_server, $email_hash ); - } + /* + * Gravatars are always served over HTTPS. + * + * The Gravatar website redirects HTTP requests to HTTPS URLs so always + * use the HTTPS scheme to avoid unnecessary redirects. + */ + $url = 'https://secure.gravatar.com/avatar/' . $email_hash; $url = add_query_arg( rawurlencode_deep( array_filter( $url_args ) ), - set_url_scheme( $url, $args['scheme'] ) + $url ); /** diff --git a/tests/phpunit/tests/avatar.php b/tests/phpunit/tests/avatar.php index 32ff9ac648f8d..97f24841e372b 100644 --- a/tests/phpunit/tests/avatar.php +++ b/tests/phpunit/tests/avatar.php @@ -11,7 +11,7 @@ class Tests_Avatar extends WP_UnitTestCase { */ public function test_get_avatar_url_gravatar_url() { $url = get_avatar_url( 1 ); - $this->assertSame( preg_match( '|^http?://[0-9]+.gravatar.com/avatar/[0-9a-f]{32}\?|', $url ), 1 ); + $this->assertSame( preg_match( '|^https?://secure.gravatar.com/avatar/[0-9a-f]{32}\?|', $url ), 1 ); } /** @@ -56,19 +56,29 @@ public function test_get_avatar_url_rating() { } /** + * Ensures the get_avatar_url always returns an HTTPS scheme for gravatars. + * * @ticket 21195 + * @ticket 37454 + * + * @covers ::get_avatar_url */ public function test_get_avatar_url_scheme() { $url = get_avatar_url( 1 ); - $this->assertSame( preg_match( '|^http://|', $url ), 1 ); + $this->assertSame( preg_match( '|^https://|', $url ), 1, 'Avatars should default to the HTTPS scheme' ); $args = array( 'scheme' => 'https' ); $url = get_avatar_url( 1, $args ); - $this->assertSame( preg_match( '|^https://|', $url ), 1 ); + $this->assertSame( preg_match( '|^https://|', $url ), 1, 'Requesting the HTTPS scheme should be respected' ); + + $args = array( 'scheme' => 'http' ); + $url = get_avatar_url( 1, $args ); + $this->assertSame( preg_match( '|^https://|', $url ), 1, 'Requesting the HTTP scheme should return an HTTPS URL to avoid redirects' ); $args = array( 'scheme' => 'lolcat' ); $url = get_avatar_url( 1, $args ); - $this->assertSame( preg_match( '|^lolcat://|', $url ), 0 ); + $this->assertSame( preg_match( '|^lolcat://|', $url ), 0, 'Unrecognized schemes should be ignored' ); + $this->assertSame( preg_match( '|^https://|', $url ), 1, 'Unrecognized schemes should return an HTTPS URL' ); } /** @@ -257,7 +267,7 @@ public function test_get_avatar_data_should_return_gravatar_url_when_input_avata $actual_data = get_avatar_data( $comment ); $this->assertTrue( is_avatar_comment_type( $comment_type ) ); - $this->assertMatchesRegularExpression( '|^http?://[0-9]+.gravatar.com/avatar/[0-9a-f]{32}\?|', $actual_data['url'] ); + $this->assertMatchesRegularExpression( '|^https?://secure.gravatar.com/avatar/[0-9a-f]{32}\?|', $actual_data['url'] ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 3c5b8a1966b7f..4d5c269357a68 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -729,9 +729,9 @@ public function test_build_wp_api_client_fixtures() { 'TagModel.meta.test_multi' => array(), 'TagModel.meta.test_tag_meta' => '', 'UsersCollection.0.link' => 'http://example.org/?author=1', - 'UsersCollection.0.avatar_urls.24' => 'http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=24&d=mm&r=g', - 'UsersCollection.0.avatar_urls.48' => 'http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=48&d=mm&r=g', - 'UsersCollection.0.avatar_urls.96' => 'http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=96&d=mm&r=g', + 'UsersCollection.0.avatar_urls.24' => 'https://secure.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=24&d=mm&r=g', + 'UsersCollection.0.avatar_urls.48' => 'https://secure.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=48&d=mm&r=g', + 'UsersCollection.0.avatar_urls.96' => 'https://secure.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=96&d=mm&r=g', 'UsersCollection.0._links.self.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/users/1', 'UsersCollection.0._links.collection.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/users', 'UsersCollection.1.id' => 2, diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index a5a2d3622eb0f..7e97f19b18588 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -13748,9 +13748,9 @@ mockedApiResponse.UsersCollection = [ "link": "http://example.org/?author=1", "slug": "admin", "avatar_urls": { - "24": "http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=24&d=mm&r=g", - "48": "http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=48&d=mm&r=g", - "96": "http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=96&d=mm&r=g" + "24": "https://secure.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=96&d=mm&r=g" }, "meta": { "meta_key": "meta_value" @@ -13776,9 +13776,9 @@ mockedApiResponse.UsersCollection = [ "link": "http://example.org/?author=2", "slug": "restapiclientfixtureuser", "avatar_urls": { - "24": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=24&d=mm&r=g", - "48": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=48&d=mm&r=g", - "96": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=96&d=mm&r=g" + "24": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=96&d=mm&r=g" }, "meta": { "meta_key": "" @@ -13806,9 +13806,9 @@ mockedApiResponse.UserModel = { "link": "http://example.org/?author=2", "slug": "restapiclientfixtureuser", "avatar_urls": { - "24": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=24&d=mm&r=g", - "48": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=48&d=mm&r=g", - "96": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=96&d=mm&r=g" + "24": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=96&d=mm&r=g" }, "meta": { "meta_key": "" @@ -13823,9 +13823,9 @@ mockedApiResponse.me = { "link": "http://example.org/?author=2", "slug": "restapiclientfixtureuser", "avatar_urls": { - "24": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=24&d=mm&r=g", - "48": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=48&d=mm&r=g", - "96": "http://2.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=96&d=mm&r=g" + "24": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/57cbd982c963c7eb2294e2eee1b4448e?s=96&d=mm&r=g" }, "meta": { "meta_key": "" @@ -13849,9 +13849,9 @@ mockedApiResponse.CommentsCollection = [ "status": "approved", "type": "comment", "author_avatar_urls": { - "24": "http://2.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=24&d=mm&r=g", - "48": "http://2.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=48&d=mm&r=g", - "96": "http://2.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=96&d=mm&r=g" + "24": "https://secure.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=96&d=mm&r=g" }, "meta": { "meta_key": "meta_value" @@ -13894,9 +13894,9 @@ mockedApiResponse.CommentModel = { "status": "approved", "type": "comment", "author_avatar_urls": { - "24": "http://2.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=24&d=mm&r=g", - "48": "http://2.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=48&d=mm&r=g", - "96": "http://2.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=96&d=mm&r=g" + "24": "https://secure.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/bd7c2b505bcf39cc71cfee564c614956?s=96&d=mm&r=g" }, "meta": { "meta_key": "meta_value" From bc87db7201e690d85c48e08409a38fc20f78fcea Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Mon, 29 Jul 2024 09:30:34 +0000 Subject: [PATCH 023/106] Twenty Seventeen: Fixes floated images having an extra space when the first image. The first image when floated in content had extra spacing. This was only for the first image in testing so the solution focuses on that. Props kjellr, sabernhardt, hmbashar, shailu25. Fixes #46785. git-svn-id: https://develop.svn.wordpress.org/trunk@58823 602fd350-edb4-49c9-b593-d223f7449a82 --- .../themes/twentyseventeen/assets/css/blocks.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wp-content/themes/twentyseventeen/assets/css/blocks.css b/src/wp-content/themes/twentyseventeen/assets/css/blocks.css index 65efb8378a16c..46ecf2ca15ffe 100644 --- a/src/wp-content/themes/twentyseventeen/assets/css/blocks.css +++ b/src/wp-content/themes/twentyseventeen/assets/css/blocks.css @@ -53,10 +53,12 @@ p.has-drop-cap:not(:focus)::first-letter { } .wp-block-image figure.alignleft { + margin-top: 0; margin-right: 1.5em; } .wp-block-image figure.alignright { + margin-top: 0; margin-left: 1.5em; } @@ -70,6 +72,11 @@ p.has-drop-cap:not(:focus)::first-letter { box-shadow: none; } +.entry-content > .wp-block-image:first-child figure.alignleft, +.entry-content > .wp-block-image:first-child figure.alignright { + margin-top: 1.5em; +} + /* Gallery */ .wp-block-gallery { From 907412bf9867dd20265f27721baad3241ab39699 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Mon, 29 Jul 2024 10:08:22 +0000 Subject: [PATCH 024/106] Twenty Twenty: Fixes Table font size when custom showing on front. The Table block was not reflecting the custom font size on the front. This solution now brings custom font sizes in for front the same as back in the editor. Props umesh84, SergeyBiryukov, sabernhardt, shailu25. Fixes #56157. git-svn-id: https://develop.svn.wordpress.org/trunk@58824 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentytwenty/style-rtl.css | 5 +++++ src/wp-content/themes/twentytwenty/style.css | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/wp-content/themes/twentytwenty/style-rtl.css b/src/wp-content/themes/twentytwenty/style-rtl.css index e346329449895..dc17ce27df54c 100644 --- a/src/wp-content/themes/twentytwenty/style-rtl.css +++ b/src/wp-content/themes/twentytwenty/style-rtl.css @@ -3389,6 +3389,11 @@ figure.wp-block-table.is-style-stripes { border-collapse: inherit; } +.wp-block-table[class*="-font-size"] table, +.wp-block-table[style*="font-size"] table { + font-size: inherit; +} + /* Block: Quote ------------------------------ */ .wp-block-quote p, diff --git a/src/wp-content/themes/twentytwenty/style.css b/src/wp-content/themes/twentytwenty/style.css index 305dc375e1fa4..3ee6dbc431f3a 100644 --- a/src/wp-content/themes/twentytwenty/style.css +++ b/src/wp-content/themes/twentytwenty/style.css @@ -3409,6 +3409,11 @@ figure.wp-block-table.is-style-stripes { border-collapse: inherit; } +.wp-block-table[class*="-font-size"] table, +.wp-block-table[style*="font-size"] table { + font-size: inherit; +} + /* Block: Quote ------------------------------ */ .wp-block-quote p, From 679cc0c4a261a77bd8fdb140cd9b0b2ff80ebf37 Mon Sep 17 00:00:00 2001 From: luisherranz Date: Mon, 29 Jul 2024 11:08:18 +0000 Subject: [PATCH 025/106] Interactivity API: Allow server derived state to appear in non-final position In some cases, derived state returns an associative array. Directives may wish to continue to access properties of the associative array, when using the syntax `state.arrayReturnedByClosure.property`. This patch continues evaluating the path after the associative array has been returned by the Closure. Props jonsurrell, luisherranz. Fixes #61741. git-svn-id: https://develop.svn.wordpress.org/trunk@58825 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-interactivity-api.php | 51 ++++++++++--------- .../interactivity-api/wpInteractivityAPI.php | 24 ++++++++- 2 files changed, 49 insertions(+), 26 deletions(-) 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 50fb4d6ac216f..b552d07938e07 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -494,6 +494,7 @@ private function _process_directives( string $html ) { * @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. * @since 6.6.0 Removed `default_namespace` and `context` arguments. + * @since 6.6.0 Add support for derived state. * * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist or the namespace is falsy. @@ -530,32 +531,32 @@ private function evaluate( $directive_value ) { } else { return null; } - } - if ( $current instanceof Closure ) { - /* - * This state getter's namespace is added to the stack so that - * `state()` or `get_config()` read that namespace when called - * without specifying one. - */ - array_push( $this->namespace_stack, $ns ); - try { - $current = $current(); - } catch ( Throwable $e ) { - _doing_it_wrong( - __METHOD__, - sprintf( - /* translators: 1: Path pointing to an Interactivity API state property, 2: Namespace for an Interactivity API store. */ - __( 'Uncaught error executing a derived state callback with path "%1$s" and namespace "%2$s".' ), - $path, - $ns - ), - '6.6.0' - ); - return null; - } finally { - // Remove the property's namespace from the stack. - array_pop( $this->namespace_stack ); + if ( $current instanceof Closure ) { + /* + * This state getter's namespace is added to the stack so that + * `state()` or `get_config()` read that namespace when called + * without specifying one. + */ + array_push( $this->namespace_stack, $ns ); + try { + $current = $current(); + } catch ( Throwable $e ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: Path pointing to an Interactivity API state property, 2: Namespace for an Interactivity API store. */ + __( 'Uncaught error executing a derived state callback with path "%1$s" and namespace "%2$s".' ), + $path, + $ns + ), + '6.6.0' + ); + return null; + } finally { + // Remove the property's namespace from the stack. + array_pop( $this->namespace_stack ); + } } } diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index b9cc2c0b83fac..e9349190ebdb2 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -1297,7 +1297,6 @@ public function test_evaluate_derived_state_defined_in_different_namespace() { $this->assertSame( "Derived state: otherPlugin-state\nDerived context: otherPlugin-context", $result ); } - /** * Tests the `evaluate` method for derived state functions that throw. * @@ -1322,6 +1321,29 @@ public function test_evaluate_derived_state_that_throws() { $this->assertNull( $result ); } + /** + * Tests the `evaluate` method for derived state intermediate values. + * + * @ticket 61741 + * + * @covers ::evaluate + */ + public function test_evaluate_derived_state_intermediate() { + $this->interactivity->state( + 'myPlugin', + array( + 'derivedState' => function () { + return array( 'property' => 'value' ); + }, + ) + ); + $this->set_internal_context_stack(); + $this->set_internal_namespace_stack( 'myPlugin' ); + + $result = $this->evaluate( 'state.derivedState.property' ); + $this->assertSame( 'value', $result ); + } + /** * Tests the `kebab_to_camel_case` method. * From 3977e6281df1c1c60fa4fce2496f31408f29fb04 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 29 Jul 2024 14:50:44 +0000 Subject: [PATCH 026/106] General: Move `wp_get_wp_version()` to a more appropriate place. This places the function in a more predictable location, next to the `is_wp_version_compatible()` and `is_php_version_compatible()` functions. Follow-up to [58813]. See #61627. git-svn-id: https://develop.svn.wordpress.org/trunk@58826 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 91afbecf963e1..a8843038935c2 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -8807,6 +8807,23 @@ function clean_dirsize_cache( $path ) { set_transient( 'dirsize_cache', $directory_cache, $expiration ); } +/** + * Returns the current WordPress version. + * + * Returns an unmodified value of `$wp_version`. Some plugins modify the global + * in an attempt to improve security through obscurity. This practice can cause + * errors in WordPress, so the ability to get an unmodified version is needed. + * + * @since 6.7.0 + * + * @return string The current WordPress version. + */ +function wp_get_wp_version() { + require ABSPATH . WPINC . '/version.php'; + + return $wp_version; +} + /** * Checks compatibility with the current WordPress version. * @@ -9006,21 +9023,3 @@ function wp_admin_notice( $message, $args = array() ) { echo wp_kses_post( wp_get_admin_notice( $message, $args ) ); } - -/** - * Returns the current WordPress Version. - * - * Returns an unmodified version of `$wp_version`. Some plugins modify the - * global in an attempt to improve security through obscurity. This - * practice can cause errors in WordPress so the ability to get an - * unmodified version is needed. - * - * @since 6.7.0 - * - * @return string The current WordPress Version. - */ -function wp_get_wp_version() { - require ABSPATH . WPINC . '/version.php'; - - return $wp_version; -} From 883d9b673185e9f0b40ac9929a59f4725af379d9 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 29 Jul 2024 15:00:44 +0000 Subject: [PATCH 027/106] General: Memoize the return value in `wp_get_wp_version()`. This aims to optimize performance by saving the return value to a static variable, so that the `version.php` file is not unnecessarily required on each function call. Follow-up to [58813]. Props Cybr, debarghyabanerjee, mukesh27. Fixes #61782. See #61627. git-svn-id: https://develop.svn.wordpress.org/trunk@58827 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index a8843038935c2..136012dd07284 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -8819,7 +8819,11 @@ function clean_dirsize_cache( $path ) { * @return string The current WordPress version. */ function wp_get_wp_version() { - require ABSPATH . WPINC . '/version.php'; + static $wp_version; + + if ( ! isset( $wp_version ) ) { + require ABSPATH . WPINC . '/version.php'; + } return $wp_version; } From c73d8a441521d3fe792c3f1ea4cff048fc358dcd Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 29 Jul 2024 17:37:48 +0000 Subject: [PATCH 028/106] HTML API: Close all elements at the end of a document. When the model of breadcrumb generation in the HTML Processor and node traversal was simplified, the change introduced a bug whereby unclosed nodes at the end of a document would remain unvisited and unclosed. In this patch, a fix is applied to ensure that all open elements close while traversing a document. A couple of minor documentation typos are fixed in the patch as well. Developed in https://github.com/wordpress/wordpress-develop/pull/7085 Discussed in https://core.trac.wordpress.org/ticket/61576 Follow-up to [58713]. Props: dmsnell, gziolo, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58828 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 17 +++++--- .../tests/html-api/wpHtmlProcessor.php | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 975f21a0f0d77..f8653022454b6 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -609,25 +609,30 @@ public function next_token(): bool { * until there are events or until there are no more * tokens works in the meantime and isn't obviously wrong. */ - while ( empty( $this->element_queue ) && $this->step() ) { - continue; + if ( empty( $this->element_queue ) && $this->step() ) { + return $this->next_token(); } // Process the next event on the queue. $this->current_element = array_shift( $this->element_queue ); if ( ! isset( $this->current_element ) ) { - return false; + // There are no tokens left, so close all remaining open elements. + while ( $this->state->stack_of_open_elements->pop() ) { + continue; + } + + return empty( $this->element_queue ) ? false : $this->next_token(); } $is_pop = WP_HTML_Stack_Event::POP === $this->current_element->operation; /* * The root node only exists in the fragment parser, and closing it - * indicates that the parse is complete. Stop before popping if from + * indicates that the parse is complete. Stop before popping it from * the breadcrumbs. */ if ( 'root-node' === $this->current_element->token->bookmark_name ) { - return ! $is_pop && $this->next_token(); + return $this->next_token(); } // Adjust the breadcrumbs for this event. @@ -638,7 +643,7 @@ public function next_token(): bool { } // Avoid sending close events for elements which don't expect a closing. - if ( $is_pop && ! static::expects_closer( $this->current_element->token ) ) { + if ( $is_pop && ! $this->expects_closer( $this->current_element->token ) ) { return $this->next_token(); } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 12f36ca742989..01e0f4f02c0b5 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -475,6 +475,47 @@ public static function data_html_with_target_element_and_depth_of_next_node_in_b ); } + /** + * Ensures that elements which are unopened at the end of a document are implicitly closed. + * + * @ticket 61576 + */ + public function test_closes_unclosed_elements() { + $processor = WP_HTML_Processor::create_fragment( '

' ); + + $this->assertTrue( + $processor->next_tag( 'SPAN' ), + 'Could not find SPAN element: check test setup.' + ); + + // This is the end of the document, but there should be three closing events. + $processor->next_token(); + $this->assertSame( + 'SPAN', + $processor->get_tag(), + 'Should have found implicit SPAN closing tag.' + ); + + $processor->next_token(); + $this->assertSame( + 'P', + $processor->get_tag(), + 'Should have found implicit P closing tag.' + ); + + $processor->next_token(); + $this->assertSame( + 'DIV', + $processor->get_tag(), + 'Should have found implicit DIV closing tag.' + ); + + $this->assertFalse( + $processor->next_token(), + "Should have failed to find any more tokens but found a '{$processor->get_token_name()}'" + ); + } + /** * Ensures that subclasses can be created from ::create_fragment method. * From c6aaf0a7db6e0555ac61b96cecb699b850fb3187 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 29 Jul 2024 17:57:12 +0000 Subject: [PATCH 029/106] HTML API: Add set_modifiable_text() for replacing text nodes. This patch introduces a new method, `set_modifiable_text()` to the Tag Processor, which makes it possible and safe to replace text nodes within an HTML document, performing the appropriate escaping. This method can be used in conjunction with other code to modify the text content of a document, and can be used for transforming HTML in a streaming fashion. Developed in https://github.com/wordpress/wordpress-develop/pull/7007 Discussed in https://core.trac.wordpress.org/ticket/61617 Props: dmsnell, gziolo, zieladam. Fixes #61617. git-svn-id: https://develop.svn.wordpress.org/trunk@58829 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-tag-processor.php | 155 +++++++++++- .../wpHtmlTagProcessorModifiableText.php | 235 ++++++++++++++++++ 2 files changed, 389 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 7d04fd31d80d2..c619806525732 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -2889,7 +2889,9 @@ public function get_modifiable_text(): string { return ''; } - $text = substr( $this->html, $this->text_starts_at, $this->text_length ); + $text = isset( $this->lexical_updates['modifiable text'] ) + ? $this->lexical_updates['modifiable text']->text + : substr( $this->html, $this->text_starts_at, $this->text_length ); /* * Pre-processing the input stream would normally happen before @@ -2956,6 +2958,157 @@ public function get_modifiable_text(): string { : str_replace( "\x00", "\u{FFFD}", $decoded ); } + /** + * Sets the modifiable text for the matched token, if matched. + * + * Modifiable text is text content that may be read and changed without + * changing the HTML structure of the document around it. This includes + * the contents of `#text` nodes in the HTML as well as the inner + * contents of HTML comments, Processing Instructions, and others, even + * though these nodes aren't part of a parsed DOM tree. They also contain + * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any + * other section in an HTML document which cannot contain HTML markup (DATA). + * + * Not all modifiable text may be set by this method, and not all content + * may be set as modifiable text. In the case that this fails it will return + * `false` indicating as much. For instance, it will not allow inserting the + * string `next_tag( 'STYLE' ) ) { + * $style = $processor->get_modifiable_text(); + * $processor->set_modifiable_text( "// Made with love on the World Wide Web\n{$style}" ); + * } + * + * // Replace smiley text with Emoji smilies. + * while ( $processor->next_token() ) { + * if ( '#text' !== $processor->get_token_name() ) { + * continue; + * } + * + * $chunk = $processor->get_modifiable_text(); + * if ( ! str_contains( $chunk, ':)' ) ) { + * continue; + * } + * + * $processor->set_modifiable_text( str_replace( ':)', '🙂', $chunk ) ); + * } + * + * @since 6.7.0 + * + * @param string $plaintext_content New text content to represent in the matched token. + * + * @return bool Whether the text was able to update. + */ + public function set_modifiable_text( string $plaintext_content ): bool { + if ( self::STATE_TEXT_NODE === $this->parser_state ) { + $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( + $this->text_starts_at, + $this->text_length, + htmlspecialchars( $plaintext_content, ENT_QUOTES | ENT_HTML5 ) + ); + + return true; + } + + // Comment data is not encoded. + if ( + self::STATE_COMMENT === $this->parser_state && + self::COMMENT_AS_HTML_COMMENT === $this->comment_type + ) { + // Check if the text could close the comment. + if ( 1 === preg_match( '/--!?>/', $plaintext_content ) ) { + return false; + } + + $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( + $this->text_starts_at, + $this->text_length, + $plaintext_content + ); + + return true; + } + + if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { + return false; + } + + switch ( $this->get_tag() ) { + case 'SCRIPT': + /* + * This is over-protective, but ensures the update doesn't break + * out of the SCRIPT element. A more thorough check would need to + * ensure that the script closing tag doesn't exist, and isn't + * also "hidden" inside the script double-escaped state. + * + * It may seem like replacing `lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( + $this->text_starts_at, + $this->text_length, + $plaintext_content + ); + + return true; + + case 'STYLE': + $plaintext_content = preg_replace_callback( + '~style)~i', + static function ( $tag_match ) { + return "\\3c\\2f{$tag_match['TAG_NAME']}"; + }, + $plaintext_content + ); + + $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( + $this->text_starts_at, + $this->text_length, + $plaintext_content + ); + + return true; + + case 'TEXTAREA': + case 'TITLE': + $plaintext_content = preg_replace_callback( + "~{$this->get_tag()})~i", + static function ( $tag_match ) { + return "</{$tag_match['TAG_NAME']}"; + }, + $plaintext_content + ); + + /* + * These don't _need_ to be escaped, but since they are decoded it's + * safe to leave them escaped and this can prevent other code from + * naively detecting tags within the contents. + * + * @todo It would be useful to prefix a multiline replacement text + * with a newline, but not necessary. This is for aesthetics. + */ + $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( + $this->text_starts_at, + $this->text_length, + $plaintext_content + ); + + return true; + } + + return false; + } + /** * Updates or creates a new attribute on the currently matched tag with the passed value. * diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 2c8c07e410b74..717d061016a2d 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -39,6 +39,90 @@ public function test_get_modifiable_text_is_idempotent() { } } + /** + * Ensures that updates to modifiable text that are shorter than the + * original text do not cause the parser to lose its orientation. + * + * @ticket 61617 + */ + public function test_setting_shorter_modifiable_text() { + $processor = new WP_HTML_Tag_Processor( '

' ); + + // Find the test node in the middle. + while ( 'TEXTAREA' !== $processor->get_token_name() && $processor->next_token() ) { + continue; + } + + $this->assertSame( + 'TEXTAREA', + $processor->get_token_name(), + 'Failed to find the test TEXTAREA node; check the test setup.' + ); + + $processor->set_modifiable_text( 'short' ); + $processor->get_updated_html(); + $this->assertSame( + 'short', + $processor->get_modifiable_text(), + 'Should have updated modifiable text to something shorter than the original.' + ); + + $this->assertTrue( + $processor->next_token(), + 'Should have advanced to the last token in the input.' + ); + + $this->assertSame( + 'DIV', + $processor->get_token_name(), + 'Should have recognized the final DIV in the input.' + ); + + $this->assertSame( + 'not a ', + $processor->get_attribute( 'id' ), + 'Should have read in the id from the last DIV as "not a "' + ); + } + + /** + * Ensures that reads to modifiable text after setting it reads the updated + * enqueued values, and not the original value. + * + * @ticket 61617 + */ + public function test_modifiable_text_reads_updates_after_setting() { + $processor = new WP_HTML_Tag_Processor( 'This is text' ); + + $processor->next_token(); + $this->assertSame( + '#text', + $processor->get_token_name(), + 'Failed to find first text node: check test setup.' + ); + + $update = 'This is new text'; + $processor->set_modifiable_text( $update ); + $this->assertSame( + $update, + $processor->get_modifiable_text(), + 'Failed to read updated enqueued value of text node.' + ); + + $processor->next_token(); + $this->assertSame( + '#comment', + $processor->get_token_name(), + 'Failed to advance to comment: check test setup.' + ); + + $this->assertSame( + ' this is not ', + $processor->get_modifiable_text(), + 'Failed to read modifiable text for next token; did it read the old enqueued value from the previous token?' + ); + } + /** * Ensures that when ignoring a newline after LISTING and PRE tags, that this * happens appropriately after seeking. @@ -108,4 +192,155 @@ public function test_get_modifiable_text_ignores_newlines_after_seeking() { 'Should not have removed the leading newline from the last DIV on its second traversal.' ); } + + /** + * Ensures that modifiable text updates are not applied where they aren't supported. + * + * @ticket 61617 + * + * @dataProvider data_tokens_not_supporting_modifiable_text_updates + * + * @param string $html Contains HTML with a token not supporting modifiable text updates. + * @param int $advance_n_tokens Count of times to run `next_token()` before reaching target node. + */ + public function test_rejects_updates_on_unsupported_match_locations( string $html, int $advance_n_tokens ) { + $processor = new WP_HTML_Tag_Processor( $html ); + while ( --$advance_n_tokens >= 0 ) { + $processor->next_token(); + } + + $this->assertFalse( + $processor->set_modifiable_text( 'Bazinga!' ), + 'Should have prevented modifying the text at the target node.' + ); + + $this->assertSame( + $html, + $processor->get_updated_html(), + 'Should not have modified the input document in any way.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_tokens_not_supporting_modifiable_text_updates() { + return array( + 'Before parsing' => array( 'nothing to see here', 0 ), + 'After parsing' => array( 'nothing here either', 2 ), + 'Incomplete document' => array( ' array( 'Text', 1, 'Blubber', 'Blubber' ), + 'Text node (middle)' => array( 'Bold move', 2, 'yo', 'yo' ), + 'Text node (end)' => array( 'of a dog', 2, 'of a cat', 'of a cat' ), + 'Encoded text node' => array( '
birds and dogs
', 2, ' & ', '
<birds> & <dogs>
' ), + 'SCRIPT tag' => array( 'beforeafter', 2, 'const img = " &
";', 'beforeafter' ), + 'STYLE tag' => array( '', 1, 'p::before { content: " & "; }', '' ), + 'TEXTAREA tag' => array( 'ab', 2, "so it ", "ab" ), + 'TEXTAREA (escape)' => array( 'ab', 2, 'but it does for ', 'ab' ), + 'TEXTAREA (escape+attrs)' => array( 'ab', 2, 'but it does for ', 'ab' ), + 'TITLE tag' => array( 'ahas no need to escapeb', 2, "so it ", "aso it <doesn't>b" ), + 'TITLE (escape)' => array( 'ahas no need to escapeb', 2, 'but it does for ', 'abut it does for </title>b' ), + 'TITLE (escape+attrs)' => array( 'ahas no need to escapeb', 2, 'but it does for ', 'abut it does for </title not an="attribute">b' ), + ); + } + + /** + * Ensures that updates with potentially-compromising values aren't accepted. + * + * For example, a modifiable text update should be allowed which would break + * the structure of the containing element, such as in a script or comment. + * + * @ticket 61617 + * + * @dataProvider data_unallowed_modifiable_text_updates + * + * @param string $html_with_nonempty_modifiable_text Will be used to find the test element. + * @param string $invalid_update Update containing possibly-compromising text. + */ + public function test_rejects_updates_with_unallowed_substrings( string $html_with_nonempty_modifiable_text, string $invalid_update ) { + $processor = new WP_HTML_Tag_Processor( $html_with_nonempty_modifiable_text ); + + while ( '' === $processor->get_modifiable_text() && $processor->next_token() ) { + continue; + } + + $original_text = $processor->get_modifiable_text(); + $this->assertNotEmpty( $original_text, 'Should have found non-empty text: check test setup.' ); + + $this->assertFalse( + $processor->set_modifiable_text( $invalid_update ), + 'Should have reject possibly-compromising modifiable text update.' + ); + + // Flush updates. + $processor->get_updated_html(); + + $this->assertSame( + $original_text, + $processor->get_modifiable_text(), + 'Should have preserved the original modifiable text before the rejected update.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unallowed_modifiable_text_updates() { + return array( + 'Comment with -->' => array( '', 'Comments end in -->' ), + 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), + 'SCRIPT with ' => array( '', 'Just a ' ), + 'SCRIPT with ' => array( '', 'beforeafter' ), + ); + } } From 345699c0711d00dc15a1f3090694c2c8a114a29b Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 29 Jul 2024 18:20:11 +0000 Subject: [PATCH 030/106] WP_Debug_Data: Extract `wp-filesystem` data into separate method. This is the first part in a larger modularization of the data in `WP_Debug_Data`. Previously this was a single massive method drawing in debug data from various groups of related data, where the groups were independent from each other. This patch separates the first of twelve groups, the `wp-filesystem` info, into a separate method focused on that data. This work precedes changes to make the `WP_Debug_Data` class more extensible for better use by plugin and theme code. Developed in https://github.com/wordpress/wordpress-develop/pull/7065 Discussed in https://core.trac.wordpress.org/ticket/61648 Props: afragen, apermo, costdev, dmsnell. See #61648. git-svn-id: https://develop.svn.wordpress.org/trunk@58830 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-debug-data.php | 122 ++++++++++-------- 1 file changed, 68 insertions(+), 54 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 109e38b0635d6..49214ff8ec91b 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -342,50 +342,6 @@ public static function debug_data() { ), ); - $is_writable_abspath = wp_is_writable( ABSPATH ); - $is_writable_wp_content_dir = wp_is_writable( WP_CONTENT_DIR ); - $is_writable_upload_dir = wp_is_writable( $upload_dir['basedir'] ); - $is_writable_wp_plugin_dir = wp_is_writable( WP_PLUGIN_DIR ); - $is_writable_template_directory = wp_is_writable( get_theme_root( get_template() ) ); - $is_writable_fonts_dir = wp_is_writable( wp_get_font_dir()['basedir'] ); - - $info['wp-filesystem'] = array( - 'label' => __( 'Filesystem Permissions' ), - 'description' => __( 'Shows whether WordPress is able to write to the directories it needs access to.' ), - 'fields' => array( - 'wordpress' => array( - 'label' => __( 'The main WordPress directory' ), - 'value' => ( $is_writable_abspath ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_abspath ? 'writable' : 'not writable' ), - ), - 'wp-content' => array( - 'label' => __( 'The wp-content directory' ), - 'value' => ( $is_writable_wp_content_dir ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_wp_content_dir ? 'writable' : 'not writable' ), - ), - 'uploads' => array( - 'label' => __( 'The uploads directory' ), - 'value' => ( $is_writable_upload_dir ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_upload_dir ? 'writable' : 'not writable' ), - ), - 'plugins' => array( - 'label' => __( 'The plugins directory' ), - 'value' => ( $is_writable_wp_plugin_dir ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_wp_plugin_dir ? 'writable' : 'not writable' ), - ), - 'themes' => array( - 'label' => __( 'The themes directory' ), - 'value' => ( $is_writable_template_directory ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_template_directory ? 'writable' : 'not writable' ), - ), - 'fonts' => array( - 'label' => __( 'The fonts directory' ), - 'value' => ( $is_writable_fonts_dir ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_fonts_dir ? 'writable' : 'not writable' ), - ), - ), - ); - // Conditionally add debug information for multisite setups. if ( is_multisite() ) { $site_id = get_current_blog_id(); @@ -1413,16 +1369,7 @@ public static function debug_data() { ); } - // Add more filesystem checks. - if ( defined( 'WPMU_PLUGIN_DIR' ) && is_dir( WPMU_PLUGIN_DIR ) ) { - $is_writable_wpmu_plugin_dir = wp_is_writable( WPMU_PLUGIN_DIR ); - - $info['wp-filesystem']['fields']['mu-plugins'] = array( - 'label' => __( 'The must use plugins directory' ), - 'value' => ( $is_writable_wpmu_plugin_dir ? __( 'Writable' ) : __( 'Not writable' ) ), - 'debug' => ( $is_writable_wpmu_plugin_dir ? 'writable' : 'not writable' ), - ); - } + $info['wp-filesystem'] = self::get_wp_filesystem(); /** * Filters the debug information shown on the Tools -> Site Health -> Info screen. @@ -1489,6 +1436,73 @@ public static function debug_data() { return $info; } + /** + * Gets the file system section of the debug data. + * + * @since 6.7.0 + * + * @return array + */ + private static function get_wp_filesystem(): array { + $upload_dir = wp_upload_dir(); + $is_writable_abspath = wp_is_writable( ABSPATH ); + $is_writable_wp_content_dir = wp_is_writable( WP_CONTENT_DIR ); + $is_writable_upload_dir = wp_is_writable( $upload_dir['basedir'] ); + $is_writable_wp_plugin_dir = wp_is_writable( WP_PLUGIN_DIR ); + $is_writable_template_directory = wp_is_writable( get_theme_root( get_template() ) ); + $is_writable_fonts_dir = wp_is_writable( wp_get_font_dir()['basedir'] ); + + $fields = array( + 'wordpress' => array( + 'label' => __( 'The main WordPress directory' ), + 'value' => ( $is_writable_abspath ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_abspath ? 'writable' : 'not writable' ), + ), + 'wp-content' => array( + 'label' => __( 'The wp-content directory' ), + 'value' => ( $is_writable_wp_content_dir ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_wp_content_dir ? 'writable' : 'not writable' ), + ), + 'uploads' => array( + 'label' => __( 'The uploads directory' ), + 'value' => ( $is_writable_upload_dir ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_upload_dir ? 'writable' : 'not writable' ), + ), + 'plugins' => array( + 'label' => __( 'The plugins directory' ), + 'value' => ( $is_writable_wp_plugin_dir ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_wp_plugin_dir ? 'writable' : 'not writable' ), + ), + 'themes' => array( + 'label' => __( 'The themes directory' ), + 'value' => ( $is_writable_template_directory ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_template_directory ? 'writable' : 'not writable' ), + ), + 'fonts' => array( + 'label' => __( 'The fonts directory' ), + 'value' => ( $is_writable_fonts_dir ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_fonts_dir ? 'writable' : 'not writable' ), + ), + ); + + // Add more filesystem checks. + if ( defined( 'WPMU_PLUGIN_DIR' ) && is_dir( WPMU_PLUGIN_DIR ) ) { + $is_writable_wpmu_plugin_dir = wp_is_writable( WPMU_PLUGIN_DIR ); + + $fields['mu-plugins'] = array( + 'label' => __( 'The must use plugins directory' ), + 'value' => ( $is_writable_wpmu_plugin_dir ? __( 'Writable' ) : __( 'Not writable' ) ), + 'debug' => ( $is_writable_wpmu_plugin_dir ? 'writable' : 'not writable' ), + ); + } + + return array( + 'label' => __( 'Filesystem Permissions' ), + 'description' => __( 'Shows whether WordPress is able to write to the directories it needs access to.' ), + 'fields' => $fields, + ); + } + /** * Returns the value of a MySQL system variable. * From 9ed3553da6a61a38a1d4c5585e5ac3c9d13d351a Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 29 Jul 2024 18:47:21 +0000 Subject: [PATCH 031/106] REST API, Meta: Store updates in database when they are equal to the defaults. This patch fixes an oversight from when default metadata values were introduced in #43941 in WordPress 5.5: metadata updates should persist in the database even if they match the registered default value (because the default values can change over time). Previously, the REST API code was comparing updated values against the value returned by the default-aware `get_metadata()` method. This meant that if no value existed in the database, and the default value was supplied to the update, WordPress would think that the updated value was already persisted and skip the database call. Now, the `get_metadata_raw()` method is called for comparing whether or not a database update is required, fixing the bug. In this patch both issues are resolved. Developed in https://github.com/wordpress/wordpress-develop/pull/6782 Discussed in https://core.trac.wordpress.org/ticket/55600 Follow-up to [48402]. Props: dmsnell, kraftner, ramon-fincken. Fixes #55600. git-svn-id: https://develop.svn.wordpress.org/trunk@58831 602fd350-edb4-49c9-b593-d223f7449a82 --- .../fields/class-wp-rest-meta-fields.php | 6 +- .../tests/rest-api/rest-post-meta-fields.php | 475 ++++++++++++++++++ 2 files changed, 479 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php index 7b46905a7aa4f..5f3b55843e23a 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php @@ -268,6 +268,7 @@ protected function delete_meta_value( $object_id, $meta_key, $name ) { * Alters the list of values in the database to match the list of provided values. * * @since 4.7.0 + * @since 6.7.0 Stores values into DB even if provided registered default value. * * @param int $object_id Object ID to update. * @param string $meta_key Key for the custom field. @@ -290,7 +291,7 @@ protected function update_multi_meta_value( $object_id, $meta_key, $name, $value ); } - $current_values = get_metadata( $meta_type, $object_id, $meta_key, false ); + $current_values = get_metadata_raw( $meta_type, $object_id, $meta_key, false ); $subtype = get_object_subtype( $meta_type, $object_id ); if ( ! is_array( $current_values ) ) { @@ -367,6 +368,7 @@ function ( $stored_value ) use ( $meta_key, $subtype, $value ) { * Updates a meta value for an object. * * @since 4.7.0 + * @since 6.7.0 Stores values into DB even if provided registered default value. * * @param int $object_id Object ID to update. * @param string $meta_key Key for the custom field. @@ -378,7 +380,7 @@ protected function update_meta_value( $object_id, $meta_key, $name, $value ) { $meta_type = $this->get_meta_type(); // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false. - $old_value = get_metadata( $meta_type, $object_id, $meta_key ); + $old_value = get_metadata_raw( $meta_type, $object_id, $meta_key ); $subtype = get_object_subtype( $meta_type, $object_id ); if ( is_array( $old_value ) && 1 === count( $old_value ) diff --git a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php index 1418db19dbce2..786396df2e215 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -3095,6 +3095,464 @@ public function test_default_is_added_to_schema() { $this->assertSame( 'Goodnight Moon', $schema['default'] ); } + /** + * Ensures that REST API calls with post meta containing the default value for the + * registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_scalar_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => $type, + 'single' => true, + 'show_in_rest' => true, + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => $default_value, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with string-cast version of default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing the default) + * for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_scalar_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => $type, + 'single' => false, + 'show_in_rest' => true, + 'default' => $default_value, + ) + ); + + // Write the default value as the sole value. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with string-cast version of default value.' + ); + + // Write multiple values, including the default, to ensure it remains. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + $default_value, + $alternative_value, + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value, (string) $alternative_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored both the default and non-default string-cast values.' + ); + } + + /** + * Ensures that REST API calls with post meta containing an object as the default + * value for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_object_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + // Register singular post meta for type. + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => 'object', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + $type => array( 'type' => $type ), + ), + ), + ), + 'default' => (object) array( $type => $default_value ), + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => (object) array( $type => $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing an object as + * the default) for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_object_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => 'object', + 'single' => false, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + $type => array( 'type' => $type ), + ), + ), + ), + 'default' => (object) array( $type => $default_value ), + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( (object) array( $type => $default_value ) ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + (object) array( $type => $default_value ), + (object) array( $type => $alternative_value ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ), array( $type => $alternative_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * Ensures that REST API calls with post meta containing a list array as the default + * value for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_array_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + // Register singular post meta for type. + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => 'array', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => $type, + ), + ), + ), + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => array( $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with an array containing only the default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing a list array as + * the default) for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_array_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => 'array', + 'single' => false, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => $type, + ), + ), + ), + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( array( $default_value ) ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + array( $default_value ), + array( $alternative_value ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ), array( $alternative_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + /** * @ticket 48823 */ @@ -3516,4 +3974,21 @@ public function data_revisioned_single_post_meta_with_posts_endpoint_page_and_cp ), ); } + + /** + * Data provider. + * + * Provides example default values of scalar types; + * in contrast to arrays, objects, etc... + * + * @return array[] + */ + public static function data_scalar_default_values() { + return array( + 'boolean default' => array( 'boolean', true, false ), + 'integer default' => array( 'integer', 42, 43 ), + 'number default' => array( 'number', 42.99, 43.99 ), + 'string default' => array( 'string', 'string', 'string2' ), + ); + } } From a16d9eea5445126582476b608ace91f8efd58eb8 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 30 Jul 2024 12:23:06 +0000 Subject: [PATCH 032/106] Site Health: Improve the wording for PHP version check. This aims to make the message more accurate by referring to the version of PHP currently recommended by WordPress, not the current version of PHP. Follow-up to [44986], [46267], [47254]. Props swb1192, psykro, swissspidy, joemcgill, mukesh27, aristath. See #61623. git-svn-id: https://develop.svn.wordpress.org/trunk@58832 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-site-health.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 12ca7f7a9adb8..cd6fb0d7db680 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -728,8 +728,8 @@ public function get_test_php_version() { $result = array( 'label' => sprintf( - /* translators: %s: The current PHP version. */ - __( 'Your site is running the current version of PHP (%s)' ), + /* translators: %s: The recommended PHP version. */ + __( 'Your site is running a recommended version of PHP (%s)' ), PHP_VERSION ), 'status' => 'good', From dfd1de09afbb5b3c4c7a959b20a1f5fa7011aa46 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 30 Jul 2024 18:44:45 +0000 Subject: [PATCH 033/106] HTML API: Add TEMPLATE and related support in HTML Processor. As part of work to add more spec support to the HTML API, this patch adds support for the IN TEMPLATE and IN HEAD insertion modes. These changes are primarily about adding support for TEMPLATE elements in the HTML Processor, but include support for other tags commonly found in the document head, such as LINK, META, SCRIPT, STYLE, and TITLE. Developed in https://github.com/wordpress/wordpress-develop/pull/7046 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell, westonruter. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58833 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-open-elements.php | 45 ++- .../html-api/class-wp-html-processor.php | 320 +++++++++++++++++- .../html-api/wpHtmlProcessorBreadcrumbs.php | 9 +- .../html-api/wpHtmlProcessorHtml5lib.php | 27 +- 4 files changed, 384 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index 1162a267f9c9b..c760009ce0c28 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -308,7 +308,20 @@ public function has_element_in_scope( string $tag_name ): bool { 'MARQUEE', 'OBJECT', 'TEMPLATE', - // @todo: Support SVG and MathML nodes when support for foreign content is added. + + /* + * @todo Support SVG and MathML nodes when support for foreign content is added. + * + * - MathML mi + * - MathML mo + * - MathML mn + * - MathML ms + * - MathML mtext + * - MathML annotation-xml + * - SVG foreignObject + * - SVG desc + * - SVG title + */ ) ); } @@ -349,7 +362,20 @@ public function has_element_in_list_item_scope( string $tag_name ): bool { 'OL', 'TEMPLATE', 'UL', - // @todo: Support SVG and MathML nodes when support for foreign content is added. + + /* + * @todo Support SVG and MathML nodes when support for foreign content is added. + * + * - MathML mi + * - MathML mo + * - MathML mn + * - MathML ms + * - MathML mtext + * - MathML annotation-xml + * - SVG foreignObject + * - SVG desc + * - SVG title + */ ) ); } @@ -386,7 +412,20 @@ public function has_element_in_button_scope( string $tag_name ): bool { 'MARQUEE', 'OBJECT', 'TEMPLATE', - // @todo: Support SVG and MathML nodes when support for foreign content is added. + + /* + * @todo Support SVG and MathML nodes when support for foreign content is added. + * + * - MathML mi + * - MathML mo + * - MathML mn + * - MathML ms + * - MathML mtext + * - MathML annotation-xml + * - SVG foreignObject + * - SVG desc + * - SVG title + */ ) ); } diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index f8653022454b6..9f2662c9e4c48 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1040,7 +1040,7 @@ private function step_before_head(): bool { * This internal function performs the 'in head' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -1050,7 +1050,211 @@ private function step_before_head(): bool { * @return bool Whether an element was found. */ private function step_in_head(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $is_closer = parent::is_tag_closer(); + $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + */ + if ( '#text' === $op ) { + $text = $this->get_modifiable_text(); + if ( '' === $text ) { + /* + * If the text is empty after processing HTML entities and stripping + * U+0000 NULL bytes then ignore the token. + */ + return $this->step(); + } + + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + // Insert the character. + $this->insert_html_element( $this->state->current_token ); + return true; + } + } + + switch ( $op ) { + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + return $this->step_in_body(); + + /* + * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link" + */ + case '+BASE': + case '+BASEFONT': + case '+BGSOUND': + case '+LINK': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A start tag whose tag name is "meta" + */ + case '+META': + $this->insert_html_element( $this->state->current_token ); + + /* + * > If the active speculative HTML parser is null, then: + * > - If the element has a charset attribute, and getting an encoding from + * > its value results in an encoding, and the confidence is currently + * > tentative, then change the encoding to the resulting encoding. + */ + $charset = $this->get_attribute( 'charset' ); + if ( is_string( $charset ) ) { + $this->bail( 'Cannot yet process META tags with charset to determine encoding.' ); + } + + /* + * > - Otherwise, if the element has an http-equiv attribute whose value is + * > an ASCII case-insensitive match for the string "Content-Type", and + * > the element has a content attribute, and applying the algorithm for + * > extracting a character encoding from a meta element to that attribute's + * > value returns an encoding, and the confidence is currently tentative, + * > then change the encoding to the extracted encoding. + */ + $http_equiv = $this->get_attribute( 'http-equiv' ); + $content = $this->get_attribute( 'content' ); + if ( + is_string( $http_equiv ) && + is_string( $content ) && + 0 === strcasecmp( $http_equiv, 'Content-Type' ) + ) { + $this->bail( 'Cannot yet process META tags with http-equiv Content-Type to determine encoding.' ); + } + + return true; + + /* + * > A start tag whose tag name is "title" + */ + case '+TITLE': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A start tag whose tag name is "noscript", if the scripting flag is enabled + * > A start tag whose tag name is one of: "noframes", "style" + * + * The scripting flag is never enabled in this parser. + */ + case '+NOFRAMES': + case '+STYLE': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A start tag whose tag name is "noscript", if the scripting flag is disabled + */ + case '+NOSCRIPT': + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT; + return true; + + /* + * > A start tag whose tag name is "script" + * + * @todo Could the adjusted insertion location be anything other than the current location? + */ + case '+SCRIPT': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > An end tag whose tag name is "head" + */ + case '-HEAD': + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD; + return true; + + /* + * > An end tag whose tag name is one of: "body", "html", "br" + */ + case '-BODY': + case '-HTML': + case '-BR': + /* + * > Act as described in the "anything else" entry below. + */ + goto in_head_anything_else; + break; + + /* + * > A start tag whose tag name is "template" + * + * @todo Could the adjusted insertion location be anything other than the current location? + */ + case '+TEMPLATE': + $this->state->active_formatting_elements->insert_marker(); + $this->state->frameset_ok = false; + + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE; + $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE; + + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > An end tag whose tag name is "template" + */ + case '-TEMPLATE': + if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) { + // @todo Indicate a parse error once it's possible. + return $this->step(); + } + + $this->generate_implied_end_tags_thoroughly(); + if ( ! $this->state->stack_of_open_elements->current_node_is( 'TEMPLATE' ) ) { + // @todo Indicate a parse error once it's possible. + } + + $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' ); + $this->state->active_formatting_elements->clear_up_to_last_marker(); + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->reset_insertion_mode(); + return true; + } + + /* + * > A start tag whose tag name is "head" + * > Any other end tag + */ + if ( '+HEAD' === $op || $is_closer ) { + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else + */ + in_head_anything_else: + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** @@ -2991,7 +3195,117 @@ private function step_in_select_in_table(): bool { * @return bool Whether an element was found. */ private function step_in_template(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $is_closer = $this->is_tag_closer(); + $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token + * > A comment token + * > A DOCTYPE token + */ + case '#text': + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + case 'html': + return $this->step_in_body(); + + /* + * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link", + * > "meta", "noframes", "script", "style", "template", "title" + * > An end tag whose tag name is "template" + */ + case '+BASE': + case '+BASEFONT': + case '+BGSOUND': + case '+LINK': + case '+META': + case '+NOFRAMES': + case '+SCRIPT': + case '+STYLE': + case '+TEMPLATE': + case '+TITLE': + case '-TEMPLATE': + return $this->step_in_head(); + + /* + * > A start tag whose tag name is one of: "caption", "colgroup", "tbody", "tfoot", "thead" + */ + case '+CAPTION': + case '+COLGROUP': + case '+TBODY': + case '+TFOOT': + case '+THEAD': + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > A start tag whose tag name is "col" + */ + case '+COL': + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > A start tag whose tag name is "tr" + */ + case '+TR': + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY; + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > A start tag whose tag name is one of: "td", "th" + */ + case '+TD': + case '+TH': + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; + return $this->step( self::REPROCESS_CURRENT_NODE ); + } + + /* + * > Any other start tag + */ + if ( ! $is_closer ) { + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + return $this->step( self::REPROCESS_CURRENT_NODE ); + } + + /* + * > Any other end tag + */ + if ( $is_closer ) { + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > An end-of-file token + */ + if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) { + // Stop parsing. + return false; + } + + // @todo Indicate a parse error once it's possible. + $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' ); + $this->state->active_formatting_elements->clear_up_to_last_marker(); + array_pop( $this->state->stack_of_template_insertion_modes ); + $this->reset_insertion_mode(); + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 7dd94747fd8e8..0dbd45cfa0ead 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -46,8 +46,10 @@ public static function data_single_tag_of_supported_elements() { 'ASIDE', 'AUDIO', 'B', + 'BASE', 'BDI', 'BDO', + 'BGSOUND', // Deprectated. 'BIG', 'BLINK', // Deprecated. 'BR', @@ -93,12 +95,14 @@ public static function data_single_tag_of_supported_elements() { 'KEYGEN', // Deprecated. 'LABEL', 'LEGEND', + 'LINK', 'LISTING', // Deprecated. 'MAIN', 'MAP', 'MARK', 'MARQUEE', // Deprecated. 'MENU', + 'META', 'METER', 'MULTICOL', // Deprecated. 'NAV', @@ -178,24 +182,19 @@ public function test_fails_when_encountering_unsupported_tag( $html ) { */ public static function data_unsupported_elements() { $unsupported_elements = array( - 'BASE', - 'BGSOUND', // Deprecated; self-closing if self-closing flag provided, otherwise normal. 'BODY', 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'IFRAME', - 'LINK', 'MATH', - 'META', 'NOEMBED', // Neutralized. 'NOFRAMES', // Neutralized. 'PLAINTEXT', // Neutralized. 'SCRIPT', 'STYLE', 'SVG', - 'TEMPLATE', 'TEXTAREA', 'TITLE', 'XMP', // Deprecated, use PRE instead. diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php index 8487df26c99dc..69329f51321ba 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php @@ -34,6 +34,7 @@ class Tests_HtmlApi_Html5lib extends WP_UnitTestCase { 'adoption01/line0046' => 'Unimplemented: Reconstruction of active formatting elements.', 'adoption01/line0159' => 'Unimplemented: Reconstruction of active formatting elements.', 'adoption01/line0318' => 'Unimplemented: Reconstruction of active formatting elements.', + 'template/line0885' => 'Unimplemented: no parsing of attributes on context node.', 'tests1/line0720' => 'Unimplemented: Reconstruction of active formatting elements.', 'tests15/line0001' => 'Unimplemented: Reconstruction of active formatting elements.', 'tests15/line0022' => 'Unimplemented: Reconstruction of active formatting elements.', @@ -163,25 +164,34 @@ private static function build_tree_representation( ?string $fragment_context, st return null; } - if ( $was_text && '#text' !== $processor->get_token_name() ) { + $token_name = $processor->get_token_name(); + $token_type = $processor->get_token_type(); + $is_closer = $processor->is_tag_closer(); + + if ( $was_text && '#text' !== $token_name ) { $output .= "{$text_node}\"\n"; $was_text = false; $text_node = ''; } - switch ( $processor->get_token_type() ) { + switch ( $token_type ) { case '#tag': - $tag_name = strtolower( $processor->get_tag() ); + $tag_name = strtolower( $token_name ); - if ( $processor->is_tag_closer() ) { + if ( $is_closer ) { --$indent_level; + + if ( 'TEMPLATE' === $token_name ) { + --$indent_level; + } + break; } - $tag_indent = count( $processor->get_breadcrumbs() ) - 1; + $tag_indent = $indent_level; if ( ! WP_HTML_Processor::is_void( $tag_name ) ) { - $indent_level = $tag_indent + 1; + ++$indent_level; } $output .= str_repeat( $indent, $tag_indent ) . "<{$tag_name}>\n"; @@ -209,6 +219,11 @@ private static function build_tree_representation( ?string $fragment_context, st $output .= str_repeat( $indent, $indent_level ) . "\"{$modifiable_text}\"\n"; } + if ( 'TEMPLATE' === $token_name ) { + $output .= str_repeat( $indent, $indent_level ) . "content\n"; + ++$indent_level; + } + if ( ! $processor->is_void( $tag_name ) && ! $processor->expects_closer() ) { --$indent_level; } From c3e1d3a8b8c2393ce8d6d50cededbea479516b8e Mon Sep 17 00:00:00 2001 From: ramonopoly Date: Wed, 31 Jul 2024 02:39:46 +0000 Subject: [PATCH 034/106] Background: add background attachment support to theme.json styles Introduces the ability to specify a value for `background.backgroundAttachment` in theme.json styles. The theme.json value determines the CSS value for the `background-attachment` property. This feature was introduced into the Gutenberg plugin in version 18.9. Props andrewserong, mukesh27, noisysocks, ramonopoly. Fixes #61720 git-svn-id: https://develop.svn.wordpress.org/trunk@58834 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-supports/background.php | 12 ++++--- src/wp-includes/class-wp-theme-json.php | 11 +++--- .../style-engine/class-wp-style-engine.php | 14 +++++--- .../wpRenderBackgroundSupport.php | 12 ++++--- .../tests/style-engine/styleEngine.php | 21 +++++++----- tests/phpunit/tests/theme/wpThemeJson.php | 34 +++++++++++-------- 6 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/wp-includes/block-supports/background.php b/src/wp-includes/block-supports/background.php index b7b501a44aca1..4e7995f53f92f 100644 --- a/src/wp-includes/block-supports/background.php +++ b/src/wp-includes/block-supports/background.php @@ -42,6 +42,7 @@ function wp_register_background_support( $block_type ) { * @since 6.4.0 * @since 6.5.0 Added support for `backgroundPosition` and `backgroundRepeat` output. * @since 6.6.0 Removed requirement for `backgroundImage.source`. A file/url is the default. + * @since 6.7.0 Added support for `backgroundAttachment` output. * * @access private * @@ -62,11 +63,12 @@ function wp_render_background_support( $block_content, $block ) { return $block_content; } - $background_styles = array(); - $background_styles['backgroundImage'] = $block_attributes['style']['background']['backgroundImage'] ?? null; - $background_styles['backgroundSize'] = $block_attributes['style']['background']['backgroundSize'] ?? null; - $background_styles['backgroundPosition'] = $block_attributes['style']['background']['backgroundPosition'] ?? null; - $background_styles['backgroundRepeat'] = $block_attributes['style']['background']['backgroundRepeat'] ?? null; + $background_styles = array(); + $background_styles['backgroundImage'] = $block_attributes['style']['background']['backgroundImage'] ?? null; + $background_styles['backgroundSize'] = $block_attributes['style']['background']['backgroundSize'] ?? null; + $background_styles['backgroundPosition'] = $block_attributes['style']['background']['backgroundPosition'] ?? null; + $background_styles['backgroundRepeat'] = $block_attributes['style']['background']['backgroundRepeat'] ?? null; + $background_styles['backgroundAttachment'] = $block_attributes['style']['background']['backgroundAttachment'] ?? null; if ( ! empty( $background_styles['backgroundImage'] ) ) { $background_styles['backgroundSize'] = $background_styles['backgroundSize'] ?? 'cover'; diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 631dd1f703e67..e6ca23bff310d 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -226,6 +226,7 @@ class WP_Theme_JSON { * @since 6.4.0 Added `writing-mode` property. * @since 6.5.0 Added `aspect-ratio` property. * @since 6.6.0 Added `background-[image|position|repeat|size]` properties. + * @since 6.7.0 Added `background-attachment` property. * * @var array */ @@ -237,6 +238,7 @@ class WP_Theme_JSON { 'background-position' => array( 'background', 'backgroundPosition' ), 'background-repeat' => array( 'background', 'backgroundRepeat' ), 'background-size' => array( 'background', 'backgroundSize' ), + 'background-attachment' => array( 'background', 'backgroundAttachment' ), 'border-radius' => array( 'border', 'radius' ), 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), @@ -520,10 +522,11 @@ class WP_Theme_JSON { */ const VALID_STYLES = array( 'background' => array( - 'backgroundImage' => null, - 'backgroundPosition' => null, - 'backgroundRepeat' => null, - 'backgroundSize' => null, + 'backgroundImage' => null, + 'backgroundPosition' => null, + 'backgroundRepeat' => null, + 'backgroundSize' => null, + 'backgroundAttachment' => null, ), 'border' => array( 'color' => null, diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 1ba813ed65b14..3012ca3eefd30 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -50,31 +50,37 @@ final class WP_Style_Engine { */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( 'background' => array( - 'backgroundImage' => array( + 'backgroundImage' => array( 'property_keys' => array( 'default' => 'background-image', ), 'value_func' => array( self::class, 'get_url_or_value_css_declaration' ), 'path' => array( 'background', 'backgroundImage' ), ), - 'backgroundPosition' => array( + 'backgroundPosition' => array( 'property_keys' => array( 'default' => 'background-position', ), 'path' => array( 'background', 'backgroundPosition' ), ), - 'backgroundRepeat' => array( + 'backgroundRepeat' => array( 'property_keys' => array( 'default' => 'background-repeat', ), 'path' => array( 'background', 'backgroundRepeat' ), ), - 'backgroundSize' => array( + 'backgroundSize' => array( 'property_keys' => array( 'default' => 'background-size', ), 'path' => array( 'background', 'backgroundSize' ), ), + 'backgroundAttachment' => array( + 'property_keys' => array( + 'default' => 'background-attachment', + ), + 'path' => array( 'background', 'backgroundAttachment' ), + ), ), 'color' => array( 'text' => array( diff --git a/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php b/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php index 3fa7da28908a3..1f965cdc3f323 100644 --- a/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php +++ b/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php @@ -69,6 +69,7 @@ public function filter_set_theme_root() { * @ticket 59357 * @ticket 60175 * @ticket 61123 + * @ticket 61720 * * @covers ::wp_render_background_support * @@ -139,20 +140,21 @@ public function data_background_block_support() { 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), - 'background image style with contain, position, and repeat is applied' => array( + 'background image style with contain, position, attachment, and repeat is applied' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-rules-are-output', 'background_settings' => array( 'backgroundImage' => true, ), 'background_style' => array( - 'backgroundImage' => array( + 'backgroundImage' => array( 'url' => 'https://example.com/image.jpg', ), - 'backgroundRepeat' => 'no-repeat', - 'backgroundSize' => 'contain', + 'backgroundRepeat' => 'no-repeat', + 'backgroundSize' => 'contain', + 'backgroundAttachment' => 'fixed', ), - 'expected_wrapper' => '
Content
', + 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), 'background image style is appended if a style attribute already exists' => array( diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index 9092ce5b6df03..686865c6803ab 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -28,6 +28,7 @@ public function tear_down() { * @ticket 58549 * @ticket 58590 * @ticket 60175 + * @ticket 61720 * * @covers ::wp_style_engine_get_styles * @@ -539,22 +540,24 @@ public function data_wp_style_engine_get_styles() { 'inline_background_image_url_with_background_size' => array( 'block_styles' => array( 'background' => array( - 'backgroundImage' => array( + 'backgroundImage' => array( 'url' => 'https://example.com/image.jpg', ), - 'backgroundPosition' => 'center', - 'backgroundRepeat' => 'no-repeat', - 'backgroundSize' => 'cover', + 'backgroundPosition' => 'center', + 'backgroundRepeat' => 'no-repeat', + 'backgroundSize' => 'cover', + 'backgroundAttachment' => 'fixed', ), ), 'options' => array(), 'expected_output' => array( - 'css' => "background-image:url('https://example.com/image.jpg');background-position:center;background-repeat:no-repeat;background-size:cover;", + 'css' => "background-image:url('https://example.com/image.jpg');background-position:center;background-repeat:no-repeat;background-size:cover;background-attachment:fixed;", 'declarations' => array( - 'background-image' => "url('https://example.com/image.jpg')", - 'background-position' => 'center', - 'background-repeat' => 'no-repeat', - 'background-size' => 'cover', + 'background-image' => "url('https://example.com/image.jpg')", + 'background-position' => 'center', + 'background-repeat' => 'no-repeat', + 'background-size' => 'cover', + 'background-attachment' => 'fixed', ), ), ), diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index bc8ef8121ddd0..57d25ab8e6f03 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -4997,6 +4997,7 @@ public function test_get_shadow_styles_for_blocks() { * * @ticket 61123 * @ticket 61165 + * @ticket 61720 */ public function test_get_top_level_background_image_styles() { $theme_json = new WP_Theme_JSON( @@ -5004,12 +5005,13 @@ public function test_get_top_level_background_image_styles() { 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'background' => array( - 'backgroundImage' => array( + 'backgroundImage' => array( 'url' => 'http://example.org/image.png', ), - 'backgroundSize' => 'contain', - 'backgroundRepeat' => 'no-repeat', - 'backgroundPosition' => 'center center', + 'backgroundSize' => 'contain', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + 'backgroundAttachment' => 'fixed', ), ), ) @@ -5020,7 +5022,7 @@ public function test_get_top_level_background_image_styles() { 'selector' => 'body', ); - $expected_styles = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}:root :where(body){background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;}"; + $expected_styles = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}:root :where(body){background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;background-attachment: fixed;}"; $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $body_node ), 'Styles returned from "::get_stylesheet()" with top-level background styles type do not match expectations' ); $theme_json = new WP_Theme_JSON( @@ -5028,21 +5030,22 @@ public function test_get_top_level_background_image_styles() { 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'background' => array( - 'backgroundImage' => "url('http://example.org/image.png')", - 'backgroundSize' => 'contain', - 'backgroundRepeat' => 'no-repeat', - 'backgroundPosition' => 'center center', + 'backgroundImage' => "url('http://example.org/image.png')", + 'backgroundSize' => 'contain', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + 'backgroundAttachment' => 'fixed', ), ), ) ); - $expected_styles = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}:root :where(body){background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;}"; $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $body_node ), 'Styles returned from "::get_stylesheet()" with top-level background image as string type do not match expectations' ); } /** * @ticket 61588 + * @ticket 61720 */ public function test_get_block_background_image_styles() { $theme_json = new WP_Theme_JSON( @@ -5052,10 +5055,11 @@ public function test_get_block_background_image_styles() { 'blocks' => array( 'core/group' => array( 'background' => array( - 'backgroundImage' => "url('http://example.org/group.png')", - 'backgroundSize' => 'cover', - 'backgroundRepeat' => 'no-repeat', - 'backgroundPosition' => 'center center', + 'backgroundImage' => "url('http://example.org/group.png')", + 'backgroundSize' => 'cover', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + 'backgroundAttachment' => 'fixed', ), ), 'core/quote' => array( @@ -5094,7 +5098,7 @@ public function test_get_block_background_image_styles() { ), ); - $group_styles = ":root :where(.wp-block-group){background-image: url('http://example.org/group.png');background-position: center center;background-repeat: no-repeat;background-size: cover;}"; + $group_styles = ":root :where(.wp-block-group){background-image: url('http://example.org/group.png');background-position: center center;background-repeat: no-repeat;background-size: cover;background-attachment: fixed;}"; $this->assertSame( $group_styles, $theme_json->get_styles_for_block( $group_node ), 'Styles returned from "::get_styles_for_block()" with block-level background styles as string type do not match expectations' ); } From 489b840cfd40ce310ba53fdc5d957d6f69bd4408 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 31 Jul 2024 14:03:24 +0000 Subject: [PATCH 035/106] Bundled Themes: Update schema version in style variation files. This ensures that settings and styles are properly handled by code editors that support schema. Reference: [https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-v2/#json-schema Theme.json Version 2 Reference: JSON Schema]. Follow-up to [58403]. Props poena, umeshsinghin. Fixes #61789. git-svn-id: https://develop.svn.wordpress.org/trunk@58835 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentytwentyfour/styles/ember.json | 2 +- src/wp-content/themes/twentytwentyfour/styles/fossil.json | 2 +- src/wp-content/themes/twentytwentyfour/styles/ice.json | 2 +- src/wp-content/themes/twentytwentyfour/styles/maelstrom.json | 2 +- src/wp-content/themes/twentytwentyfour/styles/mint.json | 2 +- src/wp-content/themes/twentytwentyfour/styles/onyx.json | 2 +- src/wp-content/themes/twentytwentyfour/styles/rust.json | 2 +- src/wp-content/themes/twentytwentythree/styles/aubergine.json | 2 +- src/wp-content/themes/twentytwentythree/styles/block-out.json | 2 +- src/wp-content/themes/twentytwentythree/styles/canary.json | 2 +- src/wp-content/themes/twentytwentythree/styles/electric.json | 2 +- src/wp-content/themes/twentytwentythree/styles/grapes.json | 2 +- src/wp-content/themes/twentytwentythree/styles/marigold.json | 2 +- src/wp-content/themes/twentytwentythree/styles/pilgrimage.json | 2 +- src/wp-content/themes/twentytwentythree/styles/pitch.json | 2 +- src/wp-content/themes/twentytwentythree/styles/sherbet.json | 2 +- src/wp-content/themes/twentytwentythree/styles/whisper.json | 2 +- src/wp-content/themes/twentytwentytwo/styles/blue.json | 1 + src/wp-content/themes/twentytwentytwo/styles/pink.json | 1 + src/wp-content/themes/twentytwentytwo/styles/swiss.json | 1 + 20 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/wp-content/themes/twentytwentyfour/styles/ember.json b/src/wp-content/themes/twentytwentyfour/styles/ember.json index f4e612ca62ba4..3c8f08d4582b0 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/ember.json +++ b/src/wp-content/themes/twentytwentyfour/styles/ember.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Ember", "settings": { diff --git a/src/wp-content/themes/twentytwentyfour/styles/fossil.json b/src/wp-content/themes/twentytwentyfour/styles/fossil.json index 44e24fca1e073..ffd8f99c17a53 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/fossil.json +++ b/src/wp-content/themes/twentytwentyfour/styles/fossil.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Fossil", "settings": { diff --git a/src/wp-content/themes/twentytwentyfour/styles/ice.json b/src/wp-content/themes/twentytwentyfour/styles/ice.json index e7896bcb24cd0..7f56a7bc10b5e 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/ice.json +++ b/src/wp-content/themes/twentytwentyfour/styles/ice.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Ice", "settings": { diff --git a/src/wp-content/themes/twentytwentyfour/styles/maelstrom.json b/src/wp-content/themes/twentytwentyfour/styles/maelstrom.json index 1f99cf5b0e27b..fe92140544fb0 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/maelstrom.json +++ b/src/wp-content/themes/twentytwentyfour/styles/maelstrom.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Maelstrom", "settings": { diff --git a/src/wp-content/themes/twentytwentyfour/styles/mint.json b/src/wp-content/themes/twentytwentyfour/styles/mint.json index 9d306e508a8e8..645d732310626 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/mint.json +++ b/src/wp-content/themes/twentytwentyfour/styles/mint.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Mint", "settings": { diff --git a/src/wp-content/themes/twentytwentyfour/styles/onyx.json b/src/wp-content/themes/twentytwentyfour/styles/onyx.json index 41afbd8ce7d26..2b23fd759d902 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/onyx.json +++ b/src/wp-content/themes/twentytwentyfour/styles/onyx.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Onyx", "settings": { diff --git a/src/wp-content/themes/twentytwentyfour/styles/rust.json b/src/wp-content/themes/twentytwentyfour/styles/rust.json index e3410b6b63927..d90a49742b260 100644 --- a/src/wp-content/themes/twentytwentyfour/styles/rust.json +++ b/src/wp-content/themes/twentytwentyfour/styles/rust.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Rust", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/aubergine.json b/src/wp-content/themes/twentytwentythree/styles/aubergine.json index 485eaf5fac80d..9e741ee028ad4 100644 --- a/src/wp-content/themes/twentytwentythree/styles/aubergine.json +++ b/src/wp-content/themes/twentytwentythree/styles/aubergine.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Aubergine", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/block-out.json b/src/wp-content/themes/twentytwentythree/styles/block-out.json index 4b09b8eed480f..cdeaab759c2cf 100644 --- a/src/wp-content/themes/twentytwentythree/styles/block-out.json +++ b/src/wp-content/themes/twentytwentythree/styles/block-out.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Block out", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/canary.json b/src/wp-content/themes/twentytwentythree/styles/canary.json index f8d8f807f69cf..b58af0a59f9a7 100644 --- a/src/wp-content/themes/twentytwentythree/styles/canary.json +++ b/src/wp-content/themes/twentytwentythree/styles/canary.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Canary", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/electric.json b/src/wp-content/themes/twentytwentythree/styles/electric.json index 077ca0920fcb0..9bd275a683b03 100644 --- a/src/wp-content/themes/twentytwentythree/styles/electric.json +++ b/src/wp-content/themes/twentytwentythree/styles/electric.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Electric", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/grapes.json b/src/wp-content/themes/twentytwentythree/styles/grapes.json index cf0c4b4a1f317..dd3c2b20b033b 100644 --- a/src/wp-content/themes/twentytwentythree/styles/grapes.json +++ b/src/wp-content/themes/twentytwentythree/styles/grapes.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Grapes", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/marigold.json b/src/wp-content/themes/twentytwentythree/styles/marigold.json index 4ad7ef40ad579..4271d9626d34d 100644 --- a/src/wp-content/themes/twentytwentythree/styles/marigold.json +++ b/src/wp-content/themes/twentytwentythree/styles/marigold.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Marigold", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/pilgrimage.json b/src/wp-content/themes/twentytwentythree/styles/pilgrimage.json index be16addf29cb3..bfcfcb4086c40 100644 --- a/src/wp-content/themes/twentytwentythree/styles/pilgrimage.json +++ b/src/wp-content/themes/twentytwentythree/styles/pilgrimage.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Pilgrimage", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/pitch.json b/src/wp-content/themes/twentytwentythree/styles/pitch.json index 583e28e98fe5f..b8283d7816488 100644 --- a/src/wp-content/themes/twentytwentythree/styles/pitch.json +++ b/src/wp-content/themes/twentytwentythree/styles/pitch.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Pitch", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/sherbet.json b/src/wp-content/themes/twentytwentythree/styles/sherbet.json index d6e20390c6f79..6e500ee9e409e 100644 --- a/src/wp-content/themes/twentytwentythree/styles/sherbet.json +++ b/src/wp-content/themes/twentytwentythree/styles/sherbet.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Sherbet", "settings": { diff --git a/src/wp-content/themes/twentytwentythree/styles/whisper.json b/src/wp-content/themes/twentytwentythree/styles/whisper.json index d1e677be4ec21..c92cce3be0a13 100644 --- a/src/wp-content/themes/twentytwentythree/styles/whisper.json +++ b/src/wp-content/themes/twentytwentythree/styles/whisper.json @@ -1,5 +1,5 @@ { - "$schema": "https://schemas.wp.org/trunk/theme.json", + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Whisper", "settings": { diff --git a/src/wp-content/themes/twentytwentytwo/styles/blue.json b/src/wp-content/themes/twentytwentytwo/styles/blue.json index 8ebedef019453..29338c5a1ace1 100644 --- a/src/wp-content/themes/twentytwentytwo/styles/blue.json +++ b/src/wp-content/themes/twentytwentytwo/styles/blue.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Blue", "settings": { diff --git a/src/wp-content/themes/twentytwentytwo/styles/pink.json b/src/wp-content/themes/twentytwentytwo/styles/pink.json index 5c9be91a4e3c6..8646dcc6beb34 100644 --- a/src/wp-content/themes/twentytwentytwo/styles/pink.json +++ b/src/wp-content/themes/twentytwentytwo/styles/pink.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Pink", "settings": { diff --git a/src/wp-content/themes/twentytwentytwo/styles/swiss.json b/src/wp-content/themes/twentytwentytwo/styles/swiss.json index 483467ccc0b83..226df86b4dc84 100644 --- a/src/wp-content/themes/twentytwentytwo/styles/swiss.json +++ b/src/wp-content/themes/twentytwentytwo/styles/swiss.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/wp/6.5/theme.json", "version": 2, "title": "Swiss", "settings": { From 883146e8a5b79f3accf6a171a6c53a4716a2b61c Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Wed, 31 Jul 2024 16:54:23 +0000 Subject: [PATCH 036/106] HTML API: Introduce full parsing mode in HTML Processor. The HTML Processor has only supported a specific kind of parsing mode called _the fragment parsing mode_, where it behaves in the same way that `node.innerHTML = html` does in the DOM. This mode assumes a context node and doesn't support parsing an entire document. As part of work to add more spec support to the HTML API, this patch introduces a full parsing mode, which can parse a full HTML document from start to end, including the doctype declaration and head tags. Developed in https://github.com/wordpress/wordpress-develop/pull/6977 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58836 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-html-processor-state.php | 32 + .../html-api/class-wp-html-processor.php | 589 ++++++++++++++++-- .../html-api/wpHtmlProcessorBreadcrumbs.php | 29 +- 3 files changed, 587 insertions(+), 63 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor-state.php b/src/wp-includes/html-api/class-wp-html-processor-state.php index e0469bea020e5..97f6da95a0012 100644 --- a/src/wp-includes/html-api/class-wp-html-processor-state.php +++ b/src/wp-includes/html-api/class-wp-html-processor-state.php @@ -428,6 +428,38 @@ class WP_HTML_Processor_State { */ public $context_node = null; + /** + * The recognized encoding of the input byte stream. + * + * > The stream of code points that comprises the input to the tokenization + * > stage will be initially seen by the user agent as a stream of bytes + * > (typically coming over the network or from the local file system). + * > The bytes encode the actual characters according to a particular character + * > encoding, which the user agent uses to decode the bytes into characters. + * + * @since 6.7.0 + * + * @var string|null + */ + public $encoding = null; + + /** + * The parser's confidence in the input encoding. + * + * > When the HTML parser is decoding an input byte stream, it uses a character + * > encoding and a confidence. The confidence is either tentative, certain, or + * > irrelevant. The encoding used, and whether the confidence in that encoding + * > is tentative or certain, is used during the parsing to determine whether to + * > change the encoding. If no encoding is necessary, e.g. because the parser is + * > operating on a Unicode stream and doesn't have to use a character encoding + * > at all, then the confidence is irrelevant. + * + * @since 6.7.0 + * + * @var string + */ + public $encoding_confidence = 'tentative'; + /** * HEAD element pointer. * diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 9f2662c9e4c48..51802ac558a60 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -256,21 +256,6 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor { */ private $context_node = null; - /** - * Whether the parser has yet processed the context node, - * if created as a fragment parser. - * - * The context node will be initially pushed onto the stack of open elements, - * but when created as a fragment parser, this context element (and the implicit - * HTML document node above it) should not be exposed as a matched token or node. - * - * This boolean indicates whether the processor should skip over the current - * node in its initial search for the first node created from the input HTML. - * - * @var bool - */ - private $has_seen_context_node = false; - /* * Public Interface Functions */ @@ -312,9 +297,11 @@ public static function create_fragment( $html, $context = '', $encoding = return null; } - $processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); - $processor->state->context_node = array( 'BODY', array() ); - $processor->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + $processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); + $processor->state->context_node = array( 'BODY', array() ); + $processor->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + $processor->state->encoding = $encoding; + $processor->state->encoding_confidence = 'certain'; // @todo Create "fake" bookmarks for non-existent but implied nodes. $processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); @@ -340,6 +327,34 @@ public static function create_fragment( $html, $context = '', $encoding = return $processor; } + /** + * Creates an HTML processor in the full parsing mode. + * + * It's likely that a fragment parser is more appropriate, unless sending an + * entire HTML document from start to finish. Consider a fragment parser with + * a context node of ``. + * + * Since UTF-8 is the only currently-accepted charset, if working with a + * document that isn't UTF-8, it's important to convert the document before + * creating the processor: pass in the converted HTML. + * + * @param string $html Input HTML document to process. + * @param string|null $known_definite_encoding Optional. If provided, specifies the charset used + * in the input byte stream. Currently must be UTF-8. + * @return static|null The created processor if successful, otherwise null. + */ + public static function create_full_parser( $html, $known_definite_encoding = 'UTF-8' ) { + if ( 'UTF-8' !== $known_definite_encoding ) { + return null; + } + + $processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); + $processor->state->encoding = $known_definite_encoding; + $processor->state->encoding_confidence = 'certain'; + + return $processor; + } + /** * Constructor. * @@ -993,7 +1008,62 @@ public function get_current_depth(): int { * @return bool Whether an element was found. */ private function step_initial(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_INITIAL . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + * + * Parse error: ignore the token. + */ + case '#text': + $text = $this->get_modifiable_text(); + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + return $this->step(); + } + goto initial_anything_else; + break; + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + $contents = $this->get_modifiable_text(); + if ( ' html' !== $contents ) { + /* + * @todo When the HTML Tag Processor fully parses the DOCTYPE declaration, + * this code should examine the contents to set the compatability mode. + */ + $this->bail( 'Cannot process any DOCTYPE other than a normative HTML5 doctype.' ); + } + + /* + * > Then, switch the insertion mode to "before html". + */ + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML; + return true; + } + + /* + * > Anything else + */ + initial_anything_else: + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** @@ -1002,7 +1072,7 @@ private function step_initial(): bool { * This internal function performs the 'before html' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -1012,7 +1082,86 @@ private function step_initial(): bool { * @return bool Whether an element was found. */ private function step_before_html(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $is_closer = parent::is_tag_closer(); + $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + * + * Parse error: ignore the token. + */ + case '#text': + $text = $this->get_modifiable_text(); + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + return $this->step(); + } + goto before_html_anything_else; + break; + + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD; + return true; + + /* + * > An end tag whose tag name is one of: "head", "body", "html", "br" + * + * Closing BR tags are always reported by the Tag Processor as opening tags. + */ + case '-HEAD': + case '-BODY': + case '-HTML': + /* + * > Act as described in the "anything else" entry below. + */ + goto before_html_anything_else; + break; + } + + /* + * > Any other end tag + */ + if ( $is_closer ) { + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else. + * + * > Create an html element whose node document is the Document object. + * > Append it to the Document object. Put this element in the stack of open elements. + * > Switch the insertion mode to "before head", then reprocess the token. + */ + before_html_anything_else: + $this->insert_virtual_node( 'HTML' ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** @@ -1031,7 +1180,86 @@ private function step_before_html(): bool { * @return bool Whether an element was found. */ private function step_before_head(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $is_closer = parent::is_tag_closer(); + $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + * + * Parse error: ignore the token. + */ + case '#text': + $text = $this->get_modifiable_text(); + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + return $this->step(); + } + goto before_head_anything_else; + break; + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + return $this->step_in_body(); + + /* + * > A start tag whose tag name is "head" + */ + case '+HEAD': + $this->insert_html_element( $this->state->current_token ); + $this->state->head_element = $this->state->current_token; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD; + return true; + + /* + * > An end tag whose tag name is one of: "head", "body", "html", "br" + * > Act as described in the "anything else" entry below. + * + * Closing BR tags are always reported by the Tag Processor as opening tags. + */ + case '-HEAD': + case '-BODY': + case '-HTML': + goto before_head_anything_else; + break; + } + + if ( $is_closer ) { + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else + * + * > Insert an HTML element for a "head" start tag token with no attributes. + */ + before_head_anything_else: + $this->state->head_element = $this->insert_virtual_node( 'HEAD' ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** @@ -1056,29 +1284,31 @@ private function step_in_head(): bool { $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; $op = "{$op_sigil}{$token_name}"; - /* - * > A character token that is one of U+0009 CHARACTER TABULATION, - * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), - * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE - */ - if ( '#text' === $op ) { - $text = $this->get_modifiable_text(); - if ( '' === $text ) { + switch ( $op ) { + case '#text': /* - * If the text is empty after processing HTML entities and stripping - * U+0000 NULL bytes then ignore the token. + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - return $this->step(); - } + $text = $this->get_modifiable_text(); + if ( '' === $text ) { + /* + * If the text is empty after processing HTML entities and stripping + * U+0000 NULL bytes then ignore the token. + */ + return $this->step(); + } - if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { - // Insert the character. - $this->insert_html_element( $this->state->current_token ); - return true; - } - } + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + // Insert the character. + $this->insert_html_element( $this->state->current_token ); + return true; + } + + goto in_head_anything_else; + break; - switch ( $op ) { /* * > A comment token */ @@ -1124,7 +1354,7 @@ private function step_in_head(): bool { * > tentative, then change the encoding to the resulting encoding. */ $charset = $this->get_attribute( 'charset' ); - if ( is_string( $charset ) ) { + if ( is_string( $charset ) && 'tentative' === $this->state->encoding_confidence ) { $this->bail( 'Cannot yet process META tags with charset to determine encoding.' ); } @@ -1141,7 +1371,8 @@ private function step_in_head(): bool { if ( is_string( $http_equiv ) && is_string( $content ) && - 0 === strcasecmp( $http_equiv, 'Content-Type' ) + 0 === strcasecmp( $http_equiv, 'Content-Type' ) && + 'tentative' === $this->state->encoding_confidence ) { $this->bail( 'Cannot yet process META tags with http-equiv Content-Type to determine encoding.' ); } @@ -1193,10 +1424,11 @@ private function step_in_head(): bool { /* * > An end tag whose tag name is one of: "body", "html", "br" + * + * BR tags are always reported by the Tag Processor as opening tags. */ case '-BODY': case '-HTML': - case '-BR': /* * > Act as described in the "anything else" entry below. */ @@ -1273,7 +1505,92 @@ private function step_in_head(): bool { * @return bool Whether an element was found. */ private function step_in_head_noscript(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $is_closer = parent::is_tag_closer(); + $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + * + * Parse error: ignore the token. + */ + case '#text': + $text = $this->get_modifiable_text(); + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + return $this->step_in_head(); + } + + goto in_head_noscript_anything_else; + break; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + return $this->step_in_body(); + + /* + * > An end tag whose tag name is "noscript" + */ + case '-NOSCRIPT': + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD; + return true; + + /* + * > A comment token + * > + * > A start tag whose tag name is one of: "basefont", "bgsound", + * > "link", "meta", "noframes", "style" + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + case '+BASEFONT': + case '+BGSOUND': + case '+LINK': + case '+META': + case '+NOFRAMES': + case '+STYLE': + return $this->step_in_head(); + + /* + * > An end tag whose tag name is "br" + * + * This should never happen, as the Tag Processor prevents showing a BR closing tag. + */ + } + + /* + * > A start tag whose tag name is one of: "head", "noscript" + * > Any other end tag + */ + if ( '+HEAD' === $op || '+NOSCRIPT' === $op || $is_closer ) { + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else + * + * Anything here is a parse error. + */ + in_head_noscript_anything_else: + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** @@ -1292,7 +1609,133 @@ private function step_in_head_noscript(): bool { * @return bool Whether an element was found. */ private function step_after_head(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $is_closer = parent::is_tag_closer(); + $op_sigil = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, + * > U+000A LINE FEED (LF), U+000C FORM FEED (FF), + * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + */ + case '#text': + $text = $this->get_modifiable_text(); + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + // Insert the character. + $this->insert_html_element( $this->state->current_token ); + return true; + } + goto after_head_anything_else; + break; + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + return $this->step_in_body(); + + /* + * > A start tag whose tag name is "body" + */ + case '+BODY': + $this->insert_html_element( $this->state->current_token ); + $this->state->frameset_ok = false; + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + return true; + + /* + * > A start tag whose tag name is "frameset" + */ + case '+FRAMESET': + $this->insert_html_element( $this->state->current_token ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET; + return true; + + /* + * > A start tag whose tag name is one of: "base", "basefont", "bgsound", + * > "link", "meta", "noframes", "script", "style", "template", "title" + * + * Anything here is a parse error. + */ + case '+BASE': + case '+BASEFONT': + case '+BGSOUND': + case '+LINK': + case '+META': + case '+NOFRAMES': + case '+SCRIPT': + case '+STYLE': + case '+TEMPLATE': + case '+TITLE': + /* + * > Push the node pointed to by the head element pointer onto the stack of open elements. + * > Process the token using the rules for the "in head" insertion mode. + * > Remove the node pointed to by the head element pointer from the stack of open elements. (It might not be the current node at this point.) + */ + $this->bail( 'Cannot process elements after HEAD which reopen the HEAD element.' ); + /* + * Do not leave this break in when adding support; it's here to prevent + * WPCS from getting confused at the switch structure without a return, + * because it doesn't know that `bail()` always throws. + */ + break; + + /* + * > An end tag whose tag name is "template" + */ + case '-TEMPLATE': + return $this->step_in_head(); + + /* + * > An end tag whose tag name is one of: "body", "html", "br" + * + * Closing BR tags are always reported by the Tag Processor as opening tags. + */ + case '-BODY': + case '-HTML': + /* + * > Act as described in the "anything else" entry below. + */ + goto after_head_anything_else; + break; + } + + /* + * > A start tag whose tag name is "head" + * > Any other end tag + */ + if ( '+HEAD' === $op || $is_closer ) { + // Parse error: ignore the token. + return $this->step(); + } + + /* + * > Anything else + * > Insert an HTML element for a "body" start tag token with no attributes. + */ + after_head_anything_else: + $this->insert_virtual_node( 'BODY' ); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** @@ -4469,14 +4912,17 @@ private function insert_html_element( WP_HTML_Token $token ): void { * @param string $token_name Name of token to create and insert into the stack of open elements. * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node. * Defaults to auto-creating a bookmark name. + * @return WP_HTML_Token Newly-created virtual token. */ - private function insert_virtual_node( $token_name, $bookmark_name = null ): void { + private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_HTML_Token { $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; $name = $bookmark_name ?? $this->bookmark_token(); $this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 ); - $this->insert_html_element( new WP_HTML_Token( $name, $token_name, false ) ); + $token = new WP_HTML_Token( $name, $token_name, false ); + $this->insert_html_element( $token ); + return $token; } /* @@ -4633,6 +5079,53 @@ public static function is_void( $tag_name ): bool { ); } + /** + * Gets an encoding from a given string. + * + * This is an algorithm defined in the WHAT-WG specification. + * + * Example: + * + * 'UTF-8' === self::get_encoding( 'utf8' ); + * 'UTF-8' === self::get_encoding( " \tUTF-8 " ); + * null === self::get_encoding( 'UTF-7' ); + * null === self::get_encoding( 'utf8; charset=' ); + * + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * + * @todo As this parser only supports UTF-8, only the UTF-8 + * encodings are detected. Add more as desired, but the + * parser will bail on non-UTF-8 encodings. + * + * @since 6.7.0 + * + * @param string $label A string which may specify a known encoding. + * @return string|null Known encoding if matched, otherwise null. + */ + protected static function get_encoding( string $label ): ?string { + /* + * > Remove any leading and trailing ASCII whitespace from label. + */ + $label = trim( $label, " \t\f\r\n" ); + + /* + * > If label is an ASCII case-insensitive match for any of the labels listed in the + * > table below, then return the corresponding encoding; otherwise return failure. + */ + switch ( strtolower( $label ) ) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8'; + + default: + return null; + } + } + /* * Constants that would pollute the top of the class if they were found there. */ diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 0dbd45cfa0ead..1486769533e96 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -25,7 +25,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase { public function test_navigates_into_normative_html_for_supported_elements( $html, $tag_name ) { $processor = WP_HTML_Processor::create_fragment( $html ); - $this->assertTrue( $processor->step(), "Failed to step into supported {$tag_name} element." ); + $this->assertTrue( $processor->next_token(), "Failed to step into supported {$tag_name} element." ); $this->assertSame( $tag_name, $processor->get_tag(), "Misread {$tag_name} as a {$processor->get_tag()} element." ); } @@ -90,6 +90,7 @@ public static function data_single_tag_of_supported_elements() { 'IMG', 'INS', 'LI', + 'LINK', 'ISINDEX', // Deprecated. 'KBD', 'KEYGEN', // Deprecated. @@ -108,6 +109,8 @@ public static function data_single_tag_of_supported_elements() { 'NAV', 'NEXTID', // Deprecated. 'NOBR', // Neutralized. + 'NOEMBED', // Neutralized. + 'NOFRAMES', // Neutralized. 'NOSCRIPT', 'OBJECT', 'OL', @@ -122,6 +125,7 @@ public static function data_single_tag_of_supported_elements() { 'RTC', // Neutralized. 'RUBY', 'SAMP', + 'SCRIPT', 'SEARCH', 'SECTION', 'SLOT', @@ -130,21 +134,29 @@ public static function data_single_tag_of_supported_elements() { 'SPAN', 'STRIKE', 'STRONG', + 'STYLE', 'SUB', 'SUMMARY', 'SUP', 'TABLE', + 'TEXTAREA', 'TIME', + 'TITLE', 'TT', 'U', 'UL', 'VAR', 'VIDEO', + 'XMP', // Deprecated, use PRE instead. ); $data = array(); foreach ( $supported_elements as $tag_name ) { - $data[ $tag_name ] = array( "<{$tag_name}>", $tag_name ); + $closer = in_array( $tag_name, array( 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) + ? "" + : ''; + + $data[ $tag_name ] = array( "<{$tag_name}>{$closer}", $tag_name ); } $data['IMAGE (treated as an IMG)'] = array( '', 'IMG' ); @@ -182,22 +194,9 @@ public function test_fails_when_encountering_unsupported_tag( $html ) { */ public static function data_unsupported_elements() { $unsupported_elements = array( - 'BODY', - 'FRAME', - 'FRAMESET', - 'HEAD', - 'HTML', - 'IFRAME', 'MATH', - 'NOEMBED', // Neutralized. - 'NOFRAMES', // Neutralized. 'PLAINTEXT', // Neutralized. - 'SCRIPT', - 'STYLE', 'SVG', - 'TEXTAREA', - 'TITLE', - 'XMP', // Deprecated, use PRE instead. ); $data = array(); From 74e03e3cbef2f2565028f446c76acb2dabf749bd Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 31 Jul 2024 22:56:32 +0000 Subject: [PATCH 037/106] Options, Meta APIs: Prime salts when stored in database. For salts generated and stored in the database, use `wp_prime_site_option_caches()` within `wp_salt()` to prime the options in a single database query, down from up to nine database queries. The options are primed when the corresponding constant is either undefined or uses the default string `put your unique phrase here`. Props joemcgill, spacedmonkey, peterwilsoncc. Fixes #59871. git-svn-id: https://develop.svn.wordpress.org/trunk@58837 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/pluggable.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 0e9e0d4579c19..ae5355f9c6903 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -2468,6 +2468,41 @@ function wp_salt( $scheme = 'auth' ) { } } + /* + * Determine which options to prime. + * + * If the salt keys are undefined, use a duplicate value or the + * default `put your unique phrase here` value the salt will be + * generated via `wp_generate_password()` and stored as a site + * option. These options will be primed to avoid repeated + * database requests for undefined salts. + */ + $options_to_prime = array(); + foreach ( array( 'auth', 'secure_auth', 'logged_in', 'nonce' ) as $key ) { + foreach ( array( 'key', 'salt' ) as $second ) { + $const = strtoupper( "{$key}_{$second}" ); + if ( ! defined( $const ) || true === $duplicated_keys[ constant( $const ) ] ) { + $options_to_prime[] = "{$key}_{$second}"; + } + } + } + + if ( ! empty( $options_to_prime ) ) { + /* + * Also prime `secret_key` used for undefined salting schemes. + * + * If the scheme is unknown the default value for `secret_key` will be + * used to for the salt. This should rarely happen so the option is only + * primed if other salts are undefined. + * + * At this point of execution is is known that a database call will be made + * to prime salts so the `secret_key` option can be primed regardless of the + * constants status. + */ + $options_to_prime[] = 'secret_key'; + wp_prime_site_option_caches( $options_to_prime ); + } + $values = array( 'key' => '', 'salt' => '', From d0566719b39b1ccc88eac525e936b1e29fef5860 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 1 Aug 2024 20:30:48 +0000 Subject: [PATCH 038/106] Docs: Correct some typos in a comment in `wp_salt()`. Follow-up to [58837]. Props kebbet. Fixes #59871. git-svn-id: https://develop.svn.wordpress.org/trunk@58838 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/pluggable.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index ae5355f9c6903..8380a597249ba 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -2491,12 +2491,12 @@ function wp_salt( $scheme = 'auth' ) { /* * Also prime `secret_key` used for undefined salting schemes. * - * If the scheme is unknown the default value for `secret_key` will be - * used to for the salt. This should rarely happen so the option is only + * If the scheme is unknown, the default value for `secret_key` will be + * used too for the salt. This should rarely happen, so the option is only * primed if other salts are undefined. * - * At this point of execution is is known that a database call will be made - * to prime salts so the `secret_key` option can be primed regardless of the + * At this point of execution it is known that a database call will be made + * to prime salts, so the `secret_key` option can be primed regardless of the * constants status. */ $options_to_prime[] = 'secret_key'; From 5b8e0ec00bde2ca14d2d06fa6a411b3871cd191a Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 1 Aug 2024 22:04:44 +0000 Subject: [PATCH 039/106] HTML API: Add support for IN COLUMN GROUP parsing. As part of work to add more spec support to the HTML API, this patch adds support for the IN COLUMN GROUP insertion mode. This small section of the spec handles rules for the `
` element. Developed in https://github.com/wordpress/wordpress-develop/pull/7042 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58839 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 51802ac558a60..5ff2aa87ffdd6 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -3068,7 +3068,7 @@ private function step_in_caption(): bool { * This internal function performs the 'in column group' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -3078,7 +3078,104 @@ private function step_in_caption(): bool { * @return bool Whether an element was found. */ private function step_in_column_group(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), + * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE + */ + case '#text': + $text = $this->get_modifiable_text(); + if ( '' === $text ) { + /* + * If the text is empty after processing HTML entities and stripping + * U+0000 NULL bytes then ignore the token. + */ + return $this->step(); + } + + if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) { + // Insert the character. + $this->insert_html_element( $this->state->current_token ); + return true; + } + + goto in_column_group_anything_else; + break; + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // @todo Indicate a parse error once it's possible. + return $this->step(); + + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + return $this->step_in_body(); + + /* + * > A start tag whose tag name is "col" + */ + case '+COL': + $this->insert_html_element( $this->state->current_token ); + $this->state->stack_of_open_elements->pop(); + return true; + + /* + * > An end tag whose tag name is "colgroup" + */ + case '-COLGROUP': + if ( ! $this->state->stack_of_open_elements->current_node_is( 'COLGROUP' ) ) { + // @todo Indicate a parse error once it's possible. + return $this->step(); + } + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + return true; + + /* + * > An end tag whose tag name is "col" + */ + case '-COL': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "template" + * > An end tag whose tag name is "template" + */ + case '+TEMPLATE': + case '-TEMPLATE': + return $this->step_in_head(); + } + + in_column_group_anything_else: + /* + * > Anything else + */ + if ( ! $this->state->stack_of_open_elements->current_node_is( 'COLGROUP' ) ) { + // @todo Indicate a parse error once it's possible. + return $this->step(); + } + $this->state->stack_of_open_elements->pop(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + return $this->step( self::REPROCESS_CURRENT_NODE ); } /** From 6afe6bc178e729459ddbda4720fbcced3299f375 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 1 Aug 2024 22:34:29 +0000 Subject: [PATCH 040/106] HTML API: Add support for IN CAPTION parsing. As part of work to add more spec support to the HTML API, this patch adds support for the IN CAPTION insertion mode. This small section of the spec handles rules for the `
` element. Developed in https://github.com/wordpress/wordpress-develop/pull/7041 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58840 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 5ff2aa87ffdd6..19ac3731f6c3f 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -3049,7 +3049,7 @@ private function step_in_table_text(): bool { * This internal function performs the 'in caption' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -3059,7 +3059,72 @@ private function step_in_table_text(): bool { * @return bool Whether an element was found. */ private function step_in_caption(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION . ' state.' ); + $tag_name = $this->get_tag(); + $op_sigil = $this->is_tag_closer() ? '-' : '+'; + $op = "{$op_sigil}{$tag_name}"; + + switch ( $op ) { + /* + * > An end tag whose tag name is "caption" + * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" + * > An end tag whose tag name is "table" + * + * These tag handling rules are identical except for the final instruction. + * Handle them in a single block. + */ + case '-CAPTION': + case '+CAPTION': + case '+COL': + case '+COLGROUP': + case '+TBODY': + case '+TD': + case '+TFOOT': + case '+TH': + case '+THEAD': + case '+TR': + case '-TABLE': + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'CAPTION' ) ) { + // Parse error: ignore the token. + return $this->step(); + } + + $this->generate_implied_end_tags(); + if ( ! $this->state->stack_of_open_elements->current_node_is( 'CAPTION' ) ) { + // @todo Indicate a parse error once it's possible. + } + + $this->state->stack_of_open_elements->pop_until( 'CAPTION' ); + $this->state->active_formatting_elements->clear_up_to_last_marker(); + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; + + // If this is not a CAPTION end tag, the token should be reprocessed. + if ( '-CAPTION' === $op ) { + return true; + } + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /** + * > An end tag whose tag name is one of: "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" + */ + case '-BODY': + case '-COL': + case '-COLGROUP': + case '-HTML': + case '-TBODY': + case '-TD': + case '-TFOOT': + case '-TH': + case '-THEAD': + case '-TR': + // Parse error: ignore the token. + return $this->step(); + } + + /** + * > Anything else + * > Process the token using the rules for the "in body" insertion mode. + */ + return $this->step_in_body(); } /** From a799101e46ef0a042814c189a6331c23a52a100c Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 1 Aug 2024 22:51:11 +0000 Subject: [PATCH 041/106] HTML API: Add support for IN SELECT IN TABLE parsing. As part of work to add more spec support to the HTML API, this patch adds support for the IN SELECT IN TABLE insertion mode. This small section of the spec handles rules for the ``. Developed in https://github.com/wordpress/wordpress-develop/pull/7044 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58841 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 19ac3731f6c3f..39ba43e467d5c 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -3771,7 +3771,7 @@ private function step_in_select(): bool { * This internal function performs the 'in select in table' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 Stub implementation. + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -3781,7 +3781,52 @@ private function step_in_select(): bool { * @return bool Whether an element was found. */ private function step_in_select_in_table(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE . ' state.' ); + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; + + switch ( $op ) { + /* + * > A start tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" + */ + case '+CAPTION': + case '+TABLE': + case '+TBODY': + case '+TFOOT': + case '+THEAD': + case '+TR': + case '+TD': + case '+TH': + // @todo Indicate a parse error once it's possible. + $this->state->stack_of_open_elements->pop_until( 'SELECT' ); + $this->reset_insertion_mode(); + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > An end tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" + */ + case '-CAPTION': + case '-TABLE': + case '-TBODY': + case '-TFOOT': + case '-THEAD': + case '-TR': + case '-TD': + case '-TH': + // @todo Indicate a parse error once it's possible. + if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $token_name ) ) { + return $this->step(); + } + $this->state->stack_of_open_elements->pop_until( 'SELECT' ); + $this->reset_insertion_mode(); + return $this->step( self::REPROCESS_CURRENT_NODE ); + } + + /* + * > Anything else + */ + return $this->step_in_select(); } /** From da0f44fdff1f9ac47f606440593ee977881f2734 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 2 Aug 2024 19:38:44 +0000 Subject: [PATCH 042/106] Docs: Improve the wording for `cron_reschedule_event_error` action description. Follow-up to [54258]. Props NekoJonez, audrasjb. See #61608. git-svn-id: https://develop.svn.wordpress.org/trunk@58842 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-cron.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-cron.php b/src/wp-cron.php index c3161d9864865..417dcce375849 100644 --- a/src/wp-cron.php +++ b/src/wp-cron.php @@ -141,7 +141,7 @@ function _get_cron_lock() { ); /** - * Fires when an error happens rescheduling a cron event. + * Fires if an error happens when rescheduling a cron event. * * @since 6.1.0 * @@ -168,7 +168,7 @@ function _get_cron_lock() { ); /** - * Fires when an error happens unscheduling a cron event. + * Fires if an error happens when unscheduling a cron event. * * @since 6.1.0 * From 0e06e2b522e812e0d62193eb7ad21f9b38dde7bb Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Fri, 2 Aug 2024 22:36:27 +0000 Subject: [PATCH 043/106] General: Use clean WordPress version in `is_wp_version_compatible()`. Update `is_wp_version_compatible()` to use `wp_get_wp_version()` introduced in [58813] to ensure the value of `$wp_version` has not been modified by a theme or plugin. Props costdev, mukesh27, Cybr, sergeybiryukov. Fixes #61781. git-svn-id: https://develop.svn.wordpress.org/trunk@58843 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 12 +++- .../tests/functions/isWpVersionCompatible.php | 66 ++++++++++++------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 136012dd07284..465383b81eeee 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -8833,13 +8833,21 @@ function wp_get_wp_version() { * * @since 5.2.0 * - * @global string $wp_version The WordPress version string. + * @global string $_wp_tests_wp_version The WordPress version string. Used only in Core tests. * * @param string $required Minimum required WordPress version. * @return bool True if required version is compatible or empty, false if not. */ function is_wp_version_compatible( $required ) { - global $wp_version; + if ( + defined( 'WP_RUN_CORE_TESTS' ) + && WP_RUN_CORE_TESTS + && isset( $GLOBALS['_wp_tests_wp_version'] ) + ) { + $wp_version = $GLOBALS['_wp_tests_wp_version']; + } else { + $wp_version = wp_get_wp_version(); + } // Strip off any -alpha, -RC, -beta, -src suffixes. list( $version ) = explode( '-', $wp_version ); diff --git a/tests/phpunit/tests/functions/isWpVersionCompatible.php b/tests/phpunit/tests/functions/isWpVersionCompatible.php index 599f3b29f0005..fba8af3e14fdd 100644 --- a/tests/phpunit/tests/functions/isWpVersionCompatible.php +++ b/tests/phpunit/tests/functions/isWpVersionCompatible.php @@ -8,12 +8,45 @@ * @covers ::is_wp_version_compatible */ class Tests_Functions_IsWpVersionCompatible extends WP_UnitTestCase { + /** + * The current WordPress version. + * + * @var string + */ + private static $wp_version; + + /** + * Sets the test WordPress version property and global before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + self::$wp_version = wp_get_wp_version(); + $GLOBALS['_wp_tests_wp_version'] = self::$wp_version; + } + + /** + * Resets the test WordPress version global after each test runs. + */ + public function tear_down() { + $GLOBALS['_wp_tests_wp_version'] = self::$wp_version; + parent::tear_down(); + } + + /** + * Unsets the test WordPress version global after all tests run. + */ + public static function tear_down_after_class() { + unset( $GLOBALS['_wp_tests_wp_version'] ); + parent::tear_down_after_class(); + } + /** * Tests is_wp_version_compatible(). * * @dataProvider data_is_wp_version_compatible * * @ticket 54257 + * @ticket 61781 * * @param mixed $required The minimum required WordPress version. * @param bool $expected The expected result. @@ -28,8 +61,7 @@ public function test_is_wp_version_compatible( $required, $expected ) { * @return array[] */ public function data_is_wp_version_compatible() { - global $wp_version; - + $wp_version = wp_get_wp_version(); $version_parts = explode( '.', $wp_version ); $lower_version = $version_parts; $higher_version = $version_parts; @@ -104,22 +136,15 @@ public function data_is_wp_version_compatible() { * @dataProvider data_is_wp_version_compatible_should_gracefully_handle_trailing_point_zero_version_numbers * * @ticket 59448 + * @ticket 61781 * * @param mixed $required The minimum required WordPress version. * @param string $wp The value for the $wp_version global variable. * @param bool $expected The expected result. */ public function test_is_wp_version_compatible_should_gracefully_handle_trailing_point_zero_version_numbers( $required, $wp, $expected ) { - global $wp_version; - $original_version = $wp_version; - $wp_version = $wp; - - $actual = is_wp_version_compatible( $required ); - - // Reset the version before the assertion in case of failure. - $wp_version = $original_version; - - $this->assertSame( $expected, $actual, 'The expected result was not returned.' ); + $GLOBALS['_wp_tests_wp_version'] = $wp; + $this->assertSame( $expected, is_wp_version_compatible( $required ), 'The expected result was not returned.' ); } /** @@ -183,22 +208,15 @@ public function data_is_wp_version_compatible_should_gracefully_handle_trailing_ * @dataProvider data_is_wp_version_compatible_with_development_versions * * @ticket 54257 + * @ticket 61781 * * @param string $required The minimum required WordPress version. * @param string $wp The value for the $wp_version global variable. * @param bool $expected The expected result. */ public function test_is_wp_version_compatible_with_development_versions( $required, $wp, $expected ) { - global $wp_version; - - $original_version = $wp_version; - $wp_version = $wp; - $actual = is_wp_version_compatible( $required ); - - // Reset the version before the assertion in case of failure. - $wp_version = $original_version; - - $this->assertSame( $expected, $actual ); + $GLOBALS['_wp_tests_wp_version'] = $wp; + $this->assertSame( $expected, is_wp_version_compatible( $required ) ); } /** @@ -207,10 +225,8 @@ public function test_is_wp_version_compatible_with_development_versions( $requir * @return array[] */ public function data_is_wp_version_compatible_with_development_versions() { - global $wp_version; - // For consistent results, remove possible suffixes. - list( $version ) = explode( '-', $wp_version ); + list( $version ) = explode( '-', wp_get_wp_version() ); $version_parts = explode( '.', $version ); $lower_version = $version_parts; From 0c46e2a0b4803a0a7e7c9b0abf1cbb7811019a1f Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 2 Aug 2024 22:57:46 +0000 Subject: [PATCH 044/106] HTML API: Indicate when WordPress rejects attribute updates. When setting an an attribute value in the HTML API, WordPress may reject an update based on rules in `kses`. In these cases, the return value from an escaping function will be an empty string, and the HTML API should reject the update. Unfortunately, it currently reports that it updates the attribute but sets an empty string value, which is misleading. In this patch, the HTML API will refuse the attribute update and return false to indicate as much when WordPress rejects the updates. Developed in https://github.com/wordpress/wordpress-develop/pull/7114 Discussed in https://core.trac.wordpress.org/ticket/61719 Follow-up to [58472]. Props: amitraj2203, dmsnell, mukesh27. Fixes #61719. git-svn-id: https://develop.svn.wordpress.org/trunk@58844 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/html-api/class-wp-html-tag-processor.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index c619806525732..5e2ee114ae9e6 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3197,6 +3197,12 @@ public function set_attribute( $name, $value ): bool { * @see https://html.spec.whatwg.org/#attributes-3 */ $escaped_new_value = in_array( $comparable_name, wp_kses_uri_attributes() ) ? esc_url( $value ) : esc_attr( $value ); + + // If the escaping functions wiped out the update, reject it and indicate it was rejected. + if ( '' === $escaped_new_value && '' !== $value ) { + return false; + } + $updated_attribute = "{$name}=\"{$escaped_new_value}\""; } From bdef9de86c7dfe4193fd9188c28fe637f64b6bd5 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 2 Aug 2024 23:46:45 +0000 Subject: [PATCH 045/106] HTML API: Fix an infinite loop in certain unclosed SCRIPT tags. When the Tag Processor (or HTML Processor) attempts to parse certain incomplete script tags, the parser enters an infinite loop and will hang indefinitely. The conditions to reach this situation are: - Input HTML ends with an open script tag. - The final character of input is `-` or `<`. The infinite loop was caused by the parser-advancing increment not being called when two `||` OR conditions short-circuited. If the first condition was true, the `$at++` code was never reached. This path resolves the issue. Developed in https://github.com/wordpress/wordpress-develop/pull/7128 Discussed in https://core.trac.wordpress.org/ticket/61810 Follow-up to [55203]. Props: dmsnell, jonsurrell. Fixes #61810. git-svn-id: https://develop.svn.wordpress.org/trunk@58845 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-tag-processor.php | 11 ++++++-- .../tests/html-api/wpHtmlTagProcessor.php | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 5e2ee114ae9e6..7a53fbea1e273 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -1431,8 +1431,15 @@ private function skip_script_data(): bool { continue; } - // Everything of interest past here starts with "<". - if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) { + if ( $at + 1 >= $doc_length ) { + return false; + } + + /* + * Everything of interest past here starts with "<". + * Check this character and advance position regardless. + */ + if ( '<' !== $html[ $at++ ] ) { continue; } diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index fceaaddb04af6..637aa38751688 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -2875,4 +2875,32 @@ public function insert_after( $new_html ) { 'Should have properly applied the update from in front of the cursor.' ); } + + /** + * Test an infinite loop bugfix in incomplete script tag parsing. + * + * @small + * + * @ticket 61810 + */ + public function test_script_tag_processing_no_infinite_loop_final_dash() { + $processor = new WP_HTML_Tag_Processor( '' ); + $processor->next_token(); + + $this->assertSame( + 'SCRIPT', + $processor->get_token_name(), + "Should have found text node but found '{$processor->get_token_name()}' instead: check test setup." + ); + + $this->assertSame( + '', + $processor->get_modifiable_text(), + 'Should have found initial test text: check test setup.' + ); + + $processor->set_modifiable_text( $after ); + $this->assertSame( + $after, + $processor->get_modifiable_text(), + 'Should have found enqueued updated text.' + ); + + $processor->get_updated_html(); + $this->assertSame( + $after, + $processor->get_modifiable_text(), + 'Should have found updated text.' + ); + } + /** * Ensures that updates to modifiable text that are shorter than the * original text do not cause the parser to lose its orientation. From de084d7d0e302027fb8f0a99bbeedf88e079efee Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 8 Aug 2024 07:23:53 +0000 Subject: [PATCH 064/106] HTML API: Add support for SVG and MathML (Foreign content) As part of work to add more spec support to the HTML API, this patch adds support for SVG and MathML elements, or more generally, "foreign content." The rules in foreign content are a mix of XML and HTML parsing rules and introduce additional complexity into the processor, but is important in order to avoid getting lost when inside these elements. Developed in https://github.com/wordpress/wordpress-develop/pull/6006 Discussed in https://core.trac.wordpress.org/ticket/61576 Props: dmsnell, jonsurrell, westonruter. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58867 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-open-elements.php | 113 ++-- .../class-wp-html-processor-state.php | 12 - .../html-api/class-wp-html-processor.php | 598 ++++++++++++++++-- .../html-api/class-wp-html-tag-processor.php | 464 +++++++++++++- .../html-api/class-wp-html-token.php | 19 + .../tests/html-api/wpHtmlProcessor.php | 31 - .../html-api/wpHtmlProcessorBreadcrumbs.php | 43 -- .../html-api/wpHtmlProcessorHtml5lib.php | 70 +- .../wpHtmlSupportRequiredOpenElements.php | 219 ------- .../wpHtmlTagProcessor-token-scanning.php | 61 ++ 10 files changed, 1199 insertions(+), 431 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index c760009ce0c28..5ce1f8feb552c 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -113,13 +113,13 @@ public function set_push_handler( Closure $handler ): void { * * @param int $nth Retrieve the nth item on the stack, with 1 being * the top element, 2 being the second, etc... - * @return string|null Name of the node on the stack at the given location, - * or `null` if the location isn't on the stack. + * @return WP_HTML_Token|null Name of the node on the stack at the given location, + * or `null` if the location isn't on the stack. */ - public function at( int $nth ): ?string { + public function at( int $nth ): ?WP_HTML_Token { foreach ( $this->walk_down() as $item ) { if ( 0 === --$nth ) { - return $item->node_name; + return $item; } } @@ -242,18 +242,22 @@ public function current_node_is( string $identity ): bool { */ public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool { foreach ( $this->walk_up() as $node ) { - if ( $node->node_name === $tag_name ) { + $namespaced_name = 'html' === $node->namespace + ? $node->node_name + : "{$node->namespace} {$node->node_name}"; + + if ( $namespaced_name === $tag_name ) { return true; } if ( '(internal: H1 through H6 - do not use)' === $tag_name && - in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + in_array( $namespaced_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; } - if ( in_array( $node->node_name, $termination_list, true ) ) { + if ( in_array( $namespaced_name, $termination_list, true ) ) { return false; } } @@ -288,7 +292,7 @@ public function has_element_in_specific_scope( string $tag_name, $termination_li * > - SVG title * * @since 6.4.0 - * @since 6.7.0 Supports all required HTML elements. + * @since 6.7.0 Full support. * * @see https://html.spec.whatwg.org/#has-an-element-in-scope * @@ -309,19 +313,16 @@ public function has_element_in_scope( string $tag_name ): bool { 'OBJECT', 'TEMPLATE', - /* - * @todo Support SVG and MathML nodes when support for foreign content is added. - * - * - MathML mi - * - MathML mo - * - MathML mn - * - MathML ms - * - MathML mtext - * - MathML annotation-xml - * - SVG foreignObject - * - SVG desc - * - SVG title - */ + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', ) ); } @@ -363,19 +364,16 @@ public function has_element_in_list_item_scope( string $tag_name ): bool { 'TEMPLATE', 'UL', - /* - * @todo Support SVG and MathML nodes when support for foreign content is added. - * - * - MathML mi - * - MathML mo - * - MathML mn - * - MathML ms - * - MathML mtext - * - MathML annotation-xml - * - SVG foreignObject - * - SVG desc - * - SVG title - */ + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', ) ); } @@ -413,19 +411,16 @@ public function has_element_in_button_scope( string $tag_name ): bool { 'OBJECT', 'TEMPLATE', - /* - * @todo Support SVG and MathML nodes when support for foreign content is added. - * - * - MathML mi - * - MathML mo - * - MathML mn - * - MathML ms - * - MathML mtext - * - MathML annotation-xml - * - SVG foreignObject - * - SVG desc - * - SVG title - */ + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', ) ); } @@ -692,11 +687,15 @@ public function walk_up( ?WP_HTML_Token $above_this_node = null ) { * @param WP_HTML_Token $item Element that was added to the stack of open elements. */ public function after_element_push( WP_HTML_Token $item ): void { + $namespaced_name = 'html' === $item->namespace + ? $item->node_name + : "{$item->namespace} {$item->node_name}"; + /* * When adding support for new elements, expand this switch to trap * cases where the precalculated value needs to change. */ - switch ( $item->node_name ) { + switch ( $namespaced_name ) { case 'APPLET': case 'BUTTON': case 'CAPTION': @@ -707,6 +706,15 @@ public function after_element_push( WP_HTML_Token $item ): void { case 'MARQUEE': case 'OBJECT': case 'TEMPLATE': + case 'math MI': + case 'math MO': + case 'math MN': + case 'math MS': + case 'math MTEXT': + case 'math ANNOTATION-XML': + case 'svg FOREIGNOBJECT': + case 'svg DESC': + case 'svg TITLE': $this->has_p_in_button_scope = false; break; @@ -750,6 +758,15 @@ public function after_element_pop( WP_HTML_Token $item ): void { case 'MARQUEE': case 'OBJECT': case 'TEMPLATE': + case 'math MI': + case 'math MO': + case 'math MN': + case 'math MS': + case 'math MTEXT': + case 'math ANNOTATION-XML': + case 'svg FOREIGNOBJECT': + case 'svg DESC': + case 'svg TITLE': $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); break; } diff --git a/src/wp-includes/html-api/class-wp-html-processor-state.php b/src/wp-includes/html-api/class-wp-html-processor-state.php index 97f6da95a0012..16875c4ac1b2b 100644 --- a/src/wp-includes/html-api/class-wp-html-processor-state.php +++ b/src/wp-includes/html-api/class-wp-html-processor-state.php @@ -299,18 +299,6 @@ class WP_HTML_Processor_State { */ const INSERTION_MODE_AFTER_AFTER_FRAMESET = 'insertion-mode-after-after-frameset'; - /** - * In foreign content insertion mode for full HTML parser. - * - * @since 6.7.0 - * - * @see https://html.spec.whatwg.org/#parsing-main-inforeign - * @see WP_HTML_Processor_State::$insertion_mode - * - * @var string - */ - const INSERTION_MODE_IN_FOREIGN_CONTENT = 'insertion-mode-in-foreign-content'; - /** * No-quirks mode document compatability mode. * diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 39ba43e467d5c..3820fe027723d 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -307,14 +307,14 @@ public static function create_fragment( $html, $context = '', $encoding = $processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); $processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 ); - $processor->state->stack_of_open_elements->push( - new WP_HTML_Token( - 'root-node', - 'HTML', - false - ) + $root_node = new WP_HTML_Token( + 'root-node', + 'HTML', + false ); + $processor->state->stack_of_open_elements->push( $root_node ); + $context_node = new WP_HTML_Token( 'context-node', $processor->state->context_node[0], @@ -392,6 +392,8 @@ function ( WP_HTML_Token $token ): void { $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name; $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real'; $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance ); + + $this->change_parsing_namespace( $token->namespace ); } ); @@ -401,6 +403,12 @@ function ( WP_HTML_Token $token ): void { $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name; $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real'; $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance ); + $adjusted_current_node = $this->get_adjusted_current_node(); + $this->change_parsing_namespace( + $adjusted_current_node + ? $adjusted_current_node->namespace + : 'html' + ); } ); @@ -767,19 +775,20 @@ public function matches_breadcrumbs( $breadcrumbs ): bool { * foreign content will also act just like a void tag, immediately * closing as soon as the processor advances to the next token. * - * @since 6.6.0 + * @todo Review the self-closing logic when no node is present, ensure it + * matches the expectations in `step()`. * - * @todo When adding support for foreign content, ensure that - * this returns false for self-closing elements in the - * SVG and MathML namespace. + * @since 6.6.0 * * @param WP_HTML_Token|null $node Optional. Node to examine, if provided. * Default is to examine current node. * @return bool|null Whether to expect a closer for the currently-matched node, * or `null` if not matched on any token. */ - public function expects_closer( $node = null ): ?bool { - $token_name = $node->node_name ?? $this->get_token_name(); + public function expects_closer( WP_HTML_Token $node = null ): ?bool { + $token_name = $node->node_name ?? $this->get_token_name(); + $token_namespace = $node->namespace ?? $this->get_namespace(); + if ( ! isset( $token_name ) ) { return null; } @@ -792,7 +801,9 @@ public function expects_closer( $node = null ): ?bool { // Void elements. self::is_void( $token_name ) || // Special atomic elements. - in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) + ( 'html' === $token_namespace && in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) || + // Self-closing elements in foreign content. + ( isset( $node ) && 'html' !== $node->namespace && $node->has_self_closing_flag ) ); } @@ -824,14 +835,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool { * * When moving on to the next node, therefore, if the bottom-most element * on the stack is a void element, it must be closed. - * - * @todo Once self-closing foreign elements and BGSOUND are supported, - * they must also be implicitly closed here too. BGSOUND is - * special since it's only self-closing if the self-closing flag - * is provided in the opening tag, otherwise it expects a tag closer. */ $top_node = $this->state->stack_of_open_elements->current_node(); - if ( isset( $top_node ) && ! static::expects_closer( $top_node ) ) { + if ( isset( $top_node ) && ! $this->expects_closer( $top_node ) ) { $this->state->stack_of_open_elements->pop(); } } @@ -848,14 +854,46 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool { return false; } - $this->state->current_token = new WP_HTML_Token( - $this->bookmark_token(), - $this->get_token_name(), - $this->has_self_closing_flag(), - $this->release_internal_bookmark_on_destruct + $adjusted_current_node = $this->get_adjusted_current_node(); + $is_closer = $this->is_tag_closer(); + $is_start_tag = WP_HTML_Tag_Processor::STATE_MATCHED_TAG === $this->parser_state && ! $is_closer; + $token_name = $this->get_token_name(); + + if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) { + $this->state->current_token = new WP_HTML_Token( + $this->bookmark_token(), + $token_name, + $this->has_self_closing_flag(), + $this->release_internal_bookmark_on_destruct + ); + } + + $parse_in_current_insertion_mode = ( + 0 === $this->state->stack_of_open_elements->count() || + 'html' === $adjusted_current_node->namespace || + ( + 'math' === $adjusted_current_node->integration_node_type && + ( + ( $is_start_tag && ! in_array( $token_name, array( 'MGLYPH', 'MALIGNMARK' ), true ) ) || + '#text' === $token_name + ) + ) || + ( + 'math' === $adjusted_current_node->namespace && + 'ANNOTATION-XML' === $adjusted_current_node->node_name && + $is_start_tag && 'SVG' === $token_name + ) || + ( + 'html' === $adjusted_current_node->integration_node_type && + ( $is_start_tag || '#text' === $token_name ) + ) ); try { + if ( ! $parse_in_current_insertion_mode ) { + return $this->step_in_foreign_content(); + } + switch ( $this->state->insertion_mode ) { case WP_HTML_Processor_State::INSERTION_MODE_INITIAL: return $this->step_initial(); @@ -923,9 +961,6 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool { case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET: return $this->step_after_after_frameset(); - case WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT: - return $this->step_in_foreign_content(); - // This should be unreachable but PHP doesn't have total type checking on switch. default: $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." ); @@ -1853,7 +1888,7 @@ private function step_in_body(): bool { case '+BODY': if ( 1 === $this->state->stack_of_open_elements->count() || - 'BODY' !== $this->state->stack_of_open_elements->at( 2 ) || + 'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) || $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) { // Ignore the token. @@ -1879,7 +1914,7 @@ private function step_in_body(): bool { case '+FRAMESET': if ( 1 === $this->state->stack_of_open_elements->count() || - 'BODY' !== $this->state->stack_of_open_elements->at( 2 ) || + 'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) || false === $this->state->frameset_ok ) { // Ignore the token. @@ -2075,7 +2110,7 @@ private function step_in_body(): bool { 'ADDRESS' !== $node->node_name && 'DIV' !== $node->node_name && 'P' !== $node->node_name && - $this->is_special( $node->node_name ) + self::is_special( $node ) ) { /* * > If node is in the special category, but is not an address, div, @@ -2136,11 +2171,6 @@ private function step_in_body(): bool { * > "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset", * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul" - * - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as `
`, and a foreign element of - * the same given name. */ case '-ADDRESS': case '-ARTICLE': @@ -2411,11 +2441,6 @@ private function step_in_body(): bool { /* * > A end tag token whose tag name is one of: "applet", "marquee", "object" - * - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as ``, and a foreign element of - * the same given name. */ case '-APPLET': case '-MARQUEE': @@ -2679,9 +2704,12 @@ private function step_in_body(): bool { * * These ought to be handled in the attribute methods. */ - - $this->bail( 'Cannot process MATH element, opening foreign content.' ); - break; + $this->state->current_token->namespace = 'math'; + $this->insert_html_element( $this->state->current_token ); + if ( $this->state->current_token->has_self_closing_flag ) { + $this->state->stack_of_open_elements->pop(); + } + return true; /* * > A start tag whose tag name is "svg" @@ -2695,9 +2723,12 @@ private function step_in_body(): bool { * * These ought to be handled in the attribute methods. */ - - $this->bail( 'Cannot process SVG element, opening foreign content.' ); - break; + $this->state->current_token->namespace = 'svg'; + $this->insert_html_element( $this->state->current_token ); + if ( $this->state->current_token->has_self_closing_flag ) { + $this->state->stack_of_open_elements->pop(); + } + return true; /* * > A start tag whose tag name is one of: "caption", "col", "colgroup", @@ -2737,17 +2768,11 @@ private function step_in_body(): bool { * close anything beyond its containing `P` or `DIV` element. */ foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) { - /* - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as ``, and a foreign element of - * the same given name. - */ - if ( $token_name === $node->node_name ) { + if ( 'html' === $node->namespace && $token_name === $node->node_name ) { break; } - if ( self::is_special( $node->node_name ) ) { + if ( self::is_special( $node ) ) { // This is a parse error, ignore the token. return $this->step(); } @@ -4069,7 +4094,284 @@ private function step_after_after_frameset(): bool { * @return bool Whether an element was found. */ private function step_in_foreign_content(): bool { - $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT . ' state.' ); + $tag_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$tag_name}"; + + /* + * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size" + * + * This section drawn out above the switch to more easily incorporate + * the additional rules based on the presence of the attributes. + */ + if ( + '+FONT' === $op && + ( + null !== $this->get_attribute( 'color' ) || + null !== $this->get_attribute( 'face' ) || + null !== $this->get_attribute( 'size' ) + ) + ) { + $op = '+FONT with attributes'; + } + + switch ( $op ) { + case '#text': + /* + * > A character token that is U+0000 NULL + * + * This is handled by `get_modifiable_text()`. + */ + + /* + * Whitespace-only text does not affect the frameset-ok flag. + * It is probably inter-element whitespace, but it may also + * contain character references which decode only to whitespace. + */ + $text = $this->get_modifiable_text(); + if ( strlen( $text ) !== strspn( $text, " \t\n\f\r" ) ) { + $this->state->frameset_ok = false; + } + + $this->insert_foreign_element( $this->state->current_token, false ); + return true; + + /* + * > A comment token + */ + case '#cdata-section': + case '#comment': + case '#funky_comment': + $this->insert_foreign_element( $this->state->current_token, false ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error: ignore the token. + return $this->step(); + + /* + * > A start tag whose tag name is "b", "big", "blockquote", "body", "br", "center", + * > "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5", + * > "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol", + * > "p", "pre", "ruby", "s", "small", "span", "strong", "strike", "sub", "sup", + * > "table", "tt", "u", "ul", "var" + * + * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size" + * + * > An end tag whose tag name is "br", "p" + * + * Closing BR tags are always reported by the Tag Processor as opening tags. + */ + case '+B': + case '+BIG': + case '+BLOCKQUOTE': + case '+BODY': + case '+BR': + case '+CENTER': + case '+CODE': + case '+DD': + case '+DIV': + case '+DL': + case '+DT': + case '+EM': + case '+EMBED': + case '+H1': + case '+H2': + case '+H3': + case '+H4': + case '+H5': + case '+H6': + case '+HEAD': + case '+HR': + case '+I': + case '+IMG': + case '+LI': + case '+LISTING': + case '+MENU': + case '+META': + case '+NOBR': + case '+OL': + case '+P': + case '+PRE': + case '+RUBY': + case '+S': + case '+SMALL': + case '+SPAN': + case '+STRONG': + case '+STRIKE': + case '+SUB': + case '+SUP': + case '+TABLE': + case '+TT': + case '+U': + case '+UL': + case '+VAR': + case '+FONT with attributes': + case '-BR': + case '-P': + // @todo Indicate a parse error once it's possible. + foreach ( $this->state->stack_of_open_elements->walk_up() as $current_node ) { + if ( + 'math' === $current_node->integration_node_type || + 'html' === $current_node->integration_node_type || + 'html' === $current_node->namespace + ) { + break; + } + + $this->state->stack_of_open_elements->pop(); + } + return $this->step( self::REPROCESS_CURRENT_NODE ); + } + + /* + * > Any other start tag + */ + if ( ! $this->is_tag_closer() ) { + $this->insert_foreign_element( $this->state->current_token, false ); + + /* + * > If the token has its self-closing flag set, then run + * > the appropriate steps from the following list: + */ + if ( $this->state->current_token->has_self_closing_flag ) { + if ( 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { + /* + * > Acknowledge the token's self-closing flag, and then act as + * > described in the steps for a "script" end tag below. + * + * @todo Verify that this shouldn't be handled by the rule for + * "An end tag whose name is 'script', if the current node + * is an SVG script element." + */ + goto in_foreign_content_any_other_end_tag; + } else { + $this->state->stack_of_open_elements->pop(); + } + } + return true; + } + + /* + * > An end tag whose name is "script", if the current node is an SVG script element. + */ + if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { + $this->state->stack_of_open_elements->pop(); + } + + /* + * > Any other end tag + */ + if ( $this->is_tag_closer() ) { + in_foreign_content_any_other_end_tag: + $node = $this->state->stack_of_open_elements->current_node(); + if ( $tag_name !== $node->node_name ) { + // @todo Indicate a parse error once it's possible. + } + in_foreign_content_end_tag_loop: + if ( $node === $this->state->stack_of_open_elements->at( 1 ) ) { + return true; + } + + /* + * > If node's tag name, converted to ASCII lowercase, is the same as the tag name + * > of the token, pop elements from the stack of open elements until node has + * > been popped from the stack, and then return. + */ + if ( 0 === strcasecmp( $node->node_name, $tag_name ) ) { + foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { + $this->state->stack_of_open_elements->pop(); + if ( $node === $item ) { + return true; + } + } + } + + foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) { + $node = $item; + break; + } + + if ( 'html' !== $node->namespace ) { + goto in_foreign_content_end_tag_loop; + } + + switch ( $this->state->insertion_mode ) { + case WP_HTML_Processor_State::INSERTION_MODE_INITIAL: + return $this->step_initial(); + + case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML: + return $this->step_before_html(); + + case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD: + return $this->step_before_head(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD: + return $this->step_in_head(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT: + return $this->step_in_head_noscript(); + + case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD: + return $this->step_after_head(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY: + return $this->step_in_body(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE: + return $this->step_in_table(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT: + return $this->step_in_table_text(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION: + return $this->step_in_caption(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP: + return $this->step_in_column_group(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY: + return $this->step_in_table_body(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW: + return $this->step_in_row(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL: + return $this->step_in_cell(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT: + return $this->step_in_select(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE: + return $this->step_in_select_in_table(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE: + return $this->step_in_template(); + + case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY: + return $this->step_after_body(); + + case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET: + return $this->step_in_frameset(); + + case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET: + return $this->step_after_frameset(); + + case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY: + return $this->step_after_after_body(); + + case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET: + return $this->step_after_after_frameset(); + + // This should be unreachable but PHP doesn't have total type checking on switch. + default: + $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." ); + } + } } /* @@ -4099,6 +4401,19 @@ private function bookmark_token() { * HTML semantic overrides for Tag Processor */ + /** + * Indicates the namespace of the current token, or "html" if there is none. + * + * @return string One of "html", "math", or "svg". + */ + public function get_namespace(): string { + if ( ! isset( $this->current_element ) ) { + return 'html'; + } + + return $this->current_element->token->namespace; + } + /** * Returns the uppercase name of the matched tag. * @@ -4734,6 +5049,28 @@ private function generate_implied_end_tags_thoroughly(): void { } } + /** + * Returns the adjusted current node. + * + * > The adjusted current node is the context element if the parser was created as + * > part of the HTML fragment parsing algorithm and the stack of open elements + * > has only one element in it (fragment case); otherwise, the adjusted current + * > node is the current node. + * + * @see https://html.spec.whatwg.org/#adjusted-current-node + * + * @since 6.7.0 + * + * @return WP_HTML_Token|null The adjusted current node. + */ + private function get_adjusted_current_node(): ?WP_HTML_Token { + if ( isset( $this->context_node ) && 1 === $this->state->stack_of_open_elements->count() ) { + return $this->context_node; + } + + return $this->state->stack_of_open_elements->current_node(); + } + /** * Reconstructs the active formatting elements. * @@ -5043,7 +5380,7 @@ private function run_adoption_agency_algorithm(): void { continue; } - if ( self::is_special( $item->node_name ) ) { + if ( self::is_special( $item ) ) { $furthest_block = $item; break; } @@ -5111,6 +5448,45 @@ private function insert_html_element( WP_HTML_Token $token ): void { $this->state->stack_of_open_elements->push( $token ); } + /** + * Inserts a foreign element on to the stack of open elements. + * + * @since 6.7.0 + * + * @see https://html.spec.whatwg.org/#insert-a-foreign-element + * + * @param WP_HTML_Token $token Insert this token. The token's namespace and + * insertion point will be updated correctly. + * @param bool $only_add_to_element_stack Whether to skip the "insert an element at the adjusted + * insertion location" algorithm when adding this element. + */ + private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack ): void { + $adjusted_current_node = $this->get_adjusted_current_node(); + + $token->namespace = $adjusted_current_node ? $adjusted_current_node->namespace : 'html'; + + if ( $this->is_mathml_integration_point() ) { + $token->integration_node_type = 'math'; + } elseif ( $this->is_html_integration_point() ) { + $token->integration_node_type = 'html'; + } + + if ( false === $only_add_to_element_stack ) { + /* + * @todo Implement the "appropriate place for inserting a node" and the + * "insert an element at the adjusted insertion location" algorithms. + * + * These algorithms mostly impacts DOM tree construction and not the HTML API. + * Here, there's no DOM node onto which the element will be appended, so the + * parser will skip this step. + * + * @see https://html.spec.whatwg.org/#insert-an-element-at-the-adjusted-insertion-location + */ + } + + $this->insert_html_element( $token ); + } + /** * Inserts a virtual element on the stack of open elements. * @@ -5136,6 +5512,88 @@ private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_H * HTML Specification Helpers */ + /** + * Indicates if the current token is a MathML integration point. + * + * @since 6.7.0 + * + * @see https://html.spec.whatwg.org/#mathml-text-integration-point + * + * @return bool Whether the current token is a MathML integration point. + */ + private function is_mathml_integration_point(): bool { + $current_token = $this->state->current_token; + if ( ! isset( $current_token ) ) { + return false; + } + + if ( 'math' !== $current_token->namespace || 'M' !== $current_token->node_name[0] ) { + return false; + } + + $tag_name = $current_token->node_name; + + return ( + 'MI' === $tag_name || + 'MO' === $tag_name || + 'MN' === $tag_name || + 'MS' === $tag_name || + 'MTEXT' === $tag_name + ); + } + + /** + * Indicates if the current token is an HTML integration point. + * + * Note that this method must be an instance method with access + * to the current token, since it needs to examine the attributes + * of the currently-matched tag, if it's in the MathML namespace. + * Otherwise it would be required to scan the HTML and ensure that + * no other accounting is overlooked. + * + * @since 6.7.0 + * + * @see https://html.spec.whatwg.org/#html-integration-point + * + * @return bool Whether the current token is an HTML integration point. + */ + private function is_html_integration_point(): bool { + $current_token = $this->state->current_token; + if ( ! isset( $current_token ) ) { + return false; + } + + if ( 'html' === $current_token->namespace ) { + return false; + } + + $tag_name = $current_token->node_name; + + if ( 'svg' === $current_token->namespace ) { + return ( + 'DESC' === $tag_name || + 'FOREIGNOBJECT' === $tag_name || + 'TITLE' === $tag_name + ); + } + + if ( 'math' === $current_token->namespace ) { + if ( 'ANNOTATION-XML' !== $tag_name ) { + return false; + } + + $encoding = $this->get_attribute( 'encoding' ); + + return ( + is_string( $encoding ) && + ( + 0 === strcasecmp( $encoding, 'application/xhtml+xml' ) || + 0 === strcasecmp( $encoding, 'text/html' ) + ) + ); + } + } + /** * Returns whether an element of a given name is in the HTML special category. * @@ -5143,11 +5601,17 @@ private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_H * * @see https://html.spec.whatwg.org/#special * - * @param string $tag_name Name of element to check. + * @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace. * @return bool Whether the element of the given name is in the special category. */ public static function is_special( $tag_name ): bool { - $tag_name = strtoupper( $tag_name ); + if ( is_string( $tag_name ) ) { + $tag_name = strtoupper( $tag_name ); + } else { + $tag_name = 'html' === $tag_name->namespace + ? strtoupper( $tag_name->node_name ) + : "{$tag_name->namespace} {$tag_name->node_name}"; + } return ( 'ADDRESS' === $tag_name || @@ -5235,17 +5699,17 @@ public static function is_special( $tag_name ): bool { 'XMP' === $tag_name || // MathML. - 'MI' === $tag_name || - 'MO' === $tag_name || - 'MN' === $tag_name || - 'MS' === $tag_name || - 'MTEXT' === $tag_name || - 'ANNOTATION-XML' === $tag_name || + 'math MI' === $tag_name || + 'math MO' === $tag_name || + 'math MN' === $tag_name || + 'math MS' === $tag_name || + 'math MTEXT' === $tag_name || + 'math ANNOTATION-XML' === $tag_name || // SVG. - 'FOREIGNOBJECT' === $tag_name || - 'DESC' === $tag_name || - 'TITLE' === $tag_name + 'svg DESC' === $tag_name || + 'svg FOREIGNOBJECT' === $tag_name || + 'svg TITLE' === $tag_name ); } diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 0ff2cdc4dd10d..fb21c15d1d96e 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -511,6 +511,23 @@ class WP_HTML_Tag_Processor { */ protected $parser_state = self::STATE_READY; + /** + * Indicates whether the parser is inside foreign content, + * e.g. inside an SVG or MathML element. + * + * One of 'html', 'svg', or 'math'. + * + * Several parsing rules change based on whether the parser + * is inside foreign content, including whether CDATA sections + * are allowed and whether a self-closing flag indicates that + * an element has no content. + * + * @since 6.7.0 + * + * @var string + */ + private $parsing_namespace = 'html'; + /** * What kind of syntax token became an HTML comment. * @@ -780,6 +797,25 @@ public function __construct( $html ) { $this->html = $html; } + /** + * Switches parsing mode into a new namespace, such as when + * encountering an SVG tag and entering foreign content. + * + * @since 6.7.0 + * + * @param string $new_namespace One of 'html', 'svg', or 'math' indicating into what + * namespace the next tokens will be processed. + * @return bool Whether the namespace was valid and changed. + */ + public function change_parsing_namespace( string $new_namespace ): bool { + if ( ! in_array( $new_namespace, array( 'html', 'math', 'svg' ), true ) ) { + return false; + } + + $this->parsing_namespace = $new_namespace; + return true; + } + /** * Finds the next tag matching the $query. * @@ -843,6 +879,7 @@ public function next_tag( $query = null ): bool { * The Tag Processor currently only supports the tag token. * * @since 6.5.0 + * @since 6.7.0 Recognizes CDATA sections within foreign content. * * @return bool Whether a token was parsed. */ @@ -956,6 +993,7 @@ private function base_class_next_token(): bool { */ if ( $this->is_closing_tag || + 'html' !== $this->parsing_namespace || 1 !== strspn( $this->html, 'iIlLnNpPsStTxX', $this->tag_name_starts_at, 1 ) ) { return true; @@ -996,7 +1034,6 @@ private function base_class_next_token(): bool { $duplicate_attributes = $this->duplicate_attributes; // Find the closing tag if necessary. - $found_closer = false; switch ( $tag_name ) { case 'SCRIPT': $found_closer = $this->skip_script_data(); @@ -1759,6 +1796,32 @@ private function parse_next_tag(): bool { return true; } + if ( + 'html' !== $this->parsing_namespace && + strlen( $html ) > $at + 8 && + '[' === $html[ $at + 2 ] && + 'C' === $html[ $at + 3 ] && + 'D' === $html[ $at + 4 ] && + 'A' === $html[ $at + 5 ] && + 'T' === $html[ $at + 6 ] && + 'A' === $html[ $at + 7 ] && + '[' === $html[ $at + 8 ] + ) { + $closer_at = strpos( $html, ']]>', $at + 9 ); + if ( false === $closer_at ) { + $this->parser_state = self::STATE_INCOMPLETE_INPUT; + + return false; + } + + $this->parser_state = self::STATE_CDATA_NODE; + $this->text_starts_at = $at + 9; + $this->text_length = $closer_at - $this->text_starts_at; + $this->token_length = $closer_at + 3 - $this->token_starts_at; + $this->bytes_already_parsed = $closer_at + 3; + return true; + } + /* * Anything else here is an incorrectly-opened comment and transitions * to the bogus comment state - skip to the nearest >. If no closer is @@ -2653,6 +2716,17 @@ public function get_attribute_names_with_prefix( $prefix ): ?array { return $matches; } + /** + * Returns the namespace of the matched token. + * + * @since 6.7.0 + * + * @return string One of 'html', 'math', or 'svg'. + */ + public function get_namespace(): string { + return $this->parsing_namespace; + } + /** * Returns the uppercase name of the matched tag. * @@ -2690,6 +2764,388 @@ public function get_tag(): ?string { return null; } + /** + * Returns the adjusted tag name for a given token, taking into + * account the current parsing context, whether HTML, SVG, or MathML. + * + * @since 6.7.0 + * + * @return string|null Name of current tag name. + */ + public function get_qualified_tag_name(): ?string { + $tag_name = $this->get_tag(); + if ( null === $tag_name ) { + return null; + } + + if ( 'html' === $this->get_namespace() ) { + return $tag_name; + } + + $lower_tag_name = strtolower( $tag_name ); + if ( 'math' === $this->get_namespace() ) { + return $lower_tag_name; + } + + if ( 'svg' === $this->get_namespace() ) { + switch ( $lower_tag_name ) { + case 'altglyph': + return 'altGlyph'; + + case 'altglyphdef': + return 'altGlyphDef'; + + case 'altglyphitem': + return 'altGlyphItem'; + + case 'animatecolor': + return 'animateColor'; + + case 'animatemotion': + return 'animateMotion'; + + case 'animatetransform': + return 'animateTransform'; + + case 'clippath': + return 'clipPath'; + + case 'feblend': + return 'feBlend'; + + case 'fecolormatrix': + return 'feColorMatrix'; + + case 'fecomponenttransfer': + return 'feComponentTransfer'; + + case 'fecomposite': + return 'feComposite'; + + case 'feconvolvematrix': + return 'feConvolveMatrix'; + + case 'fediffuselighting': + return 'feDiffuseLighting'; + + case 'fedisplacementmap': + return 'feDisplacementMap'; + + case 'fedistantlight': + return 'feDistantLight'; + + case 'fedropshadow': + return 'feDropShadow'; + + case 'feflood': + return 'feFlood'; + + case 'fefunca': + return 'feFuncA'; + + case 'fefuncb': + return 'feFuncB'; + + case 'fefuncg': + return 'feFuncG'; + + case 'fefuncr': + return 'feFuncR'; + + case 'fegaussianblur': + return 'feGaussianBlur'; + + case 'feimage': + return 'feImage'; + + case 'femerge': + return 'feMerge'; + + case 'femergenode': + return 'feMergeNode'; + + case 'femorphology': + return 'feMorphology'; + + case 'feoffset': + return 'feOffset'; + + case 'fepointlight': + return 'fePointLight'; + + case 'fespecularlighting': + return 'feSpecularLighting'; + + case 'fespotlight': + return 'feSpotLight'; + + case 'fetile': + return 'feTile'; + + case 'feturbulence': + return 'feTurbulence'; + + case 'foreignobject': + return 'foreignObject'; + + case 'glyphref': + return 'glyphRef'; + + case 'lineargradient': + return 'linearGradient'; + + case 'radialgradient': + return 'radialGradient'; + + case 'textpath': + return 'textPath'; + + default: + return $lower_tag_name; + } + } + } + + /** + * Returns the adjusted attribute name for a given attribute, taking into + * account the current parsing context, whether HTML, SVG, or MathML. + * + * @since 6.7.0 + * + * @param string $attribute_name Which attribute to adjust. + * + * @return string|null + */ + public function get_qualified_attribute_name( $attribute_name ): ?string { + if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { + return null; + } + + $namespace = $this->get_namespace(); + $lower_name = strtolower( $attribute_name ); + + if ( 'math' === $namespace && 'definitionurl' === $lower_name ) { + return 'definitionURL'; + } + + if ( 'svg' === $this->get_namespace() ) { + switch ( $lower_name ) { + case 'attributename': + return 'attributeName'; + + case 'attributetype': + return 'attributeType'; + + case 'basefrequency': + return 'baseFrequency'; + + case 'baseprofile': + return 'baseProfile'; + + case 'calcmode': + return 'calcMode'; + + case 'clippathunits': + return 'clipPathUnits'; + + case 'diffuseconstant': + return 'diffuseConstant'; + + case 'edgemode': + return 'edgeMode'; + + case 'filterunits': + return 'filterUnits'; + + case 'glyphref': + return 'glyphRef'; + + case 'gradienttransform': + return 'gradientTransform'; + + case 'gradientunits': + return 'gradientUnits'; + + case 'kernelmatrix': + return 'kernelMatrix'; + + case 'kernelunitlength': + return 'kernelUnitLength'; + + case 'keypoints': + return 'keyPoints'; + + case 'keysplines': + return 'keySplines'; + + case 'keytimes': + return 'keyTimes'; + + case 'lengthadjust': + return 'lengthAdjust'; + + case 'limitingconeangle': + return 'limitingConeAngle'; + + case 'markerheight': + return 'markerHeight'; + + case 'markerunits': + return 'markerUnits'; + + case 'markerwidth': + return 'markerWidth'; + + case 'maskcontentunits': + return 'maskContentUnits'; + + case 'maskunits': + return 'maskUnits'; + + case 'numoctaves': + return 'numOctaves'; + + case 'pathlength': + return 'pathLength'; + + case 'patterncontentunits': + return 'patternContentUnits'; + + case 'patterntransform': + return 'patternTransform'; + + case 'patternunits': + return 'patternUnits'; + + case 'pointsatx': + return 'pointsAtX'; + + case 'pointsaty': + return 'pointsAtY'; + + case 'pointsatz': + return 'pointsAtZ'; + + case 'preservealpha': + return 'preserveAlpha'; + + case 'preserveaspectratio': + return 'preserveAspectRatio'; + + case 'primitiveunits': + return 'primitiveUnits'; + + case 'refx': + return 'refX'; + + case 'refy': + return 'refY'; + + case 'repeatcount': + return 'repeatCount'; + + case 'repeatdur': + return 'repeatDur'; + + case 'requiredextensions': + return 'requiredExtensions'; + + case 'requiredfeatures': + return 'requiredFeatures'; + + case 'specularconstant': + return 'specularConstant'; + + case 'specularexponent': + return 'specularExponent'; + + case 'spreadmethod': + return 'spreadMethod'; + + case 'startoffset': + return 'startOffset'; + + case 'stddeviation': + return 'stdDeviation'; + + case 'stitchtiles': + return 'stitchTiles'; + + case 'surfacescale': + return 'surfaceScale'; + + case 'systemlanguage': + return 'systemLanguage'; + + case 'tablevalues': + return 'tableValues'; + + case 'targetx': + return 'targetX'; + + case 'targety': + return 'targetY'; + + case 'textlength': + return 'textLength'; + + case 'viewbox': + return 'viewBox'; + + case 'viewtarget': + return 'viewTarget'; + + case 'xchannelselector': + return 'xChannelSelector'; + + case 'ychannelselector': + return 'yChannelSelector'; + + case 'zoomandpan': + return 'zoomAndPan'; + } + } + + if ( 'html' !== $namespace ) { + switch ( $lower_name ) { + case 'xlink:actuate': + return 'xlink actuate'; + + case 'xlink:arcrole': + return 'xlink arcrole'; + + case 'xlink:href': + return 'xlink href'; + + case 'xlink:role': + return 'xlink role'; + + case 'xlink:show': + return 'xlink show'; + + case 'xlink:title': + return 'xlink title'; + + case 'xlink:type': + return 'xlink type'; + + case 'xml:lang': + return 'xml lang'; + + case 'xml:space': + return 'xml space'; + + case 'xmlns': + return 'xmlns'; + + case 'xmlns:xlink': + return 'xmlns xlink'; + } + } + + return $attribute_name; + } + /** * Indicates if the currently matched tag contains the self-closing flag. * @@ -2963,8 +3419,12 @@ public function get_modifiable_text(): string { * In all other contexts it's replaced by the replacement character (U+FFFD) * for security reasons (to avoid joining together strings that were safe * when separated, but not when joined). + * + * @todo Inside HTML integration points and MathML integration points, the + * text is processed according to the insertion mode, not according + * to the foreign content rules. This should strip the NULL bytes. */ - return '#text' === $tag_name + return ( '#text' === $tag_name && 'html' === $this->get_namespace() ) ? str_replace( "\x00", '', $decoded ) : str_replace( "\x00", "\u{FFFD}", $decoded ); } diff --git a/src/wp-includes/html-api/class-wp-html-token.php b/src/wp-includes/html-api/class-wp-html-token.php index 948fe343dfbaa..d5e51ac29007f 100644 --- a/src/wp-includes/html-api/class-wp-html-token.php +++ b/src/wp-includes/html-api/class-wp-html-token.php @@ -60,6 +60,24 @@ class WP_HTML_Token { */ public $has_self_closing_flag = false; + /** + * Indicates if the element is an HTML element or if it's inside foreign content. + * + * @since 6.7.0 + * + * @var string 'html', 'svg', or 'math'. + */ + public $namespace = 'html'; + + /** + * Indicates which kind of integration point the element is, if any. + * + * @since 6.7.0 + * + * @var string|null 'math', 'html', or null if not an integration point. + */ + public $integration_node_type = null; + /** * Called when token is garbage-collected or otherwise destroyed. * @@ -80,6 +98,7 @@ class WP_HTML_Token { */ public function __construct( ?string $bookmark_name, string $node_name, bool $has_self_closing_flag, ?callable $on_destroy = null ) { $this->bookmark_name = $bookmark_name; + $this->namespace = 'html'; $this->node_name = $node_name; $this->has_self_closing_flag = $has_self_closing_flag; $this->on_destroy = $on_destroy; diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 01e0f4f02c0b5..68c60a1ff85cc 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -358,37 +358,6 @@ public static function data_void_tags_not_ignored_in_body() { return $all_void_tags; } - /** - * Ensures that special handling of unsupported tags is cleaned up - * as handling is implemented. Otherwise there's risk of leaving special - * handling (that is never reached) when tag handling is implemented. - * - * @ticket 60092 - * - * @dataProvider data_unsupported_special_in_body_tags - * - * @covers WP_HTML_Processor::step_in_body - * - * @param string $tag_name Name of the tag to test. - */ - public function test_step_in_body_fails_on_unsupported_tags( $tag_name ) { - $fragment = WP_HTML_Processor::create_fragment( '<' . $tag_name . '>' ); - $this->assertFalse( $fragment->next_tag(), 'Should fail to find tag: ' . $tag_name . '.' ); - $this->assertEquals( $fragment->get_last_error(), WP_HTML_Processor::ERROR_UNSUPPORTED, 'Should have unsupported last error.' ); - } - - /** - * Data provider. - * - * @return array[] - */ - public static function data_unsupported_special_in_body_tags() { - return array( - 'MATH' => array( 'MATH' ), - 'SVG' => array( 'SVG' ), - ); - } - /** * Ensures that the HTML Processor properly reports the depth of a given element. * diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 1486769533e96..911fa8b910b37 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -164,49 +164,6 @@ public static function data_single_tag_of_supported_elements() { return $data; } - /** - * Ensures that no new HTML elements are accidentally partially-supported. - * - * When introducing support for new HTML elements, there are multiple places - * in the HTML Processor that need to be updated, until the time that the class - * has full HTML5 support. Because of this, these tests lock down the interface - * to ensure that support isn't accidentally updated in one place for a new - * element while overlooked in another. - * - * @ticket 58517 - * - * @covers WP_HTML_Processor::step - * - * @dataProvider data_unsupported_elements - * - * @param string $html HTML string containing unsupported elements. - */ - public function test_fails_when_encountering_unsupported_tag( $html ) { - $processor = WP_HTML_Processor::create_fragment( $html ); - - $this->assertFalse( $processor->step(), "Should not have stepped into unsupported {$processor->get_tag()} element." ); - } - - /** - * Data provider. - * - * @return array[] - */ - public static function data_unsupported_elements() { - $unsupported_elements = array( - 'MATH', - 'PLAINTEXT', // Neutralized. - 'SVG', - ); - - $data = array(); - foreach ( $unsupported_elements as $tag_name ) { - $data[ $tag_name ] = array( "<{$tag_name}>" ); - } - - return $data; - } - /** * @ticket 58517 * diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php index 22eef774d4e90..b6213aac8d0e9 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php @@ -181,19 +181,24 @@ private static function build_tree_representation( ?string $fragment_context, st $is_closer = $processor->is_tag_closer(); if ( $was_text && '#text' !== $token_name ) { - $output .= "{$text_node}\"\n"; + if ( '' !== $text_node ) { + $output .= "{$text_node}\"\n"; + } $was_text = false; $text_node = ''; } switch ( $token_type ) { case '#tag': - $tag_name = strtolower( $token_name ); + $namespace = $processor->get_namespace(); + $tag_name = 'html' === $namespace + ? strtolower( $processor->get_tag() ) + : "{$namespace} {$processor->get_qualified_tag_name()}"; if ( $is_closer ) { --$indent_level; - if ( 'TEMPLATE' === $token_name ) { + if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) { --$indent_level; } @@ -202,7 +207,11 @@ private static function build_tree_representation( ?string $fragment_context, st $tag_indent = $indent_level; - if ( ! WP_HTML_Processor::is_void( $tag_name ) ) { + if ( 'html' !== $namespace ) { + if ( ! $processor->has_self_closing_flag() ) { + ++$indent_level; + } + } elseif ( ! WP_HTML_Processor::is_void( $tag_name ) ) { ++$indent_level; } @@ -210,9 +219,47 @@ private static function build_tree_representation( ?string $fragment_context, st $attribute_names = $processor->get_attribute_names_with_prefix( '' ); if ( $attribute_names ) { - sort( $attribute_names, SORT_STRING ); - + $sorted_attributes = array(); foreach ( $attribute_names as $attribute_name ) { + $sorted_attributes[ $attribute_name ] = $processor->get_qualified_attribute_name( $attribute_name ); + } + + /* + * Sorts attributes to match html5lib sort order. + * + * - First comes normal HTML attributes. + * - Then come adjusted foreign attributes; these have spaces in their names. + * - Finally come non-adjusted foreign attributes; these have a colon in their names. + * + * Example: + * + * From: + * Sorted: 'definitionURL', 'xlink show', 'xlink title', 'xlink:author' + */ + uasort( + $sorted_attributes, + static function ( $a, $b ) { + $a_has_ns = str_contains( $a, ':' ); + $b_has_ns = str_contains( $b, ':' ); + + // Attributes with `:` should follow all other attributes. + if ( $a_has_ns !== $b_has_ns ) { + return $a_has_ns ? 1 : -1; + } + + $a_has_sp = str_contains( $a, ' ' ); + $b_has_sp = str_contains( $b, ' ' ); + + // Attributes with a namespace ' ' should come after those without. + if ( $a_has_sp !== $b_has_sp ) { + return $a_has_sp ? 1 : -1; + } + + return $a <=> $b; + } + ); + + foreach ( $sorted_attributes as $attribute_name => $display_name ) { $val = $processor->get_attribute( $attribute_name ); /* * Attributes with no value are `true` with the HTML API, @@ -221,7 +268,7 @@ private static function build_tree_representation( ?string $fragment_context, st if ( true === $val ) { $val = ''; } - $output .= str_repeat( $indent, $tag_indent + 1 ) . "{$attribute_name}=\"{$val}\"\n"; + $output .= str_repeat( $indent, $tag_indent + 1 ) . "{$display_name}=\"{$val}\"\n"; } } @@ -231,7 +278,7 @@ private static function build_tree_representation( ?string $fragment_context, st $output .= str_repeat( $indent, $indent_level ) . "\"{$modifiable_text}\"\n"; } - if ( 'TEMPLATE' === $token_name ) { + if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) { $output .= str_repeat( $indent, $indent_level ) . "content\n"; ++$indent_level; } @@ -242,12 +289,17 @@ private static function build_tree_representation( ?string $fragment_context, st break; + case '#cdata-section': case '#text': + $text_content = $processor->get_modifiable_text(); + if ( '' === $text_content ) { + break; + } $was_text = true; if ( '' === $text_node ) { $text_node .= str_repeat( $indent, $indent_level ) . '"'; } - $text_node .= $processor->get_modifiable_text(); + $text_node .= $text_content; break; case '#funky-comment': diff --git a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php index d2b24cd8bbcbc..e69de29bb2d1d 100644 --- a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php +++ b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php @@ -1,219 +0,0 @@ -" ); - - $this->assertFalse( $processor->step(), "Must support terminating elements in specific scope check before adding support for the {$tag_name} element." ); - } - - /** - * The check for whether an element is in a scope depends on - * looking for a number of terminating elements in the stack of open - * elements. Until the listed elements are supported in the HTML - * processor, there are no terminating elements and there's no - * point in taking the time to look for them. - * - * @since 6.4.0 - * - * @ticket 58517 - */ - public function test_has_element_in_scope_needs_support() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } - - /** - * The check for whether an element is in list item scope depends on - * the elements for any scope, plus UL and OL. - * - * The method for asserting list item scope doesn't currently exist - * because the LI element isn't yet supported and the LI element is - * the only element that needs to know about list item scope. - * - * @since 6.4.0 - * - * @ticket 58517 - * - * @covers WP_HTML_Open_Elements::has_element_in_list_item_scope - */ - public function test_has_element_in_list_item_scope_needs_support() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } - - /** - * The check for whether an element is in BUTTON scope depends on - * the elements for any scope, plus BUTTON. - * - * @since 6.4.0 - * - * @ticket 58517 - * - * @covers WP_HTML_Open_Elements::has_element_in_button_scope - */ - public function test_has_element_in_button_scope_needs_support() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } - - /** - * The optimization maintaining a flag for "P is in BUTTON scope" requires - * updating that flag every time an element is popped from the stack of - * open elements. - * - * @since 6.4.0 - * - * @ticket 58517 - * - * @covers WP_HTML_Open_Elements::after_element_pop - */ - public function test_after_element_pop_must_maintain_p_in_button_scope_flag() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } - - /** - * The optimization maintaining a flag for "P is in BUTTON scope" requires - * updating that flag every time an element is pushed onto the stack of - * open elements. - * - * @since 6.4.0 - * - * @ticket 58517 - * - * @covers WP_HTML_Open_Elements::after_element_push - */ - public function test_after_element_push_must_maintain_p_in_button_scope_flag() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } - - /** - * The check for whether an element is in TABLE scope depends on - * the HTML, TABLE, and TEMPLATE elements. - * - * @since 6.4.0 - * - * @ticket 58517 - * - * @covers WP_HTML_Open_Elements::has_element_in_table_scope - */ - public function test_has_element_in_table_scope_needs_support() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } - - /** - * The check for whether an element is in SELECT scope depends on - * the OPTGROUP and OPTION elements. - * - * @since 6.4.0 - * - * @ticket 58517 - * - * @covers WP_HTML_Open_Elements::has_element_in_select_scope - */ - public function test_has_element_in_select_scope_needs_support() { - // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML. - $this->ensure_support_is_added_everywhere( 'MATH' ); - - /* - * SVG elements: note that TITLE is both an HTML element and an SVG element - * so care must be taken when adding support for either one. - * - * FOREIGNOBJECT, DESC, TITLE. - */ - $this->ensure_support_is_added_everywhere( 'SVG' ); - } -} diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php index fbb2521233679..e8195dcfa28c6 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php @@ -512,6 +512,67 @@ public function test_basic_assertion_abruptly_closed_cdata_section() { ); } + /** + * Ensures that basic CDATA sections inside foreign content are detected. + * + * @ticket 61576 + */ + public function test_basic_cdata_in_foreign_content() { + $processor = new WP_HTML_Tag_Processor( 'this is >&gt; real CDATA' ); + $processor->next_token(); + + // Artificially change namespace; this should be done in the HTML Processor. + $processor->change_parsing_namespace( 'svg' ); + $processor->next_token(); + + $this->assertSame( + '#cdata-section', + $processor->get_token_name(), + "Should have found a CDATA section but found {$processor->get_token_name()} instead." + ); + + $this->assertNull( + $processor->get_tag(), + 'Should not have been able to query tag name on non-element token.' + ); + + $this->assertNull( + $processor->get_attribute( 'type' ), + 'Should not have been able to query attributes on non-element token.' + ); + + $this->assertSame( + 'this is >> real CDATA', + $processor->get_modifiable_text(), + 'Found incorrect modifiable text.' + ); + } + + /** + * Ensures that empty CDATA sections inside foreign content are detected. + * + * @ticket 61576 + */ + public function test_empty_cdata_in_foreign_content() { + $processor = new WP_HTML_Tag_Processor( '' ); + $processor->next_token(); + + // Artificially change namespace; this should be done in the HTML Processor. + $processor->change_parsing_namespace( 'svg' ); + $processor->next_token(); + + $this->assertSame( + '#cdata-section', + $processor->get_token_name(), + "Should have found a CDATA section but found {$processor->get_token_name()} instead." + ); + + $this->assertEmpty( + $processor->get_modifiable_text(), + 'Found non-empty modifiable text.' + ); + } + /** * Ensures that normative Processing Instruction nodes are properly parsed. * From fe9aa7c48d4c72bda6c55273015bd01cd24aa603 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 8 Aug 2024 07:31:15 +0000 Subject: [PATCH 065/106] HTML API: Add support for SVG and MathML (Foreign content) (remove file) As part of work to add more spec support to the HTML API, this patch adds support for SVG and MathML elements, or more generally, "foreign content." The rules in foreign content are a mix of XML and HTML parsing rules and introduce additional complexity into the processor, but is important in order to avoid getting lost when inside these elements. This patch follows the first by deleting the empty files, which were mistakenly left in during the initial merge. Developed in https://github.com/wordpress/wordpress-develop/pull/6006 Discussed in https://core.trac.wordpress.org/ticket/61576 Follow-up to [58867]. Props: dmsnell, jonsurrell, westonruter. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58868 602fd350-edb4-49c9-b593-d223f7449a82 --- .../phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php diff --git a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php deleted file mode 100644 index e69de29bb2d1d..0000000000000 From b9014d69e3b34db9e4ec792fd48651241ae4a4f9 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 8 Aug 2024 16:13:25 +0000 Subject: [PATCH 066/106] HTML API: `expect_closer()` should report false for self-closing foreign elements. Previously, `WP_HTML_Processor::expects_closer()` would report `true` for self-closing foreign elements when called without supplying a node in question, but it should have been reporting `true` just as it does for HTML elements. This patch adds a test case demonstrating the issue and a bugfix. The `html5lib` test runner was relying on the incorrect behavior, accidentally working. This is also corrected and the `html5lib` test now relies on the correct behavior of `expects_closer()`. Developed in https://github.com/wordpress/wordpress-develop/pull/7162 Discussed in https://core.trac.wordpress.org/ticket/61576 Follow-up to [58868]. Props: dmsnell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58870 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 8 +++++--- .../html-api/class-wp-html-tag-processor.php | 2 +- .../phpunit/tests/html-api/wpHtmlProcessor.php | 18 ++++++++++++++++++ .../tests/html-api/wpHtmlProcessorHtml5lib.php | 12 ++---------- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 3820fe027723d..415ff23eea95f 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -786,13 +786,15 @@ public function matches_breadcrumbs( $breadcrumbs ): bool { * or `null` if not matched on any token. */ public function expects_closer( WP_HTML_Token $node = null ): ?bool { - $token_name = $node->node_name ?? $this->get_token_name(); - $token_namespace = $node->namespace ?? $this->get_namespace(); + $token_name = $node->node_name ?? $this->get_token_name(); if ( ! isset( $token_name ) ) { return null; } + $token_namespace = $node->namespace ?? $this->get_namespace(); + $token_has_self_closing = $node->has_self_closing_flag ?? $this->has_self_closing_flag(); + return ! ( // Comments, text nodes, and other atomic tokens. '#' === $token_name[0] || @@ -803,7 +805,7 @@ public function expects_closer( WP_HTML_Token $node = null ): ?bool { // Special atomic elements. ( 'html' === $token_namespace && in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) || // Self-closing elements in foreign content. - ( isset( $node ) && 'html' !== $node->namespace && $node->has_self_closing_flag ) + ( 'html' !== $token_namespace && $token_has_self_closing ) ); } diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index fb21c15d1d96e..95216b08a1988 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -2921,7 +2921,7 @@ public function get_qualified_attribute_name( $attribute_name ): ?string { return null; } - $namespace = $this->get_namespace(); + $namespace = $this->get_namespace(); $lower_name = strtolower( $attribute_name ); if ( 'math' === $namespace && 'definitionurl' === $lower_name ) { diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 68c60a1ff85cc..2b56cefedcd9a 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -503,4 +503,22 @@ public function __construct( $html ) { $subclass_processor = call_user_func( array( get_class( $subclass_instance ), 'create_fragment' ), '' ); $this->assertInstanceOf( get_class( $subclass_instance ), $subclass_processor, '::create_fragment did not return subclass instance.' ); } + + /** + * Ensures that self-closing elements in foreign content properly report + * that they expect no closer. + * + * @ticket 61576 + */ + public function test_expects_closer_foreign_content_self_closing() { + $processor = WP_HTML_Processor::create_fragment( '' ); + + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SVG', $processor->get_tag() ); + $this->assertFalse( $processor->expects_closer() ); + + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'MATH', $processor->get_tag() ); + $this->assertTrue( $processor->expects_closer() ); + } } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php index b6213aac8d0e9..4de4ebd1cd5c4 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php @@ -207,11 +207,7 @@ private static function build_tree_representation( ?string $fragment_context, st $tag_indent = $indent_level; - if ( 'html' !== $namespace ) { - if ( ! $processor->has_self_closing_flag() ) { - ++$indent_level; - } - } elseif ( ! WP_HTML_Processor::is_void( $tag_name ) ) { + if ( $processor->expects_closer() ) { ++$indent_level; } @@ -275,7 +271,7 @@ static function ( $a, $b ) { // Self-contained tags contain their inner contents as modifiable text. $modifiable_text = $processor->get_modifiable_text(); if ( '' !== $modifiable_text ) { - $output .= str_repeat( $indent, $indent_level ) . "\"{$modifiable_text}\"\n"; + $output .= str_repeat( $indent, $tag_indent + 1 ) . "\"{$modifiable_text}\"\n"; } if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) { @@ -283,10 +279,6 @@ static function ( $a, $b ) { ++$indent_level; } - if ( ! $processor->is_void( $tag_name ) && ! $processor->expects_closer() ) { - --$indent_level; - } - break; case '#cdata-section': From 7ceb8394c75d52c9c263b0cf201527fda8935bf8 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 8 Aug 2024 17:02:46 +0000 Subject: [PATCH 067/106] HTML API: Test and fix SVG script handling. When support was added for foreign content, an ambiguity in the HTML specification led to code that followed the wrong path when encountering a self-closing SCRIPT element in the SVG namespace. Further, a fallthrough was discovered during manual testing. This patch adds a new test to assert the proper behaviors and fixes these issues. In the case of the SCRIPT element, the outcome was the same with the wrong code path, making the defect benign. In the case of the fallthrough, the wrong behavior would occur. The updates in this patch also resolves a todo relating to the spec ambiguity. Developed in https://github.com/wordpress/wordpress-develop/pull/7164 Discussed in https://core.trac.wordpress.org/ticket/61576 Follow-up to [58868]. Props: dmsnell, jonsurrell. See #61576. git-svn-id: https://develop.svn.wordpress.org/trunk@58871 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 29 ++++++++++--------- .../tests/html-api/wpHtmlProcessor.php | 10 +++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 415ff23eea95f..2bb6302c99781 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -4239,21 +4239,22 @@ private function step_in_foreign_content(): bool { /* * > If the token has its self-closing flag set, then run * > the appropriate steps from the following list: + * > + * > ↪ the token's tag name is "script", and the new current node is in the SVG namespace + * > Acknowledge the token's self-closing flag, and then act as + * > described in the steps for a "script" end tag below. + * > + * > ↪ Otherwise + * > Pop the current node off the stack of open elements and + * > acknowledge the token's self-closing flag. + * + * Since the rules for SCRIPT below indicate to pop the element off of the stack of + * open elements, which is the same for the Otherwise condition, there's no need to + * separate these checks. The difference comes when a parser operates with the scripting + * flag enabled, and executes the script, which this parser does not support. */ if ( $this->state->current_token->has_self_closing_flag ) { - if ( 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { - /* - * > Acknowledge the token's self-closing flag, and then act as - * > described in the steps for a "script" end tag below. - * - * @todo Verify that this shouldn't be handled by the rule for - * "An end tag whose name is 'script', if the current node - * is an SVG script element." - */ - goto in_foreign_content_any_other_end_tag; - } else { - $this->state->stack_of_open_elements->pop(); - } + $this->state->stack_of_open_elements->pop(); } return true; } @@ -4263,13 +4264,13 @@ private function step_in_foreign_content(): bool { */ if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { $this->state->stack_of_open_elements->pop(); + return true; } /* * > Any other end tag */ if ( $this->is_tag_closer() ) { - in_foreign_content_any_other_end_tag: $node = $this->state->stack_of_open_elements->current_node(); if ( $tag_name !== $node->node_name ) { // @todo Indicate a parse error once it's possible. diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 2b56cefedcd9a..0b7d72bdbee9a 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -521,4 +521,14 @@ public function test_expects_closer_foreign_content_self_closing() { $this->assertSame( 'MATH', $processor->get_tag() ); $this->assertTrue( $processor->expects_closer() ); } + + /** + * Ensures that self-closing foreign SCRIPT elements are properly found. + * + * @ticket 61576 + */ + public function test_foreign_content_script_self_closing() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $this->assertTrue( $processor->next_tag( 'script' ) ); + } } From ec58d38eef3f4387177e4bfca90df5b3a34f96be Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Thu, 8 Aug 2024 19:35:24 +0000 Subject: [PATCH 068/106] External Libraries: Update the Backbone.js library to version `1.6.0`. This updates the `backbone` library from version `1.5.0` to `1.6.0`. This is a minor bug fix release. The full list of changes can be found in the Backbone.js change log: https://backbonejs.org/#changelog. Props manooweb mardroid. Fixes #60512. git-svn-id: https://develop.svn.wordpress.org/trunk@58872 602fd350-edb4-49c9-b593-d223f7449a82 --- package-lock.json | 14 +++++++------- package.json | 2 +- src/wp-includes/script-loader.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5c985d2137f4..af97de8dc6244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "@wordpress/warning": "3.0.1", "@wordpress/widgets": "4.0.6", "@wordpress/wordcount": "4.0.1", - "backbone": "1.5.0", + "backbone": "1.6.0", "clipboard": "2.0.11", "core-js-url-browser": "3.6.4", "element-closest": "^3.0.2", @@ -10067,9 +10067,9 @@ "dev": true }, "node_modules/backbone": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.5.0.tgz", - "integrity": "sha512-RPKlstw5NW+rD2X4PnEnvgLhslRnXOugXw2iBloHkPMgOxvakP1/A+tZIGM3qCm8uvZeEf8zMm0uvcK1JwL+IA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.6.0.tgz", + "integrity": "sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==", "dependencies": { "underscore": ">=1.8.3" } @@ -41569,9 +41569,9 @@ } }, "backbone": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.5.0.tgz", - "integrity": "sha512-RPKlstw5NW+rD2X4PnEnvgLhslRnXOugXw2iBloHkPMgOxvakP1/A+tZIGM3qCm8uvZeEf8zMm0uvcK1JwL+IA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.6.0.tgz", + "integrity": "sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==", "requires": { "underscore": ">=1.8.3" } diff --git a/package.json b/package.json index 7c55cb8bc1278..eedd555935c36 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "@wordpress/warning": "3.0.1", "@wordpress/widgets": "4.0.6", "@wordpress/wordcount": "4.0.1", - "backbone": "1.5.0", + "backbone": "1.6.0", "clipboard": "2.0.11", "core-js-url-browser": "3.6.4", "element-closest": "^3.0.2", diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 24a35a291911e..2852dc2431760 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1040,7 +1040,7 @@ function wp_default_scripts( $scripts ) { did_action( 'init' ) && $scripts->add_data( 'json2', 'conditional', 'lt IE 8' ); $scripts->add( 'underscore', "/wp-includes/js/underscore$dev_suffix.js", array(), '1.13.4', 1 ); - $scripts->add( 'backbone', "/wp-includes/js/backbone$dev_suffix.js", array( 'underscore', 'jquery' ), '1.5.0', 1 ); + $scripts->add( 'backbone', "/wp-includes/js/backbone$dev_suffix.js", array( 'underscore', 'jquery' ), '1.6.0', 1 ); $scripts->add( 'wp-util', "/wp-includes/js/wp-util$suffix.js", array( 'underscore', 'jquery' ), false, 1 ); did_action( 'init' ) && $scripts->localize( From 9f09c574c0ac64041c81760f10646994d21ce49d Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 9 Aug 2024 00:16:33 +0000 Subject: [PATCH 069/106] Docs: Remove unsupported values in `plugins_api()` DocBlocks. The `group` field and the `hot_categories` action were never actually implemented on the WordPress.org side. Follow-up to [34596], [meta3227]. Props lopo, milana_cap. See #55645. git-svn-id: https://develop.svn.wordpress.org/trunk@58873 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/plugin-install.php | 38 +++++++++++------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index 38c4b50e7d049..04287736bc661 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -20,34 +20,33 @@ * * The second filter, {@see 'plugins_api'}, allows a plugin to override the WordPress.org * Plugin Installation API entirely. If `$action` is 'query_plugins' or 'plugin_information', - * an object MUST be passed. If `$action` is 'hot_tags' or 'hot_categories', an array MUST - * be passed. + * an object MUST be passed. If `$action` is 'hot_tags', an array MUST be passed. * * Finally, the third filter, {@see 'plugins_api_result'}, makes it possible to filter the * response object or array, depending on the `$action` type. * * Supported arguments per action: * - * | Argument Name | query_plugins | plugin_information | hot_tags | hot_categories | - * | -------------------- | :-----------: | :----------------: | :------: | :------------: | - * | `$slug` | No | Yes | No | No | - * | `$per_page` | Yes | No | No | No | - * | `$page` | Yes | No | No | No | - * | `$number` | No | No | Yes | Yes | - * | `$search` | Yes | No | No | No | - * | `$tag` | Yes | No | No | No | - * | `$author` | Yes | No | No | No | - * | `$user` | Yes | No | No | No | - * | `$browse` | Yes | No | No | No | - * | `$locale` | Yes | Yes | No | No | - * | `$installed_plugins` | Yes | No | No | No | - * | `$is_ssl` | Yes | Yes | No | No | - * | `$fields` | Yes | Yes | No | No | + * | Argument Name | query_plugins | plugin_information | hot_tags | + * | -------------------- | :-----------: | :----------------: | :------: | + * | `$slug` | No | Yes | No | + * | `$per_page` | Yes | No | No | + * | `$page` | Yes | No | No | + * | `$number` | No | No | Yes | + * | `$search` | Yes | No | No | + * | `$tag` | Yes | No | No | + * | `$author` | Yes | No | No | + * | `$user` | Yes | No | No | + * | `$browse` | Yes | No | No | + * | `$locale` | Yes | Yes | No | + * | `$installed_plugins` | Yes | No | No | + * | `$is_ssl` | Yes | Yes | No | + * | `$fields` | Yes | Yes | No | * * @since 2.7.0 * * @param string $action API action to perform: 'query_plugins', 'plugin_information', - * 'hot_tags' or 'hot_categories'. + * or 'hot_tags'. * @param array|object $args { * Optional. Array or object of arguments to serialize for the Plugin Info API. * @@ -91,7 +90,6 @@ * @type bool $banners Whether to return the banner images links. Default false. * @type bool $icons Whether to return the icon links. Default false. * @type bool $active_installs Whether to return the number of active installations. Default false. - * @type bool $group Whether to return the assigned group. Default false. * @type bool $contributors Whether to return the list of contributors. Default false. * } * } @@ -136,7 +134,7 @@ function plugins_api( $action, $args = array() ) { * Returning a non-false value will effectively short-circuit the WordPress.org API request. * * If `$action` is 'query_plugins' or 'plugin_information', an object MUST be passed. - * If `$action` is 'hot_tags' or 'hot_categories', an array should be passed. + * If `$action` is 'hot_tags', an array should be passed. * * @since 2.7.0 * From defaa761959a500ffb8a37d5dbf06316787a73bf Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Aug 2024 17:59:41 +0000 Subject: [PATCH 070/106] Comments: Add optional `$context` parameter to `get_edit_comment_link()` to get the URL without HTML entities. This brings the function in line with the similar `get_edit_post_link()` parameter. The 'get_edit_comment_link' filter now additionally receives the `$comment_id` and `$context` as parameters. Additionally, as a minor enhancement, the capability check is now more defensive, as it will no longer cause an error if the given comment ID is invalid. As part of the changeset, comprehensive test coverage for the `get_edit_comment_link()` including the new behavior is added. Props deepakrohilla. Fixes #61727. git-svn-id: https://develop.svn.wordpress.org/trunk@58875 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/link-template.php | 24 +++- .../phpunit/tests/link/getEditCommentLink.php | 130 ++++++++++++++++++ 2 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 tests/phpunit/tests/link/getEditCommentLink.php diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 426da57c392fa..4cfc47f83ec05 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -1595,27 +1595,39 @@ function get_delete_post_link( $post = 0, $deprecated = '', $force_delete = fals * Retrieves the edit comment link. * * @since 2.3.0 + * @since 6.7.0 The $context parameter was added. * * @param int|WP_Comment $comment_id Optional. Comment ID or WP_Comment object. - * @return string|void The edit comment link URL for the given comment. + * @param string $context Optional. Context in which the URL should be used. Either 'display', + * to include HTML entities, or 'url'. Default 'display'. + * @return string|void The edit comment link URL for the given comment, or void if the comment id does not exist or + * the current user is not allowed to edit it. */ -function get_edit_comment_link( $comment_id = 0 ) { +function get_edit_comment_link( $comment_id = 0, $context = 'display' ) { $comment = get_comment( $comment_id ); - if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { + if ( ! is_object( $comment ) || ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { return; } - $location = admin_url( 'comment.php?action=editcomment&c=' ) . $comment->comment_ID; + if ( 'display' === $context ) { + $action = 'comment.php?action=editcomment&c='; + } else { + $action = 'comment.php?action=editcomment&c='; + } + + $location = admin_url( $action ) . $comment->comment_ID; /** * Filters the comment edit link. * - * @since 2.3.0 + * @since 6.7.0 The $comment_id and $context parameters are now being passed to the filter. * * @param string $location The edit link. + * @param int $comment_id Optional. Unique ID of the comment to generate an edit link. + * @param int $context Optional. Context to include HTML entities in link. Default 'display'. */ - return apply_filters( 'get_edit_comment_link', $location ); + return apply_filters( 'get_edit_comment_link', $location, $comment_id, $context ); } /** diff --git a/tests/phpunit/tests/link/getEditCommentLink.php b/tests/phpunit/tests/link/getEditCommentLink.php new file mode 100644 index 0000000000000..1d574d40286d1 --- /dev/null +++ b/tests/phpunit/tests/link/getEditCommentLink.php @@ -0,0 +1,130 @@ +comment->create( array( 'comment_content' => 'Test comment' ) ); + + self::$user_ids = array( + 'admin' => $factory->user->create( array( 'role' => 'administrator' ) ), + 'subscriber' => $factory->user->create( array( 'role' => 'subscriber' ) ), + ); + } + + public static function wpTearDownAfterClass() { + // Delete the test comment. + wp_delete_comment( self::$comment_id, true ); + + // Delete the test users. + foreach ( self::$user_ids as $user_id ) { + self::delete_user( $user_id ); + } + } + + public function set_up() { + parent::set_up(); + wp_set_current_user( self::$user_ids['admin'] ); + } + + /** + * Tests that get_edit_comment_link() returns the correct URL by default. + */ + public function test_get_edit_comment_link_default() { + $comment_id = self::$comment_id; + $expected_url = admin_url( 'comment.php?action=editcomment&c=' . $comment_id ); + $actual_url = get_edit_comment_link( $comment_id ); + + $this->assertSame( $expected_url, $actual_url ); + } + + /** + * Tests that get_edit_comment_link() returns the correct URL with a context of 'display'. + * + * The expected result should include HTML entities. + * + * @ticket 61727 + */ + public function test_get_edit_comment_link_display_context() { + $comment_id = self::$comment_id; + $expected_url = admin_url( 'comment.php?action=editcomment&c=' . $comment_id ); + $actual_url = get_edit_comment_link( $comment_id, 'display' ); + + $this->assertSame( $expected_url, $actual_url ); + } + + /** + * Tests that get_edit_comment_link() returns the correct URL with a context of 'url'. + * + * The expected result should not include HTML entities. + * + * @ticket 61727 + */ + public function test_get_edit_comment_link_url_context() { + $comment_id = self::$comment_id; + $expected_url = admin_url( 'comment.php?action=editcomment&c=' . $comment_id ); + $actual_url = get_edit_comment_link( $comment_id, 'url' ); + + $this->assertSame( $expected_url, $actual_url ); + } + + /** + * Tests that get_edit_comment_link() returns nothing if the comment ID is invalid. + * + * @ticket 61727 + */ + public function test_get_edit_comment_link_invalid_comment() { + $comment_id = 12345; + $actual_url_display = get_edit_comment_link( $comment_id, 'display' ); + $actual_url = get_edit_comment_link( $comment_id, 'url' ); + + $this->assertNull( $actual_url_display ); + $this->assertNull( $actual_url ); + } + + /** + * Tests that get_edit_comment_link() returns nothing if the current user cannot edit it. + */ + public function test_get_edit_comment_link_user_cannot_edit() { + wp_set_current_user( self::$user_ids['subscriber'] ); + $comment_id = self::$comment_id; + $actual_url_display = get_edit_comment_link( $comment_id, 'display' ); + $actual_url = get_edit_comment_link( $comment_id, 'url' ); + + $this->assertNull( $actual_url_display ); + $this->assertNull( $actual_url ); + } + + /** + * Tests that the 'get_edit_comment_link' filter works as expected, including the additional parameters. + * + * @ticket 61727 + */ + public function test_get_edit_comment_link_filter() { + $comment_id = self::$comment_id; + $expected_url_display = admin_url( 'comment-test.php?context=display' ); + $expected_url = admin_url( 'comment-test.php?context=url' ); + + add_filter( + 'get_edit_comment_link', + function ( $location, $comment_id, $context ) { + return admin_url( 'comment-test.php?context=' . $context ); + }, + 10, + 3 + ); + + $actual_url_display = get_edit_comment_link( $comment_id, 'display' ); + $actual_url = get_edit_comment_link( $comment_id, 'url' ); + + // Assert the final URLs are as expected + $this->assertSame( $expected_url_display, $actual_url_display ); + $this->assertSame( $expected_url, $actual_url ); + } +} From 433397998a542d236c19de6c08b20b03a628738e Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 9 Aug 2024 18:29:47 +0000 Subject: [PATCH 071/106] Build/Test Tools: Avoid using `wp_delete_user()` in PHPUnit tests unless explicitly acknowledging or ignoring Multisite. `wp_delete_user()` does not actually delete the entire user when using WordPress Multisite. Therefore tests should typically use the test helper method to fully delete the user, unless explicitly ignoring Multisite or testing the `wp_delete_user()` function while taking Multisite behavior into account. Fixes #61851. git-svn-id: https://develop.svn.wordpress.org/trunk@58876 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/admin/includesPlugin.php | 10 +++++----- tests/phpunit/tests/link/getDashboardUrl.php | 6 +----- .../tests/rest-api/rest-sidebars-controller.php | 4 ++-- tests/phpunit/tests/user/queryCache.php | 2 +- tests/phpunit/tests/xmlrpc/wp/getUser.php | 2 +- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 0d29b81d028e0..e95697810d43f 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -96,7 +96,7 @@ public function test_submenu_position( $position, $expected_position ) { wp_set_current_user( $current_user ); // Clean up the temporary user. - wp_delete_user( $admin_user ); + self::delete_user( $admin_user ); // Verify the menu was inserted at the expected position. $this->assertSame( 'custom-position', $submenu[ $parent ][ $expected_position ][2] ); @@ -204,7 +204,7 @@ public function test_submenu_helpers_position( $position, $expected_position ) { } // Clean up the temporary user. - wp_delete_user( $admin_user ); + self::delete_user( $admin_user ); foreach ( $actual_positions as $test => $actual_position ) { // Verify the menu was inserted at the expected position. @@ -295,7 +295,7 @@ public function test_position_when_parent_slug_child_slug_are_the_same() { // Clean up the temporary user. wp_set_current_user( $current_user ); - wp_delete_user( $admin_user ); + self::delete_user( $admin_user ); // Verify the menu was inserted at the expected position. $this->assertSame( 'main_slug', $submenu['main_slug'][0][2] ); @@ -326,7 +326,7 @@ public function test_passing_string_as_position_fires_doing_it_wrong_submenu() { // Clean up the temporary user. wp_set_current_user( $current_user ); - wp_delete_user( $admin_user ); + self::delete_user( $admin_user ); // Verify the menu was inserted at the expected position. $this->assertSame( 'submenu_page_1', $submenu['main_slug'][1][2] ); @@ -355,7 +355,7 @@ public function test_passing_float_as_position_does_not_override_int() { // Clean up the temporary user. wp_set_current_user( $current_user ); - wp_delete_user( $admin_user ); + self::delete_user( $admin_user ); // Verify the menus were inserted. $this->assertSame( 'main_slug_1', $menu[1][2] ); diff --git a/tests/phpunit/tests/link/getDashboardUrl.php b/tests/phpunit/tests/link/getDashboardUrl.php index dafaa7b62e96a..2864db235ffb9 100644 --- a/tests/phpunit/tests/link/getDashboardUrl.php +++ b/tests/phpunit/tests/link/getDashboardUrl.php @@ -12,11 +12,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { } public static function wpTearDownAfterClass() { - if ( is_multisite() ) { - wpmu_delete_user( self::$user_id ); - } else { - wp_delete_user( self::$user_id ); - } + self::delete_user( self::$user_id ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php index 0bddf12df92b9..67a49770dbc86 100644 --- a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php +++ b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php @@ -43,8 +43,8 @@ public static function wpSetUpBeforeClass( $factory ) { } public static function wpTearDownAfterClass() { - wp_delete_user( self::$admin_id ); - wp_delete_user( self::$author_id ); + self::delete_user( self::$admin_id ); + self::delete_user( self::$author_id ); } public function set_up() { diff --git a/tests/phpunit/tests/user/queryCache.php b/tests/phpunit/tests/user/queryCache.php index 985c6d7da9f6d..0e708a55c4de8 100644 --- a/tests/phpunit/tests/user/queryCache.php +++ b/tests/phpunit/tests/user/queryCache.php @@ -388,7 +388,7 @@ public function test_query_cache_delete_user() { $this->assertSameSets( $expected, $found, 'Find author in returned values' ); - wp_delete_user( $user_id ); + self::delete_user( $user_id ); $q2 = new WP_User_Query( array( diff --git a/tests/phpunit/tests/xmlrpc/wp/getUser.php b/tests/phpunit/tests/xmlrpc/wp/getUser.php index dfeb1c8e2e226..391ec7157cee9 100644 --- a/tests/phpunit/tests/xmlrpc/wp/getUser.php +++ b/tests/phpunit/tests/xmlrpc/wp/getUser.php @@ -98,7 +98,7 @@ public function test_valid_user() { $this->assertSame( $user_data['user_login'], $result['username'] ); $this->assertContains( $user_data['role'], $result['roles'] ); - wp_delete_user( $user_id ); + self::delete_user( $user_id ); } public function test_no_fields() { From 29348dfc9a9fbddb4adcef4b8353ef695b8639ce Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Sat, 10 Aug 2024 04:58:16 +0000 Subject: [PATCH 072/106] HTML API: Remove completed TODO comments. This patch removes TODO comments indicating the need to verify certain behaviors and algorithms. Those verifications have taken place and the comments are no longer needed. Developed in https://github.com/wordpress/wordpress-develop/pull/7174 Discussed in https://core.trac.wordpress.org/ticket/61646 Follow-up to [58867], [58870]. Props jonsurrell. See #64646. git-svn-id: https://develop.svn.wordpress.org/trunk@58877 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 2bb6302c99781..6f5da5477f922 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -775,9 +775,6 @@ public function matches_breadcrumbs( $breadcrumbs ): bool { * foreign content will also act just like a void tag, immediately * closing as soon as the processor advances to the next token. * - * @todo Review the self-closing logic when no node is present, ensure it - * matches the expectations in `step()`. - * * @since 6.6.0 * * @param WP_HTML_Token|null $node Optional. Node to examine, if provided. @@ -3317,12 +3314,6 @@ private function step_in_table_body(): bool { case '-TBODY': case '-TFOOT': case '-THEAD': - /* - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as `
`, and a foreign element of - * the same given name. - */ if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); @@ -3453,12 +3444,6 @@ private function step_in_row(): bool { case '-TBODY': case '-TFOOT': case '-THEAD': - /* - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as `
`, and a foreign element of - * the same given name. - */ if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); @@ -3521,12 +3506,6 @@ private function step_in_cell(): bool { */ case '-TD': case '-TH': - /* - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as `
`, and a foreign element of - * the same given name. - */ if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); @@ -3590,12 +3569,6 @@ private function step_in_cell(): bool { case '-TFOOT': case '-THEAD': case '-TR': - /* - * @todo This needs to check if the element in scope is an HTML element, meaning that - * when SVG and MathML support is added, this needs to differentiate between an - * HTML element of the given name, such as `
`, and a foreign element of - * the same given name. - */ if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) { // Parse error: ignore the token. return $this->step(); From e0ee668d0dc17748be036e0583add30b02f0f2f9 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sat, 10 Aug 2024 22:58:07 +0000 Subject: [PATCH 073/106] Code Quality: Clarify variable names in dependency classes. Renames several variables in the `WP_Scripts` and `WP_Styles` classes to clarify their purpose for developers reading the code. Props peterwilsoncc, sergeybiryukov. See #61607. git-svn-id: https://develop.svn.wordpress.org/trunk@58878 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-scripts.php | 84 ++++++++++++++-------------- src/wp-includes/class-wp-styles.php | 24 ++++---- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 7e4bd2732a85f..77dff94c0497a 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -289,12 +289,12 @@ public function do_item( $handle, $group = false ) { $ver = $ver ? $ver . '&' . $this->args[ $handle ] : $this->args[ $handle ]; } - $src = $obj->src; - $strategy = $this->get_eligible_loading_strategy( $handle ); - $intended_strategy = (string) $this->get_data( $handle, 'strategy' ); - $cond_before = ''; - $cond_after = ''; - $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : ''; + $src = $obj->src; + $strategy = $this->get_eligible_loading_strategy( $handle ); + $intended_strategy = (string) $this->get_data( $handle, 'strategy' ); + $ie_conditional_prefix = ''; + $ie_conditional_suffix = ''; + $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : ''; if ( ! $this->is_delayed_strategy( $intended_strategy ) ) { $intended_strategy = ''; @@ -320,15 +320,15 @@ public function do_item( $handle, $group = false ) { } if ( $conditional ) { - $cond_before = "\n"; + $ie_conditional_prefix = "\n"; } $before_script = $this->get_inline_script_tag( $handle, 'before' ); $after_script = $this->get_inline_script_tag( $handle, 'after' ); if ( $before_script || $after_script ) { - $inline_script_tag = $cond_before . $before_script . $after_script . $cond_after; + $inline_script_tag = $ie_conditional_prefix . $before_script . $after_script . $ie_conditional_suffix; } else { $inline_script_tag = ''; } @@ -353,10 +353,10 @@ public function do_item( $handle, $group = false ) { * @param string $src Script loader source path. * @param string $handle Script handle. */ - $srce = apply_filters( 'script_loader_src', $src, $handle ); + $filtered_src = apply_filters( 'script_loader_src', $src, $handle ); if ( - $this->in_default_dir( $srce ) + $this->in_default_dir( $filtered_src ) && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) ) { $this->do_concat = false; @@ -364,7 +364,7 @@ public function do_item( $handle, $group = false ) { // Have to print the so-far concatenated scripts right away to maintain the right order. _print_scripts(); $this->reset(); - } elseif ( $this->in_default_dir( $srce ) && ! $conditional ) { + } elseif ( $this->in_default_dir( $filtered_src ) && ! $conditional ) { $this->print_code .= $this->print_extra_script( $handle, false ); $this->concat .= "$handle,"; $this->concat_version .= "$handle$ver"; @@ -378,13 +378,13 @@ public function do_item( $handle, $group = false ) { $has_conditional_data = $conditional && $this->get_data( $handle, 'data' ); if ( $has_conditional_data ) { - echo $cond_before; + echo $ie_conditional_prefix; } $this->print_extra_script( $handle ); if ( $has_conditional_data ) { - echo $cond_after; + echo $ie_conditional_suffix; } // A single item may alias a set of items, by having dependencies, but no source. @@ -425,9 +425,9 @@ public function do_item( $handle, $group = false ) { if ( $intended_strategy ) { $attr['data-wp-strategy'] = $intended_strategy; } - $tag = $translations . $cond_before . $before_script; + $tag = $translations . $ie_conditional_prefix . $before_script; $tag .= wp_get_script_tag( $attr ); - $tag .= $after_script . $cond_after; + $tag .= $after_script . $ie_conditional_suffix; /** * Filters the HTML script tag of an enqueued script. @@ -626,16 +626,16 @@ public function localize( $handle, $object_name, $l10n ) { */ public function set_group( $handle, $recursion, $group = false ) { if ( isset( $this->registered[ $handle ]->args ) && 1 === $this->registered[ $handle ]->args ) { - $grp = 1; + $calculated_group = 1; } else { - $grp = (int) $this->get_data( $handle, 'group' ); + $calculated_group = (int) $this->get_data( $handle, 'group' ); } - if ( false !== $group && $grp > $group ) { - $grp = $group; + if ( false !== $group && $calculated_group > $group ) { + $calculated_group = $group; } - return parent::set_group( $handle, $recursion, $grp ); + return parent::set_group( $handle, $recursion, $calculated_group ); } /** @@ -723,7 +723,7 @@ public function print_translations( $handle, $display = true ) { * @return bool True on success, false on failure. */ public function all_deps( $handles, $recursion = false, $group = false ) { - $r = parent::all_deps( $handles, $recursion, $group ); + $result = parent::all_deps( $handles, $recursion, $group ); if ( ! $recursion ) { /** * Filters the list of script dependencies left to print. @@ -734,7 +734,7 @@ public function all_deps( $handles, $recursion = false, $group = false ) { */ $this->to_do = apply_filters( 'print_scripts_array', $this->to_do ); } - return $r; + return $result; } /** @@ -889,10 +889,10 @@ private function is_delayed_strategy( $strategy ) { * @return string The best eligible loading strategy. */ private function get_eligible_loading_strategy( $handle ) { - $intended = (string) $this->get_data( $handle, 'strategy' ); + $intended_strategy = (string) $this->get_data( $handle, 'strategy' ); // Bail early if there is no intended strategy. - if ( ! $intended ) { + if ( ! $intended_strategy ) { return ''; } @@ -900,16 +900,16 @@ private function get_eligible_loading_strategy( $handle ) { * If the intended strategy is 'defer', limit the initial list of eligible * strategies, since 'async' can fallback to 'defer', but not vice-versa. */ - $initial = ( 'defer' === $intended ) ? array( 'defer' ) : null; + $initial_strategy = ( 'defer' === $intended_strategy ) ? array( 'defer' ) : null; - $eligible = $this->filter_eligible_strategies( $handle, $initial ); + $eligible_strategies = $this->filter_eligible_strategies( $handle, $initial_strategy ); // Return early once we know the eligible strategy is blocking. - if ( empty( $eligible ) ) { + if ( empty( $eligible_strategies ) ) { return ''; } - return in_array( 'async', $eligible, true ) ? 'async' : 'defer'; + return in_array( 'async', $eligible_strategies, true ) ? 'async' : 'defer'; } /** @@ -917,20 +917,20 @@ private function get_eligible_loading_strategy( $handle ) { * * @since 6.3.0 * - * @param string $handle The script handle. - * @param string[]|null $eligible Optional. The list of strategies to filter. Default null. - * @param array $checked Optional. An array of already checked script handles, used to avoid recursive loops. + * @param string $handle The script handle. + * @param string[]|null $eligible_strategies Optional. The list of strategies to filter. Default null. + * @param array $checked Optional. An array of already checked script handles, used to avoid recursive loops. * @return string[] A list of eligible loading strategies that could be used. */ - private function filter_eligible_strategies( $handle, $eligible = null, $checked = array() ) { + private function filter_eligible_strategies( $handle, $eligible_strategies = null, $checked = array() ) { // If no strategies are being passed, all strategies are eligible. - if ( null === $eligible ) { - $eligible = $this->delayed_strategies; + if ( null === $eligible_strategies ) { + $eligible_strategies = $this->delayed_strategies; } // If this handle was already checked, return early. if ( isset( $checked[ $handle ] ) ) { - return $eligible; + return $eligible_strategies; } // Mark this handle as checked. @@ -938,12 +938,12 @@ private function filter_eligible_strategies( $handle, $eligible = null, $checked // If this handle isn't registered, don't filter anything and return. if ( ! isset( $this->registered[ $handle ] ) ) { - return $eligible; + return $eligible_strategies; } // If the handle is not enqueued, don't filter anything and return. if ( ! $this->query( $handle, 'enqueued' ) ) { - return $eligible; + return $eligible_strategies; } $is_alias = (bool) ! $this->registered[ $handle ]->src; @@ -961,7 +961,7 @@ private function filter_eligible_strategies( $handle, $eligible = null, $checked // If the intended strategy is 'defer', filter out 'async'. if ( 'defer' === $intended_strategy ) { - $eligible = array( 'defer' ); + $eligible_strategies = array( 'defer' ); } $dependents = $this->get_dependents( $handle ); @@ -969,14 +969,14 @@ private function filter_eligible_strategies( $handle, $eligible = null, $checked // Recursively filter eligible strategies for dependents. foreach ( $dependents as $dependent ) { // Bail early once we know the eligible strategy is blocking. - if ( empty( $eligible ) ) { + if ( empty( $eligible_strategies ) ) { return array(); } - $eligible = $this->filter_eligible_strategies( $dependent, $eligible, $checked ); + $eligible_strategies = $this->filter_eligible_strategies( $dependent, $eligible_strategies, $checked ); } - return $eligible; + return $eligible_strategies; } /** diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 76883b54ca98a..e64378be5fc8d 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -165,14 +165,14 @@ public function do_item( $handle, $group = false ) { $ver = $ver ? $ver . '&' . $this->args[ $handle ] : $this->args[ $handle ]; } - $src = $obj->src; - $cond_before = ''; - $cond_after = ''; - $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : ''; + $src = $obj->src; + $ie_conditional_prefix = ''; + $ie_conditional_suffix = ''; + $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : ''; if ( $conditional ) { - $cond_before = "\n"; + $ie_conditional_prefix = "\n"; } $inline_style = $this->print_inline_style( $handle, false ); @@ -279,17 +279,17 @@ public function do_item( $handle, $group = false ) { } if ( $this->do_concat ) { - $this->print_html .= $cond_before; + $this->print_html .= $ie_conditional_prefix; $this->print_html .= $tag; if ( $inline_style_tag ) { $this->print_html .= $inline_style_tag; } - $this->print_html .= $cond_after; + $this->print_html .= $ie_conditional_suffix; } else { - echo $cond_before; + echo $ie_conditional_prefix; echo $tag; $this->print_inline_style( $handle ); - echo $cond_after; + echo $ie_conditional_suffix; } return true; @@ -368,7 +368,7 @@ public function print_inline_style( $handle, $display = true ) { * @return bool True on success, false on failure. */ public function all_deps( $handles, $recursion = false, $group = false ) { - $r = parent::all_deps( $handles, $recursion, $group ); + $result = parent::all_deps( $handles, $recursion, $group ); if ( ! $recursion ) { /** * Filters the array of enqueued styles before processing for output. @@ -379,7 +379,7 @@ public function all_deps( $handles, $recursion = false, $group = false ) { */ $this->to_do = apply_filters( 'print_styles_array', $this->to_do ); } - return $r; + return $result; } /** From 76256bd975ff8ad2b210df68b60cb3d3c818de82 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 10 Aug 2024 23:00:19 +0000 Subject: [PATCH 074/106] Administration: Replace contracted verb forms for better consistency. Follow-up to [14951], [37221], [52979], [52978], [55977]. Props kebbet, sabernhardt. Fixes #58639. git-svn-id: https://develop.svn.wordpress.org/trunk@58879 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/plugins.php | 2 +- src/wp-admin/themes.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php index 7045f16661fe9..783bbe9324bd3 100644 --- a/src/wp-admin/plugins.php +++ b/src/wp-admin/plugins.php @@ -566,7 +566,7 @@ '

' . __( 'The search for installed plugins will search for terms in their name, description, or author.' ) . ' ' . __( 'The search results will be updated as you type.' ) . '

' . '

' . sprintf( /* translators: %s: WordPress Plugin Directory URL. */ - __( 'If you would like to see more plugins to choose from, click on the “Add New Plugin” button and you will be able to browse or search for additional plugins from the WordPress Plugin Directory. Plugins in the WordPress Plugin Directory are designed and developed by third parties, and are compatible with the license WordPress uses. Oh, and they’re free!' ), + __( 'If you would like to see more plugins to choose from, click on the “Add New Plugin” button and you will be able to browse or search for additional plugins from the WordPress Plugin Directory. Plugins in the WordPress Plugin Directory are designed and developed by third parties, and are compatible with the license WordPress uses. Oh, and they are free!' ), __( 'https://wordpress.org/plugins/' ) ) . '

', ) diff --git a/src/wp-admin/themes.php b/src/wp-admin/themes.php index 59d3689e69366..d7ebd5c646c42 100644 --- a/src/wp-admin/themes.php +++ b/src/wp-admin/themes.php @@ -153,7 +153,7 @@ } else { $help_install = '

' . sprintf( /* translators: %s: https://wordpress.org/themes/ */ - __( 'If you would like to see more themes to choose from, click on the “Add New Theme” button and you will be able to browse or search for additional themes from the WordPress Theme Directory. Themes in the WordPress Theme Directory are designed and developed by third parties, and are compatible with the license WordPress uses. Oh, and they’re free!' ), + __( 'If you would like to see more themes to choose from, click on the “Add New Theme” button and you will be able to browse or search for additional themes from the WordPress Theme Directory. Themes in the WordPress Theme Directory are designed and developed by third parties, and are compatible with the license WordPress uses. Oh, and they are free!' ), __( 'https://wordpress.org/themes/' ) ) . '

'; } From faf75b7925b3dfcb7c711bd060fac8a3d2b9cdf2 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sat, 10 Aug 2024 23:55:22 +0000 Subject: [PATCH 075/106] External Libraries: Update the Underscore.js library to version 1.13.7. This updates the Underscore library from version 1.13.6 to 1.13.7. This is a minor bug fix release. The full list of changes can be found in the Underscore.js change log: https://underscorejs.org/#changelog. Props hbhalodia, aristath, desrosj, mcrisp1972. Fixes #61836. git-svn-id: https://develop.svn.wordpress.org/trunk@58880 602fd350-edb4-49c9-b593-d223f7449a82 --- package-lock.json | 14 +++++++------- package.json | 2 +- src/wp-includes/script-loader.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index af97de8dc6244..f392e9064e327 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,7 +97,7 @@ "react-dom": "18.3.1", "react-is": "18.3.1", "regenerator-runtime": "0.14.1", - "underscore": "1.13.6", + "underscore": "1.13.7", "whatwg-fetch": "3.6.20", "wicg-inert": "3.1.2" }, @@ -32728,9 +32728,9 @@ } }, "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" }, "node_modules/underscore.string": { "version": "3.3.5", @@ -58688,9 +58688,9 @@ "dev": true }, "underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" }, "underscore.string": { "version": "3.3.5", diff --git a/package.json b/package.json index eedd555935c36..49d03a9ed3be8 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "react-dom": "18.3.1", "react-is": "18.3.1", "regenerator-runtime": "0.14.1", - "underscore": "1.13.6", + "underscore": "1.13.7", "whatwg-fetch": "3.6.20", "wicg-inert": "3.1.2" }, diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 2852dc2431760..7c570430df441 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1039,7 +1039,7 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'json2', "/wp-includes/js/json2$suffix.js", array(), '2015-05-03' ); did_action( 'init' ) && $scripts->add_data( 'json2', 'conditional', 'lt IE 8' ); - $scripts->add( 'underscore', "/wp-includes/js/underscore$dev_suffix.js", array(), '1.13.4', 1 ); + $scripts->add( 'underscore', "/wp-includes/js/underscore$dev_suffix.js", array(), '1.13.7', 1 ); $scripts->add( 'backbone', "/wp-includes/js/backbone$dev_suffix.js", array( 'underscore', 'jquery' ), '1.6.0', 1 ); $scripts->add( 'wp-util', "/wp-includes/js/wp-util$suffix.js", array( 'underscore', 'jquery' ), false, 1 ); From d80e18cc2d4fe6450ab6b023f2822b575ed5f5b5 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sun, 11 Aug 2024 09:43:14 +0000 Subject: [PATCH 076/106] Twenty Nineteen: Fixes translatable strings with HTML code not appearing. Only the translatable part not HTML, should appear for translators to avoid issues. This resolves one string that was not appearing. This only fixed for one theme although discussion on the ticket was for multiples. Other tickets should be open for those if desireable. Props Presskopp, SergeyBiryukov, pratikkry, pento, mukesh27, laurelfulford, kjellr, desrosj, sabernhardt. Fixes #45473. git-svn-id: https://develop.svn.wordpress.org/trunk@58881 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentynineteen/single.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-content/themes/twentynineteen/single.php b/src/wp-content/themes/twentynineteen/single.php index 14a8ac5b54b8f..2760bc45b71ca 100644 --- a/src/wp-content/themes/twentynineteen/single.php +++ b/src/wp-content/themes/twentynineteen/single.php @@ -27,8 +27,7 @@ // Parent post navigation. the_post_navigation( array( - /* translators: %s: Parent post link. */ - 'prev_text' => sprintf( __( 'Published in%s', 'twentynineteen' ), '%title' ), + 'prev_text' => _x( 'Published in
%title', 'Parent post link', 'twentynineteen' ), ) ); } elseif ( is_singular( 'post' ) ) { From 0cbfd5b22c11384c9f2970c61fe7dda5386d1c5e Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Sun, 11 Aug 2024 14:24:10 +0000 Subject: [PATCH 077/106] Twenty Twelve: Fixes Code block font family difference in editors. The Code block font family was different in the front-end and the editor. This resolves the overruling in the iframe editor. Props pitamdey, sabernhardt. Fixes #61808. git-svn-id: https://develop.svn.wordpress.org/trunk@58882 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentytwelve/css/editor-blocks.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-content/themes/twentytwelve/css/editor-blocks.css b/src/wp-content/themes/twentytwelve/css/editor-blocks.css index 4a1049dae0f7e..85dd813bf2938 100644 --- a/src/wp-content/themes/twentytwelve/css/editor-blocks.css +++ b/src/wp-content/themes/twentytwelve/css/editor-blocks.css @@ -268,6 +268,7 @@ p.has-drop-cap:not(:focus)::first-letter { .wp-block-code { border: 0; + font-family: Consolas, Monaco, Lucida Console, monospace; font-size: 12px; line-height: 2; padding: 0; From 6cfec3b825adfb3c61a11093b1f76fa27a2e3a1f Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 11 Aug 2024 21:08:06 +0000 Subject: [PATCH 078/106] Docs: Switch canonical location for the `comment_row_actions` filter. This aims to bring consistency with the other `*_row_actions` filters. Follow-up to [6705], [8217], [9103], [15491], [26138], [27669]. See #61608. git-svn-id: https://develop.svn.wordpress.org/trunk@58883 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-comments-list-table.php | 11 ++++++++++- src/wp-admin/includes/dashboard.php | 12 +----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/wp-admin/includes/class-wp-comments-list-table.php b/src/wp-admin/includes/class-wp-comments-list-table.php index 6c45a4eb7c22b..9b46512c24092 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -847,7 +847,16 @@ protected function handle_row_actions( $item, $column_name, $primary ) { ); } - /** This filter is documented in wp-admin/includes/dashboard.php */ + /** + * Filters the action links displayed for each comment in the Comments list table. + * + * @since 2.6.0 + * + * @param string[] $actions An array of comment actions. Default actions include: + * 'Approve', 'Unapprove', 'Edit', 'Reply', 'Spam', + * 'Delete', and 'Trash'. + * @param WP_Comment $comment The comment object. + */ $actions = apply_filters( 'comment_row_actions', array_filter( $actions ), $comment ); $always_visible = false; diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index 022a00174c8a0..84514015e22fd 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -800,17 +800,7 @@ function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) { __( 'View' ) ); - /** - * Filters the action links displayed for each comment in the 'Recent Comments' - * dashboard widget. - * - * @since 2.6.0 - * - * @param string[] $actions An array of comment actions. Default actions include: - * 'Approve', 'Unapprove', 'Edit', 'Reply', 'Spam', - * 'Delete', and 'Trash'. - * @param WP_Comment $comment The comment object. - */ + /** This filter is documented in wp-admin/includes/class-wp-comments-list-table.php */ $actions = apply_filters( 'comment_row_actions', array_filter( $actions ), $comment ); $i = 0; From ac5fdb4660caa95d78867c6615de8e329e0e18f1 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 12 Aug 2024 04:52:55 +0000 Subject: [PATCH 079/106] Site Health: Check if directories exist before checking size. Prevents the Site Health Debug tab from stalling when reporting directory sizes if the directory does not exist. Props clorith, aristath, narenin, kowsar89, hellofromTonya, ironprogrammer, shailu25. Fixes #61638. git-svn-id: https://develop.svn.wordpress.org/trunk@58884 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-debug-data.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 1d4ab39aa8692..f626dc4c4aa61 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -1695,6 +1695,18 @@ public static function get_sizes() { 'raw' => 0, ); + // If the directory does not exist, skip checking it, as it will skew the other results. + if ( ! is_dir( $path ) ) { + $all_sizes[ $name ] = array( + 'path' => $path, + 'raw' => 0, + 'size' => __( 'The directory does not exist.' ), + 'debug' => 'directory not found', + ); + + continue; + } + if ( microtime( true ) - WP_START_TIMESTAMP < $max_execution_time ) { if ( 'wordpress_size' === $name ) { $dir_size = recurse_dirsize( $path, $exclude, $max_execution_time ); From ddc5d6aba43b85b072b442d99badb06d84373048 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Mon, 12 Aug 2024 10:54:43 +0000 Subject: [PATCH 080/106] Twenty Twelve: Fixes Button block outline style having wrong text color on front. The Button block has a different text color on the front to the editor when you apply text color. This resolves it without changing other styles. Props pitamdey, ugyensupport, sabernhardt. Fixes #61846. git-svn-id: https://develop.svn.wordpress.org/trunk@58885 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentytwelve/css/blocks.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wp-content/themes/twentytwelve/css/blocks.css b/src/wp-content/themes/twentytwelve/css/blocks.css index ca828eef4aaa2..9dfa0cd6d6f9b 100644 --- a/src/wp-content/themes/twentytwelve/css/blocks.css +++ b/src/wp-content/themes/twentytwelve/css/blocks.css @@ -263,6 +263,11 @@ pre.wp-block-code { color: #7c7c7c; } +.is-style-outline .wp-block-button__link, +.is-style-outline .wp-block-button__link:visited { + color: currentColor; +} + .wp-block-button.is-style-outline .wp-block-button__link, .wp-block-button.is-style-outline .wp-block-button__link:visited { background-color: inherit; From ac7045cedfa00e57516768551727967974c56090 Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Mon, 12 Aug 2024 13:42:09 +0000 Subject: [PATCH 081/106] Twenty Seventeen: Fixes Pullquote block text color not changing on front. The Pullquote block text color was not changing on the front. This overrides the inlined styles. Props iamjaydip, laurelfulford, sabernhardt. Fixes #46080. git-svn-id: https://develop.svn.wordpress.org/trunk@58886 602fd350-edb4-49c9-b593-d223f7449a82 --- .../themes/twentyseventeen/assets/css/editor-blocks.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-content/themes/twentyseventeen/assets/css/editor-blocks.css b/src/wp-content/themes/twentyseventeen/assets/css/editor-blocks.css index 34a4749db7313..1a4902fbbd613 100644 --- a/src/wp-content/themes/twentyseventeen/assets/css/editor-blocks.css +++ b/src/wp-content/themes/twentyseventeen/assets/css/editor-blocks.css @@ -649,6 +649,10 @@ html[lang="th"] .edit-post-visual-editor * { border: 0 solid; } +figure.wp-block-pullquote blockquote { + color: inherit; +} + .wp-block-pullquote.alignleft blockquote > .editor-rich-text p, .wp-block-pullquote.alignright blockquote > .editor-rich-text p { font-size: 20px; From 802e069d05f83db905932eebd87c87a33b37164e Mon Sep 17 00:00:00 2001 From: Tammie Lister Date: Mon, 12 Aug 2024 15:17:27 +0000 Subject: [PATCH 082/106] Twenty Twelve: Fixes navigation block submenus being cut off. The navigation block submenus were being cut off. This is the simpler method proposed in patches. Props poena, sabernhardt. Fixes #61672. git-svn-id: https://develop.svn.wordpress.org/trunk@58887 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-content/themes/twentytwelve/css/blocks.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wp-content/themes/twentytwelve/css/blocks.css b/src/wp-content/themes/twentytwelve/css/blocks.css index 9dfa0cd6d6f9b..ced4f827f8f40 100644 --- a/src/wp-content/themes/twentytwelve/css/blocks.css +++ b/src/wp-content/themes/twentytwelve/css/blocks.css @@ -340,6 +340,12 @@ pre.wp-block-code { margin-bottom: 0; } +/* Navigation */ + +.site-content .wp-block-navigation { + overflow: visible; +} + /*-------------------------------------------------------------- 5.0 Blocks - Widgets --------------------------------------------------------------*/ From 5f8f3789d16e28923772f1675e371baaea3e71ec Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 12 Aug 2024 21:26:27 +0000 Subject: [PATCH 083/106] Coding Standards: Bring some consistency to setting up comment moderation links. Follow-up to [7082], [7175], [9103], [10102], [11749], [12008], [12286], [32516]. Props kebbet. See #61607. git-svn-id: https://develop.svn.wordpress.org/trunk@58888 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-comments-list-table.php | 38 +++++++++---------- src/wp-admin/includes/dashboard.php | 26 +++++++------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/wp-admin/includes/class-wp-comments-list-table.php b/src/wp-admin/includes/class-wp-comments-list-table.php index 9b46512c24092..e7707daba901b 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -705,18 +705,18 @@ protected function handle_row_actions( $item, $column_name, $primary ) { $output = ''; - $del_nonce = esc_html( '_wpnonce=' . wp_create_nonce( "delete-comment_$comment->comment_ID" ) ); - $approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( "approve-comment_$comment->comment_ID" ) ); + $approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'approve-comment_' . $comment->comment_ID ) ); + $del_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'delete-comment_' . $comment->comment_ID ) ); - $url = "comment.php?c=$comment->comment_ID"; + $action_string = 'comment.php?action=%s&c=' . $comment->comment_ID . '&%s'; - $approve_url = esc_url( $url . "&action=approvecomment&$approve_nonce" ); - $unapprove_url = esc_url( $url . "&action=unapprovecomment&$approve_nonce" ); - $spam_url = esc_url( $url . "&action=spamcomment&$del_nonce" ); - $unspam_url = esc_url( $url . "&action=unspamcomment&$del_nonce" ); - $trash_url = esc_url( $url . "&action=trashcomment&$del_nonce" ); - $untrash_url = esc_url( $url . "&action=untrashcomment&$del_nonce" ); - $delete_url = esc_url( $url . "&action=deletecomment&$del_nonce" ); + $approve_url = sprintf( $action_string, 'approvecomment', $approve_nonce ); + $unapprove_url = sprintf( $action_string, 'unapprovecomment', $approve_nonce ); + $spam_url = sprintf( $action_string, 'spamcomment', $del_nonce ); + $unspam_url = sprintf( $action_string, 'unspamcomment', $del_nonce ); + $trash_url = sprintf( $action_string, 'trashcomment', $del_nonce ); + $untrash_url = sprintf( $action_string, 'untrashcomment', $del_nonce ); + $delete_url = sprintf( $action_string, 'deletecomment', $del_nonce ); // Preorder it: Approve | Reply | Quick Edit | Edit | Spam | Trash. $actions = array( @@ -737,7 +737,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { if ( 'approved' === $the_comment_status ) { $actions['unapprove'] = sprintf( '%s', - $unapprove_url, + esc_url( $unapprove_url ), "delete:the-comment-list:comment-{$comment->comment_ID}:e7e7d3:action=dim-comment&new=unapproved", esc_attr__( 'Unapprove this comment' ), __( 'Unapprove' ) @@ -745,7 +745,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { } elseif ( 'unapproved' === $the_comment_status ) { $actions['approve'] = sprintf( '%s', - $approve_url, + esc_url( $approve_url ), "delete:the-comment-list:comment-{$comment->comment_ID}:e7e7d3:action=dim-comment&new=approved", esc_attr__( 'Approve this comment' ), __( 'Approve' ) @@ -754,7 +754,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { } else { $actions['approve'] = sprintf( '%s', - $approve_url, + esc_url( $approve_url ), "dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved", esc_attr__( 'Approve this comment' ), __( 'Approve' ) @@ -762,7 +762,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { $actions['unapprove'] = sprintf( '%s', - $unapprove_url, + esc_url( $unapprove_url ), "dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved", esc_attr__( 'Unapprove this comment' ), __( 'Unapprove' ) @@ -772,7 +772,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { if ( 'spam' !== $the_comment_status ) { $actions['spam'] = sprintf( '%s', - $spam_url, + esc_url( $spam_url ), "delete:the-comment-list:comment-{$comment->comment_ID}::spam=1", esc_attr__( 'Mark this comment as spam' ), /* translators: "Mark as spam" link. */ @@ -781,7 +781,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { } elseif ( 'spam' === $the_comment_status ) { $actions['unspam'] = sprintf( '%s', - $unspam_url, + esc_url( $unspam_url ), "delete:the-comment-list:comment-{$comment->comment_ID}:66cc66:unspam=1", esc_attr__( 'Restore this comment from the spam' ), _x( 'Not Spam', 'comment' ) @@ -791,7 +791,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { if ( 'trash' === $the_comment_status ) { $actions['untrash'] = sprintf( '%s', - $untrash_url, + esc_url( $untrash_url ), "delete:the-comment-list:comment-{$comment->comment_ID}:66cc66:untrash=1", esc_attr__( 'Restore this comment from the Trash' ), __( 'Restore' ) @@ -801,7 +801,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { if ( 'spam' === $the_comment_status || 'trash' === $the_comment_status || ! EMPTY_TRASH_DAYS ) { $actions['delete'] = sprintf( '%s', - $delete_url, + esc_url( $delete_url ), "delete:the-comment-list:comment-{$comment->comment_ID}::delete=1", esc_attr__( 'Delete this comment permanently' ), __( 'Delete Permanently' ) @@ -809,7 +809,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { } else { $actions['trash'] = sprintf( '%s', - $trash_url, + esc_url( $trash_url ), "delete:the-comment-list:comment-{$comment->comment_ID}::trash=1", esc_attr__( 'Move this comment to the Trash' ), _x( 'Trash', 'verb' ) diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index 84514015e22fd..be1ac4224cb24 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -726,18 +726,20 @@ function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) { 'view' => '', ); - $del_nonce = esc_html( '_wpnonce=' . wp_create_nonce( "delete-comment_$comment->comment_ID" ) ); - $approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( "approve-comment_$comment->comment_ID" ) ); + $approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'approve-comment_' . $comment->comment_ID ) ); + $del_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'delete-comment_' . $comment->comment_ID ) ); - $approve_url = esc_url( "comment.php?action=approvecomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$approve_nonce" ); - $unapprove_url = esc_url( "comment.php?action=unapprovecomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$approve_nonce" ); - $spam_url = esc_url( "comment.php?action=spamcomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$del_nonce" ); - $trash_url = esc_url( "comment.php?action=trashcomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$del_nonce" ); - $delete_url = esc_url( "comment.php?action=deletecomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$del_nonce" ); + $action_string = 'comment.php?action=%s&p=' . $comment->comment_post_ID . '&c=' . $comment->comment_ID . '&%s'; + + $approve_url = sprintf( $action_string, 'approvecomment', $approve_nonce ); + $unapprove_url = sprintf( $action_string, 'unapprovecomment', $approve_nonce ); + $spam_url = sprintf( $action_string, 'spamcomment', $del_nonce ); + $trash_url = sprintf( $action_string, 'trashcomment', $del_nonce ); + $delete_url = sprintf( $action_string, 'deletecomment', $del_nonce ); $actions['approve'] = sprintf( '%s', - $approve_url, + esc_url( $approve_url ), "dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved", esc_attr__( 'Approve this comment' ), __( 'Approve' ) @@ -745,7 +747,7 @@ function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) { $actions['unapprove'] = sprintf( '%s', - $unapprove_url, + esc_url( $unapprove_url ), "dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved", esc_attr__( 'Unapprove this comment' ), __( 'Unapprove' ) @@ -768,7 +770,7 @@ function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) { $actions['spam'] = sprintf( '%s', - $spam_url, + esc_url( $spam_url ), "delete:the-comment-list:comment-{$comment->comment_ID}::spam=1", esc_attr__( 'Mark this comment as spam' ), /* translators: "Mark as spam" link. */ @@ -778,7 +780,7 @@ function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) { if ( ! EMPTY_TRASH_DAYS ) { $actions['delete'] = sprintf( '%s', - $delete_url, + esc_url( $delete_url ), "delete:the-comment-list:comment-{$comment->comment_ID}::trash=1", esc_attr__( 'Delete this comment permanently' ), __( 'Delete Permanently' ) @@ -786,7 +788,7 @@ function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) { } else { $actions['trash'] = sprintf( '%s', - $trash_url, + esc_url( $trash_url ), "delete:the-comment-list:comment-{$comment->comment_ID}::trash=1", esc_attr__( 'Move this comment to the Trash' ), _x( 'Trash', 'verb' ) From 3476790c2ed47c5d98347955b8e32f4dba708f5b Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 13 Aug 2024 16:27:32 +0000 Subject: [PATCH 084/106] Coding Standards: Replace an empty `foreach` loop in `wp_replace_in_html_tags()`. This aims to clarify the intention of the code and improve readability. Follow-up to [33359]. Props jrf, TobiasBg, mi5t4n, dhruvang21, mayura8991, nadimcse, Presskopp, SergeyBiryukov. Fixes #61860. git-svn-id: https://develop.svn.wordpress.org/trunk@58889 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/formatting.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index d170728e015b5..e618536f41a20 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -762,8 +762,8 @@ function wp_replace_in_html_tags( $haystack, $replace_pairs ) { // Optimize when searching for one item. if ( 1 === count( $replace_pairs ) ) { // Extract $needle and $replace. - foreach ( $replace_pairs as $needle => $replace ) { - } + $needle = array_key_first( $replace_pairs ); + $replace = $replace_pairs[ $needle ]; // Loop through delimiters (elements) only. for ( $i = 1, $c = count( $textarr ); $i < $c; $i += 2 ) { From fa417fc14a8b2e7504d09b4b45f91d956367ef1f Mon Sep 17 00:00:00 2001 From: Tonya Mork Date: Tue, 13 Aug 2024 17:43:18 +0000 Subject: [PATCH 085/106] Editor: Fix bumped specificity for layout styles in non-iframed editor. Fixes a regression introduced in [58241] which inadvertently bumped the specificity in a non-iframed editor for `.editor-styles-wrapper .is-layout-flow > *` from (0,1,0) to (0,2,0). This fix restores theme.json spacing rules taking precedence over the implicit spacing rules in a non-iframed editor. **The What** When the block editor is not iframed (which can happen when Custom Fields are active, or blocks that use and older `apiVersion` are present), style rules are processed using post css to append the `.editor-styles-wrapper` class name. This has the effect of scoping the the style rules to ensure they don't affect the editor chrome or admin. With [58241], one of the rules was changed to `.is-layout-flow > *`. In a iframed editor, the specificity of this rule is okay (0,1,0), but in a non-iframed editor it becomes `.editor-styles-wrapper .is-layout-flow > *`, a specificity of (0,2,0). Comparing this to before [58241], the same rule was `.editor-styles-wrapper :where(body .is-layout-flow) > *` (specificity 0,1,0). This is a regression in specificity that has caused some issues. Notably themes can no longer properly override the spacing for blocks using theme.json and have the results correctly shown in the non-iframed editor. **The How** This changeset modifies the selector to `:root :where(.is-layout-flow) > *` (still specificity 0,1,0). `transformStyles` handles 'root' selectors a little differently, it'll instead replace the `:root` part so it becomes `.editor-styles-wrapper where(.is-layout-flow) > *` (keeping the specificity at 0,1,0). The other layout selector that this affects is the `:first-child` `:last-child` selectors that are responsible for resetting margin at the start and end of a block list. They traditionally have a 0,2,0 specificity so that they can override both the above rule and any rules in the theme.json. Those selectors are also maintained at 0,2,0 with this change, they become something like `:root :where(.is-layout-flow) > :first-child`. **References:** * PHP changes from [https://github.com/WordPress/gutenberg/pull/64076 Gutenberg PR 64076]. Follow-up to [58241], [58228], [55956], [54162]. Props talldanwp, aaronrobertshaw, andrewserong, markhowellsmead, ramonopoly, hellofromTonya. Fixes #61829. git-svn-id: https://develop.svn.wordpress.org/trunk@58890 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-theme-json.php | 2 +- tests/phpunit/tests/theme/wpThemeJson.php | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 894ce25a1cf3a..f246a7ed7ad28 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1692,7 +1692,7 @@ protected function get_layout_styles( $block_metadata, $types = array() ) { $spacing_rule['selector'] ); } else { - $format = static::ROOT_BLOCK_SELECTOR === $selector ? '.%2$s %3$s' : '%1$s-%2$s %3$s'; + $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s'; $layout_selector = sprintf( $format, $selector, diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 8ca4393f0daa2..59630868316b2 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -684,6 +684,7 @@ public function test_get_stylesheet_skips_disabled_protected_properties() { * @ticket 58550 * @ticket 60936 * @ticket 61165 + * @ticket 61829 */ public function test_get_stylesheet_renders_enabled_protected_properties() { $theme_json = new WP_Theme_JSON( @@ -702,7 +703,7 @@ public function test_get_stylesheet_renders_enabled_protected_properties() { ) ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-flex {gap: 1em;}.is-layout-grid {gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSame( $expected, $theme_json->get_stylesheet() ); $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1077,6 +1078,7 @@ public function test_get_stylesheet_handles_priority_of_elements_vs_block_elemen * @ticket 58550 * @ticket 60936 * @ticket 61165 + * @ticket 61829 */ public function test_get_stylesheet_generates_layout_styles() { $theme_json = new WP_Theme_JSON( @@ -1102,7 +1104,7 @@ public function test_get_stylesheet_generates_layout_styles() { // Results also include root site blocks styles. $this->assertSame( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-flex {gap: 1em;}.is-layout-grid {gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1113,6 +1115,7 @@ public function test_get_stylesheet_generates_layout_styles() { * @ticket 58550 * @ticket 60936 * @ticket 61165 + * @ticket 61829 */ public function test_get_stylesheet_generates_layout_styles_with_spacing_presets() { $theme_json = new WP_Theme_JSON( @@ -1138,7 +1141,7 @@ public function test_get_stylesheet_generates_layout_styles_with_spacing_presets // Results also include root site blocks styles. $this->assertSame( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--60); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: var(--wp--preset--spacing--60); }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}.is-layout-flex {gap: var(--wp--preset--spacing--60);}.is-layout-grid {gap: var(--wp--preset--spacing--60);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--60); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: var(--wp--preset--spacing--60); }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}:root :where(.is-layout-flex){gap: var(--wp--preset--spacing--60);}:root :where(.is-layout-grid){gap: var(--wp--preset--spacing--60);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1239,6 +1242,7 @@ public function test_get_stylesheet_skips_layout_styles() { * @ticket 58550 * @ticket 60936 * @ticket 61165 + * @ticket 61829 */ public function test_get_stylesheet_generates_valid_block_gap_values_and_skips_null_or_false_values() { $theme_json = new WP_Theme_JSON( @@ -1285,7 +1289,7 @@ public function test_get_stylesheet_generates_valid_block_gap_values_and_skips_n ); $this->assertSame( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1rem;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1rem;margin-block-end: 0;}.is-layout-flex {gap: 1rem;}.is-layout-grid {gap: 1rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:root :where(.wp-block-post-content){color: gray;}.wp-block-social-links-is-layout-flow > :first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-flow > :last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > :first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-constrained > :last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-flex {gap: 0;}.wp-block-social-links-is-layout-grid {gap: 0;}.wp-block-buttons-is-layout-flow > :first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-flow > :last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > :first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-constrained > :last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-flex {gap: 0;}.wp-block-buttons-is-layout-grid {gap: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1rem;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1rem;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1rem;}:root :where(.is-layout-grid){gap: 1rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:root :where(.wp-block-post-content){color: gray;}:root :where(.wp-block-social-links-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flow) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flex){gap: 0;}:root :where(.wp-block-social-links-is-layout-grid){gap: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flow) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flex){gap: 0;}:root :where(.wp-block-buttons-is-layout-grid){gap: 0;}', $theme_json->get_stylesheet() ); } @@ -3833,6 +3837,7 @@ public function test_get_styles_with_content_width() { * @ticket 58550 * @ticket 60936 * @ticket 61165 + * @ticket 61829 */ public function test_get_styles_with_appearance_tools() { $theme_json = new WP_Theme_JSON( @@ -3849,7 +3854,7 @@ public function test_get_styles_with_appearance_tools() { 'selector' => 'body', ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1;margin-block-end: 0;}.is-layout-flex {gap: 1;}.is-layout-grid {gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1;}:root :where(.is-layout-grid){gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSame( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ) ); } From 7bf49470297fb9b2e1c24c0e8bcbac18c7e6acaa Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 13 Aug 2024 21:42:07 +0000 Subject: [PATCH 086/106] HTML API: Remove unnecessary skips in tests for unsupported markup. The HTML API allowed tests to be skipped for unsupported HTML, for example tags that had not yet been implemented. This provided stability to the test suite while primary support was being added. In many places, the tags are now fully supported and the test skips can be removed. Developed in https://github.com/wordpress/wordpress-develop/pull/7186 Discussed in https://core.trac.wordpress.org/ticket/61646 Props jonsurrell. See #61646. git-svn-id: https://develop.svn.wordpress.org/trunk@58892 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/html-api/wpHtmlProcessor.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 0b7d72bdbee9a..ebc41aef9b5ef 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -155,10 +155,6 @@ public function test_cannot_nest_void_tags( $tag_name ) { $found_tag = $processor->next_tag(); - if ( WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error() ) { - $this->markTestSkipped( "Tag {$tag_name} is not supported." ); - } - $this->assertTrue( $found_tag, "Could not find first {$tag_name}." @@ -220,10 +216,6 @@ public function test_expects_closer_expects_no_closer_for_self_contained_tokens( $processor = WP_HTML_Processor::create_fragment( $self_contained_token ); $found_token = $processor->next_token(); - if ( WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error() ) { - $this->markTestSkipped( "HTML '{$self_contained_token}' is not supported." ); - } - $this->assertTrue( $found_token, "Failed to find any tokens in '{$self_contained_token}': check test data provider." @@ -305,10 +297,6 @@ public function test_cannot_nest_void_tags_next_token( $tag_name ) { $found_tag = $processor->next_token(); - if ( WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error() ) { - $this->markTestSkipped( "Tag {$tag_name} is not supported." ); - } - $this->assertTrue( $found_tag, "Could not find first {$tag_name}." From c20224beaec66dd302616391df1890eba2c697f6 Mon Sep 17 00:00:00 2001 From: dmsnell Date: Tue, 13 Aug 2024 22:12:01 +0000 Subject: [PATCH 087/106] HTML API: Only stop on full matches for requested tag name. An optimization pass on the HTML API left a bug in the `matches()` method, whereby it would falsely detect a tag name match if the found tag were a lexical subset of the requested tag. This occurred because of the use of `substr_compare()` without checking that the outer lengths matched. This patch resolves the bug by adding the length check. Developed in https://github.com/wordpress/wordpress-develop/pull/7189 Discussed in https://core.trac.wordpress.org/ticket/61545 Follow-up to [58613]. Props dmsnell, westonruter. See #61545. git-svn-id: https://develop.svn.wordpress.org/trunk@58893 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-tag-processor.php | 8 +++++- .../tests/html-api/wpHtmlTagProcessor.php | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 95216b08a1988..11a0daa4b26f4 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -4009,7 +4009,13 @@ private function matches(): bool { } // Does the tag name match the requested tag name in a case-insensitive manner? - if ( isset( $this->sought_tag_name ) && 0 !== substr_compare( $this->html, $this->sought_tag_name, $this->tag_name_starts_at, $this->tag_name_length, true ) ) { + if ( + isset( $this->sought_tag_name ) && + ( + strlen( $this->sought_tag_name ) !== $this->tag_name_length || + 0 !== substr_compare( $this->html, $this->sought_tag_name, $this->tag_name_starts_at, $this->tag_name_length, true ) + ) + ) { return false; } diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index b9c6817988032..908e286a8fb21 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -601,6 +601,31 @@ public function test_next_tag_should_return_false_for_a_non_existing_tag() { $this->assertFalse( $processor->next_tag( 'p' ), 'Querying a non-existing tag did not return false' ); } + /** + * @ticket 61545 + */ + public function test_next_tag_should_not_match_on_substrings_of_a_requested_tag() { + $processor = new WP_HTML_Tag_Processor( '

' ); + + $this->assertTrue( + $processor->next_tag( 'PICTURE' ), + 'Failed to find a tag when requested: check test setup.' + ); + + $this->assertSame( + 'PICTURE', + $processor->get_tag(), + 'Should have skipped past substring tag matches, directly finding the PICTURE element.' + ); + + $processor = new WP_HTML_Tag_Processor( '

' ); + + $this->assertFalse( + $processor->next_tag( 'PICTURE' ), + "Should not have found any PICTURE element, but found '{$processor->get_token_name()}' instead." + ); + } + /** * @ticket 59209 * From 2061a52da3d9d6d3994b0a60b2df07037d95e3d8 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 13 Aug 2024 23:35:51 +0000 Subject: [PATCH 088/106] Bulk/Quick Edit: Remove duplicate HTML IDs from post list tables. Removes duplicate IDs on the post list admin pages affecting various list items, selects and checkboxes: * JavaScript duplication of the inline editing HTML for bulk editing renames various IDs to include the prefix `bulk-edit-`, * IDs in the Category Checkbox Walker make use of `wp_unique_prefixed_id()` to avoid duplicates, resulting in a numeric suffix, and, * the post parent dropdown for the bulk editor is given a custom ID `bulk_edit_post_parent`. Props peterwilsoncc, sergeybiryukov, azaozz, joedolson, siliconforks, zodiac1978, rcreators. Fixes #61014. git-svn-id: https://develop.svn.wordpress.org/trunk@58894 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/admin/inline-edit-post.js | 10 +++++++++- src/wp-admin/css/list-tables.css | 2 +- .../includes/class-walker-category-checklist.php | 10 ++++++---- src/wp-admin/includes/class-wp-posts-list-table.php | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/js/_enqueues/admin/inline-edit-post.js b/src/js/_enqueues/admin/inline-edit-post.js index 65cd342c284b2..9d6f66c3b0034 100644 --- a/src/js/_enqueues/admin/inline-edit-post.js +++ b/src/js/_enqueues/admin/inline-edit-post.js @@ -128,8 +128,16 @@ window.wp = window.wp || {}; inlineEditPost.edit( this ); }); + // Clone quick edit categories for the bulk editor. + var beCategories = $( '#inline-edit fieldset.inline-edit-categories' ).clone(); + + // Make "id" attributes globally unique. + beCategories.find( '*[id]' ).each( function() { + this.id = 'bulk-edit-' + this.id; + }); + $('#bulk-edit').find('fieldset:first').after( - $('#inline-edit fieldset.inline-edit-categories').clone() + beCategories ).siblings( 'fieldset:last' ).prepend( $( '#inline-edit .inline-edit-tags-wrap' ).clone() ); diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index de36190b51e4c..e06793ea6b12b 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -1131,7 +1131,7 @@ tr.inline-edit-row td { width: 75%; } -.inline-edit-row #post_parent, +.inline-edit-row select[name="post_parent"], .inline-edit-row select[name="page_template"] { max-width: 80%; } diff --git a/src/wp-admin/includes/class-walker-category-checklist.php b/src/wp-admin/includes/class-walker-category-checklist.php index 1deb3f9206932..7960d06114c3a 100644 --- a/src/wp-admin/includes/class-walker-category-checklist.php +++ b/src/wp-admin/includes/class-walker-category-checklist.php @@ -107,11 +107,13 @@ public function start_el( &$output, $data_object, $depth = 0, $args = array(), $ /** This filter is documented in wp-includes/category-template.php */ esc_html( apply_filters( 'the_category', $category->name, '', '' ) ) . ''; } else { - $is_selected = in_array( $category->term_id, $args['selected_cats'], true ); - $is_disabled = ! empty( $args['disabled'] ); + $is_selected = in_array( $category->term_id, $args['selected_cats'], true ); + $is_disabled = ! empty( $args['disabled'] ); + $li_element_id = wp_unique_prefixed_id( "in-{$taxonomy}-{$category->term_id}-" ); + $checkbox_element_id = wp_unique_prefixed_id( "in-{$taxonomy}-{$category->term_id}-" ); - $output .= "\n

  • " . - '