Make WordPress Core

Changeset 62752


Ignore:
Timestamp:
07/15/2026 07:22:36 AM (4 weeks ago)
Author:
westonruter
Message:

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 https://github.com/WordPress/wordpress-develop/pull/12514.
Follow-up to r59837.

Props westonruter, mukesh27, adamsilverstein, swissspidy.
See #64066, #62503, #64896, #64898.
Fixes #65624.

Location:
trunk
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-speculation-rules.php

    r61280 r62752  
    260260         *
    261261         * @since 6.8.0
    262          *
    263          * @param string $mode Speculation rules mode.
     262         * @since 7.1.0 The $mode param now allows mixed and not just string.
     263         *
     264         * @param mixed $mode Speculation rules mode.
    264265         * @return bool True if valid, false otherwise.
    265          */
    266         public static function is_valid_mode( string $mode ): bool {
    267                 return isset( self::$mode_allowlist[ $mode ] );
     266         *
     267         * @phpstan-assert-if-true 'prefetch'|'prerender' $mode
     268         */
     269        public static function is_valid_mode( $mode ): bool {
     270                return is_string( $mode ) && isset( self::$mode_allowlist[ $mode ] );
    268271        }
    269272
     
    272275         *
    273276         * @since 6.8.0
    274          *
    275          * @param string $eagerness Speculation rules eagerness.
     277         * @since 7.1.0 The $eagerness param now allows mixed and not just string.
     278         *
     279         * @param mixed $eagerness Speculation rules eagerness.
    276280         * @return bool True if valid, false otherwise.
    277          */
    278         public static function is_valid_eagerness( string $eagerness ): bool {
    279                 return isset( self::$eagerness_allowlist[ $eagerness ] );
     281         *
     282         * @phpstan-assert-if-true 'conservative'|'moderate'|'eager'|'immediate' $eagerness
     283         */
     284        public static function is_valid_eagerness( $eagerness ): bool {
     285                return is_string( $eagerness ) && isset( self::$eagerness_allowlist[ $eagerness ] );
    280286        }
    281287
  • trunk/src/wp-includes/speculative-loading.php

    r60681 r62752  
    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 */
    1826function wp_get_speculation_rules_configuration(): ?array {
     
    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(
     
    8999                'eagerness' => $config['eagerness'],
    90100        );
     101}
     102
     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 */
     124function 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 */
     158function 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;
    91178}
    92179
     
    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:
  • trunk/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php

    r59881 r62752  
    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();
     33
     34                foreach ( array( 'WP_SPECULATIVE_LOADING_DEFAULT_MODE', 'WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS' ) as $name ) {
     35                        $this->original_env[ $name ] = getenv( $name );
     36                }
    2637
    2738                add_filter(
     
    4051
    4152                update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' );
     53        }
     54
     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();
    4265        }
    4366
     
    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}
  • trunk/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php

    r59837 r62752  
    1313class 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%/' );
     30        }
     31
     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();
    1942        }
    2043
     
    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}
Note: See TracChangeset for help on using the changeset viewer.