Skip to content

Commit 40e8c31

Browse files
authored
Merge pull request #1802 from AhmarZaidi/fix/optimization-detective-non-ascii-chars-in-link-header
Prevent URL in `Link` header from including invalid characters
2 parents e793289 + c3c2512 commit 40e8c31

File tree

2 files changed

+105
-3
lines changed

2 files changed

+105
-3
lines changed

plugins/optimization-detective/class-od-link-collection.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,24 @@ public function get_response_header(): ?string {
255255
$link_headers = array();
256256

257257
foreach ( $this->get_prepared_links() as $link ) {
258-
// The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded.
259-
$link['href'] = isset( $link['href'] ) ? esc_url_raw( $link['href'] ) : 'about:blank';
260-
$link_header = '<' . $link['href'] . '>';
258+
if ( isset( $link['href'] ) ) {
259+
$decoded_url = urldecode( $link['href'] );
260+
261+
// Encode characters not allowed in a URL per RFC 3986 (anything that is not among the reserved and unreserved characters).
262+
$encoded_url = preg_replace_callback(
263+
'/[^A-Za-z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]/',
264+
static function ( $matches ) {
265+
return rawurlencode( $matches[0] );
266+
},
267+
$decoded_url
268+
);
269+
$link['href'] = esc_url_raw( $encoded_url ?? '' );
270+
} else {
271+
// The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded.
272+
$link['href'] = 'about:blank';
273+
}
274+
275+
$link_header = '<' . $link['href'] . '>';
261276
unset( $link['href'] );
262277
foreach ( $link as $name => $value ) {
263278
/*

plugins/optimization-detective/tests/test-class-od-link-collection.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ public function data_provider_to_test_add_link(): array {
4141
'expected_count' => 1,
4242
'error' => '',
4343
),
44+
'preload_imagesrcset_without_href' => array(
45+
'links_args' => array(
46+
array(
47+
array(
48+
'rel' => 'preload',
49+
'imagesrcset' => 'https://example.com/foo-400.jpg 400w, https://example.com/foo-800.jpg 800w',
50+
'imagesizes' => '(max-width: 600px) 480px, 800px',
51+
'as' => 'image',
52+
'media' => 'screen',
53+
),
54+
),
55+
),
56+
'expected_html' => '
57+
<link data-od-added-tag rel="preload" imagesrcset="https://example.com/foo-400.jpg 400w, https://example.com/foo-800.jpg 800w" imagesizes="(max-width: 600px) 480px, 800px" as="image" media="screen">
58+
',
59+
'expected_header' => 'Link: <about:blank>; rel="preload"; imagesrcset="https://example.com/foo-400.jpg 400w, https://example.com/foo-800.jpg 800w"; imagesizes="(max-width: 600px) 480px, 800px"; as="image"; media="screen"',
60+
'expected_count' => 1,
61+
'error' => '',
62+
),
4463
'preload_with_min0_max_viewport_widths' => array(
4564
'links_args' => array(
4665
array(
@@ -369,6 +388,74 @@ public function data_provider_to_test_add_link(): array {
369388
'expected_count' => 0,
370389
'error' => 'Maximum width must be greater than zero and greater than the minimum width.',
371390
),
391+
'international_domain_name' => array(
392+
'links_args' => array(
393+
array(
394+
array(
395+
'rel' => 'preload',
396+
'href' => 'https://例.example.com/תמונה.jpg',
397+
'as' => 'image',
398+
),
399+
),
400+
),
401+
'expected_html' => '
402+
<link data-od-added-tag rel="preload" href="https://例.example.com/תמונה.jpg" as="image">
403+
',
404+
'expected_header' => 'Link: <https://%E4%BE%8B.example.com/%D7%AA%D7%9E%D7%95%D7%A0%D7%94.jpg>; rel="preload"; as="image"',
405+
'expected_count' => 1,
406+
'error' => '',
407+
),
408+
'non_ascii_path' => array(
409+
'links_args' => array(
410+
array(
411+
array(
412+
'rel' => 'preload',
413+
'href' => 'https://example.com/חנות/תמונה.jpg',
414+
'as' => 'image',
415+
),
416+
),
417+
),
418+
'expected_html' => '
419+
<link data-od-added-tag rel="preload" href="https://example.com/חנות/תמונה.jpg" as="image">
420+
',
421+
'expected_header' => 'Link: <https://example.com/%D7%97%D7%A0%D7%95%D7%AA/%D7%AA%D7%9E%D7%95%D7%A0%D7%94.jpg>; rel="preload"; as="image"',
422+
'expected_count' => 1,
423+
'error' => '',
424+
),
425+
'percent-in-path' => array(
426+
'links_args' => array(
427+
array(
428+
array(
429+
'rel' => 'preload',
430+
'href' => 'https://example.com/100%25-one-hundred-percent.png?a[1]=2',
431+
'as' => 'image',
432+
),
433+
),
434+
),
435+
'expected_html' => '
436+
<link data-od-added-tag rel="preload" href="https://example.com/100%25-one-hundred-percent.png?a[1]=2" as="image">
437+
',
438+
'expected_header' => 'Link: <https://example.com/100%25-one-hundred-percent.png?a%5B1%5D=2>; rel="preload"; as="image"',
439+
'expected_count' => 1,
440+
'error' => '',
441+
),
442+
'multisite_subdirectory_non_ascii' => array(
443+
'links_args' => array(
444+
array(
445+
array(
446+
'rel' => 'preload',
447+
'href' => 'https://example.com/חנות/wp-content/uploads/2025/01/example.jpg?ver=1+2',
448+
'as' => 'image',
449+
),
450+
),
451+
),
452+
'expected_html' => '
453+
<link data-od-added-tag rel="preload" href="https://example.com/חנות/wp-content/uploads/2025/01/example.jpg?ver=1+2" as="image">
454+
',
455+
'expected_header' => 'Link: <https://example.com/%D7%97%D7%A0%D7%95%D7%AA/wp-content/uploads/2025/01/example.jpg?ver=1%202>; rel="preload"; as="image"',
456+
'expected_count' => 1,
457+
'error' => '',
458+
),
372459
);
373460
}
374461

0 commit comments

Comments
 (0)