Skip to content

Commit f2959cd

Browse files
committed
Introduce od_store_url_metric_validity filter so Image Prioritizer can validate background-image URL
1 parent a75b94f commit f2959cd

File tree

4 files changed

+94
-4
lines changed

4 files changed

+94
-4
lines changed

plugins/image-prioritizer/helper.php

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* @param string $optimization_detective_version Current version of the optimization detective plugin.
1919
*/
2020
function image_prioritizer_init( string $optimization_detective_version ): void {
21-
$required_od_version = '0.7.0';
21+
$required_od_version = '0.9.0';
2222
if ( ! version_compare( (string) strtok( $optimization_detective_version, '-' ), $required_od_version, '>=' ) ) {
2323
add_action(
2424
'admin_notices',
@@ -121,7 +121,6 @@ function image_prioritizer_filter_extension_module_urls( $extension_module_urls
121121
* @return array<string, array{type: string}> Additional properties.
122122
*/
123123
function image_prioritizer_add_element_item_schema_properties( array $additional_properties ): array {
124-
// TODO: Validation of the URL.
125124
$additional_properties['lcpElementExternalBackgroundImage'] = array(
126125
'type' => 'object',
127126
'properties' => array(
@@ -151,3 +150,63 @@ function image_prioritizer_add_element_item_schema_properties( array $additional
151150
);
152151
return $additional_properties;
153152
}
153+
154+
/**
155+
* Validates that the provided background image URL is valid.
156+
*
157+
* @since n.e.x.t
158+
*
159+
* @param bool|WP_Error|mixed $validity Validity. Valid if true or a WP_Error without any errors, or invalid otherwise.
160+
* @param OD_Strict_URL_Metric $url_metric URL Metric, already validated against the JSON Schema.
161+
* @return bool|WP_Error Validity. Valid if true or a WP_Error without any errors, or invalid otherwise.
162+
*/
163+
function image_prioritizer_filter_store_url_metric_validity( $validity, OD_Strict_URL_Metric $url_metric ) {
164+
if ( ! is_bool( $validity ) && ! ( $validity instanceof WP_Error ) ) {
165+
$validity = (bool) $validity;
166+
}
167+
168+
$data = $url_metric->get( 'lcpElementExternalBackgroundImage' );
169+
if ( ! is_array( $data ) ) {
170+
return $validity;
171+
}
172+
173+
$r = wp_safe_remote_head(
174+
$data['url'],
175+
array(
176+
'redirection' => 3, // Allow up to 3 redirects.
177+
)
178+
);
179+
if ( $r instanceof WP_Error ) {
180+
return new WP_Error(
181+
WP_DEBUG ? $r->get_error_code() : 'head_request_failure',
182+
__( 'HEAD request for background image URL failed.', 'image-prioritizer' ) . ( WP_DEBUG ? ' ' . $r->get_error_message() : '' ),
183+
array(
184+
'code' => 500,
185+
)
186+
);
187+
}
188+
$response_code = wp_remote_retrieve_response_code( $r );
189+
if ( $response_code < 200 || $response_code >= 400 ) {
190+
return new WP_Error(
191+
'background_image_response_not_ok',
192+
__( 'HEAD request for background image URL did not return with a success status code.', 'image-prioritizer' ),
193+
array(
194+
'code' => WP_DEBUG ? $response_code : 400,
195+
)
196+
);
197+
}
198+
199+
$content_type = wp_remote_retrieve_header( $r, 'Content-Type' );
200+
if ( ! is_string( $content_type ) || ! str_starts_with( $content_type, 'image/' ) ) {
201+
return new WP_Error(
202+
'background_image_response_not_image',
203+
__( 'HEAD request for background image URL did not return an image Content-Type.', 'image-prioritizer' ),
204+
array(
205+
'code' => 400,
206+
)
207+
);
208+
}
209+
210+
// TODO: Check for the Content-Length and return invalid if it is gigantic?
211+
return $validity;
212+
}

plugins/image-prioritizer/hooks.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
add_action( 'od_init', 'image_prioritizer_init' );
1414
add_filter( 'od_extension_module_urls', 'image_prioritizer_filter_extension_module_urls' );
1515
add_filter( 'od_url_metric_schema_root_additional_properties', 'image_prioritizer_add_element_item_schema_properties' );
16+
add_filter( 'od_store_url_metric_validity', 'image_prioritizer_filter_store_url_metric_validity', 10, 2 );

plugins/image-prioritizer/tests/test-helper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function data_provider_to_test_image_prioritizer_init(): array {
2020
'expected' => false,
2121
),
2222
'with_new_version' => array(
23-
'version' => '0.7.0',
23+
'version' => '99.0.0',
2424
'expected' => true,
2525
),
2626
);

plugins/optimization-detective/storage/rest-api.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function od_register_endpoint(): void {
8484
return new WP_Error(
8585
'url_metric_storage_locked',
8686
__( 'URL Metric storage is presently locked for the current IP.', 'optimization-detective' ),
87-
array( 'status' => 403 )
87+
array( 'status' => 403 ) // TODO: Consider 423 Locked status code.
8888
);
8989
}
9090
return true;
@@ -152,6 +152,7 @@ function od_handle_rest_request( WP_REST_Request $request ) {
152152
$request->get_param( 'viewport' )['width']
153153
);
154154
} catch ( InvalidArgumentException $exception ) {
155+
// Note: This should never happen because an exception only occurs if a viewport width is less than zero, and the JSON Schema enforces that the viewport.width have a minimum of zero.
155156
return new WP_Error( 'invalid_viewport_width', $exception->getMessage() );
156157
}
157158
if ( $url_metric_group->is_complete() ) {
@@ -197,6 +198,35 @@ function od_handle_rest_request( WP_REST_Request $request ) {
197198
);
198199
}
199200

201+
/**
202+
* Filters whether a URL Metric is valid for storage.
203+
*
204+
* This allows for custom validation constraints to be applied beyond what can be expressed in JSON Schema. This is
205+
* also necessary because the 'validate_callback' key in a JSON Schema is not respected when gathering the REST API
206+
* endpoint args via the {@see rest_get_endpoint_args_for_schema()} function. Besides this, the REST API doesn't
207+
* support 'validate_callback' for any nested arguments in any case, meaning that custom constraints would be able
208+
* to be applied to multidimensional objects, such as the items inside 'elements'.
209+
*
210+
* This filter only applies when storing a URL Metric via the REST API. It does not run when a stored URL Metric
211+
* loaded from the od_url_metric post type. This means that validation logic enforced via this filter can be more
212+
* expensive, such as doing filesystem checks or HTTP requests.
213+
*
214+
* @since n.e.x.t
215+
*
216+
* @param bool|WP_Error $validity Validity. Valid if true or a WP_Error without any errors, or invalid otherwise.
217+
* @param OD_Strict_URL_Metric $url_metric URL Metric, already validated against the JSON Schema.
218+
*/
219+
$validity = apply_filters( 'od_store_url_metric_validity', true, $url_metric );
220+
if ( false === $validity || ( $validity instanceof WP_Error && $validity->has_errors() ) ) {
221+
if ( false === $validity ) {
222+
$validity = new WP_Error( 'invalid_url_metric', __( 'Validity of URL Metric was rejected by filter.', 'optimization-detective' ) );
223+
}
224+
if ( ! isset( $validity->error_data['code'] ) ) {
225+
$validity->error_data['code'] = 400;
226+
}
227+
return $validity;
228+
}
229+
200230
// TODO: This should be changed from store_url_metric($slug, $url_metric) instead be update_post( $slug, $group_collection ). As it stands, store_url_metric() is duplicating logic here.
201231
$result = OD_URL_Metrics_Post_Type::store_url_metric(
202232
$request->get_param( 'slug' ),

0 commit comments

Comments
 (0)