PluginProbe
Optimization Detective / trunk
Optimization Detective vtrunk
1.0.0-beta7 trunk 0.1.0 0.1.1 0.2.0 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0 0.9.0 1.0.0-beta1 1.0.0-beta2 1.0.0-beta3 1.0.0-beta4 1.0.0-beta5 1.0.0-beta6
optimization-detective / storage / data.php

data.php in Optimization Detective trunk, at storage/data.php

480 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Metrics storage data.
4 *
5 * @package optimization-detective
6 * @since 0.1.0
7 */
8
9 declare( strict_types = 1 );
10
11 // @codeCoverageIgnoreStart
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15 // @codeCoverageIgnoreEnd
16
17 /**
18 * Gets the freshness age (TTL) for a given URL Metric.
19 *
20 * When a URL Metric expires, it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint.
21 *
22 * @since 0.1.0
23 * @access private
24 *
25 * @return int<-1, max> Expiration TTL in seconds.
26 */
27 function od_get_url_metric_freshness_ttl(): int {
28 /**
29 * Filters age (TTL) for which a URL Metric can be considered fresh.
30 *
31 * @since 0.1.0
32 * @since 1.0.0 Negative values disable timestamp-based freshness checks.
33 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_url_metric_freshness_ttl
34 *
35 * @param int $ttl Expiration TTL in seconds. Defaults to 1 week.
36 */
37 $ttl = (int) apply_filters( 'od_url_metric_freshness_ttl', WEEK_IN_SECONDS );
38 return max( -1, $ttl );
39 }
40
41 /**
42 * Gets the normalized query vars for the current request.
43 *
44 * This is used as a cache key for stored URL Metrics.
45 *
46 * @since 0.1.0
47 * @access private
48 *
49 * @return array<string, mixed> Normalized query vars.
50 */
51 function od_get_normalized_query_vars(): array {
52 global $wp;
53
54 // Note that the order of this array is naturally normalized since it is
55 // assembled by iterating over public_query_vars.
56 $normalized_query_vars = $wp->query_vars;
57
58 // Normalize unbounded query vars.
59 if ( is_404() ) {
60 $normalized_query_vars = array(
61 'error' => 404,
62 );
63 }
64
65 return $normalized_query_vars;
66 }
67
68 /**
69 * Get the URL for the current request.
70 *
71 * This is essentially the REQUEST_URI prefixed by the scheme and host for the home URL.
72 * This is needed in particular due to subdirectory installations.
73 *
74 * @since 0.1.1
75 * @access private
76 *
77 * @return string Current URL.
78 */
79 function od_get_current_url(): string {
80 $parsed_url = wp_parse_url( home_url() );
81 if ( ! is_array( $parsed_url ) ) {
82 $parsed_url = array();
83 }
84
85 if ( ! isset( $parsed_url['scheme'] ) ) {
86 $parsed_url['scheme'] = is_ssl() ? 'https' : 'http';
87 }
88 if ( ! isset( $parsed_url['host'] ) ) {
89 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
90 $parsed_url['host'] = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : 'localhost';
91 }
92
93 $current_url = $parsed_url['scheme'] . '://' . $parsed_url['host'];
94 if ( isset( $parsed_url['port'] ) ) {
95 $current_url .= ':' . $parsed_url['port'];
96 }
97 $current_url .= '/';
98
99 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
100 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
101 $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' );
102 }
103
104 // TODO: We should be able to assert that this returns an non-empty-string.
105 return esc_url_raw( $current_url );
106 }
107
108 /**
109 * Gets slug for URL Metrics.
110 *
111 * A slug is the hash of the normalized query vars.
112 *
113 * @since 0.1.0
114 * @access private
115 *
116 * @see od_get_normalized_query_vars()
117 *
118 * @param array<string, mixed> $query_vars Normalized query vars.
119 * @return non-empty-string Slug.
120 */
121 function od_get_url_metrics_slug( array $query_vars ): string {
122 // TODO: The JSON_UNESCAPED_SLASHES flag could be used here, but beware this could invalidate URL Metrics. See <https://github.com/WordPress/performance/pull/1949>.
123 return md5( (string) wp_json_encode( $query_vars ) );
124 }
125
126 /**
127 * Gets the current template for a block theme or a classic theme.
128 *
129 * @since 0.9.0
130 * @access private
131 *
132 * @global string|null $_wp_current_template_id Current template ID.
133 * @global string|null $template Template file path.
134 *
135 * @return string|WP_Block_Template|null Template.
136 */
137 function od_get_current_theme_template() {
138 global $template, $_wp_current_template_id;
139
140 if ( wp_is_block_theme() && isset( $_wp_current_template_id ) ) {
141 $block_template = get_block_template( $_wp_current_template_id );
142 if ( $block_template instanceof WP_Block_Template ) {
143 return $block_template;
144 }
145 }
146 if ( isset( $template ) && is_string( $template ) ) {
147 return basename( $template );
148 }
149 return null;
150 }
151
152 /**
153 * Gets the current ETag for URL Metrics.
154 *
155 * Generates a hash based on the IDs of registered tag visitors, the queried object,
156 * posts in The Loop, and theme information in the current environment. This ETag
157 * is used to assess if the URL Metrics are stale when its value changes.
158 *
159 * @since 0.9.0
160 * @access private
161 *
162 * @param OD_Tag_Visitor_Registry $tag_visitor_registry Tag visitor registry.
163 * @param WP_Query|null $wp_query The WP_Query instance.
164 * @param string|WP_Block_Template|null $current_template The current template being used.
165 * @return non-empty-string Current ETag.
166 */
167 function od_get_current_url_metrics_etag( OD_Tag_Visitor_Registry $tag_visitor_registry, ?WP_Query $wp_query, $current_template ): string {
168 $queried_object = $wp_query instanceof WP_Query ? $wp_query->get_queried_object() : null;
169 $queried_object_data = array(
170 'id' => null,
171 'type' => null,
172 );
173
174 if ( $queried_object instanceof WP_Post ) {
175 $queried_object_data['id'] = $queried_object->ID;
176 $queried_object_data['type'] = 'post';
177 $queried_object_data['post_modified_gmt'] = $queried_object->post_modified_gmt;
178 } elseif ( $queried_object instanceof WP_Term ) {
179 $queried_object_data['id'] = $queried_object->term_id;
180 $queried_object_data['type'] = 'term';
181 } elseif ( $queried_object instanceof WP_User ) {
182 $queried_object_data['id'] = $queried_object->ID;
183 $queried_object_data['type'] = 'user';
184 } elseif ( $queried_object instanceof WP_Post_Type ) {
185 $queried_object_data['type'] = $queried_object->name;
186 }
187
188 $active_plugins = (array) get_option( 'active_plugins', array() );
189 if ( is_multisite() ) {
190 $active_plugins = array_unique(
191 array_merge(
192 $active_plugins,
193 array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) )
194 )
195 );
196 }
197 sort( $active_plugins );
198
199 $data = array(
200 'xpath_version' => 2, // Bump whenever a major change to the XPath format occurs so that new URL Metrics are proactively gathered.
201 'tag_visitors' => array_keys( iterator_to_array( $tag_visitor_registry ) ),
202 'queried_object' => $queried_object_data,
203 'queried_posts' => array_filter(
204 array_map(
205 static function ( $post ): ?array {
206 if ( is_int( $post ) ) {
207 $post = get_post( $post );
208 }
209 if ( ! ( $post instanceof WP_Post ) ) {
210 return null;
211 }
212 return array(
213 'id' => $post->ID,
214 'post_modified_gmt' => $post->post_modified_gmt,
215 );
216 },
217 ( $wp_query instanceof WP_Query && is_array( $wp_query->posts ) ) ? $wp_query->posts : array()
218 )
219 ),
220 'active_theme' => array(
221 'template' => array(
222 'name' => get_template(),
223 'version' => wp_get_theme( get_template() )->get( 'Version' ),
224 ),
225 'stylesheet' => array(
226 'name' => get_stylesheet(),
227 'version' => wp_get_theme()->get( 'Version' ),
228 ),
229 ),
230 'active_plugins' => $active_plugins,
231 'current_template' => $current_template instanceof WP_Block_Template ? get_object_vars( $current_template ) : $current_template,
232 );
233
234 /**
235 * Filters the data that goes into computing the current ETag for URL Metrics.
236 *
237 * @since 0.9.0
238 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_current_url_metrics_etag_data
239 *
240 * @param array<string, mixed> $data Data.
241 */
242 $data = (array) apply_filters( 'od_current_url_metrics_etag_data', $data );
243
244 // TODO: The JSON_UNESCAPED_SLASHES flag could be used here.
245 return md5( (string) wp_json_encode( $data ) );
246 }
247
248 /**
249 * Computes HMAC for storing URL Metrics for a specific slug.
250 *
251 * This is used in the REST API to authenticate the storage of new URL Metrics from a given URL.
252 *
253 * @since 0.8.0
254 * @since 0.9.0 Introduced the `$current_etag` parameter.
255 * @access private
256 *
257 * @see od_verify_url_metrics_storage_hmac()
258 * @see od_get_url_metrics_slug()
259 *
260 * @param non-empty-string $slug Slug (hash of normalized query vars).
261 * @param non-empty-string $current_etag Current ETag.
262 * @param string $url URL.
263 * @param positive-int|null $cache_purge_post_id Cache purge post ID.
264 * @return non-empty-string HMAC.
265 */
266 function od_get_url_metrics_storage_hmac( string $slug, string $current_etag, string $url, ?int $cache_purge_post_id = null ): string {
267 $action = "store_url_metric:$slug:$current_etag:$url:$cache_purge_post_id";
268
269 /**
270 * HMAC.
271 *
272 * @var non-empty-string $hmac
273 */
274 $hmac = wp_hash( $action, 'nonce' );
275 return $hmac;
276 }
277
278 /**
279 * Verifies HMAC for storing URL Metrics for a specific slug.
280 *
281 * @since 0.8.0
282 * @since 0.9.0 Introduced the `$current_etag` parameter.
283 * @access private
284 *
285 * @see od_get_url_metrics_storage_hmac()
286 * @see od_get_url_metrics_slug()
287 *
288 * @param non-empty-string $hmac HMAC.
289 * @param non-empty-string $slug Slug (hash of normalized query vars).
290 * @param non-empty-string $current_etag Current ETag.
291 * @param string $url URL.
292 * @param positive-int|null $cache_purge_post_id Cache purge post ID.
293 * @return bool Whether the HMAC is valid.
294 */
295 function od_verify_url_metrics_storage_hmac( string $hmac, string $slug, string $current_etag, string $url, ?int $cache_purge_post_id = null ): bool {
296 return hash_equals( od_get_url_metrics_storage_hmac( $slug, $current_etag, $url, $cache_purge_post_id ), $hmac );
297 }
298
299 /**
300 * Gets the minimum allowed viewport aspect ratio for URL Metrics.
301 *
302 * @since 0.6.0
303 * @access private
304 *
305 * @return float Minimum viewport aspect ratio for URL Metrics.
306 */
307 function od_get_minimum_viewport_aspect_ratio(): float {
308 /**
309 * Filters the minimum allowed viewport aspect ratio for URL Metrics.
310 *
311 * @since 0.6.0
312 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_minimum_viewport_aspect_ratio
313 *
314 * @param float $minimum_viewport_aspect_ratio Minimum viewport aspect ratio.
315 */
316 return (float) apply_filters( 'od_minimum_viewport_aspect_ratio', 0.4 );
317 }
318
319 /**
320 * Gets the maximum allowed viewport aspect ratio for URL Metrics.
321 *
322 * @since 0.6.0
323 * @access private
324 *
325 * @return float Maximum viewport aspect ratio for URL Metrics.
326 */
327 function od_get_maximum_viewport_aspect_ratio(): float {
328 /**
329 * Filters the maximum allowed viewport aspect ratio for URL Metrics.
330 *
331 * @since 0.6.0
332 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_maximum_viewport_aspect_ratio
333 *
334 * @param float $maximum_viewport_aspect_ratio Maximum viewport aspect ratio.
335 */
336 return (float) apply_filters( 'od_maximum_viewport_aspect_ratio', 2.5 );
337 }
338
339 /**
340 * Gets the breakpoint max widths to group URL Metrics for various viewports.
341 *
342 * Each number represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then
343 * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three
344 * provided breakpoints (320, 480, 576), then this means there will be four groups:
345 *
346 * 1. 0-320 (small smartphone)
347 * 2. 321-480 (normal smartphone)
348 * 3. 481-576 (phablets)
349 * 4. >576 (desktop)
350 *
351 * The default breakpoints are reused from Gutenberg where the _breakpoints.scss file includes these variables:
352 *
353 * $break-medium: 782px; // adminbar goes big
354 * $break-small: 600px;
355 * $break-mobile: 480px;
356 *
357 * These breakpoints appear to be used the most in media queries that affect frontend styles.
358 *
359 * This array may be empty, in which case there are no responsive breakpoints, and all URL Metrics are collected in a
360 * single group.
361 *
362 * @since 0.1.0
363 * @access private
364 * @link https://github.com/WordPress/gutenberg/blob/093d52cbfd3e2c140843d3fb91ad3d03330320a5/packages/base-styles/_breakpoints.scss#L11-L13
365 *
366 * @return positive-int[] Breakpoint max widths, sorted in ascending order.
367 */
368 function od_get_breakpoint_max_widths(): array {
369 $breakpoint_max_widths = array_map(
370 static function ( $original_breakpoint ): int {
371 $breakpoint = $original_breakpoint;
372 if ( $breakpoint <= 0 ) {
373 $breakpoint = 1;
374 _doing_it_wrong(
375 esc_html( "Filter: 'od_breakpoint_max_widths'" ),
376 esc_html(
377 sprintf(
378 /* translators: %s is the actual breakpoint max width */
379 __( 'Breakpoint must be greater zero, but saw "%s".', 'optimization-detective' ),
380 $original_breakpoint
381 )
382 ),
383 ''
384 );
385 }
386 return $breakpoint;
387 },
388 /**
389 * Filters the breakpoint max widths to group URL Metrics for various viewports.
390 *
391 * @since 0.1.0
392 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_breakpoint_max_widths
393 *
394 * @param positive-int[] $breakpoint_max_widths Max widths for viewport breakpoints. Defaults to [480, 600, 782].
395 */
396 array_map( 'intval', (array) apply_filters( 'od_breakpoint_max_widths', array( 480, 600, 782 ) ) )
397 );
398
399 $breakpoint_max_widths = array_unique( $breakpoint_max_widths, SORT_NUMERIC );
400 sort( $breakpoint_max_widths );
401 return $breakpoint_max_widths;
402 }
403
404 /**
405 * Gets the sample size for a breakpoint's URL Metrics on a given URL.
406 *
407 * A breakpoint divides URL Metrics for viewports which are smaller and those which are larger. Given the default
408 * sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum
409 * total of 6 URL Metrics stored for a given URL: 3 for mobile and 3 for desktop.
410 *
411 * @since 0.1.0
412 * @access private
413 *
414 * @return int<1, max> Sample size.
415 */
416 function od_get_url_metrics_breakpoint_sample_size(): int {
417 /**
418 * Filters the sample size for a breakpoint's URL Metrics on a given URL.
419 *
420 * @since 0.1.0
421 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_url_metrics_breakpoint_sample_size
422 *
423 * @param int $sample_size Sample size. Defaults to 3.
424 */
425 $sample_size = (int) apply_filters( 'od_url_metrics_breakpoint_sample_size', 3 );
426
427 if ( $sample_size <= 0 ) {
428 _doing_it_wrong(
429 esc_html( "Filter: 'od_url_metrics_breakpoint_sample_size'" ),
430 esc_html(
431 sprintf(
432 /* translators: %s is the sample size */
433 __( 'Sample size must greater than zero, but saw "%s".', 'optimization-detective' ),
434 $sample_size
435 )
436 ),
437 ''
438 );
439 $sample_size = 1;
440 }
441
442 return $sample_size;
443 }
444
445 /**
446 * Gets the maximum allowed size in bytes for a URL Metric serialized to JSON.
447 *
448 * @since 1.0.0
449 * @access private
450 *
451 * @return positive-int Maximum allowed byte size.
452 */
453 function od_get_maximum_url_metric_size(): int {
454 /**
455 * Filters the maximum allowed size in bytes for a URL Metric serialized to JSON.
456 *
457 * @since 1.0.0
458 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_maximum_url_metric_size
459 *
460 * @param int $max_size Maximum allowed byte size.
461 * @return int Filtered maximum allowed byte size.
462 */
463 $size = (int) apply_filters( 'od_maximum_url_metric_size', MB_IN_BYTES );
464 if ( $size <= 0 ) {
465 _doing_it_wrong(
466 esc_html( "Filter: 'od_maximum_url_metric_size'" ),
467 esc_html(
468 sprintf(
469 /* translators: %s: size */
470 __( 'Invalid size "%s". Must be greater than zero.', 'optimization-detective' ),
471 $size
472 )
473 ),
474 'Optimization Detective 1.0.0'
475 );
476 $size = MB_IN_BYTES;
477 }
478 return $size;
479 }
480