From 8877a2159f5bbd5c5f0d688a163adf7e05644726 Mon Sep 17 00:00:00 2001
From: Huub <50170696+huubl@users.noreply.github.com>
Date: Thu, 15 Oct 2026 23:55:21 +0200
Subject: [PATCH] Add stylesheet link tag API
Build external stylesheet links with WP_HTML_Tag_Processor and expose wp_style_attributes. Migrate Core stylesheet link emitters while preserving legacy filters and add API and integration coverage.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
src/wp-admin/includes/ajax-actions.php | 14 +-
src/wp-content/themes/twentyeleven/header.php | 16 +-
src/wp-content/themes/twentyten/header.php | 16 +-
src/wp-includes/class-wp-styles.php | 40 ++-
src/wp-includes/general-template.php | 25 +-
src/wp-includes/script-loader.php | 78 ++++-
src/wp-includes/theme-compat/header.php | 10 +-
src/wp-includes/theme.php | 9 +-
tests/phpunit/tests/dependencies/styles.php | 294 ++++++++++++++++--
.../dependencies/wpStylesheetLinkTag.php | 136 ++++++++
10 files changed, 579 insertions(+), 59 deletions(-)
create mode 100644 tests/phpunit/tests/dependencies/wpStylesheetLinkTag.php
diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php
index cbf412ce4c634..7f66f13eb7ded 100644
--- a/src/wp-admin/includes/ajax-actions.php
+++ b/src/wp-admin/includes/ajax-actions.php
@@ -3894,7 +3894,12 @@ function wp_ajax_parse_embed() {
$mce_styles = wpview_media_sandbox_styles();
foreach ( $mce_styles as $style ) {
- $styles .= sprintf( '', $style );
+ $styles .= wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => $style,
+ )
+ );
}
$html = do_shortcode( $parsed );
@@ -4001,7 +4006,12 @@ function wp_ajax_parse_media_shortcode() {
$styles = wpview_media_sandbox_styles();
foreach ( $styles as $style ) {
- $head .= '';
+ $head .= wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => $style,
+ )
+ );
}
if ( ! empty( $wp_scripts ) ) {
diff --git a/src/wp-content/themes/twentyeleven/header.php b/src/wp-content/themes/twentyeleven/header.php
index 2fb3093edfec9..433ddbc954cfd 100644
--- a/src/wp-content/themes/twentyeleven/header.php
+++ b/src/wp-content/themes/twentyeleven/header.php
@@ -43,7 +43,21 @@
?>
-
+ 'stylesheet',
+ 'media' => 'all',
+ 'href' => get_stylesheet_uri() . '?ver=20260819',
+ )
+ );
+} else {
+ ?>
+
+
-
+ 'stylesheet',
+ 'media' => 'all',
+ 'href' => get_stylesheet_uri() . '?ver=20260819',
+ )
+ );
+} else {
+ ?>
+
+
_css_href( $src, $obj->ver, $handle );
+ $raw_href = $this->get_src( $handle );
+ $href = esc_url( $raw_href );
if ( ! $href ) {
return true;
}
@@ -201,14 +202,18 @@ public function do_item( $handle, $group = false ) {
$rel = isset( $obj->extra['alt'] ) && $obj->extra['alt'] ? 'alternate stylesheet' : 'stylesheet';
$title = $obj->extra['title'] ?? '';
- $tag = sprintf(
- "\n",
- $rel,
- esc_attr( $handle ),
- $title ? sprintf( " title='%s'", esc_attr( $title ) ) : '',
- $href,
- esc_attr( $media )
+ $attributes = array(
+ 'rel' => $rel,
+ 'id' => "{$handle}-css",
);
+ if ( $title ) {
+ $attributes['title'] = $title;
+ }
+ $attributes['href'] = $raw_href;
+ $attributes['media'] = $media;
+
+ // Preserve the single-quoted format expected by style_loader_tag callbacks.
+ $tag = str_replace( '"', "'", wp_get_stylesheet_link_tag( $attributes ) );
/**
* Filters the HTML link tag of an enqueued style.
@@ -227,15 +232,18 @@ public function do_item( $handle, $group = false ) {
$rtl_src = $this->get_rtl_src( $handle );
if ( null !== $rtl_src ) {
- $rtl_href = esc_url( $rtl_src );
- $rtl_tag = sprintf(
- "\n",
- $rel,
- esc_attr( $handle ),
- $title ? sprintf( " title='%s'", esc_attr( $title ) ) : '',
- $rtl_href,
- esc_attr( $media )
+ $rtl_href = esc_url( $rtl_src );
+ $rtl_attributes = array(
+ 'rel' => $rel,
+ 'id' => "{$handle}-rtl-css",
);
+ if ( $title ) {
+ $rtl_attributes['title'] = $title;
+ }
+ $rtl_attributes['href'] = $rtl_src;
+ $rtl_attributes['media'] = $media;
+
+ $rtl_tag = str_replace( '"', "'", wp_get_stylesheet_link_tag( $rtl_attributes ) );
/** This filter is documented in wp-includes/class-wp-styles.php */
$rtl_tag = apply_filters( 'style_loader_tag', $rtl_tag, $handle, $rtl_href, $media );
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index 5d6786db99bc8..d7fb613691979 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -5400,9 +5400,16 @@ function wp_admin_css( $file = 'wp-admin', $force_echo = false ) {
return;
}
- $stylesheet_link = sprintf(
- "\n",
- esc_url( wp_admin_css_uri( $file ) )
+ // Preserve the historical markup format passed to the wp_admin_css filter.
+ $stylesheet_link = str_replace(
+ '"',
+ "'",
+ wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => wp_admin_css_uri( $file ),
+ )
+ )
);
/**
@@ -5419,9 +5426,15 @@ function wp_admin_css( $file = 'wp-admin', $force_echo = false ) {
echo apply_filters( 'wp_admin_css', $stylesheet_link, $file );
if ( function_exists( 'is_rtl' ) && is_rtl() ) {
- $rtl_stylesheet_link = sprintf(
- "\n",
- esc_url( wp_admin_css_uri( "$file-rtl" ) )
+ $rtl_stylesheet_link = str_replace(
+ '"',
+ "'",
+ wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => wp_admin_css_uri( "$file-rtl" ),
+ )
+ )
);
/** This filter is documented in wp-includes/general-template.php */
diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
index cd1d53a5fb93f..a120c2a8eb784 100644
--- a/src/wp-includes/script-loader.php
+++ b/src/wp-includes/script-loader.php
@@ -2450,7 +2450,18 @@ function _print_styles() {
}
$href = $wp_styles->base_url . "/wp-admin/load-styles.php?c={$zip}&dir={$dir}" . $concatenated . '&ver=' . $ver;
- echo "\n";
+ // Preserve the concatenated link's historical single-quoted markup.
+ echo str_replace(
+ '"',
+ "'",
+ wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => $href,
+ 'media' => 'all',
+ )
+ )
+ );
if ( ! empty( $wp_styles->print_code ) ) {
$processor = new WP_HTML_Tag_Processor( '' );
@@ -3672,6 +3683,71 @@ function wp_print_inline_script_tag( $data, $attributes = array() ) {
echo wp_get_inline_script_tag( $data, $attributes );
}
+/**
+ * Formats `` tags.
+ *
+ * It is possible to inject attributes in the stylesheet link tag via the {@see 'wp_style_attributes'} filter.
+ *
+ * @since 7.3.0
+ *
+ * @param array $attributes Key-value pairs representing `` tag attributes. Values of
+ * `true` or `null` add a bare attribute, `false` omits it, and
+ * strings (including empty strings) are serialized as values.
+ * @return string String containing a `` tag.
+ *
+ * @phpstan-return non-falsy-string
+ */
+function wp_get_stylesheet_link_tag( $attributes ) {
+ /**
+ * Filters attributes to be added to a stylesheet link tag.
+ *
+ * @since 7.3.0
+ *
+ * @param array $attributes Key-value pairs representing `` tag attributes.
+ * Values of `true` or `null` add a bare attribute; `false`
+ * omits the attribute.
+ */
+ $attributes = apply_filters( 'wp_style_attributes', $attributes );
+
+ $processor = new WP_HTML_Tag_Processor( '' );
+ $processor->next_tag();
+ $seen_attributes = array();
+ foreach ( $attributes as $name => $value ) {
+ /*
+ * Lexical variations of an attribute name may represent the same attribute in HTML.
+ * Calling code should rewrite an attribute's value rather than sending a duplicate.
+ */
+ $normalized_name = strtolower( $name );
+ if ( isset( $seen_attributes[ $normalized_name ] ) ) {
+ continue;
+ }
+ $seen_attributes[ $normalized_name ] = true;
+
+ if ( null !== $processor->get_attribute( $name ) ) {
+ continue;
+ }
+
+ $processor->set_attribute( $name, $value ?? true );
+ }
+
+ return "{$processor->get_updated_html()}\n";
+}
+
+/**
+ * Prints a formatted stylesheet link tag.
+ *
+ * It is possible to inject attributes in the stylesheet link tag via the {@see 'wp_style_attributes'} filter.
+ *
+ * @since 7.3.0
+ *
+ * @param array $attributes Key-value pairs representing `` tag attributes. Values of
+ * `true` or `null` add a bare attribute, `false` omits it, and
+ * strings (including empty strings) are serialized as values.
+ */
+function wp_print_stylesheet_link_tag( $attributes ) {
+ echo wp_get_stylesheet_link_tag( $attributes );
+}
+
/**
* Allows small styles to be inlined.
*
diff --git a/src/wp-includes/theme-compat/header.php b/src/wp-includes/theme-compat/header.php
index 340477fb94f1e..82896f25b5b42 100644
--- a/src/wp-includes/theme-compat/header.php
+++ b/src/wp-includes/theme-compat/header.php
@@ -23,7 +23,15 @@
-
+ 'stylesheet',
+ 'href' => get_bloginfo( 'stylesheet_url', 'display' ),
+ 'media' => 'screen',
+ )
+);
+?>
diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php
index d81616c6c552f..db1ad1b786d6e 100644
--- a/src/wp-includes/theme.php
+++ b/src/wp-includes/theme.php
@@ -733,9 +733,12 @@ function locale_stylesheet() {
return;
}
- printf(
- '',
- $stylesheet
+ wp_print_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => $stylesheet,
+ 'media' => 'screen',
+ )
);
}
diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php
index 6f394d8c78697..19c8f5ae40d7b 100644
--- a/tests/phpunit/tests/dependencies/styles.php
+++ b/tests/phpunit/tests/dependencies/styles.php
@@ -82,6 +82,254 @@ public function test_wp_enqueue_style() {
$this->assertSame( '', get_echo( 'wp_print_styles' ) );
}
+ /**
+ * @covers WP_Styles::do_item
+ */
+ public function test_style_attributes_filter_runs_before_legacy_style_loader_tag_filter() {
+ $source = 'https://example.com/style.css?one=1&two=2';
+ $filtered_attrs = array();
+ $legacy_filter_args = array();
+
+ $attributes_filter = static function ( $attributes ) use ( &$filtered_attrs ) {
+ $filtered_attrs[] = $attributes;
+ $attributes['nonce'] = 'style-nonce';
+ return $attributes;
+ };
+
+ $legacy_filter = static function ( $tag, $handle, $href, $media ) use ( &$legacy_filter_args ) {
+ $processor = new WP_HTML_Tag_Processor( $tag );
+ $processor->next_tag( 'link' );
+
+ $legacy_filter_args[] = array(
+ 'handle' => $handle,
+ 'href' => $href,
+ 'media' => $media,
+ 'nonce' => $processor->get_attribute( 'nonce' ),
+ 'tag' => $tag,
+ );
+
+ return $tag;
+ };
+
+ add_filter( 'wp_style_attributes', $attributes_filter );
+ add_filter( 'style_loader_tag', $legacy_filter, 10, 4 );
+
+ wp_enqueue_style( 'filtered-style', $source, array(), null, 'screen' );
+ $markup = get_echo( 'wp_print_styles' );
+
+ remove_filter( 'wp_style_attributes', $attributes_filter );
+ remove_filter( 'style_loader_tag', $legacy_filter, 10 );
+
+ $this->assertCount( 1, $filtered_attrs );
+ $this->assertSame( $source, $filtered_attrs[0]['href'] );
+ $this->assertSame(
+ array(
+ 'handle' => 'filtered-style',
+ 'href' => esc_url( $source ),
+ 'media' => 'screen',
+ 'nonce' => 'style-nonce',
+ ),
+ array_diff_key( $legacy_filter_args[0], array( 'tag' => true ) )
+ );
+ $this->assertStringNotContainsString( '"', $legacy_filter_args[0]['tag'] );
+
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( 'style-nonce', $processor->get_attribute( 'nonce' ) );
+ $this->assertSame( $source, $processor->get_attribute( 'href' ) );
+ }
+
+ /**
+ * @covers WP_Styles::do_item
+ */
+ public function test_style_attribute_quotes_do_not_create_extra_attributes() {
+ $attribute_value = "It's O'Reilly's \"quoted\" text; data-injected=\"should-remain-text\"";
+ $filter = static function ( $attributes ) use ( $attribute_value ) {
+ $attributes['data-quote-test'] = $attribute_value;
+ return $attributes;
+ };
+
+ add_filter( 'wp_style_attributes', $filter );
+
+ wp_enqueue_style( 'quoted-style', 'https://example.com/style.css', array(), null );
+ $markup = get_echo( 'wp_print_styles' );
+
+ remove_filter( 'wp_style_attributes', $filter );
+
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( $attribute_value, $processor->get_attribute( 'data-quote-test' ) );
+ $this->assertNull( $processor->get_attribute( 'data-injected' ) );
+ }
+
+ /**
+ * @covers WP_Styles::do_item
+ */
+ public function test_rtl_alternate_styles_use_both_attribute_and_legacy_filters() {
+ global $wp_styles;
+
+ $wp_styles->text_direction = 'rtl';
+
+ $filtered_attrs = array();
+ $legacy_hrefs = array();
+
+ $attributes_filter = static function ( $attributes ) use ( &$filtered_attrs ) {
+ $filtered_attrs[] = $attributes;
+ $attributes['data-filter'] = 'applied';
+ return $attributes;
+ };
+
+ $legacy_filter = static function ( $tag, $handle, $href, $media ) use ( &$legacy_hrefs ) {
+ $legacy_hrefs[] = $href;
+ return $tag;
+ };
+
+ add_filter( 'wp_style_attributes', $attributes_filter );
+ add_filter( 'style_loader_tag', $legacy_filter, 10, 4 );
+
+ wp_enqueue_style( 'theme-style', 'https://example.com/theme.css', array(), null );
+ wp_style_add_data( 'theme-style', 'alt', true );
+ wp_style_add_data( 'theme-style', 'title', 'Alternate theme' );
+ wp_style_add_data( 'theme-style', 'rtl', true );
+
+ $markup = get_echo( 'wp_print_styles' );
+
+ remove_filter( 'wp_style_attributes', $attributes_filter );
+ remove_filter( 'style_loader_tag', $legacy_filter, 10 );
+
+ $tags = array();
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ while ( $processor->next_tag( 'link' ) ) {
+ $tags[] = array(
+ 'id' => $processor->get_attribute( 'id' ),
+ 'rel' => $processor->get_attribute( 'rel' ),
+ 'title' => $processor->get_attribute( 'title' ),
+ 'href' => $processor->get_attribute( 'href' ),
+ 'data-filter' => $processor->get_attribute( 'data-filter' ),
+ );
+ }
+
+ $this->assertCount( 2, $filtered_attrs );
+ $this->assertCount( 2, $legacy_hrefs );
+ $this->assertSame( 'alternate stylesheet', $tags[0]['rel'] );
+ $this->assertSame( 'theme-style-css', $tags[0]['id'] );
+ $this->assertSame( 'Alternate theme', $tags[0]['title'] );
+ $this->assertSame( 'https://example.com/theme.css', $tags[0]['href'] );
+ $this->assertSame( 'theme-style-rtl-css', $tags[1]['id'] );
+ $this->assertSame( 'https://example.com/theme-rtl.css', $tags[1]['href'] );
+ $this->assertSame( 'applied', $tags[0]['data-filter'] );
+ $this->assertSame( 'applied', $tags[1]['data-filter'] );
+ $this->assertSame( esc_url( 'https://example.com/theme.css' ), $legacy_hrefs[0] );
+ $this->assertSame( esc_url( 'https://example.com/theme-rtl.css' ), $legacy_hrefs[1] );
+ }
+
+ /**
+ * @covers ::_print_styles
+ */
+ public function test_concatenated_styles_apply_style_attributes_once_without_legacy_filter() {
+ global $wp_styles;
+
+ $wp_styles->do_concat = true;
+ $wp_styles->default_dirs = array( '/wp-admin/' );
+
+ $attributes_filter_calls = 0;
+ $legacy_filter_calls = 0;
+
+ $attributes_filter = static function ( $attributes ) use ( &$attributes_filter_calls ) {
+ ++$attributes_filter_calls;
+ $attributes['nonce'] = 'concatenated-style';
+ return $attributes;
+ };
+
+ $legacy_filter = static function ( $tag ) use ( &$legacy_filter_calls ) {
+ ++$legacy_filter_calls;
+ return $tag;
+ };
+
+ add_filter( 'wp_style_attributes', $attributes_filter );
+ add_filter( 'style_loader_tag', $legacy_filter );
+
+ wp_enqueue_style( 'concat-one', '/wp-admin/one.css', array(), null );
+ wp_enqueue_style( 'concat-two', '/wp-admin/two.css', array(), null );
+
+ wp_print_styles();
+ $markup = get_echo( '_print_styles' );
+
+ remove_filter( 'wp_style_attributes', $attributes_filter );
+ remove_filter( 'style_loader_tag', $legacy_filter );
+
+ $this->assertSame( 1, $attributes_filter_calls );
+ $this->assertSame( 0, $legacy_filter_calls );
+
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( 'concatenated-style', $processor->get_attribute( 'nonce' ) );
+ $this->assertStringContainsString( 'load-styles.php', $processor->get_attribute( 'href' ) );
+ }
+
+ /**
+ * @covers ::wp_admin_css
+ */
+ public function test_wp_admin_css_uses_style_attributes_and_legacy_filter() {
+ $legacy_files = array();
+ $legacy_tags = array();
+ $filter = static function ( $attributes ) {
+ $attributes['nonce'] = 'admin-style';
+ return $attributes;
+ };
+ $legacy_filter = static function ( $tag, $file ) use ( &$legacy_files, &$legacy_tags ) {
+ $legacy_files[] = $file;
+ $legacy_tags[] = $tag;
+ $processor = new WP_HTML_Tag_Processor( $tag );
+ $processor->next_tag( 'link' );
+ if ( 'admin-style' !== $processor->get_attribute( 'nonce' ) ) {
+ return '';
+ }
+ return $tag;
+ };
+
+ add_filter( 'wp_style_attributes', $filter );
+ add_filter( 'wp_admin_css', $legacy_filter, 10, 2 );
+
+ $markup = get_echo( 'wp_admin_css', array( 'stylesheet-link-api-test' ) );
+
+ remove_filter( 'wp_style_attributes', $filter );
+ remove_filter( 'wp_admin_css', $legacy_filter, 10 );
+
+ $this->assertSame( array( 'stylesheet-link-api-test' ), $legacy_files );
+ $this->assertStringNotContainsString( '"', $legacy_tags[0] );
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( 'admin-style', $processor->get_attribute( 'nonce' ) );
+ $this->assertStringContainsString( 'stylesheet-link-api-test.css', $processor->get_attribute( 'href' ) );
+ }
+
+ /**
+ * @covers ::locale_stylesheet
+ */
+ public function test_locale_stylesheet_uses_style_attributes_filter() {
+ $stylesheet_uri_filter = static function ( $stylesheet_uri, $stylesheet_dir_uri ) {
+ return 'https://example.com/locale.css?one=1&two=2';
+ };
+ $attributes_filter = static function ( $attributes ) {
+ $attributes['nonce'] = 'locale-style';
+ return $attributes;
+ };
+
+ add_filter( 'locale_stylesheet_uri', $stylesheet_uri_filter, 10, 2 );
+ add_filter( 'wp_style_attributes', $attributes_filter );
+
+ $markup = get_echo( 'locale_stylesheet' );
+
+ remove_filter( 'locale_stylesheet_uri', $stylesheet_uri_filter, 10 );
+ remove_filter( 'wp_style_attributes', $attributes_filter );
+
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( 'locale-style', $processor->get_attribute( 'nonce' ) );
+ $this->assertSame( 'https://example.com/locale.css?one=1&two=2', $processor->get_attribute( 'href' ) );
+ }
+
/**
* @ticket 42804
*/
@@ -109,9 +357,13 @@ public function test_wp_enqueue_style_with_html5_support_does_not_contain_type_a
public function test_awkward_handles_are_supported_consistently( $handle ) {
wp_enqueue_style( $handle, 'example.com', array(), null );
- $expected = "\n";
+ $processor = new WP_HTML_Tag_Processor( get_echo( 'wp_print_styles' ) );
- $this->assertEqualHTML( $expected, get_echo( 'wp_print_styles' ) );
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( 'stylesheet', $processor->get_attribute( 'rel' ) );
+ $this->assertSame( $handle . '-css', $processor->get_attribute( 'id' ) );
+ $this->assertSame( 'http://example.com', $processor->get_attribute( 'href' ) );
+ $this->assertSame( 'all', $processor->get_attribute( 'media' ) );
}
/**
@@ -412,37 +664,23 @@ public function test_wp_add_inline_style_for_handle_without_source() {
* @ticket 35921
* @dataProvider data_styles_with_media
*/
- public function test_wp_enqueue_style_with_media( $expected, $media ) {
+ public function test_wp_enqueue_style_with_media( $media ) {
wp_enqueue_style( 'handle', 'http://example.com', array(), 1, $media );
- $this->assertStringContainsString( $expected, get_echo( 'wp_print_styles' ) );
+
+ $processor = new WP_HTML_Tag_Processor( get_echo( 'wp_print_styles' ) );
+
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertSame( $media, $processor->get_attribute( 'media' ) );
}
public function data_styles_with_media() {
return array(
- array(
- "media='all'",
- 'all',
- ),
- array(
- "media='(orientation: portrait)'",
- '(orientation: portrait)',
- ),
- array(
- "media='(max-width: 640px)'",
- '(max-width: 640px)',
- ),
- array(
- "media='print and (min-width: 25cm)'",
- 'print and (min-width: 25cm)',
- ),
- array(
- "media='screen and (color), projection and (color)'",
- 'screen and (color), projection and (color)',
- ),
- array(
- "media='not screen and (color)'",
- 'not screen and (color)',
- ),
+ array( 'all' ),
+ array( '(orientation: portrait)' ),
+ array( '(max-width: 640px)' ),
+ array( 'print and (min-width: 25cm)' ),
+ array( 'screen and (color), projection and (color)' ),
+ array( 'not screen and (color)' ),
);
}
diff --git a/tests/phpunit/tests/dependencies/wpStylesheetLinkTag.php b/tests/phpunit/tests/dependencies/wpStylesheetLinkTag.php
new file mode 100644
index 0000000000000..6194d443f29ef
--- /dev/null
+++ b/tests/phpunit/tests/dependencies/wpStylesheetLinkTag.php
@@ -0,0 +1,136 @@
+
+
+HTML;
+
+ $this->assertEqualHTML(
+ $expected,
+ wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => 'https://example.com/style.css?one=1&two=2',
+ 'integrity' => 'sha384-example',
+ 'crossorigin' => true,
+ )
+ )
+ );
+ }
+
+ /**
+ * @covers ::wp_get_stylesheet_link_tag
+ */
+ public function test_get_stylesheet_link_tag_attribute_value_types() {
+ $expected = <<<'HTML'
+
+
+HTML;
+
+ $this->assertEqualHTML(
+ $expected,
+ wp_get_stylesheet_link_tag(
+ array(
+ 'enabled' => true,
+ 'disabled' => false,
+ 'null-value' => null,
+ 'empty-value' => '',
+ 'zero-string' => '0',
+ )
+ )
+ );
+ }
+
+ /**
+ * Case-insensitive duplicate attributes should keep the first value.
+ *
+ * @covers ::wp_get_stylesheet_link_tag
+ */
+ public function test_get_stylesheet_link_tag_ignores_duplicate_attributes() {
+ $this->assertEqualHTML(
+ "\n",
+ wp_get_stylesheet_link_tag(
+ array(
+ 'test' => 'first',
+ 'TEST' => 'second',
+ 'disabled' => false,
+ 'DISABLED' => true,
+ )
+ )
+ );
+ }
+
+ /**
+ * @covers ::wp_get_stylesheet_link_tag
+ */
+ public function test_get_stylesheet_link_tag_applies_wp_style_attributes_filter() {
+ $filter = static function ( $attributes ) {
+ $attributes['nonce'] = 'filtered-nonce';
+ return $attributes;
+ };
+
+ add_filter( 'wp_style_attributes', $filter );
+
+ $tag = wp_get_stylesheet_link_tag( array( 'rel' => 'stylesheet' ) );
+
+ remove_filter( 'wp_style_attributes', $filter );
+
+ $this->assertEqualHTML(
+ "\n",
+ $tag
+ );
+ }
+
+ /**
+ * @covers ::wp_get_stylesheet_link_tag
+ */
+ public function test_get_stylesheet_link_tag_rejects_disallowed_href_protocols() {
+ $tag = wp_get_stylesheet_link_tag(
+ array(
+ 'rel' => 'stylesheet',
+ 'href' => 'javascript:alert(1)',
+ )
+ );
+ $processor = new WP_HTML_Tag_Processor( $tag );
+
+ $this->assertTrue( $processor->next_tag( 'link' ) );
+ $this->assertNull( $processor->get_attribute( 'href' ) );
+ }
+
+ /**
+ * @covers ::wp_print_stylesheet_link_tag
+ */
+ public function test_print_stylesheet_link_tag_prints_get_stylesheet_link_tag() {
+ $attributes = array(
+ 'rel' => 'stylesheet',
+ 'href' => 'https://example.com/style.css',
+ );
+
+ $this->assertSame(
+ wp_get_stylesheet_link_tag( $attributes ),
+ get_echo( 'wp_print_stylesheet_link_tag', array( $attributes ) )
+ );
+ }
+}