Skip to content

Commit 9e94fe6

Browse files
committed
General: Allow configuration of speculative loading defaults via env variables and constants.
Introduce two overrides that let a site or hosting provider change the default speculative loading configuration that the `auto` value resolves to, without having to ship an mu-plugin: * `WP_SPECULATIVE_LOADING_DEFAULT_MODE` (`prefetch` or `prerender`) * `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` (`conservative`, `moderate`, or `eager`). Each may be supplied as an environment variable, read via `getenv()`, or as a constant of the same name that takes precedence over it, mirroring how `wp_get_environment_type()` resolves `WP_ENVIRONMENT_TYPE`. An unrecognized value falls back to the core default. These overrides only change what `auto` resolves to, so an explicit mode or eagerness supplied through the `wp_speculation_rules_configuration` filter still wins. An `eagerness` of `immediate` is rejected because WordPress does not permit it for the document-level rules it generates; accepting it would cause `WP_Speculation_Rules::add_rule()` to reject the rule and leave the page with no speculation rules at all. Also relax `WP_Speculation_Rules::is_valid_mode()` and `WP_Speculation_Rules::is_valid_eagerness()` to accept `mixed`, so an arbitrary value from the filter is validated and rejected rather than raising a `TypeError`. Fix PHPStan errors in speculative loading functions resulting from insufficient typing. Developed in WordPress#12514. Follow-up to r59837. Props westonruter, mukesh27, adamsilverstein, swissspidy. See #64066, #62503, #64896, #64898. Fixes #65624. git-svn-id: https://develop.svn.wordpress.org/trunk@62752 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 8b7872f commit 9e94fe6

5 files changed

Lines changed: 499 additions & 10 deletions

File tree

