From 86ceeead2740c7df9fd55af91728ed80deb08826 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Mon, 24 Aug 2026 16:49:58 +0530 Subject: [PATCH 01/68] feat(styles): filter inline style attributes --- src/wp-includes/class-wp-styles.php | 33 ++-- src/wp-includes/script-loader.php | 79 +++++++++- tests/phpunit/tests/dependencies/styles.php | 130 +++++++++++++++ .../tests/dependencies/wpInlineStyleTag.php | 149 ++++++++++++++++++ 4 files changed, 367 insertions(+), 24 deletions(-) create mode 100644 tests/phpunit/tests/dependencies/wpInlineStyleTag.php diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 20487ca9b7068..8cd9672c96fdd 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -157,16 +157,6 @@ public function do_item( $handle, $group = false ) { $src = $obj->src; $inline_style = $this->print_inline_style( $handle, false ); - if ( $inline_style ) { - $processor = new WP_HTML_Tag_Processor( '' ); - $processor->next_tag(); - $processor->set_attribute( 'id', "{$handle}-inline-css" ); - $processor->set_modifiable_text( "\n{$inline_style}\n" ); - $inline_style_tag = "{$processor->get_updated_html()}\n"; - } else { - $inline_style_tag = ''; - } - if ( $this->do_concat ) { if ( is_string( $src ) && $this->in_default_dir( $src ) && ! isset( $obj->extra['alt'] ) ) { $this->concat .= "$handle,"; @@ -182,7 +172,12 @@ public function do_item( $handle, $group = false ) { // A single item may alias a set of items, by having dependencies, but no source. if ( ! $src ) { - if ( $inline_style_tag ) { + if ( $inline_style ) { + $inline_style_tag = wp_get_inline_style_tag( + $inline_style, + array( 'id' => "{$handle}-inline-css" ) + ); + if ( $this->do_concat ) { $this->print_html .= $inline_style_tag; } else { @@ -253,8 +248,11 @@ public function do_item( $handle, $group = false ) { if ( $this->do_concat ) { $this->print_html .= $tag; - if ( $inline_style_tag ) { - $this->print_html .= $inline_style_tag; + if ( $inline_style ) { + $this->print_html .= wp_get_inline_style_tag( + $inline_style, + array( 'id' => "{$handle}-inline-css" ) + ); } } else { echo $tag; @@ -332,11 +330,10 @@ public function print_inline_style( $handle, $display = true ) { return $output; } - $processor = new WP_HTML_Tag_Processor( '' ); - $processor->next_tag(); - $processor->set_attribute( 'id', "{$handle}-inline-css" ); - $processor->set_modifiable_text( "\n{$output}\n" ); - echo "{$processor->get_updated_html()}\n"; + wp_print_inline_style_tag( + $output, + array( 'id' => "{$handle}-inline-css" ) + ); return true; } diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 7d5ba24e5617d..9a4b105ff3048 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2445,12 +2445,9 @@ function _print_styles() { echo "\n"; if ( ! empty( $wp_styles->print_code ) ) { - $processor = new WP_HTML_Tag_Processor( '' ); - $processor->next_tag(); - $style_tag_contents = "\n{$wp_styles->print_code}\n" - . sprintf( "/*# sourceURL=%s */\n", rawurlencode( $concat_source_url ) ); - $processor->set_modifiable_text( $style_tag_contents ); - echo "{$processor->get_updated_html()}\n"; + $style_tag_contents = $wp_styles->print_code . "\n" + . sprintf( '/*# sourceURL=%s */', rawurlencode( $concat_source_url ) ); + wp_print_inline_style_tag( $style_tag_contents ); } } @@ -3080,6 +3077,76 @@ function wp_print_inline_script_tag( $data, $attributes = array() ) { echo wp_get_inline_script_tag( $data, $attributes ); } +/** + * Constructs an inline style tag. + * + * It is possible to inject attributes in the `' ); + $processor->next_tag(); + foreach ( $attributes as $name => $value ) { + /* + * Lexical variations of an attribute name may represent the + * same attribute in HTML, therefore it is possible that the + * input array might contain duplicate attributes even though + * it is keyed on their name. Calling code should rewrite an + * attribute's value rather than sending a duplicate attribute. + * + * Example: + * + * array( 'id' => 'main', 'ID' => 'nav' ) + * + * In this example, there are two keys both describing the `id` + * attribute. PHP array iteration is in key-insertion order so + * the 'id' value will be set in the STYLE tag. + */ + if ( null !== $processor->get_attribute( $name ) ) { + continue; + } + + $processor->set_attribute( $name, $value ?? true ); + } + + $processor->set_modifiable_text( $data ); + + return "{$processor->get_updated_html()}\n"; +} + +/** + * Prints an inline style tag. + * + * It is possible to inject attributes in the `\n", + wp_get_inline_style_tag( + $this->css, + array( + 'id' => 'test-inline-css', + 'nonce' => 'test-nonce', + ) + ) + ); + } + + /** + * @ticket 51325 + */ + public function test_print_inline_style_tag_prints_get_inline_style_tag() { + $attributes = array( 'id' => 'test-inline-css' ); + + $this->assertSame( + wp_get_inline_style_tag( $this->css, $attributes ), + get_echo( + 'wp_print_inline_style_tag', + array( $this->css, $attributes ) + ) + ); + } + + /** + * @ticket 51325 + */ + public function test_inline_style_attributes_filter() { + $filtered_data = null; + + add_filter( + 'wp_inline_style_attributes', + static function ( $attributes, $data ) use ( &$filtered_data ) { + $attributes['nonce'] = 'test-nonce'; + $filtered_data = $data; + return $attributes; + }, + 10, + 2 + ); + + $this->assertSame( + "\n", + wp_get_inline_style_tag( $this->css ) + ); + $this->assertSame( "\n{$this->css}\n", $filtered_data ); + } + + /** + * Test the behavior of generated style tag attributes passed different values and types of values. + * + * @ticket 51325 + */ + public function test_inline_style_tag_attribute_value_types() { + $expected = <<<'HTML' + + +HTML; + + $this->assertEqualHTML( + $expected, + wp_get_inline_style_tag( + $this->css, + array( + 'true' => true, + 'false' => false, + 'null' => null, + 'empty-string' => '', + '0-string' => '0', + '1-string' => '1', + '0-numeric' => 0, + '1-numeric' => 1, + ) + ) + ); + } + + /** + * Test the behavior of generated style tag repeated attributes. + * + * HTML will ignore case-insensitive repeated attributes. Ensure that the handling of input + * attributes aligns with expectations. + * + * @ticket 51325 + */ + public function test_inline_style_tag_repeat_attributes() { + $expected = <<<'HTML' + + +HTML; + + $this->assertEqualHTML( + $expected, + wp_get_inline_style_tag( + $this->css, + array( + 'test' => 'test-a', + 'tesT' => 'tesT-b', + 'teST' => 'teST-c', + 'tEST' => 'tEST-d', + 'TEST' => 'TEST-e', + ) + ) + ); + } + + /** + * @ticket 51325 + */ + public function test_inline_style_tag_escapes_closing_style_tag() { + $this->assertSame( + "\n", + wp_get_inline_style_tag( 'body::after { content: ""; }' ) + ); + } +} From 1eb9b67d1e9da9ff5fca9c34dfc03194464977b4 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Wed, 7 Oct 2026 14:36:45 +0530 Subject: [PATCH 02/68] refactor: tighten inline style API types --- src/wp-includes/script-loader.php | 15 ++++++--- .../tests/dependencies/wpInlineStyleTag.php | 32 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 2ab1bf660a067..e189892431cf8 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3668,10 +3668,13 @@ function wp_print_inline_script_tag( $data, $attributes = array() ) { * @since 7.2.0 * * @param string $data CSS for the style tag. - * @param array $attributes Optional. Key-value pairs representing ` + -HTML; + HTML; $this->assertEqualHTML( $expected, @@ -116,11 +116,11 @@ public function test_inline_style_tag_attribute_value_types() { */ public function test_inline_style_tag_repeat_attributes() { $expected = <<<'HTML' - + -HTML; + HTML; $this->assertEqualHTML( $expected, From b3c0fa57c4c56e1b92cb05ddb689ad8c5da6375b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 10 Oct 2026 11:13:35 -0700 Subject: [PATCH 03/68] Address PHPStan errors in tests --- tests/phpunit/tests/dependencies/styles.php | 82 ++++++++----------- .../tests/dependencies/wpInlineStyleTag.php | 33 +++++--- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index 890b8453e0c00..c37a979144c47 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -9,23 +9,19 @@ * @covers ::wp_add_inline_style */ class Tests_Dependencies_Styles extends WP_UnitTestCase { - private $old_wp_styles; - private $old_wp_scripts; + private ?WP_Styles $old_wp_styles; + private ?WP_Scripts $old_wp_scripts; + /** + * @global WP_Scripts|null $wp_scripts + * @global WP_Styles|null $wp_styles + */ public function set_up() { + global $wp_scripts, $wp_styles; parent::set_up(); - if ( empty( $GLOBALS['wp_styles'] ) ) { - $GLOBALS['wp_styles'] = null; - } - - $this->old_wp_styles = $GLOBALS['wp_styles']; - - if ( empty( $GLOBALS['wp_scripts'] ) ) { - $GLOBALS['wp_scripts'] = null; - } - - $this->old_wp_styles = $GLOBALS['wp_scripts']; + $this->old_wp_styles = $wp_styles; + $this->old_wp_scripts = $wp_scripts; remove_action( 'wp_default_styles', 'wp_default_styles' ); remove_action( 'wp_print_styles', 'print_emoji_styles' ); @@ -37,9 +33,14 @@ public function set_up() { $GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' ); } + /** + * @global WP_Scripts|null $wp_scripts + * @global WP_Styles|null $wp_styles + */ public function tear_down() { - $GLOBALS['wp_styles'] = $this->old_wp_styles; - $GLOBALS['wp_scripts'] = $this->old_wp_scripts; + global $wp_scripts, $wp_styles; + $wp_styles = $this->old_wp_styles; + $wp_scripts = $this->old_wp_scripts; add_action( 'wp_default_styles', 'wp_default_styles' ); add_action( 'wp_print_styles', 'print_emoji_styles' ); @@ -231,23 +232,23 @@ public function test_inline_styles_concat() { * @ticket 51325 */ public function test_inline_style_attributes_filter() { - $filter_calls = 0; + $mock_action = new MockAction(); + add_filter( 'wp_inline_style_attributes', array( $mock_action, 'filter' ) ); add_filter( 'wp_inline_style_attributes', - static function ( $attributes ) use ( &$filter_calls ) { - ++$filter_calls; + static function ( array $attributes ) { $attributes['nonce'] = 'test-nonce'; return $attributes; } ); - wp_enqueue_style( 'handle', 'http://example.com', array(), 1 ); + wp_enqueue_style( 'handle', 'http://example.com', array(), '1' ); wp_add_inline_style( 'handle', 'body { color: #123456; }' ); $printed = get_echo( 'wp_print_styles' ); $this->assertStringContainsString( '\n", + "\n", wp_get_inline_style_tag( - $this->css, + $this->get_test_css(), array( 'id' => 'test-inline-css', 'nonce' => 'test-nonce', @@ -35,10 +33,10 @@ public function test_print_inline_style_tag_prints_get_inline_style_tag() { $attributes = array( 'id' => 'test-inline-css' ); $this->assertSame( - wp_get_inline_style_tag( $this->css, $attributes ), + wp_get_inline_style_tag( $this->get_test_css(), $attributes ), get_echo( 'wp_print_inline_style_tag', - array( $this->css, $attributes ) + array( $this->get_test_css(), $attributes ) ) ); } @@ -51,7 +49,7 @@ public function test_inline_style_attributes_filter() { add_filter( 'wp_inline_style_attributes', - static function ( $attributes, $data ) use ( &$filtered_data ) { + static function ( array $attributes, string $data ) use ( &$filtered_data ) { $attributes['nonce'] = 'test-nonce'; $filtered_data = $data; return $attributes; @@ -61,10 +59,10 @@ static function ( $attributes, $data ) use ( &$filtered_data ) { ); $this->assertSame( - "\n", - wp_get_inline_style_tag( $this->css ) + "\n", + wp_get_inline_style_tag( $this->get_test_css() ) ); - $this->assertSame( "\n{$this->css}\n", $filtered_data ); + $this->assertSame( "\n{$this->get_test_css()}\n", $filtered_data ); } /** @@ -91,8 +89,8 @@ public function test_inline_style_tag_attribute_value_types() { $this->assertEqualHTML( $expected, wp_get_inline_style_tag( - $this->css, - array( + $this->get_test_css(), + array( // @phpstan-ignore argument.type 'true' => true, 'false' => false, 'null' => null, @@ -125,7 +123,7 @@ public function test_inline_style_tag_repeat_attributes() { $this->assertEqualHTML( $expected, wp_get_inline_style_tag( - $this->css, + $this->get_test_css(), array( 'test' => 'test-a', 'tesT' => 'tesT-b', @@ -146,4 +144,13 @@ public function test_inline_style_tag_escapes_closing_style_tag() { wp_get_inline_style_tag( 'body::after { content: ""; }' ) ); } + + /** + * Gets test CSS code. + * + * @return non-falsy-string + */ + private function get_test_css(): string { + return 'body { color: #123456; }'; + } } From 0468fec555f0e29908d65573cde75732c1b2d90f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 10 Oct 2026 11:17:19 -0700 Subject: [PATCH 04/68] Make inline style tag tests less brittle Replace substring and exact-string assertions with assertEqualHTML() for fully deterministic output, so attribute order, quoting style and self-closing syntax no longer cause spurious failures. Use WP_HTML_Tag_Processor where full-markup comparison is a poor fit: the core concatenation test (whose load-styles.php URL is noisy) and the closing-tag escaping test, which now checks that the CSS cannot break out of the STYLE element rather than pinning the exact escape sequence. Also verify the attributes passed to the wp_inline_style_attributes filter during normal style output. Co-Authored-By: Claude Opus 5.5 --- tests/phpunit/tests/dependencies/styles.php | 62 ++++++++++++++++--- .../tests/dependencies/wpInlineStyleTag.php | 36 ++++++++--- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index c37a979144c47..3786da4e2070c 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -245,10 +245,18 @@ static function ( array $attributes ) { wp_enqueue_style( 'handle', 'http://example.com', array(), '1' ); wp_add_inline_style( 'handle', 'body { color: #123456; }' ); - $printed = get_echo( 'wp_print_styles' ); + $expected = <<<'HTML' + + - $this->assertStringContainsString( ' + + HTML; - $this->assertStringContainsString( ' + + HTML; + + $this->assertEqualHTML( $expected, wp_styles()->print_html ); } /** @@ -321,9 +343,29 @@ static function ( array $attributes ) { wp_add_inline_style( 'handle', 'body { color: #123456; }' ); wp_print_styles(); - $printed = get_echo( '_print_styles' ); + $processor = new WP_HTML_Tag_Processor( get_echo( '_print_styles' ) ); + + $this->assertTrue( $processor->next_tag(), 'Expected the concatenated stylesheet LINK.' ); + $this->assertSame( 'LINK', $processor->get_tag() ); + $this->assertSame( 'stylesheet', $processor->get_attribute( 'rel' ) ); + $href = $processor->get_attribute( 'href' ); + $this->assertIsString( $href ); + $this->assertStringContainsString( '/wp-admin/load-styles.php?', $href ); + + $this->assertTrue( $processor->next_tag(), 'Expected the concatenated inline STYLE.' ); + $this->assertSame( 'STYLE', $processor->get_tag() ); + $this->assertSame( + array( 'nonce' ), + $processor->get_attribute_names_with_prefix( '' ), + 'Expected the filtered nonce to be the only attribute.' + ); + $this->assertSame( 'test-nonce', $processor->get_attribute( 'nonce' ) ); + $this->assertSame( + "\nbody { color: #123456; }\n/*# sourceURL=css-inline-concat-handle */\n", + $processor->get_modifiable_text() + ); - $this->assertStringContainsString( '\n", + $expected = <<<'HTML' + + + HTML; + + $this->assertEqualHTML( + $expected, wp_get_inline_style_tag( $this->get_test_css(), array( @@ -58,10 +65,14 @@ static function ( array $attributes, string $data ) use ( &$filtered_data ) { 2 ); - $this->assertSame( - "\n", - wp_get_inline_style_tag( $this->get_test_css() ) - ); + $expected = <<<'HTML' + + + HTML; + + $this->assertEqualHTML( $expected, wp_get_inline_style_tag( $this->get_test_css() ) ); $this->assertSame( "\n{$this->get_test_css()}\n", $filtered_data ); } @@ -139,10 +150,15 @@ public function test_inline_style_tag_repeat_attributes() { * @ticket 51325 */ public function test_inline_style_tag_escapes_closing_style_tag() { - $this->assertSame( - "\n", - wp_get_inline_style_tag( 'body::after { content: ""; }' ) - ); + $css = 'body::after { content: ""; } body::before { content: "