-
Notifications
You must be signed in to change notification settings - Fork 143
Expand file tree
/
Copy pathsite-health.php
More file actions
322 lines (295 loc) · 12.1 KB
/
site-health.php
File metadata and controls
322 lines (295 loc) · 12.1 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
314
315
316
317
318
319
320
321
322
<?php
/**
* Site Health checks.
*
* @package optimization-detective
* @since 1.0.0
*/
declare( strict_types = 1 );
// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd
/**
* Adds the Optimization Detective REST API check to site health tests.
*
* @since 1.0.0
* @access private
*
* @param array{direct: array<string, array{label: string, test: string}>}|mixed $tests Site Health Tests.
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
*/
function od_add_rest_api_availability_test( $tests ): array {
if ( ! is_array( $tests ) ) {
$tests = array();
}
$tests['direct']['optimization_detective_rest_api'] = array(
'label' => __( 'Optimization Detective REST API Endpoint Availability', 'optimization-detective' ),
'test' => static function () {
// Note: A closure is used here to improve symbol discovery for the sake of potential refactoring.
return od_test_rest_api_availability();
},
);
return $tests;
}
/**
* Tests availability of the Optimization Detective REST API endpoint.
*
* @since 1.0.0
* @access private
*
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function od_test_rest_api_availability(): array {
$response = od_get_rest_api_health_check_response( false );
$result = od_compose_site_health_result( $response );
$is_unavailable = 'good' !== $result['status'];
update_option(
'od_rest_api_unavailable',
$is_unavailable ? '1' : '0',
true // Intentionally autoloaded since used on every frontend request.
);
return $result;
}
/**
* Checks whether the Optimization Detective REST API endpoint is unavailable.
*
* This merely checks the database option what was previously computed in the Site Health test as done in {@see od_test_rest_api_availability()}.
* This is to avoid checking for REST API availability during a frontend request. Note that when the plugin is first
* installed, the 'od_rest_api_unavailable' option will not be in the database, as the check has not been performed
* yet. Once Site Health's weekly check happens or when a user accesses the admin so that the admin_init action fires,
* then at this point the check will be performed at {@see od_maybe_run_rest_api_health_check()}. In practice, this will
* happen immediately after the user activates a plugin since the user is redirected back to the plugin list table in
* the admin. The reason for storing the negative unavailable state as opposed to the positive available state is that
* when an option does not exist, then `get_option()` returns `false` which is the same falsy value as the stored `'0'`.
*
* @since 1.0.0
* @access private
*
* @return bool Whether unavailable.
*/
function od_is_rest_api_unavailable(): bool {
return 1 === (int) get_option( 'od_rest_api_unavailable', '0' );
}
/**
* Tests availability of the Optimization Detective REST API endpoint.
*
* @since 1.0.0
* @access private
*
* @param array<string, mixed>|WP_Error $response REST API response.
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function od_compose_site_health_result( $response ): array {
$common_description_html = '<p>' . wp_kses(
sprintf(
/* translators: %s is the REST API endpoint */
__( 'To collect URL Metrics from visitors, the REST API must be available to unauthenticated users. Specifically, visitors must be able to perform a <code>POST</code> request to the <code>%s</code> endpoint.', 'optimization-detective' ),
'/' . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE
),
array( 'code' => array() )
) . '</p>';
$result = array(
'label' => __( 'The Optimization Detective REST API endpoint is available', 'optimization-detective' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Optimization Detective', 'optimization-detective' ),
'color' => 'blue',
),
'description' => $common_description_html . '<p><strong>' . esc_html__( 'This appears to be working properly.', 'optimization-detective' ) . '</strong></p>',
'actions' => '',
'test' => 'optimization_detective_rest_api',
);
$error_label = __( 'The Optimization Detective REST API endpoint is unavailable', 'optimization-detective' );
$error_description_html = '<p>' . esc_html__( 'You may have a plugin active or server configuration which restricts access to logged-in users. Unauthenticated access must be restored in order for Optimization Detective to work.', 'optimization-detective' ) . '</p>';
if ( is_wp_error( $response ) ) {
$result['status'] = 'recommended';
$result['label'] = $error_label;
$result['description'] = $common_description_html . $error_description_html . '<p>' . wp_kses(
sprintf(
/* translators: %s is the error code */
__( 'The REST API responded with the error code <code>%s</code> and the following error message:', 'optimization-detective' ),
esc_html( (string) $response->get_error_code() )
),
array( 'code' => array() )
) . '</p><blockquote>' . esc_html( $response->get_error_message() ) . '</blockquote>';
} else {
$code = wp_remote_retrieve_response_code( $response );
$message = wp_remote_retrieve_response_message( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
$header = wp_remote_retrieve_header( $response, 'content-type' );
if ( is_array( $header ) ) {
$header = array_pop( $header );
}
$is_expected = (
400 === $code &&
isset( $data['code'], $data['data']['params'] ) &&
'rest_missing_callback_param' === $data['code'] &&
is_array( $data['data']['params'] ) &&
count( $data['data']['params'] ) > 0
);
if ( ! $is_expected ) {
$result['status'] = 'recommended';
if ( 401 === $code ) {
$result['label'] = __( 'The Optimization Detective REST API endpoint is unavailable to logged-out users', 'optimization-detective' );
} else {
$result['label'] = $error_label;
}
$result['description'] = $common_description_html . $error_description_html . '<p>' . wp_kses(
sprintf(
/* translators: %d is the HTTP status code, %s is the status header description */
__( 'The REST API returned with an HTTP status of <code>%1$d %2$s</code>.', 'optimization-detective' ),
$code,
esc_html( $message )
),
array( 'code' => array() )
) . '</p>';
if ( isset( $data['message'] ) && is_string( $data['message'] ) ) {
$result['description'] .= '<blockquote>' . esc_html( $data['message'] ) . '</blockquote>';
}
if ( '' !== $body ) {
$result['description'] .= '<details>';
$result['description'] .= '<summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary>';
if ( is_string( $header ) && str_contains( $header, 'html' ) ) {
$escaped_content = htmlspecialchars( $body, ENT_QUOTES, 'UTF-8' );
$result['description'] .= '<iframe srcdoc="' . $escaped_content . '" sandbox width="100%" height="300"></iframe>';
} else {
$result['description'] .= '<pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre>';
}
$result['description'] .= '</details>';
}
}
}
return $result;
}
/**
* Gets the response to an Optimization Detective REST API store request to confirm it is available to unauthenticated requests.
*
* @since 1.0.0
* @access private
*
* @param bool $use_cached Whether to use a previous response cached in a transient.
* @return array{ response: array{ code: int, message: string }, body: string }|WP_Error Response.
*/
function od_get_rest_api_health_check_response( bool $use_cached ) {
$transient_key = 'od_rest_api_health_check_response';
$response = $use_cached ? get_transient( $transient_key ) : false;
if ( false !== $response ) {
return $response;
}
$rest_url = get_rest_url( null, OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE . OD_REST_URL_Metrics_Store_Endpoint::ROUTE_BASE );
$response = wp_remote_post(
$rest_url,
array(
'headers' => array( 'Content-Type' => 'application/json' ),
'sslverify' => false,
)
);
// This transient will be used when showing the admin notice with the plugin on the plugins screen.
// The 1-day expiration allows for fresher content than the weekly check initiated by Site Health.
set_transient( $transient_key, $response, DAY_IN_SECONDS );
return $response;
}
/**
* Renders an admin notice if the REST API health check fails.
*
* @since 1.0.0
* @access private
*
* @param bool $in_plugin_row Whether the notice is to be printed in the plugin row.
*/
function od_maybe_render_rest_api_health_check_admin_notice( bool $in_plugin_row ): void {
if ( ! od_is_rest_api_unavailable() ) {
return;
}
$response = od_get_rest_api_health_check_response( true );
$result = od_compose_site_health_result( $response );
if ( 'good' === $result['status'] ) {
// There's a slight chance the DB option is stale in the initial if statement.
return;
}
$message = sprintf(
$in_plugin_row
? '<summary style="margin: 0.5em 0">%s %s</summary>'
: '<p><strong>%s %s</strong></p>',
esc_html__( 'Warning:', 'optimization-detective' ),
esc_html( $result['label'] )
);
$message .= $result['description']; // This has already gone through Kses.
if ( current_user_can( 'view_site_health_checks' ) ) {
$site_health_message = wp_kses(
sprintf(
/* translators: %s is the URL to the Site Health admin screen */
__( 'Please visit <a href="%s">Site Health</a> to re-check this once you believe you have resolved the issue.', 'optimization-detective' ),
esc_url( admin_url( 'site-health.php' ) )
),
array( 'a' => array( 'href' => array() ) )
);
$message .= "<p><em>$site_health_message</em></p>";
}
if ( $in_plugin_row ) {
$message = "<details>$message</details>";
}
$notice = wp_get_admin_notice(
$message,
array(
'type' => 'warning',
'additional_classes' => $in_plugin_row ? array( 'inline', 'notice-alt' ) : array(),
'paragraph_wrap' => false,
)
);
echo wp_kses(
$notice,
array_merge(
wp_kses_allowed_html( 'post' ),
array(
'iframe' => array_fill_keys( array( 'srcdoc', 'sandbox', 'width', 'height' ), true ),
)
)
);
}
/**
* Displays an admin notice on the plugin row if the REST API health check fails.
*
* @since 1.0.0
* @access private
*
* @param non-empty-string $plugin_file Plugin file.
*/
function od_render_rest_api_health_check_admin_notice_in_plugin_row( string $plugin_file ): void {
if ( 'optimization-detective/load.php' !== $plugin_file ) { // TODO: What if a different plugin slug is used?
return;
}
od_maybe_render_rest_api_health_check_admin_notice( true );
}
/**
* Runs the REST API health check if it hasn't been run yet.
*
* This happens at the `admin_init` action to avoid running the check on the frontend. This will run on the first admin
* page load after the plugin has been activated. This allows for this function to add an action at `admin_notices` so
* that an error message can be displayed after performing that plugin activation request. Note that a plugin activation
* hook cannot be used for this purpose due to not being compatible with multisite. While the site health notice is
* shown at the `admin_notices` action once, the notice will only be displayed inline with the plugin row thereafter
* via {@see od_render_rest_api_health_check_admin_notice_in_plugin_row()}.
*
* @since 1.0.0
* @access private
*/
function od_maybe_run_rest_api_health_check(): void {
// If the option already exists, then the REST API health check has already been performed.
if ( false !== get_option( 'od_rest_api_unavailable' ) ) {
return;
}
// This will populate the od_rest_api_unavailable option so that the function won't execute on the next page load.
if ( 'good' !== od_test_rest_api_availability()['status'] ) {
// Show any notice in the main admin notices area for the first page load (e.g. after plugin activation).
add_action(
'admin_notices',
static function (): void {
od_maybe_render_rest_api_health_check_admin_notice( false );
}
);
}
}