src/wp-includes/class-wp-speculation-rules.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,24 +259,30 @@ private function is_valid_id( string $id ): bool {
259259
* Checks whether the given speculation rules mode is valid.
260260
*
261261
* @since 6.8.0
262+
* @since 7.1.0 The $mode param now allows mixed and not just string.
262263
*
263-
* @param string $mode Speculation rules mode.
264+
* @param mixed $mode Speculation rules mode.
264265
* @return bool True if valid, false otherwise.
266+
*
267+
* @phpstan-assert-if-true 'prefetch'|'prerender' $mode
265268
*/
266-
public static function is_valid_mode( string $mode ): bool {
267-
return isset( self::$mode_allowlist[ $mode ] );
269+
public static function is_valid_mode( $mode ): bool {
270+
return is_string( $mode ) && isset( self::$mode_allowlist[ $mode ] );
268271
}
269272

270273
/**
271274
* Checks whether the given speculation rules eagerness is valid.
272275
*
273276
* @since 6.8.0
277+
* @since 7.1.0 The $eagerness param now allows mixed and not just string.
274278
*
275-
* @param string $eagerness Speculation rules eagerness.
279+
* @param mixed $eagerness Speculation rules eagerness.
276280
* @return bool True if valid, false otherwise.
281+
*
282+
* @phpstan-assert-if-true 'conservative'|'moderate'|'eager'|'immediate' $eagerness
277283
*/
278-
public static function is_valid_eagerness( string $eagerness ): bool {
279-
return isset( self::$eagerness_allowlist[ $eagerness ] );
284+
public static function is_valid_eagerness( $eagerness ): bool {
285+
return is_string( $eagerness ) && isset( self::$eagerness_allowlist[ $eagerness ] );
280286
}
281287

282288
/**

src/wp-includes/speculative-loading.php

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@
1111
* Returns the speculation rules configuration.
1212
*
1313
* @since 6.8.0
14+
* @since 7.1.0 The `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS` constants and
15+
* environment variables can now specify the default mode and eagerness, respectively.
16+
*
17+
* @see wp_get_speculation_rules_default_configuration()
1418
*
1519
* @return array<string, string>|null Associative array with 'mode' and 'eagerness' keys, or null if speculative
1620
* loading is disabled.
21+
* @phpstan-return array{
22+
* mode: 'prefetch'|'prerender',
23+
* eagerness: 'conservative'|'moderate'|'eager',
24+
* }|null
1725
*/
1826
function wp_get_speculation_rules_configuration(): ?array {
1927
// By default, speculative loading is only enabled for sites with pretty permalinks when no user is logged in.
@@ -59,8 +67,10 @@ function wp_get_speculation_rules_configuration(): ?array {
5967
}
6068

6169
// Sanitize the configuration and replace 'auto' with current defaults.
62-
$default_mode = 'prefetch';
63-
$default_eagerness = 'conservative';
70+
$defaults = wp_get_speculation_rules_default_configuration();
71+
$default_mode = $defaults['mode'];
72+
$default_eagerness = $defaults['eagerness'];
73+
6474
if ( ! is_array( $config ) ) {
6575
return array(
6676
'mode' => $default_mode,
@@ -90,6 +100,83 @@ function wp_get_speculation_rules_configuration(): ?array {
90100
);
91101
}
92102

103+
/**
104+
* Returns the default speculation rules configuration that the value 'auto' resolves to.
105+
*
106+
* WordPress Core defaults to a mode of 'prefetch' and an eagerness of 'conservative'. Hosting providers can override
107+
* either default by way of the `WP_SPECULATIVE_LOADING_DEFAULT_MODE` and `WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS`
108+
* constants or environment variables. This only changes what the 'auto' value resolves to, so a plugin which supplies
109+
* an explicit mode or eagerness via the {@see 'wp_speculation_rules_configuration'} filter continues to take
110+
* precedence.
111+
*
112+
* Note that an eagerness of 'immediate' is not permitted as a default, since WordPress does not allow it for the
113+
* document-level rules that it generates.
114+
*
115+
* @since 7.1.0
116+
* @access private
117+
*
118+
* @return array<string, string> Associative array with 'mode' and 'eagerness' keys.
119+
* @phpstan-return array{
120+
* mode: 'prefetch'|'prerender',
121+
* eagerness: 'conservative'|'moderate'|'eager',
122+
* }
123+
*/
124+
function wp_get_speculation_rules_default_configuration(): array {
125+
$default_mode = 'prefetch';
126+
$mode = wp_get_speculative_loading_override( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE' );
127+
if ( WP_Speculation_Rules::is_valid_mode( $mode ) ) {
128+
$default_mode = $mode;
129+
}
130+
131+
$default_eagerness = 'conservative';
132+
$eagerness = wp_get_speculative_loading_override( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' );
133+
if (
134+
WP_Speculation_Rules::is_valid_eagerness( $eagerness ) &&
135+
// 'immediate' is a valid eagerness, but for safety WordPress does not allow it for document-level rules.
136+
'immediate' !== $eagerness
137+
) {
138+
$default_eagerness = $eagerness;
139+
}
140+
141+
return array(
142+
'mode' => $default_mode,
143+
'eagerness' => $default_eagerness,
144+
);
145+
}
146+
147+
/**
148+
* Returns the value of a speculative loading override, as supplied by a constant or an environment variable.
149+
*
150+
* The constant takes precedence over the environment variable, consistent with {@see wp_get_environment_type()}.
151+
*
152+
* @since 7.1.0
153+
* @access private
154+
*
155+
* @param string $name Name of the constant and environment variable to look up.
156+
* @return string|null The override value, or null if neither is set.
157+
*/
158+
function wp_get_speculative_loading_override( string $name ): ?string {
159+
$value = null;
160+
161+
// Check if the environment variable has been set, if `getenv` is available on the system.
162+
if ( function_exists( 'getenv' ) ) {
163+
$has_env = getenv( $name );
164+
if ( false !== $has_env ) {
165+
$value = $has_env;
166+
}
167+
}
168+
169+
// Fetch the value from a constant, which overrides the environment variable.
170+
if ( defined( $name ) ) {
171+
$has_constant = constant( $name );
172+
if ( is_string( $has_constant ) ) {
173+
$value = $has_constant;
174+
}
175+
}
176+
177+
return $value;
178+
}
179+
93180
/**
94181
* Returns the full speculation rules data based on the configuration.
95182
*
@@ -152,6 +239,7 @@ function wp_get_speculation_rules(): ?WP_Speculation_Rules {
152239
* @param string $mode Mode used to apply speculative loading. Either 'prefetch' or 'prerender'.
153240
*/
154241
$href_exclude_paths = (array) apply_filters( 'wp_speculation_rules_href_exclude_paths', array(), $mode );
242+
$href_exclude_paths = array_filter( $href_exclude_paths, 'is_string' );
155243

156244
// Ensure that:
157245
// 1. There are no duplicates.

tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,20 @@ class Tests_Speculative_Loading_wpGetSpeculationRules extends WP_UnitTestCase {
2121
'eagerness' => 'conservative',
2222
);
2323

24-
public function set_up() {
24+
/**
25+
* Stores the original speculative loading env values for cleanup.
26+
*
27+
* @var array<string, string|false>
28+
*/
29+
private array $original_env = array();
30+
31+
public function set_up(): void {
2532
parent::set_up();
2633

34+
foreach ( array( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ) as $name ) {
35+
$this->original_env[ $name ] = getenv( $name );
36+
}
37+
2738
add_filter(
2839
'template_directory_uri',
2940
static function () {
@@ -41,6 +52,18 @@ static function () {
4152
update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' );
4253
}
4354

55+
public function tear_down(): void {
56+
foreach ( $this->original_env as $name => $value ) {
57+
if ( false === $value ) {
58+
putenv( $name );
59+
} else {
60+
putenv( "{$name}={$value}" );
61+
}
62+
}
63+
64+
parent::tear_down();
65+
}
66+
4467
/**
4568
* Tests speculation rules output with prefetch for the different eagerness levels.
4669
*
@@ -558,4 +581,43 @@ function () {
558581
$this->assertSame( 'moderate', $rules['prerender'][0]['eagerness'] );
559582
$this->assertSame( 'eager', $rules['prerender'][1]['eagerness'] );
560583
}
584+
585+
/**
586+
* Tests that an overridden default eagerness is applied to the main document-level rule.
587+
*
588+
* @ticket 65624
589+
*/
590+
public function test_wp_get_speculation_rules_with_overridden_default_eagerness(): void {
591+
putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' );
592+
593+
$rules = wp_get_speculation_rules();
594+
$this->assertInstanceOf( WP_Speculation_Rules::class, $rules );
595+
596+
$rules = $rules->jsonSerialize();
597+
598+
$this->assertArrayHasKey( 'prefetch', $rules );
599+
$this->assertCount( 1, $rules['prefetch'] );
600+
$this->assertSame( 'moderate', $rules['prefetch'][0]['eagerness'] );
601+
}
602+
603+
/**
604+
* Tests that an eagerness of 'immediate' cannot be forced as the default.
605+
*
606+
* WordPress does not allow 'immediate' for the document-level rules it generates, so allowing it as a default
607+
* would cause the main rule to be rejected, leaving no speculation rules at all.
608+
*
609+
* @ticket 65624
610+
*/
611+
public function test_wp_get_speculation_rules_with_immediate_default_eagerness(): void {
612+
putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=immediate' );
613+
614+
$rules = wp_get_speculation_rules();
615+
$this->assertInstanceOf( WP_Speculation_Rules::class, $rules );
616+
617+
$rules = $rules->jsonSerialize();
618+
619+
$this->assertArrayHasKey( 'prefetch', $rules );
620+
$this->assertCount( 1, $rules['prefetch'] );
621+
$this->assertSame( 'conservative', $rules['prefetch'][0]['eagerness'] );
622+
}
561623
}

tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,35 @@
1212
*/
1313
class Tests_Speculative_Loading_wpGetSpeculationRulesConfiguration extends WP_UnitTestCase {
1414

15-
public function set_up() {
15+
/**
16+
* Stores the original speculative loading env values for cleanup.
17+
*
18+
* @var array<string, string|false>
19+
*/
20+
private array $original_env = array();
21+
22+
public function set_up(): void {
1623
parent::set_up();
1724

25+
foreach ( array( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ) as $name ) {
26+
$this->original_env[ $name ] = getenv( $name );
27+
}
28+
1829
update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' );
1930
}
2031

32+
public function tear_down(): void {
33+
foreach ( $this->original_env as $name => $value ) {
34+
if ( false === $value ) {
35+
putenv( $name );
36+
} else {
37+
putenv( "{$name}={$value}" );
38+
}
39+
}
40+
41+
parent::tear_down();
42+
}
43+
2144
/**
2245
* Tests that the default configuration is the expected value.
2346
*
@@ -264,4 +287,63 @@ public static function data_wp_get_speculation_rules_configuration_filter(): arr
264287
),
265288
);
266289
}
290+
291+
/**
292+
* Tests that an overridden default is what the 'auto' value resolves to.
293+
*
294+
* @ticket 65624
295+
*/
296+
public function test_wp_get_speculation_rules_configuration_with_overridden_default(): void {
297+
putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' );
298+
299+
$this->assertSame(
300+
array(
301+
'mode' => 'prefetch',
302+
'eagerness' => 'moderate',
303+
),
304+
wp_get_speculation_rules_configuration()
305+
);
306+
}
307+
308+
/**
309+
* Tests that an explicit eagerness from the filter takes precedence over an overridden default.
310+
*
311+
* This ensures a plugin such as Speculative Loading continues to win over a hosting provider's default.
312+
*
313+
* @ticket 65624
314+
*/
315+
public function test_wp_get_speculation_rules_configuration_filter_beats_overridden_default(): void {
316+
putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' );
317+
318+
add_filter(
319+
'wp_speculation_rules_configuration',
320+
static function () {
321+
return array(
322+
'mode' => 'auto',
323+
'eagerness' => 'conservative',
324+
);
325+
}
326+
);
327+
328+
$this->assertSame(
329+
array(
330+
'mode' => 'prefetch',
331+
'eagerness' => 'conservative',
332+
),
333+
wp_get_speculation_rules_configuration()
334+
);
335+
}
336+
337+
/**
338+
* Tests that speculative loading remains disabled when a default is overridden.
339+
*
340+
* @ticket 65624
341+
*/
342+
public function test_wp_get_speculation_rules_configuration_with_overridden_default_while_disabled(): void {
343+
putenv( 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS=moderate' );
344+
345+
add_filter( 'wp_speculation_rules_configuration', '__return_null' );
346+
347+
$this->assertNull( wp_get_speculation_rules_configuration() );
348+
}
267349
}

0 commit comments

Comments
 (0)