From 41539514ae530c1aeca344ee2a808b9be0934900 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 4 Mar 2025 19:23:10 +0530 Subject: [PATCH 01/24] Add string compression using CompressionStream API for URL Metric payload --- plugins/optimization-detective/detect.js | 33 +++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 769b466177..37978d2bc9 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -310,6 +310,23 @@ function extendElementData( xpath, properties ) { Object.assign( elementData, properties ); } +/** + * Compresses a string using CompressionStream API. + * + * @param {string} string String to compress. + * @return {Promise} Compressed data. + */ +async function compress( string ) { + const encodedData = new TextEncoder().encode( string ); + const compressedDataStream = new Blob( [ encodedData ] ) + .stream() + .pipeThrough( new CompressionStream( 'gzip' ) ); + const compressedDataBuffer = await new Response( + compressedDataStream + ).arrayBuffer(); + return new Blob( [ compressedDataBuffer ], { type: 'application/gzip' } ); +} + /** * @typedef {{timestamp: number, creationDate: Date}} UrlMetricDebugData * @typedef {{groups: Array<{url_metrics: Array}>}} CollectionDebugData @@ -775,8 +792,9 @@ export default async function detect( { // TODO: Consider adding replacer to reduce precision on numbers in DOMRect to reduce payload size. const jsonBody = JSON.stringify( urlMetric ); + const compressedJsonBody = await compress( jsonBody ); const percentOfBudget = - ( jsonBody.length / ( maxBodyLengthKiB * 1000 ) ) * 100; + ( compressedJsonBody.size / ( maxBodyLengthKiB * 1000 ) ) * 100; /* * According to the fetch() spec: @@ -784,10 +802,10 @@ export default async function detect( { * This is what browsers also implement for navigator.sendBeacon(). Therefore, if the size of the JSON is greater * than the maximum, we should avoid even trying to send it. */ - if ( jsonBody.length > maxBodyLengthBytes ) { + if ( compressedJsonBody.size > maxBodyLengthBytes ) { if ( isDebug ) { error( - `Unable to send URL Metric because it is ${ jsonBody.length.toLocaleString() } bytes, ${ Math.round( + `Unable to send URL Metric because it is ${ compressedJsonBody.size.toLocaleString() } bytes, ${ Math.round( percentOfBudget ) }% of ${ maxBodyLengthKiB } KiB limit:`, urlMetric @@ -806,7 +824,7 @@ export default async function detect( { String( getCurrentTime() ) ); - const message = `Sending URL Metric (${ jsonBody.length.toLocaleString() } bytes, ${ Math.round( + const message = `Sending URL Metric (${ compressedJsonBody.size.toLocaleString() } bytes, ${ Math.round( percentOfBudget ) }% of ${ maxBodyLengthKiB } KiB limit):`; @@ -830,12 +848,7 @@ export default async function detect( { ); } url.searchParams.set( 'hmac', urlMetricHMAC ); - navigator.sendBeacon( - url, - new Blob( [ jsonBody ], { - type: 'application/json', - } ) - ); + navigator.sendBeacon( url, compressedJsonBody ); // Clean up. breadcrumbedElementsMap.clear(); From 53476575fe6a3549063ba45c43941e3b845b89e1 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 4 Mar 2025 20:03:31 +0530 Subject: [PATCH 02/24] Add decompression for REST API request body in URL Metrics endpoint --- .../storage/rest-api.php | 91 ++++++++++++++----- 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 2cd3b1711c..b125cb38c0 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -182,7 +182,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } - $data = $request->get_json_params(); + $data = $request->get_body_params(); if ( ! is_array( $data ) ) { return new WP_Error( 'missing_array_json_body', @@ -218,27 +218,6 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } - /* - * 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; - $content_length = strlen( (string) wp_json_encode( $url_metric ) ); - 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 */ - __( '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 ) - ); - } - try { $url_metric_group->add_url_metric( $url_metric ); } catch ( InvalidArgumentException $e ) { @@ -346,3 +325,71 @@ function od_trigger_page_cache_invalidation( int $cache_purge_post_id ): void { /** This action is documented in wp-includes/post.php. */ do_action( 'save_post', $post->ID, $post, /* $update */ true ); } + +/** + * Decompresses the REST API request body for the URL Metrics endpoint. + * + * @since n.e.x.t + * @access private + * + * @phpstan-param WP_REST_Request> $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 Response to replace the requested version with. + */ +function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_REST_Request $request ) { + if ( + $request->get_route() === '/' . OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE && + 'application/gzip' === $request->get_header( 'Content-Type' ) + ) { + $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 ); + if ( false === $decompressed_body ) { + return new WP_Error( + 'rest_invalid_payload', + __( 'Unable to decompress the gzip payload.', 'optimization-detective' ), + array( 'status' => 400 ) + ); + } + + $decoded_body = json_decode( $decompressed_body, true ); + if ( JSON_ERROR_NONE !== json_last_error() ) { + return new WP_Error( + 'rest_invalid_json', + __( 'Invalid JSON in decompressed payload.', 'optimization-detective' ), + array( 'status' => 400 ) + ); + } + + // Update the request so later handlers see the decompressed JSON. + $request->set_body( $decompressed_body ); + $request->set_body_params( $decoded_body ); + $request->set_header( 'Content-Type', 'application/json' ); + } + return $result; +} +add_filter( 'rest_pre_dispatch', 'od_decompress_rest_request_body', 10, 3 ); From 29fff3c0d3f3f7cccc53662bdb7973c18a5ab634 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 6 Mar 2025 00:43:25 +0530 Subject: [PATCH 03/24] Fix failing test --- .../storage/rest-api.php | 19 +++++++------------ .../tests/storage/test-rest-api.php | 14 ++++++-------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index b125cb38c0..2d13d9b1fc 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -182,7 +182,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } - $data = $request->get_body_params(); + $data = $request->get_json_params(); if ( ! is_array( $data ) ) { return new WP_Error( 'missing_array_json_body', @@ -367,7 +367,12 @@ function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_RE ); } - $decompressed_body = gzdecode( $compressed_body ); + try { + $decompressed_body = gzdecode( $compressed_body ); + } catch ( Exception $e ) { + $decompressed_body = false; + } + if ( false === $decompressed_body ) { return new WP_Error( 'rest_invalid_payload', @@ -376,18 +381,8 @@ function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_RE ); } - $decoded_body = json_decode( $decompressed_body, true ); - if ( JSON_ERROR_NONE !== json_last_error() ) { - return new WP_Error( - 'rest_invalid_json', - __( 'Invalid JSON in decompressed payload.', 'optimization-detective' ), - array( 'status' => 400 ) - ); - } - // Update the request so later handlers see the decompressed JSON. $request->set_body( $decompressed_body ); - $request->set_body_params( $decoded_body ); $request->set_header( 'Content-Type', 'application/json' ); } return $result; diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index d049d0f099..1174264a4e 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -349,16 +349,14 @@ public function data_provider_invalid_params(): array { 'params' => array_merge( $valid_params, array( - // Repeat the elements until the JSON will surpass 64 KiB. - 'elements' => array_fill( - 0, - 200, + // Fill the JSON with more than 64KB of data. + 'elements' => array( array_merge( $valid_element, array( - 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::DIV]', + 'xpath' => bin2hex( random_bytes( 65000 ) ), ) - ) + ), ), ) ), @@ -938,11 +936,11 @@ private function create_request( array $params ): WP_REST_Request { * @var WP_REST_Request> $request */ $request = new WP_REST_Request( 'POST', self::ROUTE ); - $request->set_header( 'Content-Type', 'application/json' ); + $request->set_header( 'Content-Type', 'application/gzip' ); $request->set_query_params( wp_array_slice_assoc( $params, array( 'hmac', 'current_etag', 'slug', 'cache_purge_post_id' ) ) ); $request->set_header( 'Origin', home_url() ); unset( $params['hmac'], $params['slug'], $params['current_etag'], $params['cache_purge_post_id'] ); - $request->set_body( wp_json_encode( $params ) ); + $request->set_body( gzencode( wp_json_encode( $params ) ) ); return $request; } } From 36bc4756e2142fa0367808c922cf22beabf9f2a6 Mon Sep 17 00:00:00 2001 From: Aditya Dhade <76063440+b1ink0@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:45:10 +0530 Subject: [PATCH 04/24] Remove redundant comment and improve doc comment Co-authored-by: Weston Ruter --- plugins/optimization-detective/detect.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 37978d2bc9..2cf6c5b442 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -313,7 +313,7 @@ function extendElementData( xpath, properties ) { /** * Compresses a string using CompressionStream API. * - * @param {string} string String to compress. + * @param {string} string - String to compress. * @return {Promise} Compressed data. */ async function compress( string ) { @@ -790,7 +790,6 @@ export default async function detect( { const maxBodyLengthKiB = 64; const maxBodyLengthBytes = maxBodyLengthKiB * 1024; - // TODO: Consider adding replacer to reduce precision on numbers in DOMRect to reduce payload size. const jsonBody = JSON.stringify( urlMetric ); const compressedJsonBody = await compress( jsonBody ); const percentOfBudget = From a4632d1664900cf73072f930e23aecc32d6e0366 Mon Sep 17 00:00:00 2001 From: Aditya Dhade <76063440+b1ink0@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:48:25 +0530 Subject: [PATCH 05/24] Keep XPath for matching the required pattern Co-authored-by: Weston Ruter --- plugins/optimization-detective/tests/storage/test-rest-api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 1174264a4e..55dc79df6f 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -354,7 +354,7 @@ public function data_provider_invalid_params(): array { array_merge( $valid_element, array( - 'xpath' => bin2hex( random_bytes( 65000 ) ), + 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', bin2hex( random_bytes( 65000 ) ) ), ) ), ), From 14409a6e5f8dcd3f5d76c304c6e14916bba123cc Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 6 Mar 2025 14:04:50 +0530 Subject: [PATCH 06/24] Remove `isDebug` conditions and improve JSDoc comments for consistency --- plugins/optimization-detective/detect.js | 60 +++++++++++------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 2cf6c5b442..de706b9009 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -197,7 +197,7 @@ function getCurrentTime() { /** * Recursively freezes an object to prevent mutation. * - * @param {Object} obj Object to recursively freeze. + * @param {Object} obj - Object to recursively freeze. */ function recursiveFreeze( obj ) { for ( const prop of Object.getOwnPropertyNames( obj ) ) { @@ -276,7 +276,7 @@ const reservedElementPropertyKeys = new Set( [ /** * Gets element data. * - * @param {string} xpath XPath. + * @param {string} xpath - XPath. * @return {ElementData|null} Element data, or null if no element for the XPath exists. */ function getElementData( xpath ) { @@ -292,8 +292,8 @@ function getElementData( xpath ) { /** * Extends element data. * - * @param {string} xpath XPath. - * @param {ExtendedElementData} properties Properties. + * @param {string} xpath - XPath. + * @param {ExtendedElementData} properties - Properties. */ function extendElementData( xpath, properties ) { if ( ! elementsByXPath.has( xpath ) ) { @@ -335,23 +335,23 @@ async function compress( string ) { /** * Detects the LCP element, loaded images, client viewport and store for future optimizations. * - * @param {Object} args Args. - * @param {string[]} args.extensionModuleUrls URLs for extension script modules to import. - * @param {number} args.minViewportAspectRatio Minimum aspect ratio allowed for the viewport. - * @param {number} args.maxViewportAspectRatio Maximum aspect ratio allowed for the viewport. - * @param {boolean} args.isDebug Whether to show debug messages. - * @param {string} args.restApiEndpoint URL for where to send the detection data. - * @param {string} [args.restApiNonce] Nonce for the REST API when the user is logged-in. - * @param {string} args.currentETag Current ETag. - * @param {string} args.currentUrl Current URL. - * @param {string} args.urlMetricSlug Slug for URL Metric. - * @param {number|null} args.cachePurgePostId Cache purge post ID. - * @param {string} args.urlMetricHMAC HMAC for URL Metric storage. - * @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses URL Metric group statuses. - * @param {number} args.storageLockTTL The TTL (in seconds) for the URL Metric storage lock. - * @param {number} args.freshnessTTL The freshness age (TTL) for a given URL Metric. - * @param {string} args.webVitalsLibrarySrc The URL for the web-vitals library. - * @param {CollectionDebugData} [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode. + * @param {Object} args - Args. + * @param {string[]} args.extensionModuleUrls - URLs for extension script modules to import. + * @param {number} args.minViewportAspectRatio - Minimum aspect ratio allowed for the viewport. + * @param {number} args.maxViewportAspectRatio - Maximum aspect ratio allowed for the viewport. + * @param {boolean} args.isDebug - Whether to show debug messages. + * @param {string} args.restApiEndpoint - URL for where to send the detection data. + * @param {string} [args.restApiNonce] - Nonce for the REST API when the user is logged-in. + * @param {string} args.currentETag - Current ETag. + * @param {string} args.currentUrl - Current URL. + * @param {string} args.urlMetricSlug - Slug for URL Metric. + * @param {number|null} args.cachePurgePostId - Cache purge post ID. + * @param {string} args.urlMetricHMAC - HMAC for URL Metric storage. + * @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses - URL Metric group statuses. + * @param {number} args.storageLockTTL - The TTL (in seconds) for the URL Metric storage lock. + * @param {number} args.freshnessTTL - The freshness age (TTL) for a given URL Metric. + * @param {string} args.webVitalsLibrarySrc - The URL for the web-vitals library. + * @param {CollectionDebugData} [args.urlMetricGroupCollection] - URL Metric group collection, when in debug mode. */ export default async function detect( { minViewportAspectRatio, @@ -665,9 +665,7 @@ export default async function detect( { for ( const elementIntersection of elementIntersections ) { const xpath = breadcrumbedElementsMap.get( elementIntersection.target ); if ( ! xpath ) { - if ( isDebug ) { - error( 'Unable to look up XPath for element' ); - } + warn( 'Unable to look up XPath for element' ); continue; } @@ -802,14 +800,12 @@ export default async function detect( { * than the maximum, we should avoid even trying to send it. */ if ( compressedJsonBody.size > maxBodyLengthBytes ) { - if ( isDebug ) { - error( - `Unable to send URL Metric because it is ${ compressedJsonBody.size.toLocaleString() } bytes, ${ Math.round( - percentOfBudget - ) }% of ${ maxBodyLengthKiB } KiB limit:`, - urlMetric - ); - } + error( + `Unable to send URL Metric because it is ${ compressedJsonBody.size.toLocaleString() } bytes, ${ Math.round( + percentOfBudget + ) }% of ${ maxBodyLengthKiB } KiB limit:`, + urlMetric + ); return; } From 2f81ec0f0b535f40b62d313f14318da78f850abb Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 6 Mar 2025 15:33:40 +0530 Subject: [PATCH 07/24] Add REST API request body decompression and related tests --- plugins/optimization-detective/hooks.php | 1 + .../storage/rest-api.php | 1 - .../tests/storage/test-rest-api.php | 36 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/plugins/optimization-detective/hooks.php b/plugins/optimization-detective/hooks.php index 2e9c9176b0..d80681937e 100644 --- a/plugins/optimization-detective/hooks.php +++ b/plugins/optimization-detective/hooks.php @@ -24,4 +24,5 @@ add_filter( 'site_status_tests', 'od_add_rest_api_availability_test' ); add_action( 'admin_init', 'od_maybe_run_rest_api_health_check' ); add_action( 'after_plugin_row_meta', 'od_render_rest_api_health_check_admin_notice_in_plugin_row', 30 ); +add_filter( 'rest_pre_dispatch', 'od_decompress_rest_request_body', 10, 3 ); // @codeCoverageIgnoreEnd diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 2d13d9b1fc..f40db10f3f 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -387,4 +387,3 @@ function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_RE } return $result; } -add_filter( 'rest_pre_dispatch', 'od_decompress_rest_request_body', 10, 3 ); diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 55dc79df6f..942958df76 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -588,6 +588,23 @@ public function test_rest_request_non_array_json_body(): void { $this->assertSame( 0, did_action( 'od_url_metric_stored' ) ); } + + /** + * Test invalid compressed JSON body. + * + * @covers ::od_register_endpoint + * @covers ::od_handle_rest_request + * @covers ::od_decompress_rest_request_body + */ + public function test_rest_request_invalid_compressed_json_body(): void { + $request = $this->create_request( $this->get_valid_params() ); + $request->set_body( 'Invalid compressed JSON body' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 400, $response->get_status(), 'Response: ' . wp_json_encode( $response ) ); + $this->assertSame( 'rest_invalid_payload', $response->get_data()['code'], 'Response: ' . wp_json_encode( $response ) ); + $this->assertSame( 0, did_action( 'od_url_metric_stored' ) ); + } + /** * Test timestamp ignored. * @@ -854,6 +871,25 @@ public function test_od_trigger_page_cache_invalidation_invalid_post_id(): void $this->assertSame( $before_save_post_count, did_action( 'save_post' ) ); } + /** + * Test that the request is modified by od_decompress_rest_request_body(). + * + * @covers ::od_register_endpoint + * @covers ::od_handle_rest_request + * @covers ::od_decompress_rest_request_body + */ + public function test_od_decompress_rest_request_body_modifies_request(): void { + $params = $this->get_valid_params(); + $request = $this->create_request( $this->get_valid_params() ); + unset( $params['hmac'], $params['slug'], $params['current_etag'], $params['cache_purge_post_id'] ); + $json_data = wp_json_encode( $params ); + $result = od_decompress_rest_request_body( null, rest_get_server(), $request ); + + $this->assertNotWPError( $result ); + $this->assertEquals( $json_data, $request->get_body() ); + $this->assertEquals( 'application/json', $request->get_header( 'Content-Type' ) ); + } + /** * Populate URL Metrics. * From 90aca14c355e5f245ab457efcddf9f6425c84f70 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 6 Mar 2025 16:05:48 +0530 Subject: [PATCH 08/24] Add test for `rest_pre_dispatch` hook and cover annotations --- plugins/optimization-detective/tests/storage/test-rest-api.php | 2 ++ plugins/optimization-detective/tests/test-hooks.php | 1 + 2 files changed, 3 insertions(+) diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 942958df76..162e00114e 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -75,6 +75,7 @@ static function ( array $properties ) use ( $property_name ): array { * @covers ::od_register_endpoint * @covers ::od_handle_rest_request * @covers ::od_trigger_page_cache_invalidation + * @covers ::od_decompress_rest_request_body * @covers OD_Strict_URL_Metric::set_additional_properties_to_false * @covers OD_URL_Metric_Store_Request_Context::__construct * @covers OD_URL_Metric_Store_Request_Context::__get @@ -456,6 +457,7 @@ public function data_provider_invalid_params(): array { * * @covers ::od_register_endpoint * @covers ::od_handle_rest_request + * @covers ::od_decompress_rest_request_body * @covers OD_Strict_URL_Metric::set_additional_properties_to_false * * @dataProvider data_provider_invalid_params diff --git a/plugins/optimization-detective/tests/test-hooks.php b/plugins/optimization-detective/tests/test-hooks.php index 1a0ad677ed..822a0030f9 100644 --- a/plugins/optimization-detective/tests/test-hooks.php +++ b/plugins/optimization-detective/tests/test-hooks.php @@ -21,5 +21,6 @@ public function test_hooks_added(): void { $this->assertEquals( 10, has_filter( 'site_status_tests', 'od_add_rest_api_availability_test' ) ); $this->assertEquals( 10, has_action( 'admin_init', 'od_maybe_run_rest_api_health_check' ) ); $this->assertEquals( 30, has_action( 'after_plugin_row_meta', 'od_render_rest_api_health_check_admin_notice_in_plugin_row' ) ); + $this->assertEquals( 10, has_filter( 'rest_pre_dispatch', 'od_decompress_rest_request_body' ) ); } } From 8b84ed3de4be7f7e37a67323202c814347e88adf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 10 Mar 2025 12:31:33 -0700 Subject: [PATCH 09/24] Suppress unused default export error --- plugins/optimization-detective/detect.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index de706b9009..a16a3ec928 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -1,3 +1,5 @@ +// noinspection JSUnusedGlobalSymbols + /** * @typedef {import("web-vitals").LCPMetric} LCPMetric * @typedef {import("web-vitals").LCPMetricWithAttribution} LCPMetricWithAttribution From c59c3ed36ebc6bd945ce27a1698a41f088ea7641 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 10 Mar 2025 12:36:59 -0700 Subject: [PATCH 10/24] Add ext-zlib to composer.json --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c4e3d954a7..9e7792e3c7 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ }, "require": { "php": "^7.2 || ^8.0", - "ext-json": "*" + "ext-json": "*", + "ext-zlib": "*" }, "suggest": { "ext-imagick": "Required to use Modern Image Format's Dominant_Color_Image_Editor_Imagick class", From 130277a556435d0d79357760102894a6c3996b4f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 10 Mar 2025 12:40:41 -0700 Subject: [PATCH 11/24] Switch ext-zlip from require to suggest --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 9e7792e3c7..ad714869f2 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,12 @@ }, "require": { "php": "^7.2 || ^8.0", - "ext-json": "*", - "ext-zlib": "*" + "ext-json": "*" }, "suggest": { "ext-imagick": "Required to use Modern Image Format's Dominant_Color_Image_Editor_Imagick class", - "ext-gd": "Required to use Modern Image Format's Dominant_Color_Image_Editor_GD class" + "ext-gd": "Required to use Modern Image Format's Dominant_Color_Image_Editor_GD class", + "ext-zlib": "Required for compression of URL Metric data submitted to the REST API for storage in Optmization Detective" }, "require-dev": { "phpcompatibility/php-compatibility": "^9.3", From cbfba125ad507ecbada2956e5f91285697e172b9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 10 Mar 2025 13:01:52 -0700 Subject: [PATCH 12/24] Run composer update --- composer.lock | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/composer.lock b/composer.lock index f1cbf362b5..beb6cf9c37 100644 --- a/composer.lock +++ b/composer.lock @@ -157,16 +157,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -205,7 +205,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -213,7 +213,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "phar-io/manifest", @@ -335,16 +335,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.7.1", + "version": "v6.7.2", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1" + "reference": "c04f96cb232fab12a3cbcccf5a47767f0665c3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/83448e918bf06d1ed3d67ceb6a985fc266a02fd1", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/c04f96cb232fab12a3cbcccf5a47767f0665c3f4", + "reference": "c04f96cb232fab12a3cbcccf5a47767f0665c3f4", "shasum": "" }, "require-dev": { @@ -377,9 +377,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.7.1" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.7.2" }, - "time": "2024-11-24T03:57:09+00:00" + "time": "2025-02-12T04:51:58+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -738,16 +738,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.14", + "version": "1.12.21", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e73868f809e68fff33be961ad4946e2e43ec9e38" + "reference": "14276fdef70575106a3392a4ed553c06a984df28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e73868f809e68fff33be961ad4946e2e43ec9e38", - "reference": "e73868f809e68fff33be961ad4946e2e43ec9e38", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/14276fdef70575106a3392a4ed553c06a984df28", + "reference": "14276fdef70575106a3392a4ed553c06a984df28", "shasum": "" }, "require": { @@ -792,7 +792,7 @@ "type": "github" } ], - "time": "2024-12-31T07:26:13+00:00" + "time": "2025-03-09T09:24:50+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -895,16 +895,16 @@ }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.6.1", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "daeec748b53de80a97498462513066834ec28f8b" + "reference": "b564ca479e7e735f750aaac4935af965572a7845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b", - "reference": "daeec748b53de80a97498462513066834ec28f8b", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/b564ca479e7e735f750aaac4935af965572a7845", + "reference": "b564ca479e7e735f750aaac4935af965572a7845", "shasum": "" }, "require": { @@ -938,9 +938,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.2" }, - "time": "2024-09-20T14:04:44+00:00" + "time": "2025-01-19T13:02:24+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2583,16 +2583,16 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^7.2 || ^8.0", "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 0ca808f3ecb1cc9a2fe6aa9dc1b00f34601fcd22 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 10 Mar 2025 13:03:56 -0700 Subject: [PATCH 13/24] Fix misspelling --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ad714869f2..a0de99af67 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "suggest": { "ext-imagick": "Required to use Modern Image Format's Dominant_Color_Image_Editor_Imagick class", "ext-gd": "Required to use Modern Image Format's Dominant_Color_Image_Editor_GD class", - "ext-zlib": "Required for compression of URL Metric data submitted to the REST API for storage in Optmization Detective" + "ext-zlib": "Required for compression of URL Metric data submitted to the REST API for storage in Optimization Detective" }, "require-dev": { "phpcompatibility/php-compatibility": "^9.3", From e1d4b1e5c76e0827ecb0c7899134fa8ec52b731a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 10 Mar 2025 13:43:15 -0700 Subject: [PATCH 14/24] Keep timestamps for URL Metrics current in fixture Addresses oversight from #1903 --- .../tests/test-class-od-url-metrics-group.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php b/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php index 2f769012e4..209bb2b26d 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php @@ -450,6 +450,9 @@ public function test_get_lcp_element_when_group_half_stale( bool $order_reversed $url_metrics = array(); $etag_counts = array(); foreach ( $url_metrics_data as $url_metric_data ) { + // Make sure the timestamp is always current as otherwise groups will never be complete, regardless of having a current ETag. + $url_metric_data['timestamp'] = microtime( true ); + $url_metric = new OD_URL_Metric( $url_metric_data ); $etag = $url_metric->get_etag(); if ( ! isset( $etag_counts[ $etag ] ) ) { From af1d1dcb1a620414f0d8a290a9cf4631a2bb0415 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Wed, 12 Mar 2025 23:42:11 +0530 Subject: [PATCH 15/24] Compress URL metrics only when `gzdecode` is available --- plugins/optimization-detective/detect.js | 16 ++++++++++------ plugins/optimization-detective/detection.php | 3 +++ .../optimization-detective/storage/rest-api.php | 9 +++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index a16a3ec928..713790dbb2 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -344,6 +344,7 @@ async function compress( string ) { * @param {boolean} args.isDebug - Whether to show debug messages. * @param {string} args.restApiEndpoint - URL for where to send the detection data. * @param {string} [args.restApiNonce] - Nonce for the REST API when the user is logged-in. + * @param {boolean} [args.gzdecodeAvailable] - Whether application/gzip can be sent to the REST API. * @param {string} args.currentETag - Current ETag. * @param {string} args.currentUrl - Current URL. * @param {string} args.urlMetricSlug - Slug for URL Metric. @@ -362,6 +363,7 @@ export default async function detect( { extensionModuleUrls, restApiEndpoint, restApiNonce, + gzdecodeAvailable, currentETag, currentUrl, urlMetricSlug, @@ -791,9 +793,11 @@ export default async function detect( { const maxBodyLengthBytes = maxBodyLengthKiB * 1024; const jsonBody = JSON.stringify( urlMetric ); - const compressedJsonBody = await compress( jsonBody ); + const payloadBlob = gzdecodeAvailable + ? await compress( jsonBody ) + : new Blob( [ jsonBody ], { type: 'application/json' } ); const percentOfBudget = - ( compressedJsonBody.size / ( maxBodyLengthKiB * 1000 ) ) * 100; + ( payloadBlob.size / ( maxBodyLengthKiB * 1000 ) ) * 100; /* * According to the fetch() spec: @@ -801,9 +805,9 @@ export default async function detect( { * This is what browsers also implement for navigator.sendBeacon(). Therefore, if the size of the JSON is greater * than the maximum, we should avoid even trying to send it. */ - if ( compressedJsonBody.size > maxBodyLengthBytes ) { + if ( payloadBlob.size > maxBodyLengthBytes ) { error( - `Unable to send URL Metric because it is ${ compressedJsonBody.size.toLocaleString() } bytes, ${ Math.round( + `Unable to send URL Metric because it is ${ payloadBlob.size.toLocaleString() } bytes, ${ Math.round( percentOfBudget ) }% of ${ maxBodyLengthKiB } KiB limit:`, urlMetric @@ -821,7 +825,7 @@ export default async function detect( { String( getCurrentTime() ) ); - const message = `Sending URL Metric (${ compressedJsonBody.size.toLocaleString() } bytes, ${ Math.round( + const message = `Sending URL Metric (${ payloadBlob.size.toLocaleString() } bytes, ${ Math.round( percentOfBudget ) }% of ${ maxBodyLengthKiB } KiB limit):`; @@ -845,7 +849,7 @@ export default async function detect( { ); } url.searchParams.set( 'hmac', urlMetricHMAC ); - navigator.sendBeacon( url, compressedJsonBody ); + navigator.sendBeacon( url, payloadBlob ); // Clean up. breadcrumbedElementsMap.clear(); diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 9ab3f6d0ae..267e3ef9d4 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -144,6 +144,9 @@ static function ( OD_URL_Metric_Group $group ): array { if ( is_user_logged_in() ) { $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); } + if ( function_exists( 'gzdecode' ) ) { + $detect_args['gzdecodeAvailable'] = true; + } if ( WP_DEBUG ) { $detect_args['urlMetricGroupCollection'] = $group_collection; } diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index f40db10f3f..86a5535e6b 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -342,7 +342,8 @@ function od_trigger_page_cache_invalidation( int $cache_purge_post_id ): void { function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_REST_Request $request ) { if ( $request->get_route() === '/' . OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE && - 'application/gzip' === $request->get_header( 'Content-Type' ) + 'application/gzip' === $request->get_header( 'Content-Type' ) && + function_exists( 'gzdecode' ) ) { $compressed_body = $request->get_body(); @@ -367,11 +368,7 @@ function od_decompress_rest_request_body( $result, WP_REST_Server $server, WP_RE ); } - try { - $decompressed_body = gzdecode( $compressed_body ); - } catch ( Exception $e ) { - $decompressed_body = false; - } + $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( From 82fe8e92be930b3e4e9c44138ea11ccadb531c7d Mon Sep 17 00:00:00 2001 From: Aditya Dhade <76063440+b1ink0@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:55:26 +0530 Subject: [PATCH 16/24] Always pass `gzdecodeAvailable` to client Co-authored-by: Weston Ruter --- plugins/optimization-detective/detect.js | 2 +- plugins/optimization-detective/detection.php | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 452c654093..7fab68bf8a 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -361,7 +361,7 @@ async function compress( string ) { * @param {boolean} args.isDebug - Whether to show debug messages. * @param {string} args.restApiEndpoint - URL for where to send the detection data. * @param {string} [args.restApiNonce] - Nonce for the REST API when the user is logged-in. - * @param {boolean} [args.gzdecodeAvailable] - Whether application/gzip can be sent to the REST API. + * @param {boolean} args.gzdecodeAvailable - Whether application/gzip can be sent to the REST API. * @param {string} args.currentETag - Current ETag. * @param {string} args.currentUrl - Current URL. * @param {string} args.urlMetricSlug - Slug for URL Metric. diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 267e3ef9d4..42614933fe 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -144,9 +144,7 @@ static function ( OD_URL_Metric_Group $group ): array { if ( is_user_logged_in() ) { $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); } - if ( function_exists( 'gzdecode' ) ) { - $detect_args['gzdecodeAvailable'] = true; - } + $detect_args['gzdecodeAvailable'] = function_exists( 'gzdecode' ); if ( WP_DEBUG ) { $detect_args['urlMetricGroupCollection'] = $group_collection; } From 1984db087e2d79161a387df4a5370e99e9551edf Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 13 Mar 2025 21:40:05 +0530 Subject: [PATCH 17/24] Add max URL metrics size constraints --- plugins/optimization-detective/detect.js | 10 ++++++++++ plugins/optimization-detective/detection.php | 3 ++- plugins/optimization-detective/helper.php | 20 +++++++++++++++++++ .../storage/rest-api.php | 16 +++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 7fab68bf8a..e8a7c874df 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -362,6 +362,7 @@ async function compress( string ) { * @param {string} args.restApiEndpoint - URL for where to send the detection data. * @param {string} [args.restApiNonce] - Nonce for the REST API when the user is logged-in. * @param {boolean} args.gzdecodeAvailable - Whether application/gzip can be sent to the REST API. + * @param {number} args.maxUrlMetricSize - Maximum size of the URL Metric to send. * @param {string} args.currentETag - Current ETag. * @param {string} args.currentUrl - Current URL. * @param {string} args.urlMetricSlug - Slug for URL Metric. @@ -381,6 +382,7 @@ export default async function detect( { restApiEndpoint, restApiNonce, gzdecodeAvailable, + maxUrlMetricSize, currentETag, currentUrl, urlMetricSlug, @@ -815,6 +817,14 @@ export default async function detect( { const maxBodyLengthBytes = maxBodyLengthKiB * 1024; const jsonBody = JSON.stringify( urlMetric ); + if ( jsonBody.length > maxUrlMetricSize ) { + error( + `URL Metric is ${ jsonBody.length.toLocaleString() } bytes, exceeding the maximum size of ${ maxUrlMetricSize.toLocaleString() } bytes:`, + urlMetric + ); + return; + } + const payloadBlob = gzdecodeAvailable ? await compress( jsonBody ) : new Blob( [ jsonBody ], { type: 'application/json' } ); diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 42614933fe..03ce5892a6 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -140,11 +140,12 @@ static function ( OD_URL_Metric_Group $group ): array { 'storageLockTTL' => OD_Storage_Lock::get_ttl(), 'freshnessTTL' => od_get_url_metric_freshness_ttl(), 'webVitalsLibrarySrc' => $web_vitals_lib_src, + 'gzdecodeAvailable' => function_exists( 'gzdecode' ), + 'maxUrlMetricSize' => od_get_max_url_metric_size(), ); if ( is_user_logged_in() ) { $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); } - $detect_args['gzdecodeAvailable'] = function_exists( 'gzdecode' ); if ( WP_DEBUG ) { $detect_args['urlMetricGroupCollection'] = $group_collection; } diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index 46656607cc..5367ed04ad 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -122,3 +122,23 @@ function od_get_asset_path( string $src_path, ?string $min_path = null ): string return $min_path; } + +/** + * Gets the maximum allowed size in bytes for a URL Metric serialized to JSON. + * + * @since n.e.x.t + * @access private + * + * @return int Maximum allowed byte size. + */ +function od_get_max_url_metric_size(): int { + /** + * Filters the maximum allowed size in bytes for a URL Metric serialized to JSON. + * + * @since n.e.x.t + * + * @param int $max_size Maximum allowed byte size. + * @return int Filtered maximum allowed byte size. + */ + return (int) apply_filters( 'od_max_url_metric_size', MB_IN_BYTES ); +} diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 86a5535e6b..4100f5ff28 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -218,6 +218,22 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } + // Limit JSON payload size to safeguard against clients sending possibly malicious payloads much larger than allowed. + $max_size = od_get_max_url_metric_size(); + $content_length = strlen( (string) wp_json_encode( $url_metric ) ); + 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 */ + __( '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 ) + ); + } + try { $url_metric_group->add_url_metric( $url_metric ); } catch ( InvalidArgumentException $e ) { From 5f0be2976926deeae4b1a7c7f3d4e3ff43c94ba9 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 13 Mar 2025 23:47:58 +0530 Subject: [PATCH 18/24] Add tests for max URL metrics size filter in REST API --- .../tests/storage/test-rest-api.php | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index ea3a7de867..a5c5ea000f 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -349,12 +349,30 @@ public function data_provider_invalid_params(): array { 'params' => array_merge( $valid_params, array( - // Fill the JSON with more than 64KB of data. + // Fill the JSON with more than 64KB of incomprehensible data. 'elements' => array( array_merge( $valid_element, array( - 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', bin2hex( random_bytes( 65000 ) ) ), + 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', bin2hex( random_bytes( KB_IN_BYTES * 65 ) ) ), + ) + ), + ), + ) + ), + 'expected_status' => 413, + 'expected_code' => 'rest_content_too_large', + ), + 'invalid_decoded_json_body_content_length' => array( + 'params' => array_merge( + $valid_params, + array( + // Fill the JSON with more than 1MB of highly compressible data. + 'elements' => array( + array_merge( + $valid_element, + array( + 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', str_repeat( 'A', MB_IN_BYTES ) ), ) ), ), @@ -891,6 +909,100 @@ public function test_od_decompress_rest_request_body_modifies_request(): void { $this->assertEquals( 'application/json', $request->get_header( 'Content-Type' ) ); } + /** + * Test that the `od_max_url_metric_size` filter can be used to modify the maximum size of URL Metrics. + * + * @dataProvider data_provider_max_url_metrics_size_filter + * + * @covers ::od_register_endpoint + * @covers ::od_handle_rest_request + * @covers ::od_get_max_url_metric_size + * + * @param Closure $set_up Set up function. + * @param array $params Params. + * @param int $expected_status Expected status. + * @param string|null $expected_code Expected code. + */ + public function test_max_url_metrics_size_filter( Closure $set_up, array $params, int $expected_status, ?string $expected_code ): void { + $set_up(); + $request = $this->create_request( $params ); + unset( $params['hmac'], $params['slug'], $params['current_etag'], $params['cache_purge_post_id'] ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( $expected_status, $response->get_status(), 'Response: ' . wp_json_encode( $response->get_data() ) ); + if ( null !== $expected_code ) { + $this->assertSame( $expected_code, $response->get_data()['code'] ); + } + } + + /** + * Data provider for test_max_url_metrics_size_filter. + * + * @return array Test data. + */ + public function data_provider_max_url_metrics_size_filter(): array { + $valid_params = $this->get_valid_params(); + $valid_element = $valid_params['elements'][0]; + + return array( + 'url_metrics_should_be_accepted_because_of_increased_max_url_metrics_size' => array( + 'set_up' => static function (): void { + add_filter( + 'od_max_url_metric_size', + static function (): int { + return MB_IN_BYTES * 2; + } + ); + }, + 'params' => array_merge( + $valid_params, + array( + // Fill the JSON with more than 1MB of data. + 'elements' => array( + array_merge( + $valid_element, + array( + 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', str_repeat( 'A', MB_IN_BYTES ) ), + ) + ), + ), + ) + ), + 'expected_status' => 200, + 'expected_code' => null, + ), + 'url_metrics_should_be_rejected_because_of_decreased_max_url_metrics_size' => array( + 'set_up' => static function (): void { + add_filter( + 'od_max_url_metric_size', + static function (): int { + return MB_IN_BYTES / 2; + } + ); + }, + 'params' => array_merge( + $valid_params, + array( + // Fill the JSON with more than 1MB of data. + 'elements' => array( + array_merge( + $valid_element, + array( + 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', str_repeat( 'A', MB_IN_BYTES ) ), + ) + ), + ), + ) + ), + 'expected_status' => 413, + 'expected_code' => 'rest_content_too_large', + ), + + ); + } + /** * Populate URL Metrics. * From f9d7fe1ab5984d2260b4ca8907981d4f0cc070bb Mon Sep 17 00:00:00 2001 From: Aditya Dhade <76063440+b1ink0@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:05:26 +0530 Subject: [PATCH 19/24] Ensure `od_get_max_url_metric_size` returns a valid positive value with fallback Co-authored-by: Weston Ruter --- plugins/optimization-detective/helper.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index 5367ed04ad..a8f90726ba 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -129,7 +129,7 @@ function od_get_asset_path( string $src_path, ?string $min_path = null ): string * @since n.e.x.t * @access private * - * @return int Maximum allowed byte size. + * @return positive-int Maximum allowed byte size. */ function od_get_max_url_metric_size(): int { /** @@ -140,5 +140,20 @@ function od_get_max_url_metric_size(): int { * @param int $max_size Maximum allowed byte size. * @return int Filtered maximum allowed byte size. */ - return (int) apply_filters( 'od_max_url_metric_size', MB_IN_BYTES ); + $size = (int) apply_filters( 'od_max_url_metric_size', MB_IN_BYTES ); + if ( $size <= 0 ) { + _doing_it_wrong( + __FUNCTION__, + esc_html( + sprintf( + /* translators: 1: filter name, 2: size */ + __( 'Filter %1$s returned invalid "%2$s". Must be greater than zero.', 'optimization-detective' ), + $size + ) + ), + '' + ); + $size = MB_IN_BYTES; + } + return $size; } From 2a11bf5f4f43b308c46d13657b8a5635577ffd0a Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Fri, 14 Mar 2025 00:34:57 +0530 Subject: [PATCH 20/24] Improve DOC comment for filter --- plugins/optimization-detective/helper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index a8f90726ba..f1a6849cd0 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -135,6 +135,8 @@ function od_get_max_url_metric_size(): int { /** * Filters the maximum allowed size in bytes for a URL Metric serialized to JSON. * + * The default value is 1 MB. + * * @since n.e.x.t * * @param int $max_size Maximum allowed byte size. From 746a410b8ebc6ac84738284d3b8bfa6e94778973 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Mar 2025 19:05:03 -0700 Subject: [PATCH 21/24] Use maximum instead of max in PHP; add test coverage --- plugins/optimization-detective/detection.php | 2 +- plugins/optimization-detective/helper.php | 12 +-- .../storage/rest-api.php | 2 +- .../tests/storage/test-rest-api.php | 78 +++++++++++++------ 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 03ce5892a6..a82040af63 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -141,7 +141,7 @@ static function ( OD_URL_Metric_Group $group ): array { 'freshnessTTL' => od_get_url_metric_freshness_ttl(), 'webVitalsLibrarySrc' => $web_vitals_lib_src, 'gzdecodeAvailable' => function_exists( 'gzdecode' ), - 'maxUrlMetricSize' => od_get_max_url_metric_size(), + 'maxUrlMetricSize' => od_get_maximum_url_metric_size(), ); if ( is_user_logged_in() ) { $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index f1a6849cd0..a290435147 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -131,7 +131,7 @@ function od_get_asset_path( string $src_path, ?string $min_path = null ): string * * @return positive-int Maximum allowed byte size. */ -function od_get_max_url_metric_size(): int { +function od_get_maximum_url_metric_size(): int { /** * Filters the maximum allowed size in bytes for a URL Metric serialized to JSON. * @@ -142,18 +142,18 @@ function od_get_max_url_metric_size(): int { * @param int $max_size Maximum allowed byte size. * @return int Filtered maximum allowed byte size. */ - $size = (int) apply_filters( 'od_max_url_metric_size', MB_IN_BYTES ); + $size = (int) apply_filters( 'od_maximum_url_metric_size', MB_IN_BYTES ); if ( $size <= 0 ) { _doing_it_wrong( - __FUNCTION__, + esc_html( "Filter: 'od_maximum_url_metric_size'" ), esc_html( sprintf( - /* translators: 1: filter name, 2: size */ - __( 'Filter %1$s returned invalid "%2$s". Must be greater than zero.', 'optimization-detective' ), + /* translators: %s: size */ + __( 'Invalid size "%s". Must be greater than zero.', 'optimization-detective' ), $size ) ), - '' + 'Optimization Detective 1.0.0' ); $size = MB_IN_BYTES; } diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 4100f5ff28..2b9cf0b19e 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -219,7 +219,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { } // Limit JSON payload size to safeguard against clients sending possibly malicious payloads much larger than allowed. - $max_size = od_get_max_url_metric_size(); + $max_size = od_get_maximum_url_metric_size(); $content_length = strlen( (string) wp_json_encode( $url_metric ) ); if ( $content_length > $max_size ) { return new WP_Error( diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index a5c5ea000f..83ceccba02 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -910,21 +910,25 @@ public function test_od_decompress_rest_request_body_modifies_request(): void { } /** - * Test that the `od_max_url_metric_size` filter can be used to modify the maximum size of URL Metrics. + * Test that the `od_maximum_url_metric_size` filter can be used to modify the maximum size of URL Metrics. * - * @dataProvider data_provider_max_url_metrics_size_filter + * @dataProvider data_provider_maximum_url_metrics_size_filter * * @covers ::od_register_endpoint * @covers ::od_handle_rest_request - * @covers ::od_get_max_url_metric_size + * @covers ::od_get_maximum_url_metric_size * - * @param Closure $set_up Set up function. - * @param array $params Params. - * @param int $expected_status Expected status. - * @param string|null $expected_code Expected code. + * @param Closure $set_up Set up function. + * @param array $params Params. + * @param int $expected_status Expected status. + * @param string|null $expected_code Expected code. + * @param bool $expected_incorrect_usage Expected incorrect usage. */ - public function test_max_url_metrics_size_filter( Closure $set_up, array $params, int $expected_status, ?string $expected_code ): void { + public function test_maximum_url_metrics_size_filter( Closure $set_up, array $params, int $expected_status, ?string $expected_code, bool $expected_incorrect_usage ): void { $set_up(); + if ( $expected_incorrect_usage ) { + $this->setExpectedIncorrectUsage( 'Filter: 'od_maximum_url_metric_size'' ); + } $request = $this->create_request( $params ); unset( $params['hmac'], $params['slug'], $params['current_etag'], $params['cache_purge_post_id'] ); $request->set_header( 'Content-Type', 'application/json' ); @@ -938,25 +942,25 @@ public function test_max_url_metrics_size_filter( Closure $set_up, array $params } /** - * Data provider for test_max_url_metrics_size_filter. + * Data provider for test_maximum_url_metrics_size_filter. * * @return array Test data. */ - public function data_provider_max_url_metrics_size_filter(): array { + public function data_provider_maximum_url_metrics_size_filter(): array { $valid_params = $this->get_valid_params(); $valid_element = $valid_params['elements'][0]; return array( - 'url_metrics_should_be_accepted_because_of_increased_max_url_metrics_size' => array( - 'set_up' => static function (): void { + 'url_metrics_should_be_accepted_because_of_increased_maximum_url_metrics_size' => array( + 'set_up' => static function (): void { add_filter( - 'od_max_url_metric_size', + 'od_maximum_url_metric_size', static function (): int { return MB_IN_BYTES * 2; } ); }, - 'params' => array_merge( + 'params' => array_merge( $valid_params, array( // Fill the JSON with more than 1MB of data. @@ -970,19 +974,20 @@ static function (): int { ), ) ), - 'expected_status' => 200, - 'expected_code' => null, + 'expected_status' => 200, + 'expected_code' => null, + 'expected_incorrect_usage' => false, ), - 'url_metrics_should_be_rejected_because_of_decreased_max_url_metrics_size' => array( - 'set_up' => static function (): void { + 'url_metrics_should_be_rejected_because_of_decreased_maximum_url_metrics_size' => array( + 'set_up' => static function (): void { add_filter( - 'od_max_url_metric_size', + 'od_maximum_url_metric_size', static function (): int { return MB_IN_BYTES / 2; } ); }, - 'params' => array_merge( + 'params' => array_merge( $valid_params, array( // Fill the JSON with more than 1MB of data. @@ -996,10 +1001,37 @@ static function (): int { ), ) ), - 'expected_status' => 413, - 'expected_code' => 'rest_content_too_large', + 'expected_status' => 413, + 'expected_code' => 'rest_content_too_large', + 'expected_incorrect_usage' => false, + ), + 'negative_maximum_url_metric_size_is_treated_as_1mb' => array( + 'set_up' => static function (): void { + add_filter( + 'od_maximum_url_metric_size', + static function (): int { + return -1; + } + ); + }, + 'params' => array_merge( + $valid_params, + array( + // Fill the JSON with more than 1MB of data. + 'elements' => array( + array_merge( + $valid_element, + array( + 'xpath' => sprintf( '/HTML/BODY/DIV[@id=\'%s\']/*[1][self::DIV]', str_repeat( 'A', MB_IN_BYTES / 2 ) ), + ) + ), + ), + ) + ), + 'expected_status' => 200, + 'expected_code' => null, + 'expected_incorrect_usage' => true, ), - ); } From d41da3cbad5ae41081a72f8c8d19a10b7c49d0e3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Mar 2025 19:15:57 -0700 Subject: [PATCH 22/24] Move od_get_maximum_url_metric_size to storage/data.php --- plugins/optimization-detective/helper.php | 37 ------------------- .../optimization-detective/storage/data.php | 37 +++++++++++++++++++ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index a290435147..46656607cc 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -122,40 +122,3 @@ function od_get_asset_path( string $src_path, ?string $min_path = null ): string return $min_path; } - -/** - * Gets the maximum allowed size in bytes for a URL Metric serialized to JSON. - * - * @since n.e.x.t - * @access private - * - * @return positive-int Maximum allowed byte size. - */ -function od_get_maximum_url_metric_size(): int { - /** - * Filters the maximum allowed size in bytes for a URL Metric serialized to JSON. - * - * The default value is 1 MB. - * - * @since n.e.x.t - * - * @param int $max_size Maximum allowed byte size. - * @return int Filtered maximum allowed byte size. - */ - $size = (int) apply_filters( 'od_maximum_url_metric_size', MB_IN_BYTES ); - if ( $size <= 0 ) { - _doing_it_wrong( - esc_html( "Filter: 'od_maximum_url_metric_size'" ), - esc_html( - sprintf( - /* translators: %s: size */ - __( 'Invalid size "%s". Must be greater than zero.', 'optimization-detective' ), - $size - ) - ), - 'Optimization Detective 1.0.0' - ); - $size = MB_IN_BYTES; - } - return $size; -} diff --git a/plugins/optimization-detective/storage/data.php b/plugins/optimization-detective/storage/data.php index fb67b6ae91..17521c11b5 100644 --- a/plugins/optimization-detective/storage/data.php +++ b/plugins/optimization-detective/storage/data.php @@ -462,3 +462,40 @@ function od_get_url_metrics_breakpoint_sample_size(): int { return $sample_size; } + +/** + * Gets the maximum allowed size in bytes for a URL Metric serialized to JSON. + * + * @since n.e.x.t + * @access private + * + * @return positive-int Maximum allowed byte size. + */ +function od_get_maximum_url_metric_size(): int { + /** + * Filters the maximum allowed size in bytes for a URL Metric serialized to JSON. + * + * The default value is 1 MB. + * + * @since n.e.x.t + * + * @param int $max_size Maximum allowed byte size. + * @return int Filtered maximum allowed byte size. + */ + $size = (int) apply_filters( 'od_maximum_url_metric_size', MB_IN_BYTES ); + if ( $size <= 0 ) { + _doing_it_wrong( + esc_html( "Filter: 'od_maximum_url_metric_size'" ), + esc_html( + sprintf( + /* translators: %s: size */ + __( 'Invalid size "%s". Must be greater than zero.', 'optimization-detective' ), + $size + ) + ), + 'Optimization Detective 1.0.0' + ); + $size = MB_IN_BYTES; + } + return $size; +} From ad6d264a431b4c31655d7286025ce96e24d27d58 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Mar 2025 19:21:50 -0700 Subject: [PATCH 23/24] Fix passing filter name to _doing_it_wrong() --- plugins/optimization-detective/storage/data.php | 10 ++++------ .../optimization-detective/tests/storage/test-data.php | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/plugins/optimization-detective/storage/data.php b/plugins/optimization-detective/storage/data.php index 17521c11b5..9c50188205 100644 --- a/plugins/optimization-detective/storage/data.php +++ b/plugins/optimization-detective/storage/data.php @@ -37,7 +37,7 @@ function od_get_url_metric_freshness_ttl(): int { if ( $freshness_ttl < 0 ) { _doing_it_wrong( - __FUNCTION__, + esc_html( "Filter: 'od_url_metric_freshness_ttl'" ), esc_html( sprintf( /* translators: %s is the TTL freshness */ @@ -382,15 +382,13 @@ function od_get_maximum_viewport_aspect_ratio(): float { * @return positive-int[] Breakpoint max widths, sorted in ascending order. */ function od_get_breakpoint_max_widths(): array { - $function_name = __FUNCTION__; - $breakpoint_max_widths = array_map( - static function ( $original_breakpoint ) use ( $function_name ): int { + static function ( $original_breakpoint ): int { $breakpoint = $original_breakpoint; if ( $breakpoint <= 0 ) { $breakpoint = 1; _doing_it_wrong( - esc_html( $function_name ), + esc_html( "Filter: 'od_breakpoint_max_widths'" ), esc_html( sprintf( /* translators: %s is the actual breakpoint max width */ @@ -447,7 +445,7 @@ function od_get_url_metrics_breakpoint_sample_size(): int { if ( $sample_size <= 0 ) { _doing_it_wrong( - __FUNCTION__, + esc_html( "Filter: 'od_url_metrics_breakpoint_sample_size'" ), esc_html( sprintf( /* translators: %s is the sample size */ diff --git a/plugins/optimization-detective/tests/storage/test-data.php b/plugins/optimization-detective/tests/storage/test-data.php index 0d154a9d5a..df72483063 100644 --- a/plugins/optimization-detective/tests/storage/test-data.php +++ b/plugins/optimization-detective/tests/storage/test-data.php @@ -48,10 +48,10 @@ static function (): int { /** * Test bad od_get_url_metric_freshness_ttl(). * - * @expectedIncorrectUsage od_get_url_metric_freshness_ttl * @covers ::od_get_url_metric_freshness_ttl */ public function test_bad_od_get_url_metric_freshness_ttl(): void { + $this->setExpectedIncorrectUsage( 'Filter: 'od_url_metric_freshness_ttl'' ); add_filter( 'od_url_metric_freshness_ttl', static function (): int { @@ -728,13 +728,13 @@ public function data_provider_test_bad_od_get_breakpoint_max_widths(): array { * * @covers ::od_get_breakpoint_max_widths * - * @expectedIncorrectUsage od_get_breakpoint_max_widths * @dataProvider data_provider_test_bad_od_get_breakpoint_max_widths * * @param int[] $breakpoints Breakpoints. * @param int[] $expected Expected breakpoints. */ public function test_bad_od_get_breakpoint_max_widths( array $breakpoints, array $expected ): void { + $this->setExpectedIncorrectUsage( 'Filter: 'od_breakpoint_max_widths'' ); add_filter( 'od_breakpoint_max_widths', static function () use ( $breakpoints ): array { @@ -766,10 +766,10 @@ static function (): string { /** * Test bad od_get_url_metrics_breakpoint_sample_size(). * - * @expectedIncorrectUsage od_get_url_metrics_breakpoint_sample_size * @covers ::od_get_url_metrics_breakpoint_sample_size */ public function test_bad_od_get_url_metrics_breakpoint_sample_size(): void { + $this->setExpectedIncorrectUsage( 'Filter: 'od_url_metrics_breakpoint_sample_size'' ); add_filter( 'od_url_metrics_breakpoint_sample_size', static function (): int { From c0e3291f455af9c3d14ae6b0bffc00ab776c039b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Mar 2025 22:49:20 -0700 Subject: [PATCH 24/24] Rename string to jsonString to avoid potential type confusion --- plugins/optimization-detective/detect.js | 8 ++++---- plugins/optimization-detective/storage/data.php | 2 +- plugins/optimization-detective/storage/rest-api.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index e8a7c874df..cd42fd583a 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -330,13 +330,13 @@ function extendElementData( xpath, properties ) { } /** - * Compresses a string using CompressionStream API. + * Compresses a (JSON) string using CompressionStream API. * - * @param {string} string - String to compress. + * @param {string} jsonString - JSON string to compress. * @return {Promise} Compressed data. */ -async function compress( string ) { - const encodedData = new TextEncoder().encode( string ); +async function compress( jsonString ) { + const encodedData = new TextEncoder().encode( jsonString ); const compressedDataStream = new Blob( [ encodedData ] ) .stream() .pipeThrough( new CompressionStream( 'gzip' ) ); diff --git a/plugins/optimization-detective/storage/data.php b/plugins/optimization-detective/storage/data.php index 9c50188205..0ad854ce22 100644 --- a/plugins/optimization-detective/storage/data.php +++ b/plugins/optimization-detective/storage/data.php @@ -486,7 +486,7 @@ function od_get_maximum_url_metric_size(): int { esc_html( "Filter: 'od_maximum_url_metric_size'" ), esc_html( sprintf( - /* translators: %s: size */ + /* translators: %s: size */ __( 'Invalid size "%s". Must be greater than zero.', 'optimization-detective' ), $size ) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 2b9cf0b19e..ceda7bd891 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -353,7 +353,7 @@ function od_trigger_page_cache_invalidation( int $cache_purge_post_id ): void { * @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 Response to replace the requested version with. + * @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 ) { if (