Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 143 additions & 48 deletions src/wp-includes/general-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -3945,6 +3945,29 @@ function wp_resource_hints() {
'prerender' => array(),
);

/*
* Attributes allowed on a resource hint. Any attribute not listed as a key is
* dropped. A value of `true` means the attribute may also be printed as a
* boolean (valueless) attribute, when it is supplied as `true` or as a list
* item. Attributes with a value of `false` are printed only with a value.
*
* Other attributes of the link element are omitted because the HTML spec only
* allows them with other link types: blocking, color, disabled, imagesizes,
* imagesrcset, integrity, and sizes.

@dmsnell dmsnell Nov 10, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is internal-only, so not exposing a private method, but I find the false are printed only with a value confusing, and possibly easy to mess up later on.

$allowed_attributes = array(
    'as'             => 'value-optional',
    'crossorigin'    => 'requires-value',

	...
);

...

if ( ! isset( ... ) || null !== ... ) {
	continue;
}

$has_value   = is_string( $key ) && is_scalar( $value ) && ! is_bool( $value );
$needs_value = is_string( $key ) && 'requires-value' === $allowed_attributes[ $key ];

if ( $has_value ) {
	$value = (string) $value;
}

if ( $needs_value ) {
	continue;
}

$attr  = $value;
$value = true;

this is not a blocker

*/
$allowed_attributes = array(
'as' => false,
Comment thread
sirreal marked this conversation as resolved.
Resolved
'crossorigin' => true,
'fetchpriority' => false,
'href' => false,
'hreflang' => false,
'media' => false,
'referrerpolicy' => false,
'rel' => false,
'title' => false,
'type' => false,
);

