-
Notifications
You must be signed in to change notification settings - Fork 143
Expand file tree
/
Copy pathdetection.php
More file actions
313 lines (282 loc) · 12.4 KB
/
detection.php
File metadata and controls
313 lines (282 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
<?php
/**
* Detection for Optimization Detective.
*
* @package optimization-detective
* @since 0.1.0
*/
declare( strict_types = 1 );
// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd
/**
* Gets the ID for a post related to this response so that page caches can be told to invalidate their cache.
*
* If the queried object for the response is a post, then that post's ID is used. Otherwise, it uses the ID of the first
* post in The Loop.
*
* When the queried object is a post (e.g. is_singular, is_posts_page, is_front_page w/ show_on_front=page), then this
* is the perfect match. A page caching plugin will be able to most reliably invalidate the cache for a URL via
* this ID if the relevant actions are triggered for the post (e.g. clean_post_cache, save_post, transition_post_status).
*
* Otherwise, if the response is an archive page or the front page where show_on_front=posts (i.e. is_home), then
* there is no singular post object that represents the URL. In this case, we get the first post in the main
* loop. By triggering the relevant actions for this post ID, page caches will be more likely able to invalidate
* the related URLs. Page caching plugins which leverage surrogate keys will be the most reliable here. Otherwise,
* caching plugins may just resort to automatically purging the cache for the homepage whenever any post is edited,
* which is better than nothing.
*
* There should not be any situation by default in which a page optimized with Optimization Detective does not have such
* a post available for cache purging. As seen in {@see od_can_optimize_response()}, when such a post ID is not
* available for cache purging, then it returns false, as it also does in another case like if is_404().
*
* @since 0.8.0
* @access private
*
* @global WP_Query $wp_query WordPress Query object.
*
* @return positive-int|null Post ID or null if none found.
*/
function od_get_cache_purge_post_id(): ?int {
$queried_object = get_queried_object();
if ( $queried_object instanceof WP_Post && $queried_object->ID > 0 ) {
return $queried_object->ID;
}
global $wp_query;
if (
$wp_query instanceof WP_Query
&&
$wp_query->post_count > 0
&&
isset( $wp_query->posts[0] )
&&
$wp_query->posts[0] instanceof WP_Post
&&
$wp_query->posts[0]->ID > 0
) {
return $wp_query->posts[0]->ID;
}
return null;
}
/**
* Prints the scripts for the detect loader.
*
* @since 0.1.0
* @since 1.0.0 Renamed from od_get_detection_script().
* @access private
*
* @param non-empty-string $slug URL Metrics slug.
* @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection.
*/
function od_get_detection_scripts( string $slug, OD_URL_Metric_Group_Collection $group_collection ): string {
/**
* Filters whether to use the web-vitals.js build with attribution.
*
* @since 1.0.0
* @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_use_web_vitals_attribution_build
*
* @param bool $use_attribution_build Whether to use the attribution build.
*/
$use_attribution_build = (bool) apply_filters( 'od_use_web_vitals_attribution_build', false );
$web_vitals_lib_data = require __DIR__ . '/build/web-vitals.asset.php';
$web_vitals_lib_src = $use_attribution_build ?
plugins_url( 'build/web-vitals-attribution.js', __FILE__ ) :
plugins_url( 'build/web-vitals.js', __FILE__ );
$web_vitals_lib_src = add_query_arg( 'ver', $web_vitals_lib_data['version'], $web_vitals_lib_src );
/**
* Filters the list of extension script module URLs to import when performing detection.
*
* @since 0.7.0
* @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_extension_module_urls
*
* @param string[] $extension_module_urls Extension module URLs.
*/
$extension_module_urls = (array) apply_filters( 'od_extension_module_urls', array() );
$cache_purge_post_id = od_get_cache_purge_post_id();
$current_url = od_get_current_url();
$current_etag = $group_collection->get_current_etag();
/**
* Filters whether URL Metric JSON data should be compressed with gzip when being submitted to the `/url-metrics:store` REST API endpoint.
*
* @since 1.0.0
* @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_gzip_url_metric_store_request_payloads
*
* @param bool $gzip_url_metric_store_request_payloads Whether to use gzip to compress URL Metric JSON.
*/
$gzdecode_available = function_exists( 'gzdecode' ) && apply_filters( 'od_gzip_url_metric_store_request_payloads', true );
$detect_src = add_query_arg(
array( 'ver' => OPTIMIZATION_DETECTIVE_VERSION ),
plugins_url( od_get_asset_path( 'detect.js' ), __FILE__ )
);
$detect_args = array(
'minViewportAspectRatio' => od_get_minimum_viewport_aspect_ratio(),
'maxViewportAspectRatio' => od_get_maximum_viewport_aspect_ratio(),
'isDebug' => WP_DEBUG,
'extensionModuleUrls' => $extension_module_urls,
'restApiEndpoint' => rest_url( OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE ),
'currentETag' => $current_etag,
'currentUrl' => $current_url,
'urlMetricSlug' => $slug,
'cachePurgePostId' => od_get_cache_purge_post_id(),
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_etag, $current_url, $cache_purge_post_id ),
'urlMetricGroupStatuses' => array_map(
static function ( OD_URL_Metric_Group $group ): array {
return array(
'minimumViewportWidth' => $group->get_minimum_viewport_width(), // Exclusive.
'maximumViewportWidth' => $group->get_maximum_viewport_width(), // Inclusive.
'complete' => $group->is_complete(),
);
},
iterator_to_array( $group_collection )
),
'storageLockTTL' => OD_Storage_Lock::get_ttl(),
'freshnessTTL' => od_get_url_metric_freshness_ttl(),
'webVitalsLibrarySrc' => $web_vitals_lib_src,
'gzdecodeAvailable' => $gzdecode_available,
'maxUrlMetricSize' => od_get_maximum_url_metric_size(),
);
if ( is_user_logged_in() ) {
$detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' );
}
if ( WP_DEBUG ) {
$detect_args['urlMetricGroupCollection'] = $group_collection;
}
$json_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
if ( SCRIPT_DEBUG ) {
$json_flags |= JSON_PRETTY_PRINT;
}
$json_script = wp_get_inline_script_tag(
(string) wp_json_encode(
array( $detect_src, $detect_args ),
$json_flags
),
array(
'type' => 'application/json',
'id' => 'optimization-detective-detect-args',
)
);
$module_js = file_get_contents( __DIR__ . '/' . od_get_asset_path( 'detect-loader.js' ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request.
$module_js .= sprintf(
"\n//# sourceURL=%s",
add_query_arg(
array( 'ver' => OPTIMIZATION_DETECTIVE_VERSION ),
plugins_url( od_get_asset_path( 'detect-loader.js' ), __FILE__ )
)
);
$module_script = wp_get_inline_script_tag(
$module_js,
array( 'type' => 'module' )
);
return $json_script . $module_script;
}
/**
* Registers the REST API endpoint for storing URL Metrics.
*
* @since 1.0.0
* @access private
*/
function od_register_rest_url_metric_store_endpoint(): void {
$endpoint_controller = new OD_REST_URL_Metrics_Store_Endpoint();
register_rest_route(
$endpoint_controller::ROUTE_NAMESPACE,
$endpoint_controller::ROUTE_BASE,
$endpoint_controller->get_registration_args()
);
}
/**
* Decompresses the REST API request body for the URL Metrics endpoint.
*
* @since 1.0.0
* @access private
*
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
*
* @param mixed $result Response to replace the requested version with. Can be anything a normal endpoint can return, or null to not hijack the request.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
* @return mixed Passed through $result if successful, or otherwise a WP_Error.
*/
function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_REST_Request $request ) {
unset( $server ); // Unused.
if (
function_exists( 'gzdecode' ) &&
$request->get_route() === '/' . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE &&
'gzip' === $request->get_header( 'Content-Encoding' )
) {
$compressed_body = $request->get_body();
/*
* The limit for data sent via navigator.sendBeacon() is 64 KiB. This limit is checked in detect.js so that the
* request will not even be attempted if the payload is too large. This server-side restriction is added as a
* safeguard against clients sending possibly malicious payloads much larger than 64 KiB which should never be
* getting sent.
*/
$max_size = 64 * 1024; // 64 KB
$content_length = strlen( $compressed_body );
if ( $content_length > $max_size ) {
return new WP_Error(
'rest_content_too_large',
sprintf(
/* translators: 1: the size of the payload, 2: the maximum allowed payload size */
__( 'Compressed JSON payload size is %1$s bytes which is larger than the maximum allowed size of %2$s bytes.', 'optimization-detective' ),
number_format_i18n( $content_length ),
number_format_i18n( $max_size )
),
array( 'status' => 413 )
);
}
$decompressed_body = @gzdecode( $compressed_body ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- We need to suppress errors here.
if ( false === $decompressed_body ) {
return new WP_Error(
'rest_invalid_payload',
__( 'Unable to decompress the gzip payload.', 'optimization-detective' ),
array( 'status' => 400 )
);
}
// Update the request so later handlers see the decompressed JSON.
$request->set_body( $decompressed_body );
$request->remove_header( 'Content-Encoding' );
}
return $result;
}
/**
* Triggers post update actions for page caches to invalidate their caches related to the supplied cache purge post ID.
*
* This is intended to flush any page cache for the URL after the new URL Metric was submitted so that the optimizations
* which depend on that URL Metric can start to take effect.
*
* @since 1.0.0
*
* @param positive-int $cache_purge_post_id Cache purge post ID.
*/
function od_trigger_post_update_actions( int $cache_purge_post_id ): void {
$post = get_post( $cache_purge_post_id );
if ( ! ( $post instanceof WP_Post ) ) {
return;
}
// Fire actions that page caching plugins listen to flush caches.
/*
* The clean_post_cache action is used to flush page caches by:
* - Pantheon Advanced Cache <https://github.com/pantheon-systems/pantheon-advanced-page-cache/blob/e3b5552b0cb9268d9b696cb200af56cc044920d9/pantheon-advanced-page-cache.php#L185>
* - WP Super Cache <https://github.com/Automattic/wp-super-cache/blob/73b428d2fce397fd874b3056ad3120c343bc1a0c/wp-cache-phase2.php#L1615>
* - Batcache <https://github.com/Automattic/batcache/blob/ed0e6b2d9bcbab3924c49a6c3247646fb87a0957/batcache.php#L18>
*/
/** This action is documented in wp-includes/post.php. */
do_action( 'clean_post_cache', $post->ID, $post );
/*
* The transition_post_status action is used to flush page caches by:
* - Jetpack Boost <https://github.com/Automattic/jetpack-boost-production/blob/4090a3f9414c2171cd52d8a397f00b0d1151475f/app/modules/optimizations/page-cache/pre-wordpress/Boost_Cache.php#L76>
* - WP Super Cache <https://github.com/Automattic/wp-super-cache/blob/73b428d2fce397fd874b3056ad3120c343bc1a0c/wp-cache-phase2.php#L1616>
* - LightSpeed Cache <https://github.com/litespeedtech/lscache_wp/blob/7c707469b3c88b4f45d9955593b92f9aeaed54c3/src/purge.cls.php#L68>
*/
/** This action is documented in wp-includes/post.php. */
do_action( 'transition_post_status', $post->post_status, $post->post_status, $post );
/*
* The clean_post_cache action is used to flush page caches by:
* - W3 Total Cache <https://github.com/BoldGrid/w3-total-cache/blob/ab08f104294c6a8dcb00f1c66aaacd0615c42850/Util_AttachToActions.php#L32>
* - WP Rocket <https://github.com/wp-media/wp-rocket/blob/e5bca6673a3669827f3998edebc0c785210fe561/inc/common/purge.php#L283>
*/
/** This action is documented in wp-includes/post.php. */
do_action( 'save_post', $post->ID, $post, /* $update */ true );
}