wp/wp-includes/speculative-loading.php
changeset 22 8c2e4d02f4ef
equal deleted inserted replaced
21:48c4eec2b7e6 22:8c2e4d02f4ef
       
     1 <?php
       
     2 /**
       
     3  * Speculative loading functions.
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage Speculative Loading
       
     7  * @since 6.8.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Returns the speculation rules configuration.
       
    12  *
       
    13  * @since 6.8.0
       
    14  *
       
    15  * @return array<string, string>|null Associative array with 'mode' and 'eagerness' keys, or null if speculative
       
    16  *                                    loading is disabled.
       
    17  */
       
    18 function wp_get_speculation_rules_configuration(): ?array {
       
    19 	// By default, speculative loading is only enabled for sites with pretty permalinks when no user is logged in.
       
    20 	if ( ! is_user_logged_in() && get_option( 'permalink_structure' ) ) {
       
    21 		$config = array(
       
    22 			'mode'      => 'auto',
       
    23 			'eagerness' => 'auto',
       
    24 		);
       
    25 	} else {
       
    26 		$config = null;
       
    27 	}
       
    28 
       
    29 	/**
       
    30 	 * Filters the way that speculation rules are configured.
       
    31 	 *
       
    32 	 * The Speculation Rules API is a web API that allows to automatically prefetch or prerender certain URLs on the
       
    33 	 * page, which can lead to near-instant page load times. This is also referred to as speculative loading.
       
    34 	 *
       
    35 	 * There are two aspects to the configuration:
       
    36 	 * * The "mode" (whether to "prefetch" or "prerender" URLs).
       
    37 	 * * The "eagerness" (whether to speculatively load URLs in an "eager", "moderate", or "conservative" way).
       
    38 	 *
       
    39 	 * By default, the speculation rules configuration is decided by WordPress Core ("auto"). This filter can be used
       
    40 	 * to force a certain configuration, which could for instance load URLs more or less eagerly.
       
    41 	 *
       
    42 	 * For logged-in users or for sites that are not configured to use pretty permalinks, the default value is `null`,
       
    43 	 * indicating that speculative loading is entirely disabled.
       
    44 	 *
       
    45 	 * @since 6.8.0
       
    46 	 * @see https://developer.chrome.com/docs/web-platform/prerender-pages
       
    47 	 *
       
    48 	 * @param array<string, string>|null $config Associative array with 'mode' and 'eagerness' keys, or `null`. The
       
    49 	 *                                           default value for both of the keys is 'auto'. Other possible values
       
    50 	 *                                           for 'mode' are 'prefetch' and 'prerender'. Other possible values for
       
    51 	 *                                           'eagerness' are 'eager', 'moderate', and 'conservative'. The value
       
    52 	 *                                           `null` is used to disable speculative loading entirely.
       
    53 	 */
       
    54 	$config = apply_filters( 'wp_speculation_rules_configuration', $config );
       
    55 
       
    56 	// Allow the value `null` to indicate that speculative loading is disabled.
       
    57 	if ( null === $config ) {
       
    58 		return null;
       
    59 	}
       
    60 
       
    61 	// Sanitize the configuration and replace 'auto' with current defaults.
       
    62 	$default_mode      = 'prefetch';
       
    63 	$default_eagerness = 'conservative';
       
    64 	if ( ! is_array( $config ) ) {
       
    65 		return array(
       
    66 			'mode'      => $default_mode,
       
    67 			'eagerness' => $default_eagerness,
       
    68 		);
       
    69 	}
       
    70 	if (
       
    71 		! isset( $config['mode'] ) ||
       
    72 		'auto' === $config['mode'] ||
       
    73 		! WP_Speculation_Rules::is_valid_mode( $config['mode'] )
       
    74 	) {
       
    75 		$config['mode'] = $default_mode;
       
    76 	}
       
    77 	if (
       
    78 		! isset( $config['eagerness'] ) ||
       
    79 		'auto' === $config['eagerness'] ||
       
    80 		! WP_Speculation_Rules::is_valid_eagerness( $config['eagerness'] ) ||
       
    81 		// 'immediate' is a valid eagerness, but for safety WordPress does not allow it for document-level rules.
       
    82 		'immediate' === $config['eagerness']
       
    83 	) {
       
    84 		$config['eagerness'] = $default_eagerness;
       
    85 	}
       
    86 
       
    87 	return array(
       
    88 		'mode'      => $config['mode'],
       
    89 		'eagerness' => $config['eagerness'],
       
    90 	);
       
    91 }
       
    92 
       
    93 /**
       
    94  * Returns the full speculation rules data based on the configuration.
       
    95  *
       
    96  * Plugins with features that rely on frontend URLs to exclude from prefetching or prerendering should use the
       
    97  * {@see 'wp_speculation_rules_href_exclude_paths'} filter to ensure those URL patterns are excluded.
       
    98  *
       
    99  * Additional speculation rules other than the default rule from WordPress Core can be provided by using the
       
   100  * {@see 'wp_load_speculation_rules'} action and amending the passed WP_Speculation_Rules object.
       
   101  *
       
   102  * @since 6.8.0
       
   103  * @access private
       
   104  *
       
   105  * @return WP_Speculation_Rules|null Object representing the speculation rules to use, or null if speculative loading
       
   106  *                                   is disabled in the current context.
       
   107  */
       
   108 function wp_get_speculation_rules(): ?WP_Speculation_Rules {
       
   109 	$configuration = wp_get_speculation_rules_configuration();
       
   110 	if ( null === $configuration ) {
       
   111 		return null;
       
   112 	}
       
   113 
       
   114 	$mode      = $configuration['mode'];
       
   115 	$eagerness = $configuration['eagerness'];
       
   116 
       
   117 	$prefixer = new WP_URL_Pattern_Prefixer();
       
   118 
       
   119 	$base_href_exclude_paths = array(
       
   120 		$prefixer->prefix_path_pattern( '/wp-*.php', 'site' ),
       
   121 		$prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ),
       
   122 		$prefixer->prefix_path_pattern( '/*', 'uploads' ),
       
   123 		$prefixer->prefix_path_pattern( '/*', 'content' ),
       
   124 		$prefixer->prefix_path_pattern( '/*', 'plugins' ),
       
   125 		$prefixer->prefix_path_pattern( '/*', 'template' ),
       
   126 		$prefixer->prefix_path_pattern( '/*', 'stylesheet' ),
       
   127 	);
       
   128 
       
   129 	/*
       
   130 	 * If pretty permalinks are enabled, exclude any URLs with query parameters.
       
   131 	 * Otherwise, exclude specifically the URLs with a `_wpnonce` query parameter or any other query parameter
       
   132 	 * containing the word `nonce`.
       
   133 	 */
       
   134 	if ( get_option( 'permalink_structure' ) ) {
       
   135 		$base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?(.+)', 'home' );
       
   136 	} else {
       
   137 		$base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?*(^|&)*nonce*=*', 'home' );
       
   138 	}
       
   139 
       
   140 	/**
       
   141 	 * Filters the paths for which speculative loading should be disabled.
       
   142 	 *
       
   143 	 * All paths should start in a forward slash, relative to the root document. The `*` can be used as a wildcard.
       
   144 	 * If the WordPress site is in a subdirectory, the exclude paths will automatically be prefixed as necessary.
       
   145 	 *
       
   146 	 * Note that WordPress always excludes certain path patterns such as `/wp-login.php` and `/wp-admin/*`, and those
       
   147 	 * cannot be modified using the filter.
       
   148 	 *
       
   149 	 * @since 6.8.0
       
   150 	 *
       
   151 	 * @param string[] $href_exclude_paths Additional path patterns to disable speculative loading for.
       
   152 	 * @param string   $mode               Mode used to apply speculative loading. Either 'prefetch' or 'prerender'.
       
   153 	 */
       
   154 	$href_exclude_paths = (array) apply_filters( 'wp_speculation_rules_href_exclude_paths', array(), $mode );
       
   155 
       
   156 	// Ensure that:
       
   157 	// 1. There are no duplicates.
       
   158 	// 2. The base paths cannot be removed.
       
   159 	// 3. The array has sequential keys (i.e. array_is_list()).
       
   160 	$href_exclude_paths = array_values(
       
   161 		array_unique(
       
   162 			array_merge(
       
   163 				$base_href_exclude_paths,
       
   164 				array_map(
       
   165 					static function ( string $href_exclude_path ) use ( $prefixer ): string {
       
   166 						return $prefixer->prefix_path_pattern( $href_exclude_path );
       
   167 					},
       
   168 					$href_exclude_paths
       
   169 				)
       
   170 			)
       
   171 		)
       
   172 	);
       
   173 
       
   174 	$speculation_rules = new WP_Speculation_Rules();
       
   175 
       
   176 	$main_rule_conditions = array(
       
   177 		// Include any URLs within the same site.
       
   178 		array(
       
   179 			'href_matches' => $prefixer->prefix_path_pattern( '/*' ),
       
   180 		),
       
   181 		// Except for excluded paths.
       
   182 		array(
       
   183 			'not' => array(
       
   184 				'href_matches' => $href_exclude_paths,
       
   185 			),
       
   186 		),
       
   187 		// Also exclude rel=nofollow links, as certain plugins use that on their links that perform an action.
       
   188 		array(
       
   189 			'not' => array(
       
   190 				'selector_matches' => 'a[rel~="nofollow"]',
       
   191 			),
       
   192 		),
       
   193 		// Also exclude links that are explicitly marked to opt out, either directly or via a parent element.
       
   194 		array(
       
   195 			'not' => array(
       
   196 				'selector_matches' => ".no-{$mode}, .no-{$mode} a",
       
   197 			),
       
   198 		),
       
   199 	);
       
   200 
       
   201 	// If using 'prerender', also exclude links that opt out of 'prefetch' because it's part of 'prerender'.
       
   202 	if ( 'prerender' === $mode ) {
       
   203 		$main_rule_conditions[] = array(
       
   204 			'not' => array(
       
   205 				'selector_matches' => '.no-prefetch, .no-prefetch a',
       
   206 			),
       
   207 		);
       
   208 	}
       
   209 
       
   210 	$speculation_rules->add_rule(
       
   211 		$mode,
       
   212 		'main',
       
   213 		array(
       
   214 			'source'    => 'document',
       
   215 			'where'     => array(
       
   216 				'and' => $main_rule_conditions,
       
   217 			),
       
   218 			'eagerness' => $eagerness,
       
   219 		)
       
   220 	);
       
   221 
       
   222 	/**
       
   223 	 * Fires when speculation rules data is loaded, allowing to amend the rules.
       
   224 	 *
       
   225 	 * @since 6.8.0
       
   226 	 *
       
   227 	 * @param WP_Speculation_Rules $speculation_rules Object representing the speculation rules to use.
       
   228 	 */
       
   229 	do_action( 'wp_load_speculation_rules', $speculation_rules );
       
   230 
       
   231 	return $speculation_rules;
       
   232 }
       
   233 
       
   234 /**
       
   235  * Prints the speculation rules.
       
   236  *
       
   237  * For browsers that do not support speculation rules yet, the `script[type="speculationrules"]` tag will be ignored.
       
   238  *
       
   239  * @since 6.8.0
       
   240  * @access private
       
   241  */
       
   242 function wp_print_speculation_rules(): void {
       
   243 	$speculation_rules = wp_get_speculation_rules();
       
   244 	if ( null === $speculation_rules ) {
       
   245 		return;
       
   246 	}
       
   247 
       
   248 	wp_print_inline_script_tag(
       
   249 		(string) wp_json_encode(
       
   250 			$speculation_rules
       
   251 		),
       
   252 		array( 'type' => 'speculationrules' )
       
   253 	);
       
   254 }