|
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 } |