Skip to content

Commit

Permalink
Eliminate od_store_url_metric_data filter in favor of reusing rest_re…
Browse files Browse the repository at this point in the history
…quest_before_callbacks
  • Loading branch information
westonruter committed Dec 17, 2024
1 parent 42c3de8 commit d4c9f40
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 196 deletions.
32 changes: 20 additions & 12 deletions plugins/image-prioritizer/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,26 +264,34 @@ static function ( $host ) {
}

/**
* Filters the validity of a URL Metric with an LCP element background image.
* Filters the response before executing any REST API callbacks.
*
* This removes the lcpElementExternalBackgroundImage from the URL Metric prior to it being stored if the background
* image URL is not valid. Removal of the property is preferable to
* image URL is not valid. Removal of the property is preferable to invalidating the entire URL Metric because then
* potentially no URL Metrics would ever be collected if, for example, the background image URL is pointing to a
* disallowed origin. Then none of the other optimizations would be able to be applied.
*
* @since n.e.x.t
* @access private
*
* @param array<string, mixed>|mixed $data URL Metric data.
* @return array<string, mixed> Sanitized URL Metric data.
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array<string, mixed> $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*
* @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed Result to send to the client.
* @noinspection PhpDocMissingThrowsInspection
*/
function image_prioritizer_filter_store_url_metric_data( $data ): array {
if ( ! is_array( $data ) ) {
$data = array();
function image_prioritizer_filter_rest_request_before_callbacks( $response, array $handler, WP_REST_Request $request ) {
if ( $request->get_method() !== 'POST' || OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE !== trim( $request->get_route(), '/' ) ) {
return $response;
}

if ( isset( $data['lcpElementExternalBackgroundImage']['url'] ) && is_string( $data['lcpElementExternalBackgroundImage']['url'] ) ) {
$image_validity = image_prioritizer_validate_background_image_url( $data['lcpElementExternalBackgroundImage']['url'] );
$lcp_external_background_image = $request['lcpElementExternalBackgroundImage'];
if ( is_array( $lcp_external_background_image ) && isset( $lcp_external_background_image['url'] ) && is_string( $lcp_external_background_image['url'] ) ) {
$image_validity = image_prioritizer_validate_background_image_url( $lcp_external_background_image['url'] );
if ( is_wp_error( $image_validity ) ) {
/**
* No WP_Exception is thrown by wp_trigger_error() since E_USER_ERROR is not passed as the error level.
Expand All @@ -296,14 +304,14 @@ function image_prioritizer_filter_store_url_metric_data( $data ): array {
/* translators: 1: error message. 2: image url */
__( 'Error: %1$s. Background image URL: %2$s.', 'image-prioritizer' ),
rtrim( $image_validity->get_error_message(), '.' ),
$data['lcpElementExternalBackgroundImage']['url']
$lcp_external_background_image['url']
)
);
unset( $data['lcpElementExternalBackgroundImage'] );
unset( $request['lcpElementExternalBackgroundImage'] );
}
}

return $data;
return $response;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion plugins/image-prioritizer/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
add_action( 'od_init', 'image_prioritizer_init' );
add_filter( 'od_extension_module_urls', 'image_prioritizer_filter_extension_module_urls' );
add_filter( 'od_url_metric_schema_root_additional_properties', 'image_prioritizer_add_element_item_schema_properties' );
add_filter( 'od_store_url_metric_data', 'image_prioritizer_filter_store_url_metric_data' );
add_filter( 'rest_request_before_callbacks', 'image_prioritizer_filter_rest_request_before_callbacks', 10, 3 );
86 changes: 65 additions & 21 deletions plugins/image-prioritizer/tests/test-helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -747,10 +747,23 @@ public function test_image_prioritizer_validate_background_image_url( Closure $s
*
* @return array<string, mixed>
*/
public function data_provider_to_test_image_prioritizer_filter_store_url_metric_data(): array {
public function data_provider_to_test_image_prioritizer_filter_rest_request_before_callbacks(): array {
$get_sample_url_metric_data = function (): array {
return $this->get_sample_url_metric( array() )->jsonSerialize();
};

$create_request = static function ( array $url_metric_data ): WP_REST_Request {
$request = new WP_REST_Request( 'POST', '/' . OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE );
$request->set_header( 'content-type', 'application/json' );
$request->set_body( wp_json_encode( $url_metric_data ) );
return $request;
};

return array(
'invalid_external_bg_image' => array(
'set_up' => static function ( array $url_metric_data ): array {
'set_up' => static function () use ( $get_sample_url_metric_data, $create_request ): WP_REST_Request {
$url_metric_data = $get_sample_url_metric_data();

$url_metric_data['lcpElementExternalBackgroundImage'] = array(
'url' => 'https://bad-origin.example.com/image.jpg',
'tag' => 'DIV',
Expand All @@ -759,22 +772,23 @@ public function data_provider_to_test_image_prioritizer_filter_store_url_metric_
);
$url_metric_data['viewport']['width'] = 10101;
$url_metric_data['viewport']['height'] = 20202;
return $url_metric_data;
return $create_request( $url_metric_data );
},
'assert' => function ( array $url_metric_data ): void {
$this->assertArrayNotHasKey( 'lcpElementExternalBackgroundImage', $url_metric_data );
'assert' => function ( WP_REST_Request $request ): void {
$this->assertArrayNotHasKey( 'lcpElementExternalBackgroundImage', $request );
$this->assertSame(
array(
'width' => 10101,
'height' => 20202,
),
$url_metric_data['viewport']
$request['viewport']
);
},
),

'valid_external_bg_image' => array(
'set_up' => static function ( array $url_metric_data ): array {
'set_up' => static function () use ( $get_sample_url_metric_data, $create_request ): WP_REST_Request {
$url_metric_data = $get_sample_url_metric_data();
$image_url = home_url( '/good.jpg' );

add_filter(
Expand Down Expand Up @@ -808,45 +822,75 @@ static function ( $pre, $parsed_args, $url ) use ( $image_url ) {

$url_metric_data['viewport']['width'] = 30303;
$url_metric_data['viewport']['height'] = 40404;
return $url_metric_data;
return $create_request( $url_metric_data );
},
'assert' => function ( array $url_metric_data ): void {
$this->assertArrayHasKey( 'lcpElementExternalBackgroundImage', $url_metric_data );
$this->assertIsArray( $url_metric_data['lcpElementExternalBackgroundImage'] );
'assert' => function ( WP_REST_Request $request ): void {
$this->assertArrayHasKey( 'lcpElementExternalBackgroundImage', $request );
$this->assertIsArray( $request['lcpElementExternalBackgroundImage'] );
$this->assertSame(
array(
'url' => home_url( '/good.jpg' ),
'tag' => 'DIV',
'id' => null,
'class' => null,
),
$url_metric_data['lcpElementExternalBackgroundImage']
$request['lcpElementExternalBackgroundImage']
);
$this->assertSame(
array(
'width' => 30303,
'height' => 40404,
),
$url_metric_data['viewport']
$request['viewport']
);
},
),

'not_store_post_request' => array(
'set_up' => static function () use ( $get_sample_url_metric_data, $create_request ): WP_REST_Request {
$url_metric_data = $get_sample_url_metric_data();
$url_metric_data['lcpElementExternalBackgroundImage'] = 'https://totally-different.example.com/';
$request = $create_request( $url_metric_data );
$request->set_method( 'GET' );
return $request;
},
'assert' => function ( WP_REST_Request $request ): void {
$this->assertArrayHasKey( 'lcpElementExternalBackgroundImage', $request );
$this->assertSame( 'https://totally-different.example.com/', $request['lcpElementExternalBackgroundImage'] );
},
),

'not_store_request' => array(
'set_up' => static function () use ( $get_sample_url_metric_data, $create_request ): WP_REST_Request {
$url_metric_data = $get_sample_url_metric_data();
$url_metric_data['lcpElementExternalBackgroundImage'] = 'https://totally-different.example.com/';
$request = $create_request( $url_metric_data );
$request->set_route( '/foo/v2/bar' );
return $request;
},
'assert' => function ( WP_REST_Request $request ): void {
$this->assertArrayHasKey( 'lcpElementExternalBackgroundImage', $request );
$this->assertSame( 'https://totally-different.example.com/', $request['lcpElementExternalBackgroundImage'] );
},
),
);
}

/**
* Tests image_prioritizer_filter_store_url_metric_data().
* Tests image_prioritizer_filter_rest_request_before_callbacks().
*
* @dataProvider data_provider_to_test_image_prioritizer_filter_store_url_metric_data
* @dataProvider data_provider_to_test_image_prioritizer_filter_rest_request_before_callbacks
*
* @covers ::image_prioritizer_filter_store_url_metric_data
* @covers ::image_prioritizer_filter_rest_request_before_callbacks
* @covers ::image_prioritizer_validate_background_image_url
*/
public function test_image_prioritizer_filter_store_url_metric_data( Closure $set_up, Closure $assert ): void {
$url_metric_data = $set_up( $this->get_sample_url_metric( array() )->jsonSerialize() );

$url_metric_data = image_prioritizer_filter_store_url_metric_data( $url_metric_data );
$assert( $url_metric_data );
public function test_image_prioritizer_filter_rest_request_before_callbacks( Closure $set_up, Closure $assert ): void {
$request = $set_up();
$response = new WP_REST_Response();
$handler = array();
$filtered_response = image_prioritizer_filter_rest_request_before_callbacks( $response, $handler, $request );
$this->assertSame( $response, $filtered_response );
$assert( $request );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion plugins/image-prioritizer/tests/test-hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ public function test_hooks_added(): void {
$this->assertEquals( 10, has_action( 'od_init', 'image_prioritizer_init' ) );
$this->assertEquals( 10, has_filter( 'od_extension_module_urls', 'image_prioritizer_filter_extension_module_urls' ) );
$this->assertEquals( 10, has_filter( 'od_url_metric_schema_root_additional_properties', 'image_prioritizer_add_element_item_schema_properties' ) );
$this->assertEquals( 10, has_filter( 'od_store_url_metric_data', 'image_prioritizer_filter_store_url_metric_data' ) );
$this->assertEquals( 10, has_filter( 'rest_request_before_callbacks', 'image_prioritizer_filter_rest_request_before_callbacks' ) );
}
}
8 changes: 0 additions & 8 deletions plugins/optimization-detective/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,6 @@ The ETag is a unique identifier that changes whenever the underlying data used t

When the ETag for URL Metrics in a complete viewport group no longer matches the current environment's ETag, new URL Metrics will then begin to be collected until there are no more stored URL Metrics with the old ETag. These new URL Metrics will include data relevant to the newly activated plugins and their tag visitors.

**Filter:** `od_url_metric_data_pre_storage` (default arg: URL Metric data array)

Filters the URL Metric data prior to validation for storage.

This allows for custom sanitization and validation constraints to be applied beyond what can be expressed in JSON Schema. This filter only applies when storing a URL Metric via the REST API. It does not run when a stored URL Metric is loaded from the od_url_metrics post type. This means that sanitization and validation logic enforced via this filter can be more expensive, such as doing filesystem checks or HTTP requests.

To fail validation for the provided URL Metric data, an `OD_Data_Validation_Exception` exception should be thrown. Otherwise, any filter callbacks must return an array consisting of the sanitized URL Metric data.

**Action:** `od_url_metric_stored` (argument: `OD_URL_Metric_Store_Request_Context`)

Fires whenever a URL Metric was successfully stored.
Expand Down
74 changes: 19 additions & 55 deletions plugins/optimization-detective/storage/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function od_handle_rest_request( WP_REST_Request $request ) {
);
}

$data = $request->get_json_params();
$data = $request->get_json_params(); // TODO: Why not just get_params()?
if ( ! is_array( $data ) ) {
return new WP_Error(
'missing_array_json_body',
Expand All @@ -186,64 +186,28 @@ function od_handle_rest_request( WP_REST_Request $request ) {
OD_Storage_Lock::set_lock();

try {
$data = array_merge(
$data,
array(
// Now supply the readonly args which were omitted from the REST API params due to being `readonly`.
'timestamp' => microtime( true ),
'uuid' => wp_generate_uuid4(),
'etag' => $request->get_param( 'current_etag' ),
// The "strict" URL Metric class is being used here to ensure additionalProperties of all objects are disallowed.
$url_metric = new OD_Strict_URL_Metric(
array_merge(
$data,
array(
// Now supply the readonly args which were omitted from the REST API params due to being `readonly`.
'timestamp' => microtime( true ),
'uuid' => wp_generate_uuid4(),
'etag' => $request->get_param( 'current_etag' ),
)
)
);

/**
* Filters the URL Metric data prior to validation for storage.
*
* This allows for custom sanitization and validation constraints to be applied beyond what can be expressed in
* JSON Schema. This is also necessary because the 'validate_callback' key in a JSON Schema is not respected when
* gathering the REST API endpoint args via the {@see rest_get_endpoint_args_for_schema()} function. Besides this,
* the REST API doesn't support 'validate_callback' for any nested arguments in any case, meaning that custom
* constraints would be able to be applied to multidimensional objects, such as the items inside 'elements'.
*
* This filter only applies when storing a URL Metric via the REST API. It does not run when a stored URL Metric is
* loaded from the od_url_metrics post type. This means that sanitization and validation logic enforced via this
* filter can be more expensive, such as doing filesystem checks or HTTP requests.
*
* To fail validation for the provided URL Metric data, an OD_Data_Validation_Exception exception should be thrown.
* Otherwise, any filter callbacks must return an array consisting of the sanitized URL Metric data.
*
* @since n.e.x.t
*
* @param array<string, mixed> $data URL Metric data. This is the Data type from OD_URL_Metric.
*/
$data = apply_filters( 'od_store_url_metric_data', $data );

// The "strict" URL Metric class is being used here to ensure additionalProperties of all objects are disallowed.
$url_metric = new OD_Strict_URL_Metric( $data );
} catch ( Exception $e ) {
$error_data = array();
if ( $e instanceof OD_Data_Validation_Exception ) {
$error_message = sprintf(
/* translators: %s is exception name */
} catch ( OD_Data_Validation_Exception $e ) {
return new WP_Error(
'rest_invalid_param',
sprintf(
/* translators: %s is exception message */
__( 'Failed to validate URL Metric: %s', 'optimization-detective' ),
$e->getMessage()
);
$error_data['status'] = 400;
} else {
$error_message = sprintf(
/* translators: %s is exception name */
__( 'Failed to validate URL Metric: %s', 'optimization-detective' ),
__( 'An unrecognized exception was thrown', 'optimization-detective' )
);
$error_data['status'] = 500;
if ( WP_DEBUG ) {
$error_data['exception_class'] = get_class( $e );
$error_data['exception_message'] = $e->getMessage();
$error_data['exception_code'] = $e->getCode();
}
}

return new WP_Error( 'rest_invalid_param', $error_message, $error_data );
),
array( 'status' => 400 )
);
}

// 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.
Expand Down
Loading

0 comments on commit d4c9f40

Please sign in to comment.