From 136d4eab134f350a057555e61dc3321564c9a286 Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Mon, 27 Jul 2026 20:00:42 +0530 Subject: [PATCH 1/3] Privacy: Introduce wp_count_user_requests() to publicly expose user request counts by type --- .../class-wp-privacy-requests-table.php | 35 +---------- src/wp-includes/user.php | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/wp-admin/includes/class-wp-privacy-requests-table.php b/src/wp-admin/includes/class-wp-privacy-requests-table.php index 854419069511a..c798f03887950 100644 --- a/src/wp-admin/includes/class-wp-privacy-requests-table.php +++ b/src/wp-admin/includes/class-wp-privacy-requests-table.php @@ -102,43 +102,10 @@ protected function get_default_primary_column_name() { * * @since 4.9.6 * - * @global wpdb $wpdb WordPress database abstraction object. - * * @return object Number of posts for each status. */ protected function get_request_counts() { - global $wpdb; - - $cache_key = $this->post_type . '-' . $this->request_type; - $counts = wp_cache_get( $cache_key, 'counts' ); - - if ( false !== $counts ) { - return $counts; - } - - $results = (array) $wpdb->get_results( - $wpdb->prepare( - "SELECT post_status, COUNT( * ) AS num_posts - FROM {$wpdb->posts} - WHERE post_type = %s - AND post_name = %s - GROUP BY post_status", - $this->post_type, - $this->request_type - ), - ARRAY_A - ); - - $counts = array_fill_keys( get_post_stati(), 0 ); - - foreach ( $results as $row ) { - $counts[ $row['post_status'] ] = $row['num_posts']; - } - - $counts = (object) $counts; - wp_cache_set( $cache_key, $counts, 'counts' ); - - return $counts; + return wp_count_user_requests( $this->request_type ); } /** diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index d682762acb7e7..643e908f6aea0 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -4845,6 +4845,65 @@ function wp_create_user_request( $email_address = '', $action_name = '', $reques return $request_id; } +/** + * Counts the number of user requests for a given request type, grouped by status. + * + * @since 6.8.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $type Request type. Accepted values are `'export_personal_data'` + * and `'remove_personal_data'`. + * @return object Number of requests for each status, or an empty object for an invalid type. + */ +function wp_count_user_requests( $type = '' ) { + global $wpdb; + + if ( ! in_array( $type, _wp_privacy_action_request_types(), true ) ) { + return new stdClass(); + } + + $cache_key = 'user-request-' . $type; + $counts = wp_cache_get( $cache_key, 'counts' ); + + if ( false !== $counts ) { + /** This filter is documented in wp-includes/user.php */ + return apply_filters( 'wp_count_user_requests', $counts, $type ); + } + + $results = (array) $wpdb->get_results( + $wpdb->prepare( + "SELECT post_status, COUNT( * ) AS num_posts + FROM {$wpdb->posts} + WHERE post_type = %s + AND post_name = %s + GROUP BY post_status", + 'user_request', + $type + ), + ARRAY_A + ); + + $counts = array_fill_keys( get_post_stati(), 0 ); + + foreach ( $results as $row ) { + $counts[ $row['post_status'] ] = $row['num_posts']; + } + + $counts = (object) $counts; + wp_cache_set( $cache_key, $counts, 'counts' ); + + /** + * Filters the number of user requests for a given type, by status. + * + * @since 6.8.0 + * + * @param object $counts Number of requests for each status. + * @param string $type Request type. + */ + return apply_filters( 'wp_count_user_requests', $counts, $type ); +} + /** * Gets action description from the name and return a string. * From 2b7839ceea6a360a994bca4e209449fb26332cc7 Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Mon, 27 Jul 2026 20:17:46 +0530 Subject: [PATCH 2/3] fix: implement salted caching for wp_count_user_requests --- src/wp-includes/user.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 643e908f6aea0..0f23e76d9a13b 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -4863,8 +4863,9 @@ function wp_count_user_requests( $type = '' ) { return new stdClass(); } - $cache_key = 'user-request-' . $type; - $counts = wp_cache_get( $cache_key, 'counts' ); + $cache_key = 'user-request-' . $type; + $last_changed = wp_cache_get_last_changed( 'posts' ); + $counts = wp_cache_get_salted( $cache_key, 'counts', $last_changed ); if ( false !== $counts ) { /** This filter is documented in wp-includes/user.php */ @@ -4891,7 +4892,7 @@ function wp_count_user_requests( $type = '' ) { } $counts = (object) $counts; - wp_cache_set( $cache_key, $counts, 'counts' ); + wp_cache_set_salted( $cache_key, $counts, 'counts', $last_changed ); /** * Filters the number of user requests for a given type, by status. From 6a99ce735ae984f6e1029d0302afbb062a762ae2 Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Tue, 28 Jul 2026 21:39:04 +0530 Subject: [PATCH 3/3] test: add unit tests for wp_count_user_requests() functionality --- .../tests/privacy/wpCountUserRequests.php | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/phpunit/tests/privacy/wpCountUserRequests.php diff --git a/tests/phpunit/tests/privacy/wpCountUserRequests.php b/tests/phpunit/tests/privacy/wpCountUserRequests.php new file mode 100644 index 0000000000000..c2638d4e61c65 --- /dev/null +++ b/tests/phpunit/tests/privacy/wpCountUserRequests.php @@ -0,0 +1,162 @@ +post->create( + array( + 'post_type' => 'user_request', + 'post_name' => 'export_personal_data', + 'post_status' => 'request-pending', + 'post_title' => 'export@local.test', + ) + ); + + self::$erase_request_id = $factory->post->create( + array( + 'post_type' => 'user_request', + 'post_name' => 'remove_personal_data', + 'post_status' => 'request-confirmed', + 'post_title' => 'erase@local.test', + ) + ); + } + + /** + * Set up before each test. + * + * @since 6.8.0 + */ + public function set_up() { + parent::set_up(); + // Clear counts cache before each test. + wp_cache_delete( 'user-request-export_personal_data', 'counts' ); + wp_cache_delete( 'user-request-remove_personal_data', 'counts' ); + } + + /** + * Returns an empty stdClass for an invalid or empty type string. + * + * @ticket 44034 + */ + public function test_returns_empty_object_for_invalid_type() { + $actual_empty = wp_count_user_requests( '' ); + $this->assertInstanceOf( 'stdClass', $actual_empty ); + $this->assertEmpty( (array) $actual_empty ); + + $actual_invalid = wp_count_user_requests( 'invalid_type' ); + $this->assertInstanceOf( 'stdClass', $actual_invalid ); + $this->assertEmpty( (array) $actual_invalid ); + } + + /** + * Counts export requests correctly by status. + * + * @ticket 44034 + */ + public function test_counts_export_requests_by_status() { + $actual = wp_count_user_requests( 'export_personal_data' ); + + $this->assertSame( 1, (int) $actual->{'request-pending'} ); + $this->assertSame( 0, (int) $actual->{'request-confirmed'} ); + } + + /** + * Counts erase requests correctly by status. + * + * @ticket 44034 + */ + public function test_counts_erase_requests_by_status() { + $actual = wp_count_user_requests( 'remove_personal_data' ); + + $this->assertSame( 0, (int) $actual->{'request-pending'} ); + $this->assertSame( 1, (int) $actual->{'request-confirmed'} ); + } + + /** + * Result contains all registered post statuses as keys. + * + * @ticket 44034 + */ + public function test_result_contains_all_post_statuses() { + $actual = wp_count_user_requests( 'export_personal_data' ); + $stati = get_post_stati(); + $actual_keys = array_keys( (array) $actual ); + + foreach ( $stati as $status ) { + $this->assertContains( $status, $actual_keys, "Missing status: $status" ); + } + } + + /** + * Result is cached after first call. + * + * @ticket 44034 + */ + public function test_result_is_cached() { + wp_count_user_requests( 'export_personal_data' ); + + $last_changed = wp_cache_get_last_changed( 'posts' ); + $cached = wp_cache_get_salted( 'user-request-export_personal_data', 'counts', $last_changed ); + + $this->assertNotFalse( $cached ); + $this->assertInstanceOf( 'stdClass', $cached ); + } + + /** + * The `wp_count_user_requests` filter can modify the returned counts. + * + * @ticket 44034 + */ + public function test_filter_can_modify_counts() { + add_filter( + 'wp_count_user_requests', + function ( $counts, $type ) { + $this->assertSame( 'export_personal_data', $type ); + $modified = new stdClass(); + $modified->{'request-pending'} = 999; + return $modified; + }, + 10, + 2 + ); + + $actual = wp_count_user_requests( 'export_personal_data' ); + + $this->assertSame( 999, (int) $actual->{'request-pending'} ); + } +}