From 803d0b5057b55cf56f03adb72b468764399854cd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Jan 2025 14:05:46 -0800 Subject: [PATCH 01/11] Disable storage locking for administrators --- plugins/optimization-detective/docs/hooks.md | 8 ++++++-- .../storage/class-od-storage-lock.php | 14 +++++++++----- .../tests/storage/test-class-od-storage-lock.php | 6 ++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/plugins/optimization-detective/docs/hooks.md b/plugins/optimization-detective/docs/hooks.md index 09e20bad44..2417f65ede 100644 --- a/plugins/optimization-detective/docs/hooks.md +++ b/plugins/optimization-detective/docs/hooks.md @@ -102,9 +102,11 @@ add_filter( 'od_url_metrics_breakpoint_sample_size', function (): int { } ); ``` -### Filter: `od_url_metric_storage_lock_ttl` (default: 1 minute in seconds) +### Filter: `od_url_metric_storage_lock_ttl` (default: 60 seconds, except 0 for admins) -Filters how long a given IP is locked from submitting another metric-storage REST API request. Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: +Filters how long the current IP is locked from submitting another URL metric storage REST API request. + +Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: ```php add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { @@ -112,6 +114,8 @@ add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { } ); ``` +By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. + During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: ```php diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 889ef2d204..03fff94b9c 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -26,25 +26,29 @@ final class OD_Storage_Lock { * @since 0.1.0 * @access private * - * @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. + * @return int<0, max> TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. */ public static function get_ttl(): int { + $ttl = current_user_can( 'manage_options' ) ? 0 : MINUTE_IN_SECONDS; /** - * Filters how long a given IP is locked from submitting another metric-storage REST API request. + * Filters how long the current IP is locked from submitting another URL metric storage REST API request. * - * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable + * Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable * locking when a user is logged-in with code like the following: * * add_filter( 'od_metrics_storage_lock_ttl', static function ( int $ttl ): int { * return is_user_logged_in() ? 0 : $ttl; * } ); * + * By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. + * * @since 0.1.0 + * @since 1.0.0 This now defaults to zero (0) for administrator users. * - * @param int $ttl TTL. + * @param int $ttl TTL. Defaults to 0 for administrators, and 60 for everyone else. */ - $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', MINUTE_IN_SECONDS ); + $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', $ttl ); return max( 0, $ttl ); } diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index 4c115f162f..2166ad9262 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -28,6 +28,12 @@ public function data_provider_get_ttl(): array { 'set_up' => static function (): void {}, 'expected' => MINUTE_IN_SECONDS, ), + 'unfiltered_admin' => array( + 'set_up' => static function (): void { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + }, + 'expected' => 0, + ), 'filtered_hour' => array( 'set_up' => static function (): void { add_filter( From 85ce02b3fd4ce3749716b3e34949b34a0c46b9aa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Jan 2025 14:42:29 -0800 Subject: [PATCH 02/11] Introduce od_store_url_metric_now meta cap which maps to manage_options by default --- plugins/optimization-detective/docs/hooks.md | 2 +- plugins/optimization-detective/hooks.php | 1 + .../storage/class-od-storage-lock.php | 49 +++++++++++++- .../storage/test-class-od-storage-lock.php | 16 +++++ .../tests/test-detection.php | 64 +++++++++++++++++-- 5 files changed, 124 insertions(+), 8 deletions(-) diff --git a/plugins/optimization-detective/docs/hooks.md b/plugins/optimization-detective/docs/hooks.md index 2417f65ede..a844695553 100644 --- a/plugins/optimization-detective/docs/hooks.md +++ b/plugins/optimization-detective/docs/hooks.md @@ -114,7 +114,7 @@ add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { } ); ``` -By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. +By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. Whether the current user is an administrator is determined by whether the user has the `od_store_url_metric_now` capability. This meta capability by default maps to the `manage_options` capability via the `map_meta_cap` filter. During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: diff --git a/plugins/optimization-detective/hooks.php b/plugins/optimization-detective/hooks.php index ff2908390f..2e9c9176b0 100644 --- a/plugins/optimization-detective/hooks.php +++ b/plugins/optimization-detective/hooks.php @@ -18,6 +18,7 @@ add_action( 'init', 'od_initialize_extensions', PHP_INT_MAX ); add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX ); OD_URL_Metrics_Post_Type::add_hooks(); +OD_Storage_Lock::add_hooks(); add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' ); add_action( 'wp_head', 'od_render_generator_meta_tag' ); add_filter( 'site_status_tests', 'od_add_rest_api_availability_test' ); diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 03fff94b9c..009079fa19 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -20,6 +20,49 @@ */ final class OD_Storage_Lock { + /** + * Capability for being able to store a URL Metric now. + * + * @since n.e.x.t + * @access private + * @var string + */ + const STORE_URL_METRIC_NOW_CAPABILITY = 'od_store_url_metric_now'; + + /** + * Adds hooks. + * + * @since n.e.x.t + * @access private + */ + public static function add_hooks(): void { + add_filter( 'map_meta_cap', array( __CLASS__, 'filter_map_meta_cap' ), 10, 3 ); + } + + /** + * Filters map_meta_cap to grant the `od_store_url_metric_now` capability to administrators by default. + * + * @since n.e.x.t + * @access private + * + * @param string[]|mixed $caps Capabilities. + * @param string $cap Capability. + * @param int $user_id User ID. + * @return string[] Granted capabilities. + */ + public static function filter_map_meta_cap( $caps, string $cap, int $user_id ): array { + if ( ! is_array( $caps ) ) { + $caps = array(); + } + + $primitive_cap = 'manage_options'; + if ( 'od_store_url_metric_now' === $cap && user_can( $user_id, $primitive_cap ) ) { + $caps = array( $primitive_cap ); + } + + return $caps; + } + /** * Gets the TTL (in seconds) for the URL Metric storage lock. * @@ -29,7 +72,7 @@ final class OD_Storage_Lock { * @return int<0, max> TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. */ public static function get_ttl(): int { - $ttl = current_user_can( 'manage_options' ) ? 0 : MINUTE_IN_SECONDS; + $ttl = current_user_can( self::STORE_URL_METRIC_NOW_CAPABILITY ) ? 0 : MINUTE_IN_SECONDS; /** * Filters how long the current IP is locked from submitting another URL metric storage REST API request. @@ -41,7 +84,9 @@ public static function get_ttl(): int { * return is_user_logged_in() ? 0 : $ttl; * } ); * - * By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. + * By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. Whether the current + * user is an administrator is determined by whether the user has the `od_store_url_metric_now` capability. This + * meta capability by default maps to the `manage_options` capability via the `map_meta_cap` filter. * * @since 0.1.0 * @since 1.0.0 This now defaults to zero (0) for administrator users. diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index 2166ad9262..954f7b71fe 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -17,6 +17,22 @@ public function tear_down(): void { parent::tear_down(); } + /** + * Test add_hooks(). + * + * @covers ::add_hooks + */ + public function test_add_hooks(): void { + remove_all_filters( 'map_meta_cap' ); + + OD_Storage_Lock::add_hooks(); + + $this->assertSame( + 10, + has_filter( 'map_meta_cap', array( OD_Storage_Lock::class, 'filter_map_meta_cap' ) ) + ); + } + /** * Data provider. * diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index cfea6df604..e783b079b7 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -84,15 +84,27 @@ public function test_od_get_cache_purge_post_id( Closure $set_up, bool $expected */ public function data_provider_od_get_detection_script(): array { return array( - 'unfiltered' => array( + 'unfiltered' => array( 'set_up' => static function (): void {}, 'expected_exports' => array( 'storageLockTTL' => MINUTE_IN_SECONDS, 'extensionModuleUrls' => array(), + 'cachePurgePostId' => null, ), 'expected_standard_build' => true, ), - 'filtered' => array( + 'unfiltered_admin' => array( + 'set_up' => static function (): void { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + }, + 'expected_exports' => array( + 'storageLockTTL' => 0, + 'extensionModuleUrls' => array(), + 'cachePurgePostId' => null, + ), + 'expected_standard_build' => true, + ), + 'filtered' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_storage_lock_ttl', @@ -107,11 +119,31 @@ static function ( array $urls ): array { return $urls; } ); + add_filter( + 'od_minimum_viewport_aspect_ratio', + static function () { + return 0; + } + ); + add_filter( + 'od_maximum_viewport_aspect_ratio', + static function () { + return 2; + } + ); add_filter( 'od_use_web_vitals_attribution_build', '__return_true' ); + add_filter( + 'od_url_metric_storage_lock_ttl', + static function () { + return DAY_IN_SECONDS; + } + ); }, 'expected_exports' => array( - 'storageLockTTL' => HOUR_IN_SECONDS, - 'extensionModuleUrls' => array( home_url( '/my-extension.js', 'https' ) ), + 'storageLockTTL' => DAY_IN_SECONDS, + 'extensionModuleUrls' => array( home_url( '/my-extension.js', 'https' ) ), + 'minViewportAspectRatio' => 0, + 'maxViewportAspectRatio' => 2, ), 'expected_standard_build' => false, ), @@ -123,6 +155,15 @@ static function ( array $urls ): array { * * @covers ::od_get_detection_script * @covers ::od_get_asset_path + * @covers OD_Storage_Lock::get_ttl + * @covers ::od_get_cache_purge_post_id + * @covers ::od_get_minimum_viewport_aspect_ratio + * @covers ::od_get_maximum_viewport_aspect_ratio + * @covers ::od_get_current_url + * @covers ::od_get_url_metrics_storage_hmac + * @covers OD_URL_Metric_Group::get_minimum_viewport_width + * @covers OD_URL_Metric_Group::is_complete + * @covers OD_URL_Metric_Group_Collection::get_current_etag * * @dataProvider data_provider_od_get_detection_script * @@ -132,9 +173,21 @@ static function ( array $urls ): array { */ public function test_od_get_detection_script_returns_script( Closure $set_up, array $expected_exports, bool $expected_standard_build ): void { $set_up(); - $slug = od_get_url_metrics_slug( array( 'p' => '1' ) ); + $slug = od_get_url_metrics_slug( array() ); $current_etag = md5( '' ); + $expected_exports = array_merge( + array( + 'minViewportAspectRatio' => od_get_minimum_viewport_aspect_ratio(), + 'maxViewportAspectRatio' => od_get_maximum_viewport_aspect_ratio(), + 'isDebug' => WP_DEBUG, + 'currentUrl' => od_get_current_url(), + 'urlMetricSlug' => $slug, + 'cachePurgePostId' => od_get_cache_purge_post_id(), + ), + $expected_exports + ); + $breakpoints = array( 480, 600, 782 ); $group_collection = new OD_URL_Metric_Group_Collection( array(), $current_etag, $breakpoints, 3, HOUR_IN_SECONDS ); @@ -145,6 +198,7 @@ public function test_od_get_detection_script_returns_script( Closure $set_up, ar foreach ( $expected_exports as $key => $value ) { $this->assertStringContainsString( sprintf( '%s:%s', wp_json_encode( $key ), wp_json_encode( $value ) ), $script ); } + $this->assertStringContainsString( '"urlMetricHMAC":', $script ); $this->assertSame( 1, preg_match( '/"webVitalsLibrarySrc":("[^"]+?")/', $script, $matches ) ); $web_vitals_library_src = json_decode( $matches[1] ); $this->assertStringContainsString( From 9f89cd45700890f489c74ef1d2f26865170ea448 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Jan 2025 19:58:05 -0800 Subject: [PATCH 03/11] Include REST API nonce in URL Metric storage requests when the user is logged-in --- plugins/optimization-detective/detect.js | 5 +++++ plugins/optimization-detective/detection.php | 3 +++ plugins/optimization-detective/docs/hooks.md | 4 ++-- .../storage/class-od-storage-lock.php | 6 +++--- plugins/optimization-detective/tests/test-detection.php | 5 +++++ 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index cabcbe6c61..18eca7da42 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -253,6 +253,7 @@ function extendElementData( xpath, properties ) { * @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. @@ -269,6 +270,7 @@ export default async function detect( { isDebug, extensionModuleUrls, restApiEndpoint, + restApiNonce, currentETag, currentUrl, urlMetricSlug, @@ -664,6 +666,9 @@ export default async function detect( { } const url = new URL( restApiEndpoint ); + if ( typeof restApiNonce === 'string' ) { + url.searchParams.set( '_wpnonce', restApiNonce ); + } url.searchParams.set( 'slug', urlMetricSlug ); url.searchParams.set( 'current_etag', currentETag ); if ( typeof cachePurgePostId === 'number' ) { diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 66eb08e999..c6fb4ff0ce 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -137,6 +137,9 @@ static function ( OD_URL_Metric_Group $group ): array { 'storageLockTTL' => OD_Storage_Lock::get_ttl(), 'webVitalsLibrarySrc' => $web_vitals_lib_src, ); + if ( is_user_logged_in() ) { + $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); + } if ( WP_DEBUG ) { $detect_args['urlMetricGroupCollection'] = $group_collection; } diff --git a/plugins/optimization-detective/docs/hooks.md b/plugins/optimization-detective/docs/hooks.md index a844695553..b0849fed33 100644 --- a/plugins/optimization-detective/docs/hooks.md +++ b/plugins/optimization-detective/docs/hooks.md @@ -102,7 +102,7 @@ add_filter( 'od_url_metrics_breakpoint_sample_size', function (): int { } ); ``` -### Filter: `od_url_metric_storage_lock_ttl` (default: 60 seconds, except 0 for admins) +### Filter: `od_url_metric_storage_lock_ttl` (default: 60 seconds, except 0 for authorized logged-in users) Filters how long the current IP is locked from submitting another URL metric storage REST API request. @@ -114,7 +114,7 @@ add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { } ); ``` -By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. Whether the current user is an administrator is determined by whether the user has the `od_store_url_metric_now` capability. This meta capability by default maps to the `manage_options` capability via the `map_meta_cap` filter. +By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This meta capability by default maps to the `manage_options` primitive capability via the `map_meta_cap` filter. During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 009079fa19..a6c6927dab 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -84,9 +84,9 @@ public static function get_ttl(): int { * return is_user_logged_in() ? 0 : $ttl; * } ); * - * By default, the TTL is zero (0) for administrator users and sixty (60) for everyone else. Whether the current - * user is an administrator is determined by whether the user has the `od_store_url_metric_now` capability. This - * meta capability by default maps to the `manage_options` capability via the `map_meta_cap` filter. + * By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current + * user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This + * meta capability by default maps to the `manage_options` primitive capability via the `map_meta_cap` filter. * * @since 0.1.0 * @since 1.0.0 This now defaults to zero (0) for administrator users. diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index e783b079b7..ac7d524403 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -210,5 +210,10 @@ public function test_od_get_detection_script_returns_script( Closure $set_up, ar $this->assertStringContainsString( '"minimumViewportWidth":601', $script ); $this->assertStringContainsString( '"minimumViewportWidth":783', $script ); $this->assertStringContainsString( '"complete":false', $script ); + if ( is_user_logged_in() ) { + $this->assertStringContainsString( '"restApiNonce":', $script ); + } else { + $this->assertStringNotContainsString( '"restApiNonce":', $script ); + } } } From f6fb96478cae6fbb80bf6dac25bd92b0971e7b5e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 31 Jan 2025 14:38:40 -0800 Subject: [PATCH 04/11] Add missing test for filter_map_meta_cap --- .../storage/class-od-storage-lock.php | 10 +-- .../storage/test-class-od-storage-lock.php | 86 ++++++++++++++++++- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index a6c6927dab..9e0d406a78 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -45,10 +45,10 @@ public static function add_hooks(): void { * @since n.e.x.t * @access private * - * @param string[]|mixed $caps Capabilities. - * @param string $cap Capability. - * @param int $user_id User ID. - * @return string[] Granted capabilities. + * @param string[]|mixed $caps Primitive capabilities required of the user. + * @param string $cap Capability being checked. + * @param int $user_id The user ID. + * @return string[] Primitive capabilities required of the user. */ public static function filter_map_meta_cap( $caps, string $cap, int $user_id ): array { if ( ! is_array( $caps ) ) { @@ -56,7 +56,7 @@ public static function filter_map_meta_cap( $caps, string $cap, int $user_id ): } $primitive_cap = 'manage_options'; - if ( 'od_store_url_metric_now' === $cap && user_can( $user_id, $primitive_cap ) ) { + if ( self::STORE_URL_METRIC_NOW_CAPABILITY === $cap && user_can( $user_id, $primitive_cap ) ) { $caps = array( $primitive_cap ); } diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index 954f7b71fe..8fad213244 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -33,6 +33,66 @@ public function test_add_hooks(): void { ); } + + /** + * Data provider. + * + * @return array + */ + public function data_filter_map_meta_cap(): array { + return array( + 'bad_caps_relevant' => array( + 'caps' => null, + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'user_role' => 'administrator', + 'expected' => array( 'manage_options' ), + ), + 'bad_caps_irrelevant' => array( + 'caps' => null, + 'cap' => 'edit_posts', + 'user_role' => 'administrator', + 'expected' => array(), + ), + 'caps_authorized' => array( + 'caps' => array(), + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'user_role' => 'administrator', + 'expected' => array( 'manage_options' ), + ), + 'caps_unauthorized' => array( + 'caps' => array(), + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'user_role' => 'subscriber', + 'expected' => array(), + ), + 'caps_anonymous' => array( + 'caps' => array(), + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'user_role' => null, + 'expected' => array(), + ), + ); + } + + /** + * Test filter_map_meta_cap(). + * + * @dataProvider data_filter_map_meta_cap + * @covers ::filter_map_meta_cap + * + * @param string[]|mixed $caps Primitive capabilities required of the user. + * @param string $cap Capability being checked. + * @param string|null $user_role Current user role. + * @param string[] $expected Expected primitive capabilities required of the user. + */ + public function test_filter_map_meta_cap( $caps, string $cap, ?string $user_role, array $expected ): void { + if ( null !== $user_role ) { + wp_set_current_user( self::factory()->user->create( array( 'role' => $user_role ) ) ); + } + $return = OD_Storage_Lock::filter_map_meta_cap( $caps, $cap, wp_get_current_user()->ID ); + $this->assertSame( $expected, $return ); + } + /** * Data provider. * @@ -40,17 +100,17 @@ public function test_add_hooks(): void { */ public function data_provider_get_ttl(): array { return array( - 'unfiltered' => array( + 'unfiltered' => array( 'set_up' => static function (): void {}, 'expected' => MINUTE_IN_SECONDS, ), - 'unfiltered_admin' => array( + 'unfiltered_admin' => array( 'set_up' => static function (): void { wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); }, 'expected' => 0, ), - 'filtered_hour' => array( + 'filtered_hour' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_storage_lock_ttl', @@ -61,7 +121,7 @@ static function (): int { }, 'expected' => HOUR_IN_SECONDS, ), - 'filtered_negative' => array( + 'filtered_negative' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_storage_lock_ttl', @@ -72,6 +132,23 @@ static function (): int { }, 'expected' => 0, ), + 'granted_subscriber' => array( + 'set_up' => static function (): void { + add_filter( + 'map_meta_cap', + static function ( array $caps, string $cap, int $user_id ): array { + $primitive_cap = 'exist'; + if ( OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY === $cap && user_can( $user_id, $primitive_cap ) ) { + $caps = array( $primitive_cap ); + } + return $caps; + }, + 10, + 3 + ); + }, + 'expected' => 0, + ), ); } @@ -79,6 +156,7 @@ static function (): int { * Test get_ttl(). * * @covers ::get_ttl + * @covers ::filter_map_meta_cap * * @dataProvider data_provider_get_ttl * From a57e080f02f81a3c823c197dc816413fecaf0806 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 3 Feb 2025 16:35:35 -0800 Subject: [PATCH 05/11] Prevent submitting duplicate URL Metrics from one client session --- plugins/optimization-detective/detect.js | 117 +++++++++++++++--- plugins/optimization-detective/detection.php | 1 + .../tests/test-detection.php | 9 ++ 3 files changed, 109 insertions(+), 18 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 05b66a4db6..394173173f 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -96,33 +96,65 @@ function error( ...message ) { } /** - * Checks whether the URL Metric(s) for the provided viewport width is needed. + * Gets the status for the URL Metric group for the provided viewport width. * * The comparison logic here corresponds with the PHP logic in `OD_URL_Metric_Group::is_viewport_width_in_range()`. + * This function is also similar to the PHP logic in `\OD_URL_Metric_Group_Collection::get_group_for_viewport_width()`. * * @param {number} viewportWidth - Current viewport width. * @param {URLMetricGroupStatus[]} urlMetricGroupStatuses - Viewport group statuses. - * @return {boolean} Whether URL Metrics are needed. + * @return {URLMetricGroupStatus} The URL metric group for the viewport width. */ -function isViewportNeeded( viewportWidth, urlMetricGroupStatuses ) { - if ( viewportWidth === 0 ) { - return false; - } - - for ( const { - minimumViewportWidth, - maximumViewportWidth, - complete, - } of urlMetricGroupStatuses ) { +function getGroupForViewportWidth( viewportWidth, urlMetricGroupStatuses ) { + for ( const urlMetricGroupStatus of urlMetricGroupStatuses ) { if ( - viewportWidth > minimumViewportWidth && - ( null === maximumViewportWidth || - viewportWidth <= maximumViewportWidth ) + viewportWidth > urlMetricGroupStatus.minimumViewportWidth && + ( null === urlMetricGroupStatus.maximumViewportWidth || + viewportWidth <= urlMetricGroupStatus.maximumViewportWidth ) ) { - return complete; + return urlMetricGroupStatus; } } - return false; + throw new Error( + `${ consoleLogPrefix } Unexpectedly unable to locate group for the current viewport width.` + ); +} + +/** + * Gets the sessionStorage key for keeping track of whether the current client session already submitted a URL Metric. + * + * @param {string} currentETag - Current ETag. + * @param {string} currentUrl - Current URL. + * @param {URLMetricGroupStatus} urlMetricGroupStatus - URL Metric group status. + * @return {Promise} Session storage key. + */ +async function getAlreadySubmittedSessionStorageKey( + currentETag, + currentUrl, + urlMetricGroupStatus +) { + const message = [ + currentETag, + currentUrl, + urlMetricGroupStatus.minimumViewportWidth, + urlMetricGroupStatus.maximumViewportWidth || '', + ].join( '-' ); + + /* + * Note that the components are hashed for a couple of reasons: + * + * 1. It results in a consistent length string devoid of any special characters that could cause problems. + * 2. Since the key includes the URL, hashing it avoids potential privacy concerns where the sessionStorage is + * examined to see which URLs the client went to. + * + * The SHA-1 algorithm is chosen since it is the fastest and there is no need for cryptographic security. + */ + const msgBuffer = new TextEncoder().encode( message ); + const hashBuffer = await crypto.subtle.digest( 'SHA-1', msgBuffer ); + const hashHex = Array.from( new Uint8Array( hashBuffer ) ) + .map( ( b ) => b.toString( 16 ).padStart( 2, '0' ) ) + .join( '' ); + return `odSubmitted-${ hashHex }`; } /** @@ -272,6 +304,7 @@ function extendElementData( xpath, properties ) { * @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. */ @@ -289,6 +322,7 @@ export default async function detect( { urlMetricHMAC, urlMetricGroupStatuses, storageLockTTL, + freshnessTTL, webVitalsLibrarySrc, urlMetricGroupCollection, } ) { @@ -310,14 +344,55 @@ export default async function detect( { ); } + if ( win.innerWidth === 0 || win.innerHeight === 0 ) { + if ( isDebug ) { + log( + 'Window must have non-zero dimensions for URL Metric collection.' + ); + } + return; + } + // Abort if the current viewport is not among those which need URL Metrics. - if ( ! isViewportNeeded( win.innerWidth, urlMetricGroupStatuses ) ) { + const urlMetricGroupStatus = getGroupForViewportWidth( + win.innerWidth, + urlMetricGroupStatuses + ); + if ( urlMetricGroupStatus.complete ) { if ( isDebug ) { log( 'No need for URL Metrics from the current viewport.' ); } return; } + // Abort if the client already submitted a URL Metric for this URL and viewport group. + // TODO: Remove the console timing. + console.time( 'sha1' ); // eslint-disable-line no-console + const alreadySubmittedSessionStorageKey = + await getAlreadySubmittedSessionStorageKey( + currentETag, + currentUrl, + urlMetricGroupStatus + ); + console.timeEnd( 'sha1' ); // eslint-disable-line no-console + if ( alreadySubmittedSessionStorageKey in sessionStorage ) { + const previousVisitTime = parseInt( + sessionStorage.getItem( alreadySubmittedSessionStorageKey ), + 10 + ); + if ( + ! isNaN( previousVisitTime ) && + ( getCurrentTime() - previousVisitTime ) / 1000 < freshnessTTL + ) { + if ( isDebug ) { + log( + 'The current client session already submitted a fresh URL Metric for this URL so a new one will not be collected now.' + ); + return; + } + } + } + // Abort if the viewport aspect ratio is not in a common range. const aspectRatio = win.innerWidth / win.innerHeight; if ( @@ -672,6 +747,12 @@ export default async function detect( { // because we can't look at the response when sending a beacon. setStorageLock( getCurrentTime() ); + // Remember that the URL Metric was submitted for this URL to avoid having multiple entries submitted by the same client. + sessionStorage.setItem( + alreadySubmittedSessionStorageKey, + String( getCurrentTime() ) + ); + if ( isDebug ) { log( 'Sending URL Metric:', urlMetric ); } diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 1f253f6c96..9ab3f6d0ae 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -138,6 +138,7 @@ static function ( OD_URL_Metric_Group $group ): array { iterator_to_array( $group_collection ) ), 'storageLockTTL' => OD_Storage_Lock::get_ttl(), + 'freshnessTTL' => od_get_url_metric_freshness_ttl(), 'webVitalsLibrarySrc' => $web_vitals_lib_src, ); if ( is_user_logged_in() ) { diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index 1b655ae4eb..31876d0fad 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -90,6 +90,7 @@ public function data_provider_od_get_detection_script(): array { 'storageLockTTL' => MINUTE_IN_SECONDS, 'extensionModuleUrls' => array(), 'cachePurgePostId' => null, + 'freshnessTTL' => DAY_IN_SECONDS, ), 'expected_standard_build' => true, ), @@ -138,9 +139,16 @@ static function () { return DAY_IN_SECONDS; } ); + add_filter( + 'od_url_metric_freshness_ttl', + static function () { + return WEEK_IN_SECONDS; + } + ); }, 'expected_exports' => array( 'storageLockTTL' => DAY_IN_SECONDS, + 'freshnessTTL' => WEEK_IN_SECONDS, 'extensionModuleUrls' => array( home_url( '/my-extension.js', 'https' ) ), 'minViewportAspectRatio' => 0, 'maxViewportAspectRatio' => 2, @@ -184,6 +192,7 @@ public function test_od_get_detection_script_returns_script( Closure $set_up, ar 'currentUrl' => od_get_current_url(), 'urlMetricSlug' => $slug, 'cachePurgePostId' => od_get_cache_purge_post_id(), + 'freshnessTTL' => od_get_url_metric_freshness_ttl(), ), $expected_exports ); From bbba00163b8d949b00c9553484dcd606757cf62e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Feb 2025 09:46:40 -0800 Subject: [PATCH 06/11] Remove unnecessary checks from map_meta_cap Co-authored-by: felixarntz --- .../storage/class-od-storage-lock.php | 14 +++--- .../storage/test-class-od-storage-lock.php | 50 +++++++------------ 2 files changed, 24 insertions(+), 40 deletions(-) diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 990a5e7bad..2f32831600 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -36,30 +36,28 @@ final class OD_Storage_Lock { * @access private */ public static function add_hooks(): void { - add_filter( 'map_meta_cap', array( __CLASS__, 'filter_map_meta_cap' ), 10, 3 ); + add_filter( 'map_meta_cap', array( __CLASS__, 'filter_map_meta_cap' ), 10, 2 ); } /** - * Filters map_meta_cap to grant the `od_store_url_metric_now` capability to administrators by default. + * Filters map_meta_cap to grant the `od_store_url_metric_now` capability to `manage_options` by default. * * @since n.e.x.t * @access private * - * @param string[]|mixed $caps Primitive capabilities required of the user. - * @param string $cap Capability being checked. - * @param int $user_id The user ID. + * @param string[]|mixed $caps Primitive capabilities required of the user. + * @param string $cap Capability being checked. * @return string[] Primitive capabilities required of the user. */ - public static function filter_map_meta_cap( $caps, string $cap, int $user_id ): array { + public static function filter_map_meta_cap( $caps, string $cap ): array { if ( ! is_array( $caps ) ) { $caps = array(); } $primitive_cap = 'manage_options'; - if ( self::STORE_URL_METRIC_NOW_CAPABILITY === $cap && user_can( $user_id, $primitive_cap ) ) { + if ( self::STORE_URL_METRIC_NOW_CAPABILITY === $cap ) { $caps = array( $primitive_cap ); } - return $caps; } diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index 8fad213244..b21f2a68bd 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -41,35 +41,25 @@ public function test_add_hooks(): void { */ public function data_filter_map_meta_cap(): array { return array( - 'bad_caps_relevant' => array( - 'caps' => null, - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'user_role' => 'administrator', - 'expected' => array( 'manage_options' ), + 'caps_null_irrelevant' => array( + 'caps' => null, + 'cap' => 'edit_posts', + 'expected' => array(), ), - 'bad_caps_irrelevant' => array( - 'caps' => null, - 'cap' => 'edit_posts', - 'user_role' => 'administrator', - 'expected' => array(), + 'caps_irrelevant' => array( + 'caps' => array( 'edit_posts' ), + 'cap' => 'edit_posts', + 'expected' => array( 'edit_posts' ), ), - 'caps_authorized' => array( - 'caps' => array(), - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'user_role' => 'administrator', - 'expected' => array( 'manage_options' ), + 'caps_null_relevant' => array( + 'caps' => null, + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'expected' => array( 'manage_options' ), ), - 'caps_unauthorized' => array( - 'caps' => array(), - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'user_role' => 'subscriber', - 'expected' => array(), - ), - 'caps_anonymous' => array( - 'caps' => array(), - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'user_role' => null, - 'expected' => array(), + 'caps_normal_relevant' => array( + 'caps' => array( OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY ), + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'expected' => array( 'manage_options' ), ), ); } @@ -82,14 +72,10 @@ public function data_filter_map_meta_cap(): array { * * @param string[]|mixed $caps Primitive capabilities required of the user. * @param string $cap Capability being checked. - * @param string|null $user_role Current user role. * @param string[] $expected Expected primitive capabilities required of the user. */ - public function test_filter_map_meta_cap( $caps, string $cap, ?string $user_role, array $expected ): void { - if ( null !== $user_role ) { - wp_set_current_user( self::factory()->user->create( array( 'role' => $user_role ) ) ); - } - $return = OD_Storage_Lock::filter_map_meta_cap( $caps, $cap, wp_get_current_user()->ID ); + public function test_filter_map_meta_cap( $caps, string $cap, array $expected ): void { + $return = OD_Storage_Lock::filter_map_meta_cap( $caps, $cap ); $this->assertSame( $expected, $return ); } From 78a266e19e91c144bff8c461a4034cb2fb98daf3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Feb 2025 09:49:47 -0800 Subject: [PATCH 07/11] Account for additional caps being required --- .../storage/class-od-storage-lock.php | 5 ++++- .../tests/storage/test-class-od-storage-lock.php | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 2f32831600..80004ee978 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -56,7 +56,10 @@ public static function filter_map_meta_cap( $caps, string $cap ): array { $primitive_cap = 'manage_options'; if ( self::STORE_URL_METRIC_NOW_CAPABILITY === $cap ) { - $caps = array( $primitive_cap ); + $i = array_search( self::STORE_URL_METRIC_NOW_CAPABILITY, $caps, true ); + if ( false !== $i ) { + $caps[ $i ] = $primitive_cap; + } } return $caps; } diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index b21f2a68bd..8b0c2dbc81 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -54,13 +54,18 @@ public function data_filter_map_meta_cap(): array { 'caps_null_relevant' => array( 'caps' => null, 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'expected' => array( 'manage_options' ), + 'expected' => array(), ), 'caps_normal_relevant' => array( 'caps' => array( OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY ), 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, 'expected' => array( 'manage_options' ), ), + 'caps_extra_relevant' => array( + 'caps' => array( 'edit_posts', OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY ), + 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, + 'expected' => array( 'edit_posts', 'manage_options' ), + ), ); } From a9437b6e6c7ff42e43c3b678d22bd8709e711307 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Feb 2025 10:47:03 -0800 Subject: [PATCH 08/11] Remove console timing --- plugins/optimization-detective/detect.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 394173173f..f909a3473e 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -366,15 +366,12 @@ export default async function detect( { } // Abort if the client already submitted a URL Metric for this URL and viewport group. - // TODO: Remove the console timing. - console.time( 'sha1' ); // eslint-disable-line no-console const alreadySubmittedSessionStorageKey = await getAlreadySubmittedSessionStorageKey( currentETag, currentUrl, urlMetricGroupStatus ); - console.timeEnd( 'sha1' ); // eslint-disable-line no-console if ( alreadySubmittedSessionStorageKey in sessionStorage ) { const previousVisitTime = parseInt( sessionStorage.getItem( alreadySubmittedSessionStorageKey ), From 5ec3783129d33d9cf7619384cb235faa3d5bf388 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Feb 2025 10:52:19 -0800 Subject: [PATCH 09/11] Revert "Account for additional caps being required" This reverts commit 78a266e19e91c144bff8c461a4034cb2fb98daf3. --- .../storage/class-od-storage-lock.php | 5 +---- .../tests/storage/test-class-od-storage-lock.php | 7 +------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 80004ee978..2f32831600 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -56,10 +56,7 @@ public static function filter_map_meta_cap( $caps, string $cap ): array { $primitive_cap = 'manage_options'; if ( self::STORE_URL_METRIC_NOW_CAPABILITY === $cap ) { - $i = array_search( self::STORE_URL_METRIC_NOW_CAPABILITY, $caps, true ); - if ( false !== $i ) { - $caps[ $i ] = $primitive_cap; - } + $caps = array( $primitive_cap ); } return $caps; } diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index 8b0c2dbc81..b21f2a68bd 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -54,18 +54,13 @@ public function data_filter_map_meta_cap(): array { 'caps_null_relevant' => array( 'caps' => null, 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'expected' => array(), + 'expected' => array( 'manage_options' ), ), 'caps_normal_relevant' => array( 'caps' => array( OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY ), 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, 'expected' => array( 'manage_options' ), ), - 'caps_extra_relevant' => array( - 'caps' => array( 'edit_posts', OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY ), - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'expected' => array( 'edit_posts', 'manage_options' ), - ), ); } From ddfb6627fe808294a1dc8055f1bdd5e8ccd21188 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Feb 2025 11:38:53 -0800 Subject: [PATCH 10/11] Use user_has_cap instead of map_meta_cap Co-authored-by: felixarntz --- .../storage/class-od-storage-lock.php | 23 +++---- .../storage/test-class-od-storage-lock.php | 64 +++++++++++-------- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 2f32831600..74425ca212 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -36,29 +36,26 @@ final class OD_Storage_Lock { * @access private */ public static function add_hooks(): void { - add_filter( 'map_meta_cap', array( __CLASS__, 'filter_map_meta_cap' ), 10, 2 ); + add_filter( 'user_has_cap', array( __CLASS__, 'filter_user_has_cap' ) ); } /** - * Filters map_meta_cap to grant the `od_store_url_metric_now` capability to `manage_options` by default. + * Filters `user_has_cap` to grant the `od_store_url_metric_now` capability to users who can `manage_options` by default. * * @since n.e.x.t * @access private * - * @param string[]|mixed $caps Primitive capabilities required of the user. - * @param string $cap Capability being checked. - * @return string[] Primitive capabilities required of the user. + * @param array|mixed $allcaps Capability names mapped to boolean values for whether the user has that capability. + * @return array Capability names mapped to boolean values for whether the user has that capability. */ - public static function filter_map_meta_cap( $caps, string $cap ): array { - if ( ! is_array( $caps ) ) { - $caps = array(); + public static function filter_user_has_cap( $allcaps ): array { + if ( ! is_array( $allcaps ) ) { + $allcaps = array(); } - - $primitive_cap = 'manage_options'; - if ( self::STORE_URL_METRIC_NOW_CAPABILITY === $cap ) { - $caps = array( $primitive_cap ); + if ( isset( $allcaps['manage_options'] ) ) { + $allcaps['od_store_url_metric_now'] = $allcaps['manage_options']; } - return $caps; + return $allcaps; } /** diff --git a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php index b21f2a68bd..23db2ef483 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-storage-lock.php @@ -23,13 +23,13 @@ public function tear_down(): void { * @covers ::add_hooks */ public function test_add_hooks(): void { - remove_all_filters( 'map_meta_cap' ); + remove_all_filters( 'user_has_cap' ); OD_Storage_Lock::add_hooks(); $this->assertSame( 10, - has_filter( 'map_meta_cap', array( OD_Storage_Lock::class, 'filter_map_meta_cap' ) ) + has_filter( 'user_has_cap', array( OD_Storage_Lock::class, 'filter_user_has_cap' ) ) ); } @@ -39,44 +39,52 @@ public function test_add_hooks(): void { * * @return array */ - public function data_filter_map_meta_cap(): array { + public function data_filter_user_has_cap(): array { return array( - 'caps_null_irrelevant' => array( - 'caps' => null, - 'cap' => 'edit_posts', + 'caps_null_irrelevant' => array( + 'allcaps' => null, 'expected' => array(), ), - 'caps_irrelevant' => array( - 'caps' => array( 'edit_posts' ), - 'cap' => 'edit_posts', - 'expected' => array( 'edit_posts' ), + 'caps_irrelevant' => array( + 'allcaps' => array( 'edit_posts' => true ), + 'expected' => array( 'edit_posts' => true ), ), - 'caps_null_relevant' => array( - 'caps' => null, - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'expected' => array( 'manage_options' ), + 'caps_relevant_allowed' => array( + 'allcaps' => array( + 'edit_posts' => true, + 'manage_options' => true, + ), + 'expected' => array( + 'edit_posts' => true, + 'manage_options' => true, + OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY => true, + ), ), - 'caps_normal_relevant' => array( - 'caps' => array( OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY ), - 'cap' => OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY, - 'expected' => array( 'manage_options' ), + 'caps_relevant_disallowed' => array( + 'allcaps' => array( + 'edit_posts' => true, + 'manage_options' => false, + ), + 'expected' => array( + 'edit_posts' => true, + 'manage_options' => false, + OD_Storage_Lock::STORE_URL_METRIC_NOW_CAPABILITY => false, + ), ), ); } /** - * Test filter_map_meta_cap(). + * Test filter_user_has_cap(). * - * @dataProvider data_filter_map_meta_cap - * @covers ::filter_map_meta_cap + * @dataProvider data_filter_user_has_cap + * @covers ::filter_user_has_cap * - * @param string[]|mixed $caps Primitive capabilities required of the user. - * @param string $cap Capability being checked. - * @param string[] $expected Expected primitive capabilities required of the user. + * @param array|null $allcaps Existing capabilities. + * @param array $expected Expected capabilities. */ - public function test_filter_map_meta_cap( $caps, string $cap, array $expected ): void { - $return = OD_Storage_Lock::filter_map_meta_cap( $caps, $cap ); - $this->assertSame( $expected, $return ); + public function test_filter_user_has_cap( ?array $allcaps, array $expected ): void { + $this->assertSame( $expected, OD_Storage_Lock::filter_user_has_cap( $allcaps ) ); } /** @@ -142,7 +150,7 @@ static function ( array $caps, string $cap, int $user_id ): array { * Test get_ttl(). * * @covers ::get_ttl - * @covers ::filter_map_meta_cap + * @covers ::filter_user_has_cap * * @dataProvider data_provider_get_ttl * From 2c5ea505dc18256d24e38578b27038e1220b0218 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Feb 2025 12:35:16 -0800 Subject: [PATCH 11/11] Update docs --- plugins/optimization-detective/docs/hooks.md | 2 +- .../storage/class-od-storage-lock.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/optimization-detective/docs/hooks.md b/plugins/optimization-detective/docs/hooks.md index b0849fed33..29edbb9347 100644 --- a/plugins/optimization-detective/docs/hooks.md +++ b/plugins/optimization-detective/docs/hooks.md @@ -114,7 +114,7 @@ add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int { } ); ``` -By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This meta capability by default maps to the `manage_options` primitive capability via the `map_meta_cap` filter. +By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: diff --git a/plugins/optimization-detective/storage/class-od-storage-lock.php b/plugins/optimization-detective/storage/class-od-storage-lock.php index 74425ca212..1ceb705e4e 100644 --- a/plugins/optimization-detective/storage/class-od-storage-lock.php +++ b/plugins/optimization-detective/storage/class-od-storage-lock.php @@ -81,12 +81,12 @@ public static function get_ttl(): int { * * By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current * user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This - * meta capability by default maps to the `manage_options` primitive capability via the `map_meta_cap` filter. + * custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. * * @since 0.1.0 - * @since 1.0.0 This now defaults to zero (0) for administrator users. + * @since 1.0.0 This now defaults to zero (0) for authorized users. * - * @param int $ttl TTL. Defaults to 0 for administrators, and 60 for everyone else. + * @param int $ttl TTL. Defaults to 60, except zero (0) for authorized users. */ $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', $ttl ); return max( 0, $ttl );