diff --git a/src/wp-admin/includes/class-wp-privacy-data-export-requests-list-table.php b/src/wp-admin/includes/class-wp-privacy-data-export-requests-list-table.php index 73052467983ad..2f46318e73fcb 100644 --- a/src/wp-admin/includes/class-wp-privacy-data-export-requests-list-table.php +++ b/src/wp-admin/includes/class-wp-privacy-data-export-requests-list-table.php @@ -66,7 +66,7 @@ public function column_email( $item ) { $row_actions['download-data'] = $download_data_markup; - if ( 'request-completed' !== $status ) { + if ( ! in_array( $status, array( 'request-completed', 'request-archived' ), true ) ) { $complete_request_markup = ''; $complete_request_markup .= sprintf( '%s', @@ -142,18 +142,41 @@ public function column_next_steps( $item ) { echo ''; break; case 'request-completed': - echo '' . esc_html__( 'Remove request' ) . ''; + printf( + '%s', + esc_url( $this->get_request_action_url( 'archive', $item->ID ) ), + esc_attr( + sprintf( + /* translators: %s: Request email. */ + __( 'Archive request for “%s”' ), + $item->email + ) + ), + esc_html__( 'Archive request' ) + ); + break; + case 'request-archived': + printf( + '%s | %s', + esc_url( $this->get_request_action_url( 'unarchive', $item->ID ) ), + esc_attr( + sprintf( + /* translators: %s: Request email. */ + __( 'Restore request for “%s”' ), + $item->email + ) + ), + esc_html__( 'Restore' ), + esc_url( $this->get_request_action_url( 'delete', $item->ID ) ), + esc_attr( + sprintf( + /* translators: %s: Request email. */ + __( 'Delete request for “%s” permanently' ), + $item->email + ) + ), + esc_html__( 'Delete permanently' ) + ); break; } } diff --git a/src/wp-admin/includes/class-wp-privacy-data-removal-requests-list-table.php b/src/wp-admin/includes/class-wp-privacy-data-removal-requests-list-table.php index c2863c29d3a63..5024e8e96ade6 100644 --- a/src/wp-admin/includes/class-wp-privacy-data-removal-requests-list-table.php +++ b/src/wp-admin/includes/class-wp-privacy-data-removal-requests-list-table.php @@ -72,7 +72,7 @@ public function column_email( $item ) { $row_actions['remove-data'] = $remove_data_markup; } - if ( 'request-completed' !== $status ) { + if ( ! in_array( $status, array( 'request-completed', 'request-archived' ), true ) ) { $complete_request_markup = ''; $complete_request_markup .= sprintf( '%s', @@ -149,18 +149,41 @@ public function column_next_steps( $item ) { echo ''; break; case 'request-completed': - echo '' . esc_html__( 'Remove request' ) . ''; + printf( + '%s', + esc_url( $this->get_request_action_url( 'archive', $item->ID ) ), + esc_attr( + sprintf( + /* translators: %s: Request email. */ + __( 'Archive request for “%s”' ), + $item->email + ) + ), + esc_html__( 'Archive request' ) + ); + break; + case 'request-archived': + printf( + '%s | %s', + esc_url( $this->get_request_action_url( 'unarchive', $item->ID ) ), + esc_attr( + sprintf( + /* translators: %s: Request email. */ + __( 'Restore request for “%s”' ), + $item->email + ) + ), + esc_html__( 'Restore' ), + esc_url( $this->get_request_action_url( 'delete', $item->ID ) ), + esc_attr( + sprintf( + /* translators: %s: Request email. */ + __( 'Delete request for “%s” permanently' ), + $item->email + ) + ), + esc_html__( 'Delete permanently' ) + ); break; } } 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 106fc298dd619..83bc5cbda4770 100644 --- a/src/wp-admin/includes/class-wp-privacy-requests-table.php +++ b/src/wp-admin/includes/class-wp-privacy-requests-table.php @@ -153,7 +153,16 @@ protected function get_views() { $statuses = _wp_privacy_statuses(); $views = array(); $counts = $this->get_request_counts(); - $total_requests = absint( array_sum( (array) $counts ) ); + $total_requests = 0; + + // Statuses excluded from search (e.g. archived) are not part of the "All" view. + foreach ( (array) $counts as $status => $count ) { + $post_status = get_post_status_object( $status ); + + if ( $post_status && empty( $post_status->exclude_from_search ) ) { + $total_requests += absint( $count ); + } + } // Normalized admin URL. $admin_url = $this->get_admin_url(); @@ -208,22 +217,56 @@ protected function get_views() { * Gets bulk actions. * * @since 4.9.6 + * @since 7.3.0 Added the `archive` and `unarchive` actions. * * @return array Array of bulk action labels keyed by their action. */ protected function get_bulk_actions() { + $current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : ''; + + if ( 'request-archived' === $current_status ) { + return array( + 'unarchive' => __( 'Restore requests' ), + 'delete' => __( 'Delete requests permanently' ), + ); + } + return array( 'resend' => __( 'Resend confirmation requests' ), 'complete' => __( 'Mark requests as completed' ), + 'archive' => __( 'Archive requests' ), 'delete' => __( 'Delete requests' ), ); } + /** + * Returns the URL that performs a single-request action on the current screen. + * + * @since 7.3.0 + * + * @param string $action The bulk action to perform, e.g. 'archive'. + * @param int $request_id Request ID. + * @return string Nonced action URL. + */ + protected function get_request_action_url( $action, $request_id ) { + return wp_nonce_url( + add_query_arg( + array( + 'action' => $action, + 'request_id' => array( $request_id ), + ), + $this->get_admin_url() + ), + 'bulk-privacy_requests' + ); + } + /** * Process bulk actions. * * @since 4.9.6 * @since 5.6.0 Added support for the `complete` action. + * @since 7.3.0 Added support for the `archive` and `unarchive` actions. */ public function process_bulk_action() { $action = $this->current_action(); @@ -311,6 +354,100 @@ public function process_bulk_action() { ); break; + case 'archive': + foreach ( $request_ids as $request_id ) { + $result = _wp_privacy_archive_request( $request_id ); + + if ( $result && ! is_wp_error( $result ) ) { + ++$count; + } else { + ++$failures; + } + } + + if ( $failures ) { + add_settings_error( + 'bulk_action', + 'bulk_action', + sprintf( + /* translators: %d: Number of requests. */ + _n( + '%d request could not be archived. Only completed or failed requests can be archived.', + '%d requests could not be archived. Only completed or failed requests can be archived.', + $failures + ), + $failures + ), + 'error' + ); + } + + if ( $count ) { + add_settings_error( + 'bulk_action', + 'bulk_action', + sprintf( + /* translators: %d: Number of requests. */ + _n( + '%d request archived.', + '%d requests archived.', + $count + ), + $count + ), + 'success' + ); + } + + break; + + case 'unarchive': + foreach ( $request_ids as $request_id ) { + $result = _wp_privacy_unarchive_request( $request_id ); + + if ( $result && ! is_wp_error( $result ) ) { + ++$count; + } else { + ++$failures; + } + } + + if ( $failures ) { + add_settings_error( + 'bulk_action', + 'bulk_action', + sprintf( + /* translators: %d: Number of requests. */ + _n( + '%d request failed to restore.', + '%d requests failed to restore.', + $failures + ), + $failures + ), + 'error' + ); + } + + if ( $count ) { + add_settings_error( + 'bulk_action', + 'bulk_action', + sprintf( + /* translators: %d: Number of requests. */ + _n( + '%d request restored.', + '%d requests restored.', + $count + ), + $count + ), + 'success' + ); + } + + break; + case 'delete': foreach ( $request_ids as $request_id ) { if ( wp_delete_post( $request_id, true ) ) { @@ -455,6 +592,9 @@ public function column_status( $item ) { case 'request-completed': $timestamp = $item->completed_timestamp; break; + case 'request-archived': + $timestamp = $item->archived_timestamp; + break; } echo ''; diff --git a/src/wp-admin/includes/privacy-tools.php b/src/wp-admin/includes/privacy-tools.php index 17e660c8c0d0e..dde811a40965f 100644 --- a/src/wp-admin/includes/privacy-tools.php +++ b/src/wp-admin/includes/privacy-tools.php @@ -64,6 +64,86 @@ function _wp_privacy_completed_request( $request_id ) { return $result; } +/** + * Archives a completed or failed request and logs the current timestamp. + * + * Archived requests are hidden from the "All" view of the requests list table, + * but are kept in the database so they can be audited, or used to re-apply + * erasures after restoring a backup, until they are deleted permanently. + * + * @since 7.3.0 + * @access private + * + * @param int $request_id Request ID. + * @return int|WP_Error Request ID on success, or a WP_Error on failure. + */ +function _wp_privacy_archive_request( $request_id ) { + $request_id = absint( $request_id ); + $request = wp_get_user_request( $request_id ); + + if ( ! $request ) { + return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) ); + } + + if ( ! in_array( $request->status, array( 'request-completed', 'request-failed' ), true ) ) { + return new WP_Error( 'privacy_request_error', __( 'Only completed or failed personal data requests can be archived.' ) ); + } + + update_post_meta( $request_id, '_wp_user_request_pre_archive_status', $request->status ); + update_post_meta( $request_id, '_wp_user_request_archived_timestamp', time() ); + + return wp_update_post( + array( + 'ID' => $request_id, + 'post_status' => 'request-archived', + ), + true + ); +} + +/** + * Restores an archived request to the status it had before it was archived. + * + * @since 7.3.0 + * @access private + * + * @param int $request_id Request ID. + * @return int|WP_Error Request ID on success, or a WP_Error on failure. + */ +function _wp_privacy_unarchive_request( $request_id ) { + $request_id = absint( $request_id ); + $request = wp_get_user_request( $request_id ); + + if ( ! $request ) { + return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) ); + } + + if ( 'request-archived' !== $request->status ) { + return new WP_Error( 'privacy_request_error', __( 'This personal data request is not archived.' ) ); + } + + $status = get_post_meta( $request_id, '_wp_user_request_pre_archive_status', true ); + + if ( ! in_array( $status, array( 'request-completed', 'request-failed' ), true ) ) { + $status = 'request-completed'; + } + + $result = wp_update_post( + array( + 'ID' => $request_id, + 'post_status' => $status, + ), + true + ); + + if ( ! is_wp_error( $result ) ) { + delete_post_meta( $request_id, '_wp_user_request_pre_archive_status' ); + delete_post_meta( $request_id, '_wp_user_request_archived_timestamp' ); + } + + return $result; +} + /** * Handle list table actions. * diff --git a/src/wp-includes/class-wp-user-request.php b/src/wp-includes/class-wp-user-request.php index 9bf80752d8fb4..dd489112313e1 100644 --- a/src/wp-includes/class-wp-user-request.php +++ b/src/wp-includes/class-wp-user-request.php @@ -81,6 +81,14 @@ final class WP_User_Request { */ public $completed_timestamp = null; + /** + * Timestamp this request was archived. + * + * @since 7.3.0 + * @var int|null + */ + public $archived_timestamp = null; + /** * Misc data assigned to this request. * @@ -116,6 +124,7 @@ public function __construct( $post ) { $this->modified_timestamp = strtotime( $post->post_modified_gmt ); $this->confirmed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_confirmed_timestamp', true ); $this->completed_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_completed_timestamp', true ); + $this->archived_timestamp = (int) get_post_meta( $post->ID, '_wp_user_request_archived_timestamp', true ); $this->request_data = json_decode( $post->post_content, true ); $this->confirm_key = $post->post_password; } diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 5806d879f1c5e..0800ab49cea8c 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -831,6 +831,22 @@ function create_initial_post_types() { 'exclude_from_search' => false, ) ); + + register_post_status( + 'request-archived', + array( + 'label' => _x( 'Archived', 'request status' ), + 'internal' => true, + '_builtin' => true, /* internal use only. */ + /* translators: %s: Number of archived requests. */ + 'label_count' => _n_noop( + 'Archived (%s)', + 'Archived (%s)' + ), + // Archived requests are kept out of the "All" view, the same way as trashed posts. + 'exclude_from_search' => true, + ) + ); } /** @@ -1400,6 +1416,7 @@ function get_page_statuses() { * Returns statuses for privacy requests. * * @since 4.9.6 + * @since 7.3.0 Added the `request-archived` status. * @access private * * @return string[] Array of privacy request status labels keyed by their status. @@ -1410,6 +1427,7 @@ function _wp_privacy_statuses() { 'request-confirmed' => _x( 'Confirmed', 'request status' ), // User has confirmed the action. 'request-failed' => _x( 'Failed', 'request status' ), // User failed to confirm the action. 'request-completed' => _x( 'Completed', 'request status' ), // Admin has handled the request. + 'request-archived' => _x( 'Archived', 'request status' ), // Admin has archived a handled request. ); } diff --git a/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php b/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php index 6b339add4d7f7..0c2bd6357feb8 100644 --- a/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php +++ b/tests/phpunit/tests/admin/wpPrivacyRequestsTable.php @@ -303,4 +303,149 @@ public static function data_request_tables() { 'removal requests list' => array( 'WP_Privacy_Data_Removal_Requests_List_Table', 'remove_personal_data' ), ); } + + /** + * Archived requests get their own view and are not counted in "All". + * + * @ticket 44222 + * + * @covers WP_Privacy_Requests_Table::get_views + */ + public function test_get_views_should_exclude_archived_requests_from_all() { + $completed = wp_create_user_request( 'completed@example.com', 'export_personal_data' ); + $archived = wp_create_user_request( 'archived@example.com', 'export_personal_data' ); + wp_create_user_request( 'pending@example.com', 'export_personal_data' ); + + _wp_privacy_completed_request( $completed ); + _wp_privacy_completed_request( $archived ); + _wp_privacy_archive_request( $archived ); + + $views = $this->get_mocked_class_instance()->get_views(); + + $this->assertStringContainsString( 'All (2)', $views['all'] ); + $this->assertArrayHasKey( 'request-archived', $views ); + $this->assertStringContainsString( 'filter-status=request-archived', $views['request-archived'] ); + $this->assertStringContainsString( 'Archived (1)', $views['request-archived'] ); + } + + /** + * @ticket 44222 + * + * @covers WP_Privacy_Requests_Table::get_bulk_actions + */ + public function test_get_bulk_actions_should_depend_on_archived_view() { + $method = new ReflectionMethod( 'WP_Privacy_Requests_Table', 'get_bulk_actions' ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + $table = $this->get_mocked_class_instance(); + + $actions = $method->invoke( $table ); + $this->assertArrayHasKey( 'archive', $actions ); + $this->assertArrayNotHasKey( 'unarchive', $actions ); + + $_REQUEST['filter-status'] = 'request-archived'; + $actions = $method->invoke( $table ); + unset( $_REQUEST['filter-status'] ); + + $this->assertSame( array( 'unarchive', 'delete' ), array_keys( $actions ) ); + } + + /** + * @ticket 44222 + * + * @covers WP_Privacy_Requests_Table::process_bulk_action + */ + public function test_process_bulk_action_should_archive_and_restore_requests() { + global $wp_settings_errors; + + // Other tests may leave settings errors behind; start from a clean slate. + $wp_settings_errors = array(); + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + $completed = wp_create_user_request( 'completed@example.com', 'export_personal_data' ); + $pending = wp_create_user_request( 'pending@example.com', 'export_personal_data' ); + _wp_privacy_completed_request( $completed ); + + $table = $this->get_mocked_class_instance(); + + $_REQUEST['_wpnonce'] = wp_create_nonce( 'bulk-privacy_requests' ); + $_REQUEST['request_id'] = array( $completed, $pending ); + $_REQUEST['action'] = 'archive'; + $table->process_bulk_action(); + + $archived_status = get_post_status( $completed ); + $archive_notices = wp_list_pluck( get_settings_errors( 'bulk_action' ), 'type' ); + + $wp_settings_errors = array(); + $_REQUEST['request_id'] = array( $completed ); + $_REQUEST['action'] = 'unarchive'; + $table->process_bulk_action(); + + $restored_status = get_post_status( $completed ); + $restore_notices = wp_list_pluck( get_settings_errors( 'bulk_action' ), 'type' ); + + unset( $_REQUEST['_wpnonce'], $_REQUEST['request_id'], $_REQUEST['action'] ); + $wp_settings_errors = array(); + + $this->assertSame( 'request-archived', $archived_status, 'The completed request should be archived.' ); + $this->assertSame( 'request-pending', get_post_status( $pending ), 'The pending request should not be archived.' ); + $this->assertSame( array( 'error', 'success' ), $archive_notices, 'Archiving should report one failure and one success.' ); + $this->assertSame( 'request-completed', $restored_status, 'The archived request should be restored as completed.' ); + $this->assertSame( array( 'success' ), $restore_notices, 'Restoring should report one success.' ); + } + + /** + * Completed requests offer "Archive", archived ones offer "Restore" and "Delete permanently". + * + * @ticket 44222 + * + * @dataProvider data_request_list_tables + * + * @param string $table_class List table class name. + * @param string $request_type Request type. + */ + public function test_next_steps_should_offer_archive_actions( $table_class, $request_type ) { + $request_id = wp_create_user_request( 'requester@example.org', $request_type ); + _wp_privacy_completed_request( $request_id ); + + $table = new $table_class( + array( + 'plural' => 'privacy_requests', + 'singular' => 'privacy_request', + 'screen' => $request_type, + ) + ); + + ob_start(); + $table->column_next_steps( wp_get_user_request( $request_id ) ); + $completed_output = ob_get_clean(); + + $this->assertStringContainsString( 'action=archive', $completed_output ); + $this->assertStringNotContainsString( 'action=delete', $completed_output ); + + _wp_privacy_archive_request( $request_id ); + $request = wp_get_user_request( $request_id ); + + ob_start(); + $table->column_next_steps( $request ); + $archived_output = ob_get_clean(); + + $this->assertStringContainsString( 'action=unarchive', $archived_output ); + $this->assertStringContainsString( 'action=delete', $archived_output ); + $this->assertStringNotContainsString( 'complete-request', $table->column_email( $request ), 'Archived requests should not offer "Complete request".' ); + } + + /** + * Data provider for test_next_steps_should_offer_archive_actions(). + * + * @return array + */ + public static function data_request_list_tables() { + return array( + 'export requests list' => array( 'WP_Privacy_Data_Export_Requests_List_Table', 'export_personal_data' ), + 'removal requests list' => array( 'WP_Privacy_Data_Removal_Requests_List_Table', 'remove_personal_data' ), + ); + } } diff --git a/tests/phpunit/tests/privacy/wpPrivacyArchiveRequest.php b/tests/phpunit/tests/privacy/wpPrivacyArchiveRequest.php new file mode 100644 index 0000000000000..11feba5fe1f7d --- /dev/null +++ b/tests/phpunit/tests/privacy/wpPrivacyArchiveRequest.php @@ -0,0 +1,227 @@ +request_id = wp_create_user_request( 'requester@example.com', 'remove_personal_data' ); + } + + /** + * @ticket 44222 + */ + public function test_archive_should_return_error_for_invalid_request_id() { + $actual = _wp_privacy_archive_request( 0 ); + $this->assertWPError( $actual ); + $this->assertSame( 'privacy_request_error', $actual->get_error_code() ); + + $actual = _wp_privacy_archive_request( PHP_INT_MAX ); + $this->assertWPError( $actual ); + $this->assertSame( 'privacy_request_error', $actual->get_error_code() ); + } + + /** + * Requests still in progress must not be archived. + * + * @ticket 44222 + * + * @dataProvider data_unarchivable_statuses + * + * @param string $status Request status. + */ + public function test_archive_should_return_error_for_requests_in_progress( $status ) { + wp_update_post( + array( + 'ID' => $this->request_id, + 'post_status' => $status, + ) + ); + + $actual = _wp_privacy_archive_request( $this->request_id ); + + $this->assertWPError( $actual ); + $this->assertSame( 'privacy_request_error', $actual->get_error_code() ); + $this->assertSame( $status, get_post_status( $this->request_id ) ); + $this->assertEmpty( get_post_meta( $this->request_id, '_wp_user_request_archived_timestamp', true ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unarchivable_statuses() { + return array( + 'pending' => array( 'request-pending' ), + 'confirmed' => array( 'request-confirmed' ), + ); + } + + /** + * @ticket 44222 + * + * @dataProvider data_archivable_statuses + * + * @param string $status Request status. + */ + public function test_archive_should_mark_request_archived_and_log_timestamp( $status ) { + wp_update_post( + array( + 'ID' => $this->request_id, + 'post_status' => $status, + ) + ); + + $this->assertSame( $this->request_id, _wp_privacy_archive_request( $this->request_id ) ); + $this->assertSame( 'request-archived', get_post_status( $this->request_id ) ); + $this->assertSame( $status, get_post_meta( $this->request_id, '_wp_user_request_pre_archive_status', true ) ); + + $request = wp_get_user_request( $this->request_id ); + $this->assertGreaterThan( 0, $request->archived_timestamp ); + } + + /** + * @ticket 44222 + * + * @dataProvider data_archivable_statuses + * + * @param string $status Request status. + */ + public function test_unarchive_should_restore_previous_status_and_clear_meta( $status ) { + wp_update_post( + array( + 'ID' => $this->request_id, + 'post_status' => $status, + ) + ); + _wp_privacy_archive_request( $this->request_id ); + + $this->assertSame( $this->request_id, _wp_privacy_unarchive_request( $this->request_id ) ); + $this->assertSame( $status, get_post_status( $this->request_id ) ); + $this->assertSame( '', get_post_meta( $this->request_id, '_wp_user_request_pre_archive_status', true ) ); + $this->assertSame( 0, wp_get_user_request( $this->request_id )->archived_timestamp ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_archivable_statuses() { + return array( + 'completed' => array( 'request-completed' ), + 'failed' => array( 'request-failed' ), + ); + } + + /** + * Archiving must keep the completion data intact. + * + * @ticket 44222 + */ + public function test_archive_should_keep_completed_timestamp() { + _wp_privacy_completed_request( $this->request_id ); + $completed = wp_get_user_request( $this->request_id )->completed_timestamp; + + _wp_privacy_archive_request( $this->request_id ); + + $this->assertSame( $completed, wp_get_user_request( $this->request_id )->completed_timestamp ); + } + + /** + * @ticket 44222 + */ + public function test_unarchive_should_return_error_for_request_that_is_not_archived() { + _wp_privacy_completed_request( $this->request_id ); + + $actual = _wp_privacy_unarchive_request( $this->request_id ); + + $this->assertWPError( $actual ); + $this->assertSame( 'privacy_request_error', $actual->get_error_code() ); + $this->assertSame( 'request-completed', get_post_status( $this->request_id ) ); + } + + /** + * Requests archived without a recorded previous status are restored as completed. + * + * @ticket 44222 + */ + public function test_unarchive_should_fall_back_to_completed_status() { + wp_update_post( + array( + 'ID' => $this->request_id, + 'post_status' => 'request-archived', + ) + ); + + $this->assertSame( $this->request_id, _wp_privacy_unarchive_request( $this->request_id ) ); + $this->assertSame( 'request-completed', get_post_status( $this->request_id ) ); + } + + /** + * Archived requests are excluded from `post_status => any` queries, which the "All" view uses. + * + * @ticket 44222 + */ + public function test_archived_requests_should_be_excluded_from_any_status_queries() { + _wp_privacy_completed_request( $this->request_id ); + _wp_privacy_archive_request( $this->request_id ); + + $query_args = array( + 'post_type' => 'user_request', + 'fields' => 'ids', + 'post_status' => 'any', + ); + + $this->assertNotContains( $this->request_id, get_posts( $query_args ) ); + + $query_args['post_status'] = 'request-archived'; + $this->assertSame( array( $this->request_id ), get_posts( $query_args ) ); + } + + /** + * An archived request must not block a new request for the same email and action. + * + * @ticket 44222 + */ + public function test_archived_request_should_not_block_new_request() { + _wp_privacy_completed_request( $this->request_id ); + _wp_privacy_archive_request( $this->request_id ); + + $new_request_id = wp_create_user_request( 'requester@example.com', 'remove_personal_data' ); + + $this->assertIsInt( $new_request_id ); + $this->assertNotSame( $this->request_id, $new_request_id ); + } + + /** + * @ticket 44222 + * + * @covers ::_wp_privacy_statuses + */ + public function test_archived_status_is_registered() { + $this->assertArrayHasKey( 'request-archived', _wp_privacy_statuses() ); + + $status = get_post_status_object( 'request-archived' ); + $this->assertInstanceOf( 'stdClass', $status ); + $this->assertTrue( $status->exclude_from_search ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 7f4f09d78b308..2ac335cd17a03 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -5058,6 +5058,7 @@ public function test_status_array_enum_args() { 'request-confirmed', 'request-failed', 'request-completed', + 'request-archived', 'any', ), 'type' => 'string', diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 807eda9d2cf92..02d954780cbc6 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -447,6 +447,7 @@ mockedApiResponse.Schema = { "request-confirmed", "request-failed", "request-completed", + "request-archived", "any" ], "type": "string" @@ -1858,6 +1859,7 @@ mockedApiResponse.Schema = { "request-confirmed", "request-failed", "request-completed", + "request-archived", "any" ], "type": "string" @@ -3962,6 +3964,7 @@ mockedApiResponse.Schema = { "request-confirmed", "request-failed", "request-completed", + "request-archived", "any" ], "type": "string" @@ -4798,6 +4801,7 @@ mockedApiResponse.Schema = { "request-confirmed", "request-failed", "request-completed", + "request-archived", "any" ], "type": "string" @@ -7434,6 +7438,7 @@ mockedApiResponse.Schema = { "request-confirmed", "request-failed", "request-completed", + "request-archived", "any" ], "type": "string"