foreach ( $hints as $relation_type => $urls ) {
$unique_urls = array();

Expand All @@ -3954,19 +3977,32 @@ function wp_resource_hints() {
* @since 4.6.0
* @since 4.7.0 The `$urls` parameter accepts arrays of specific HTML attributes
* as its child elements.
* @since 7.3.0 The obsolete `pr` attribute is no longer printed. The `crossorigin`
Comment thread
sirreal marked this conversation as resolved.
Resolved
* attribute may be `true` to print it without a value. List items
* other than `crossorigin` are no longer printed. Added the
* `fetchpriority`, `hreflang`, `media`, `referrerpolicy`, and `title`
* attributes.
*
* @param array $urls {
* Array of resources and their attributes, or URLs to print for resource hints.
*
* @type array|string ...$0 {
* Array of resource attributes, or a URL string.
* Array of resource attributes, or a URL string. A valueless attribute
* may also be supplied as a list item, as in `array( 'crossorigin' )`.
*
* @type string $href URL to include in resource hints. Required.
* @type string $as How the browser should treat the resource
* (`script`, `style`, `image`, `document`, etc).
* @type string $crossorigin Indicates the CORS policy of the specified resource.
* @type float $pr Expected probability that the resource hint will be used.
* @type string $type Type of the resource (`text/html`, `text/css`, etc).
* @type string $href URL to include in resource hints. Required.
* @type string $as How the browser should treat the resource
* (`script`, `style`, `image`, `document`, etc).
* @type string|bool $crossorigin Indicates the CORS policy of the specified resource.
* When `true`, it is printed without a value, which
* is equivalent to `anonymous`.
* @type string $fetchpriority Priority of the fetch relative to other resources
* (`high`, `low`, or `auto`).
* @type string $hreflang Language of the resource.
* @type string $media Media query the resource applies to.
* @type string $referrerpolicy Referrer policy for the fetch.
* @type string $title Title of the resource hint.
* @type string $type Type of the resource (`text/html`, `text/css`, etc).
* }
* }
* @param string $relation_type The relation type the URLs are printed for. One of
Expand Down Expand Up @@ -4018,28 +4054,41 @@ function wp_resource_hints() {
}

foreach ( $unique_urls as $atts ) {
$html = '';
$processor = new WP_HTML_Tag_Processor( '<link>' );
$processor->next_tag();

foreach ( $atts as $attr => $value ) {
if ( ! is_scalar( $value )
|| ( ! in_array( $attr, array( 'as', 'crossorigin', 'href', 'pr', 'rel', 'type' ), true ) && ! is_numeric( $attr ) )
) {
// A list item supplies a valueless attribute, as in `array( 'crossorigin', 'href' => … )`.
if ( is_int( $attr ) ) {
if ( ! is_string( $value ) ) {
continue;
}
$attr = $value;
$value = true;
}

/*
* An attribute may be supplied twice, as a list item and as a key. As in HTML,
* the first one wins, since the HTML API would otherwise apply the last one.
*/
if ( ! isset( $allowed_attributes[ $attr ] ) || null !== $processor->get_attribute( $attr ) ) {
continue;
}

$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );

if ( ! is_string( $attr ) ) {
$html .= " $value";
if ( is_bool( $value ) ) {
if ( ! $allowed_attributes[ $attr ] ) {
continue;
}
} elseif ( is_scalar( $value ) ) {
$value = (string) $value;
} else {
$html .= " $attr='$value'";
continue;
}
}

$html = trim( $html );
$processor->set_attribute( $attr, $value );
Comment thread
sirreal marked this conversation as resolved.
Resolved
}

echo "<link $html />\n";
echo $processor->get_updated_html() . "\n";
}
}
}
Expand All @@ -4066,23 +4115,28 @@ function wp_preload_resources() {
*
* @since 6.1.0
* @since 6.6.0 Added the `$fetchpriority` attribute.
* @since 7.3.0 The `crossorigin` attribute may be `true` to print it without a value.
* List items other than `crossorigin` are no longer printed.
*
* @param array $preload_resources {
* Array of resources and their attributes, or URLs to print for resource preloads.
*
* @type array ...$0 {
* Array of resource attributes.
*
* @type string $href URL to include in resource preloads. May be omitted
* for an image preloaded by its `$imagesrcset`.
* @type string $as How the browser should treat the resource
* (`script`, `style`, `image`, `document`, etc).
* @type string $crossorigin Indicates the CORS policy of the specified resource.
* @type string $type Type of the resource (`text/html`, `text/css`, etc).
* @type string $media Accepts media types or media queries. Allows responsive preloading.
* @type string $imagesizes Responsive source size to the source Set.
* @type string $imagesrcset Responsive image sources to the source set.
* @type string $fetchpriority Fetchpriority value for the resource.
* Array of resource attributes. A valueless attribute may also be supplied
* as a list item, as in `array( 'crossorigin' )`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do these live in between associative arrays? I find the "list item" language unclear.

*     Array of resource attributes. Valueless attributes may be passed with
*     `true` as their value, or as the string name of the attribute in lieu
*     of a full type.
*
* Example:
*
*     array( 'href' => 'https://example.com', 'crossorigin', 'as' => 'style' );

and I guess this is where I get confused, because this syntax doesn’t work. we’d have to do something awkward like 1 => 'crossorigin' so I wonder if this is the intended case or if we should clarify that the entire array should either be string names, or an associative array with attribute names as keys.

Maybe the legitimate cases doing this are like the following?

$attributes = array(
	'href' => 'https://example.com',
	'as'   => 'style',
);

$attributes[] = 'crossorigin';

though this still seems a bit out of place in WordPress code.

with the few guiding words we have to share in the docs, what are we trying to suggest people write when receiving and sending these values?

*
* @type string $href URL to include in resource preloads. May be omitted
* for an image preloaded by its `$imagesrcset`.
* @type string $as How the browser should treat the resource
* (`script`, `style`, `image`, `document`, etc).
* @type string|bool $crossorigin Indicates the CORS policy of the specified resource.
* When `true`, it is printed without a value, which
* is equivalent to `anonymous`.
* @type string $type Type of the resource (`text/html`, `text/css`, etc).
* @type string $media Accepts media types or media queries. Allows responsive preloading.
* @type string $imagesizes Responsive source size to the source Set.
* @type string $imagesrcset Responsive image sources to the source set.
* @type string $fetchpriority Fetchpriority value for the resource.
* }
* }
*/
Expand All @@ -4101,35 +4155,83 @@ function wp_preload_resources() {
}

$attributes = $resource;
if ( isset( $resource['href'] ) ) {
$href = $resource['href'];

// An href which is not a string or which uses a disallowed protocol is unusable.
$href = '';
if ( isset( $resource['href'] ) && is_string( $resource['href'] ) ) {
$href = sanitize_url( $resource['href'], array( 'http', 'https' ) );
}

if ( '' !== $href ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth checking additionally if sanitization changed the URL, as the '' only checks cases it rejects, but allows through URLs which might resolve to different resources after sanitization than before

- if ( '' !== $href ) {
+ if ( $resource['href'] !== $href ) {

if ( isset( $unique_resources[ $href ] ) ) {
continue;
}
$attributes['href'] = $href;
$unique_resources[ $href ] = $attributes;
// Media can use imagesrcset and not href.
} elseif ( isset( $resource['as'], $resource['imagesrcset'] ) && 'image' === $resource['as'] ) {
} elseif (
isset( $resource['as'], $resource['imagesrcset'] ) &&
'image' === $resource['as'] &&
is_string( $resource['imagesrcset'] )
) {
if ( isset( $unique_resources[ $resource['imagesrcset'] ] ) ) {
continue;
}
unset( $attributes['href'] );
$unique_resources[ $resource['imagesrcset'] ] = $attributes;
} else {
continue;
}
}

/*
* Attributes allowed on a preload. Any attribute not listed as a key is
* dropped. A value of `true` means the attribute may also be printed as a
* boolean (valueless) attribute, when it is supplied as `true` or as a list
* item. Attributes with a value of `false` are printed only with a value.
*/
$allowed_attributes = array(
'as' => false,
Comment thread
sirreal marked this conversation as resolved.
Resolved
'crossorigin' => true,
'fetchpriority' => false,
'href' => false,
'imagesizes' => false,
'imagesrcset' => false,
'media' => false,
'type' => false,
);

// Build and output the HTML for each unique resource.
foreach ( $unique_resources as $unique_resource ) {
$html = '';
$processor = new WP_HTML_Tag_Processor( '<link rel="preload">' );
$processor->next_tag();

foreach ( $unique_resource as $resource_key => $resource_value ) {
if ( ! is_scalar( $resource_value ) ) {
// A list item supplies a valueless attribute, as in `array( 'crossorigin', 'href' => … )`.
if ( is_int( $resource_key ) ) {
if ( ! is_string( $resource_value ) ) {
continue;
}
$resource_key = $resource_value;
$resource_value = true;
}

/*
* Ignore non-supported attributes. An attribute may also be supplied twice, as a list
* item and as a key. As in HTML, the first one wins, since the HTML API would
* otherwise apply the last one.
*/
if ( ! isset( $allowed_attributes[ $resource_key ] ) || null !== $processor->get_attribute( $resource_key ) ) {
continue;
}

// Ignore non-supported attributes.
$non_supported_attributes = array( 'as', 'crossorigin', 'href', 'imagesrcset', 'imagesizes', 'type', 'media', 'fetchpriority' );
if ( ! in_array( $resource_key, $non_supported_attributes, true ) && ! is_numeric( $resource_key ) ) {
if ( is_bool( $resource_value ) ) {
if ( ! $allowed_attributes[ $resource_key ] ) {
continue;
}
} elseif ( is_scalar( $resource_value ) ) {
$resource_value = (string) $resource_value;
} else {
continue;
}

Expand All @@ -4145,17 +4247,10 @@ function wp_preload_resources() {
continue;
}

$resource_value = ( 'href' === $resource_key ) ? esc_url( $resource_value, array( 'http', 'https' ) ) : esc_attr( $resource_value );

if ( ! is_string( $resource_key ) ) {
$html .= " $resource_value";
} else {
$html .= " $resource_key='$resource_value'";
}
$processor->set_attribute( $resource_key, $resource_value );
Comment thread
sirreal marked this conversation as resolved.
Resolved
}
$html = trim( $html );

printf( "<link rel='preload' %s />\n", $html );
echo $processor->get_updated_html() . "\n";
}
}

Expand Down
Loading
Loading