|
1 <?php |
|
2 /** |
|
3 * Class 'WP_Speculation_Rules'. |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Speculative Loading |
|
7 * @since 6.8.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Class representing a set of speculation rules. |
|
12 * |
|
13 * @since 6.8.0 |
|
14 * @access private |
|
15 */ |
|
16 final class WP_Speculation_Rules implements JsonSerializable { |
|
17 |
|
18 /** |
|
19 * Stored rules, as a map of `$mode => $rules` pairs. |
|
20 * |
|
21 * Every `$rules` value is a map of `$id => $rule` pairs. |
|
22 * |
|
23 * @since 6.8.0 |
|
24 * @var array<string, array<string, mixed>> |
|
25 */ |
|
26 private $rules_by_mode = array(); |
|
27 |
|
28 /** |
|
29 * The allowed speculation rules modes as a map, used for validation. |
|
30 * |
|
31 * @since 6.8.0 |
|
32 * @var array<string, bool> |
|
33 */ |
|
34 private static $mode_allowlist = array( |
|
35 'prefetch' => true, |
|
36 'prerender' => true, |
|
37 ); |
|
38 |
|
39 /** |
|
40 * The allowed speculation rules eagerness levels as a map, used for validation. |
|
41 * |
|
42 * @since 6.8.0 |
|
43 * @var array<string, bool> |
|
44 */ |
|
45 private static $eagerness_allowlist = array( |
|
46 'immediate' => true, |
|
47 'eager' => true, |
|
48 'moderate' => true, |
|
49 'conservative' => true, |
|
50 ); |
|
51 |
|
52 /** |
|
53 * The allowed speculation rules sources as a map, used for validation. |
|
54 * |
|
55 * @since 6.8.0 |
|
56 * @var array<string, bool> |
|
57 */ |
|
58 private static $source_allowlist = array( |
|
59 'list' => true, |
|
60 'document' => true, |
|
61 ); |
|
62 |
|
63 /** |
|
64 * Adds a speculation rule to the speculation rules to consider. |
|
65 * |
|
66 * @since 6.8.0 |
|
67 * |
|
68 * @param string $mode Speculative loading mode. Either 'prefetch' or 'prerender'. |
|
69 * @param string $id Unique string identifier for the speculation rule. |
|
70 * @param array<string, mixed> $rule Associative array of rule arguments. |
|
71 * @return bool True on success, false if invalid parameters are provided. |
|
72 */ |
|
73 public function add_rule( string $mode, string $id, array $rule ): bool { |
|
74 if ( ! self::is_valid_mode( $mode ) ) { |
|
75 _doing_it_wrong( |
|
76 __METHOD__, |
|
77 sprintf( |
|
78 /* translators: %s: invalid mode value */ |
|
79 __( 'The value "%s" is not a valid speculation rules mode.' ), |
|
80 esc_html( $mode ) |
|
81 ), |
|
82 '6.8.0' |
|
83 ); |
|
84 return false; |
|
85 } |
|
86 |
|
87 if ( ! $this->is_valid_id( $id ) ) { |
|
88 _doing_it_wrong( |
|
89 __METHOD__, |
|
90 sprintf( |
|
91 /* translators: %s: invalid ID value */ |
|
92 __( 'The value "%s" is not a valid ID for a speculation rule.' ), |
|
93 esc_html( $id ) |
|
94 ), |
|
95 '6.8.0' |
|
96 ); |
|
97 return false; |
|
98 } |
|
99 |
|
100 if ( $this->has_rule( $mode, $id ) ) { |
|
101 _doing_it_wrong( |
|
102 __METHOD__, |
|
103 sprintf( |
|
104 /* translators: %s: invalid ID value */ |
|
105 __( 'A speculation rule with ID "%s" already exists.' ), |
|
106 esc_html( $id ) |
|
107 ), |
|
108 '6.8.0' |
|
109 ); |
|
110 return false; |
|
111 } |
|
112 |
|
113 /* |
|
114 * Perform some basic speculation rule validation. |
|
115 * Every rule must have either a 'where' key or a 'urls' key, but not both. |
|
116 * The presence of a 'where' key implies a 'source' of 'document', while the presence of a 'urls' key implies |
|
117 * a 'source' of 'list'. |
|
118 */ |
|
119 if ( |
|
120 ( ! isset( $rule['where'] ) && ! isset( $rule['urls'] ) ) || |
|
121 ( isset( $rule['where'] ) && isset( $rule['urls'] ) ) |
|
122 ) { |
|
123 _doing_it_wrong( |
|
124 __METHOD__, |
|
125 sprintf( |
|
126 /* translators: 1: allowed key, 2: alternative allowed key */ |
|
127 __( 'A speculation rule must include either a "%1$s" key or a "%2$s" key, but not both.' ), |
|
128 'where', |
|
129 'urls' |
|
130 ), |
|
131 '6.8.0' |
|
132 ); |
|
133 return false; |
|
134 } |
|
135 if ( isset( $rule['source'] ) ) { |
|
136 if ( ! self::is_valid_source( $rule['source'] ) ) { |
|
137 _doing_it_wrong( |
|
138 __METHOD__, |
|
139 sprintf( |
|
140 /* translators: %s: invalid source value */ |
|
141 __( 'The value "%s" is not a valid source for a speculation rule.' ), |
|
142 esc_html( $rule['source'] ) |
|
143 ), |
|
144 '6.8.0' |
|
145 ); |
|
146 return false; |
|
147 } |
|
148 |
|
149 if ( 'list' === $rule['source'] && isset( $rule['where'] ) ) { |
|
150 _doing_it_wrong( |
|
151 __METHOD__, |
|
152 sprintf( |
|
153 /* translators: 1: source value, 2: forbidden key */ |
|
154 __( 'A speculation rule of source "%1$s" must not include a "%2$s" key.' ), |
|
155 'list', |
|
156 'where' |
|
157 ), |
|
158 '6.8.0' |
|
159 ); |
|
160 return false; |
|
161 } |
|
162 |
|
163 if ( 'document' === $rule['source'] && isset( $rule['urls'] ) ) { |
|
164 _doing_it_wrong( |
|
165 __METHOD__, |
|
166 sprintf( |
|
167 /* translators: 1: source value, 2: forbidden key */ |
|
168 __( 'A speculation rule of source "%1$s" must not include a "%2$s" key.' ), |
|
169 'document', |
|
170 'urls' |
|
171 ), |
|
172 '6.8.0' |
|
173 ); |
|
174 return false; |
|
175 } |
|
176 } |
|
177 |
|
178 // If there is an 'eagerness' key specified, make sure it's valid. |
|
179 if ( isset( $rule['eagerness'] ) ) { |
|
180 if ( ! self::is_valid_eagerness( $rule['eagerness'] ) ) { |
|
181 _doing_it_wrong( |
|
182 __METHOD__, |
|
183 sprintf( |
|
184 /* translators: %s: invalid eagerness value */ |
|
185 __( 'The value "%s" is not a valid eagerness for a speculation rule.' ), |
|
186 esc_html( $rule['eagerness'] ) |
|
187 ), |
|
188 '6.8.0' |
|
189 ); |
|
190 return false; |
|
191 } |
|
192 |
|
193 if ( isset( $rule['where'] ) && 'immediate' === $rule['eagerness'] ) { |
|
194 _doing_it_wrong( |
|
195 __METHOD__, |
|
196 sprintf( |
|
197 /* translators: %s: forbidden eagerness value */ |
|
198 __( 'The eagerness value "%s" is forbidden for document-level speculation rules.' ), |
|
199 'immediate' |
|
200 ), |
|
201 '6.8.0' |
|
202 ); |
|
203 return false; |
|
204 } |
|
205 } |
|
206 |
|
207 if ( ! isset( $this->rules_by_mode[ $mode ] ) ) { |
|
208 $this->rules_by_mode[ $mode ] = array(); |
|
209 } |
|
210 |
|
211 $this->rules_by_mode[ $mode ][ $id ] = $rule; |
|
212 return true; |
|
213 } |
|
214 |
|
215 /** |
|
216 * Checks whether a speculation rule for the given mode and ID already exists. |
|
217 * |
|
218 * @since 6.8.0 |
|
219 * |
|
220 * @param string $mode Speculative loading mode. Either 'prefetch' or 'prerender'. |
|
221 * @param string $id Unique string identifier for the speculation rule. |
|
222 * @return bool True if the rule already exists, false otherwise. |
|
223 */ |
|
224 public function has_rule( string $mode, string $id ): bool { |
|
225 return isset( $this->rules_by_mode[ $mode ][ $id ] ); |
|
226 } |
|
227 |
|
228 /** |
|
229 * Returns the speculation rules data ready to be JSON-encoded. |
|
230 * |
|
231 * @since 6.8.0 |
|
232 * |
|
233 * @return array<string, array<string, mixed>> Speculation rules data. |
|
234 */ |
|
235 #[ReturnTypeWillChange] |
|
236 public function jsonSerialize() { |
|
237 // Strip the IDs for JSON output, since they are not relevant for the Speculation Rules API. |
|
238 return array_map( |
|
239 static function ( array $rules ) { |
|
240 return array_values( $rules ); |
|
241 }, |
|
242 array_filter( $this->rules_by_mode ) |
|
243 ); |
|
244 } |
|
245 |
|
246 /** |
|
247 * Checks whether the given ID is valid. |
|
248 * |
|
249 * @since 6.8.0 |
|
250 * |
|
251 * @param string $id Unique string identifier for the speculation rule. |
|
252 * @return bool True if the ID is valid, false otherwise. |
|
253 */ |
|
254 private function is_valid_id( string $id ): bool { |
|
255 return (bool) preg_match( '/^[a-z][a-z0-9_-]+$/', $id ); |
|
256 } |
|
257 |
|
258 /** |
|
259 * Checks whether the given speculation rules mode is valid. |
|
260 * |
|
261 * @since 6.8.0 |
|
262 * |
|
263 * @param string $mode Speculation rules mode. |
|
264 * @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 ] ); |
|
268 } |
|
269 |
|
270 /** |
|
271 * Checks whether the given speculation rules eagerness is valid. |
|
272 * |
|
273 * @since 6.8.0 |
|
274 * |
|
275 * @param string $eagerness Speculation rules eagerness. |
|
276 * @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 ] ); |
|
280 } |
|
281 |
|
282 /** |
|
283 * Checks whether the given speculation rules source is valid. |
|
284 * |
|
285 * @since 6.8.0 |
|
286 * |
|
287 * @param string $source Speculation rules source. |
|
288 * @return bool True if valid, false otherwise. |
|
289 */ |
|
290 public static function is_valid_source( string $source ): bool { |
|
291 return isset( self::$source_allowlist[ $source ] ); |
|
292 } |
|
293 } |