From 655b36f11c25867fbd7da08fb49ccef777d04499 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:51:24 +0000 Subject: [PATCH 01/10] Initial plan From e51e34252c916cff98e04ff6a45272bb8ef54127 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:05:49 +0000 Subject: [PATCH 02/10] Add wp user privacy-request commands for GDPR personal data management Agent-Logs-Url: https://github.com/wp-cli/entity-command/sessions/945cce56-1ac8-48a6-adc2-32f7ea452224 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 7 + entity-command.php | 1 + features/user-privacy-request.feature | 239 +++++++++++ phpcs.xml.dist | 2 +- src/User_Privacy_Request_Command.php | 572 ++++++++++++++++++++++++++ 5 files changed, 820 insertions(+), 1 deletion(-) create mode 100644 features/user-privacy-request.feature create mode 100644 src/User_Privacy_Request_Command.php diff --git a/composer.json b/composer.json index 0be82481d..e9367775f 100644 --- a/composer.json +++ b/composer.json @@ -239,6 +239,13 @@ "user meta patch", "user meta pluck", "user meta update", + "user privacy-request", + "user privacy-request complete", + "user privacy-request create", + "user privacy-request delete", + "user privacy-request erase", + "user privacy-request export", + "user privacy-request list", "user remove-cap", "user remove-role", "user reset-password", diff --git a/entity-command.php b/entity-command.php index 8b1be319d..9bcb0068e 100644 --- a/entity-command.php +++ b/entity-command.php @@ -95,6 +95,7 @@ ) ); WP_CLI::add_command( 'user meta', 'User_Meta_Command' ); +WP_CLI::add_command( 'user privacy-request', 'User_Privacy_Request_Command' ); WP_CLI::add_command( 'user session', 'User_Session_Command' ); WP_CLI::add_command( 'user term', 'User_Term_Command' ); diff --git a/features/user-privacy-request.feature b/features/user-privacy-request.feature new file mode 100644 index 000000000..736aa757a --- /dev/null +++ b/features/user-privacy-request.feature @@ -0,0 +1,239 @@ +Feature: Manage user privacy requests + + @require-wp-4.9 + Scenario: Create and list privacy requests + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --porcelain` + Then STDOUT should be a number + And save STDOUT as {REQUEST_ID} + + When I run `wp user privacy-request list --format=csv --fields=ID,user_email,action_name,status` + Then STDOUT should contain: + """ + {REQUEST_ID},admin@example.com,export_personal_data,request-pending + """ + + When I run `wp user privacy-request list --format=ids` + Then STDOUT should contain: + """ + {REQUEST_ID} + """ + + When I run `wp user privacy-request list --format=count` + Then STDOUT should be: + """ + 1 + """ + + @require-wp-4.9 + Scenario: Create requests with confirmed status + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --status=confirmed --porcelain` + Then STDOUT should be a number + And save STDOUT as {REQUEST_ID} + + When I run `wp user privacy-request list --format=csv --fields=ID,status` + Then STDOUT should contain: + """ + {REQUEST_ID},request-confirmed + """ + + @require-wp-4.9 + Scenario: Filter privacy request list by action type + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --porcelain` + Then save STDOUT as {EXPORT_ID} + + When I run `wp user privacy-request create admin@example.com remove_personal_data --porcelain` + Then save STDOUT as {ERASE_ID} + + When I run `wp user privacy-request list --action-type=export_personal_data --format=ids` + Then STDOUT should contain: + """ + {EXPORT_ID} + """ + And STDOUT should not contain: + """ + {ERASE_ID} + """ + + When I run `wp user privacy-request list --action-type=remove_personal_data --format=ids` + Then STDOUT should contain: + """ + {ERASE_ID} + """ + And STDOUT should not contain: + """ + {EXPORT_ID} + """ + + @require-wp-4.9 + Scenario: Filter privacy request list by status + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --status=confirmed --porcelain` + Then save STDOUT as {CONFIRMED_ID} + + When I run `wp user privacy-request create admin@example.com remove_personal_data --porcelain` + Then save STDOUT as {PENDING_ID} + + When I run `wp user privacy-request list --status=request-confirmed --format=ids` + Then STDOUT should contain: + """ + {CONFIRMED_ID} + """ + And STDOUT should not contain: + """ + {PENDING_ID} + """ + + When I run `wp user privacy-request list --status=request-pending --format=ids` + Then STDOUT should contain: + """ + {PENDING_ID} + """ + And STDOUT should not contain: + """ + {CONFIRMED_ID} + """ + + @require-wp-4.9 + Scenario: Delete privacy requests + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --porcelain` + Then save STDOUT as {REQUEST_ID} + + When I run `wp user privacy-request delete {REQUEST_ID}` + Then STDOUT should contain: + """ + Success: Deleted 1 of 1 privacy requests. + """ + + When I run `wp user privacy-request list --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I try `wp user privacy-request delete 9999` + Then STDERR should contain: + """ + Warning: Could not find privacy request with ID 9999. + """ + + @require-wp-4.9 + Scenario: Complete privacy requests + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --status=confirmed --porcelain` + Then save STDOUT as {REQUEST_ID} + + When I run `wp user privacy-request complete {REQUEST_ID}` + Then STDOUT should contain: + """ + Success: Completed 1 of 1 privacy requests. + """ + + When I run `wp user privacy-request list --status=request-completed --format=ids` + Then STDOUT should contain: + """ + {REQUEST_ID} + """ + + When I try `wp user privacy-request complete 9999` + Then STDERR should contain: + """ + Warning: Could not find privacy request with ID 9999. + """ + + @require-wp-4.9 + Scenario: Erase personal data for a request + Given a WP install + + When I run `wp user privacy-request create admin@example.com remove_personal_data --status=confirmed --porcelain` + Then save STDOUT as {REQUEST_ID} + + When I run `wp user privacy-request erase {REQUEST_ID}` + Then STDOUT should contain: + """ + Success: Erased personal data for request {REQUEST_ID}. + """ + + When I run `wp user privacy-request list --status=request-completed --format=ids` + Then STDOUT should contain: + """ + {REQUEST_ID} + """ + + @require-wp-4.9 + Scenario: Erase command fails for non-erasure requests + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --status=confirmed --porcelain` + Then save STDOUT as {REQUEST_ID} + + When I try `wp user privacy-request erase {REQUEST_ID}` + Then STDERR should contain: + """ + Error: Request {REQUEST_ID} is not a 'remove_personal_data' request. + """ + + @require-wp-4.9 + Scenario: Export personal data for a request + Given a WP install + + When I run `wp user privacy-request create admin@example.com export_personal_data --status=confirmed --porcelain` + Then save STDOUT as {REQUEST_ID} + + When I run `wp user privacy-request export {REQUEST_ID}` + Then STDOUT should contain: + """ + Success: Exported personal data to: + """ + And STDOUT should contain: + """ + .zip + """ + + When I run `wp user privacy-request list --status=request-completed --format=ids` + Then STDOUT should contain: + """ + {REQUEST_ID} + """ + + @require-wp-4.9 + Scenario: Export command fails for non-export requests + Given a WP install + + When I run `wp user privacy-request create admin@example.com remove_personal_data --status=confirmed --porcelain` + Then save STDOUT as {REQUEST_ID} + + When I try `wp user privacy-request export {REQUEST_ID}` + Then STDERR should contain: + """ + Error: Request {REQUEST_ID} is not an 'export_personal_data' request. + """ + + @require-wp-4.9 + Scenario: Create request with invalid action type + Given a WP install + + When I try `wp user privacy-request create admin@example.com invalid_action` + Then STDERR should contain: + """ + Error: Invalid action type 'invalid_action'. + """ + + @require-wp-4.9 + Scenario: Create request with invalid status + Given a WP install + + When I try `wp user privacy-request create admin@example.com export_personal_data --status=invalid` + Then STDERR should contain: + """ + Error: Invalid status 'invalid'. + """ diff --git a/phpcs.xml.dist b/phpcs.xml.dist index b4b4651da..834afe6a0 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -71,7 +71,7 @@ */src/Signup_Command\.php$ */src/Site(_Meta|_Option)?_Command\.php$ */src/Term(_Meta)?_Command\.php$ - */src/User(_Application_Password|_Meta|_Session|_Term)?_Command\.php$ + */src/User(_Application_Password|_Meta|_Privacy_Request|_Session|_Term)?_Command\.php$ diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php new file mode 100644 index 000000000..eb916a208 --- /dev/null +++ b/src/User_Privacy_Request_Command.php @@ -0,0 +1,572 @@ + + */ + const REQUEST_FIELDS = [ + 'ID', + 'user_email', + 'action_name', + 'status', + 'created_timestamp', + ]; + + /** + * Lists privacy requests. + * + * ## OPTIONS + * + * [--action-type=] + * : Filter the list by action type. + * --- + * options: + * - export_personal_data + * - remove_personal_data + * --- + * + * [--status=] + * : Filter the list by request status. + * --- + * options: + * - request-pending + * - request-confirmed + * - request-failed + * - request-completed + * --- + * + * [--field=] + * : Prints the value of a single field for each request. + * + * [--fields=] + * : Limit the output to specific object fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - ids + * - json + * - count + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each request: + * + * * ID + * * user_email + * * action_name + * * status + * * created_timestamp + * + * These fields are optionally available: + * + * * user_id + * * confirmed_timestamp + * * completed_timestamp + * + * ## EXAMPLES + * + * # List all privacy requests. + * $ wp user privacy-request list + * +----+-------------------+----------------------+-------------------+--------------------+ + * | ID | user_email | action_name | status | created_timestamp | + * +----+-------------------+----------------------+-------------------+--------------------+ + * | 1 | bob@example.com | export_personal_data | request-pending | 1713779524 | + * +----+-------------------+----------------------+-------------------+--------------------+ + * + * # List only export requests. + * $ wp user privacy-request list --action-type=export_personal_data + * + * # List only completed requests. + * $ wp user privacy-request list --status=request-completed + * + * # List request IDs only. + * $ wp user privacy-request list --format=ids + * 1 2 + * + * @subcommand list + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of associative arguments. + */ + public function list_( $args, $assoc_args ) { + $query_args = [ + 'post_type' => 'user_request', + 'posts_per_page' => -1, + 'post_status' => 'any', + 'orderby' => 'ID', + 'order' => 'ASC', + ]; + + $action_type = Utils\get_flag_value( $assoc_args, 'action-type' ); + if ( $action_type ) { + $query_args['name'] = sanitize_key( $action_type ); + } + + $status = Utils\get_flag_value( $assoc_args, 'status' ); + if ( $status ) { + $query_args['post_status'] = sanitize_key( $status ); + } + + $posts = get_posts( $query_args ); + $requests = array_map( [ $this, 'get_request_data' ], $posts ); + $requests = array_filter( $requests ); + + $format = Utils\get_flag_value( $assoc_args, 'format', 'table' ); + + if ( empty( $assoc_args['fields'] ) ) { + $assoc_args['fields'] = self::REQUEST_FIELDS; + } + + $formatter = new Formatter( $assoc_args, self::REQUEST_FIELDS ); + + if ( 'ids' === $format ) { + WP_CLI::line( implode( ' ', wp_list_pluck( $requests, 'ID' ) ) ); + } else { + $formatter->display_items( $requests ); + } + } + + /** + * Creates a privacy request for a user. + * + * ## OPTIONS + * + * + * : The email address of the user to create the request for. + * + * + * : The type of personal data request. + * --- + * options: + * - export_personal_data + * - remove_personal_data + * --- + * + * [--status=] + * : The initial status of the request. + * --- + * default: pending + * options: + * - pending + * - confirmed + * --- + * + * [--send-email] + * : If set, sends a confirmation email to the user. + * + * [--porcelain] + * : Output just the new request ID. + * + * ## EXAMPLES + * + * # Create a new data export request with pending status. + * $ wp user privacy-request create bob@example.com export_personal_data + * Success: Created privacy request 1. + * + * # Create a confirmed data erasure request. + * $ wp user privacy-request create bob@example.com remove_personal_data --status=confirmed + * Success: Created privacy request 2. + * + * # Get just the new request ID. + * $ wp user privacy-request create bob@example.com export_personal_data --porcelain + * 3 + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of associative arguments. + */ + public function create( $args, $assoc_args ) { + list( $email_address, $action_name ) = $args; + + $valid_actions = [ 'export_personal_data', 'remove_personal_data' ]; + if ( ! in_array( $action_name, $valid_actions, true ) ) { + WP_CLI::error( "Invalid action type '{$action_name}'. Use 'export_personal_data' or 'remove_personal_data'." ); + } + + $status = Utils\get_flag_value( $assoc_args, 'status', 'pending' ); + $valid_statuses = [ 'pending', 'confirmed' ]; + if ( ! in_array( $status, $valid_statuses, true ) ) { + WP_CLI::error( "Invalid status '{$status}'. Use 'pending' or 'confirmed'." ); + } + + $request_id = wp_create_user_request( $email_address, $action_name, [], $status ); + + if ( is_wp_error( $request_id ) ) { + WP_CLI::error( $request_id ); + } + + if ( Utils\get_flag_value( $assoc_args, 'send-email', false ) ) { + wp_send_user_request( $request_id ); + } + + if ( Utils\get_flag_value( $assoc_args, 'porcelain', false ) ) { + WP_CLI::line( (string) $request_id ); + return; + } + + WP_CLI::success( "Created privacy request {$request_id}." ); + } + + /** + * Deletes one or more privacy requests. + * + * ## OPTIONS + * + * ... + * : One or more IDs of the privacy requests to delete. + * + * ## EXAMPLES + * + * # Delete privacy request 1. + * $ wp user privacy-request delete 1 + * Privacy request 1 deleted. + * Success: Deleted 1 of 1 privacy requests. + * + * # Delete multiple privacy requests. + * $ wp user privacy-request delete 1 2 3 + * Privacy request 1 deleted. + * Privacy request 2 deleted. + * Privacy request 3 deleted. + * Success: Deleted 3 of 3 privacy requests. + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of associative arguments. + */ + public function delete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + $successes = 0; + $errors = 0; + + foreach ( $args as $request_id ) { + $request_id = (int) $request_id; + $request = $this->get_request( $request_id ); + + if ( ! $request ) { + WP_CLI::warning( "Could not find privacy request with ID {$request_id}." ); + ++$errors; + continue; + } + + $result = wp_delete_user_request( $request_id ); // @phpstan-ignore function.notFound + + if ( is_wp_error( $result ) ) { + WP_CLI::warning( "Failed deleting privacy request {$request_id}: " . $result->get_error_message() ); + ++$errors; + } else { + WP_CLI::log( "Privacy request {$request_id} deleted." ); + ++$successes; + } + } + + $count = count( $args ); + Utils\report_batch_operation_results( 'privacy request', 'delete', $count, $successes, $errors ); + } + + /** + * Erases personal data for a given privacy request. + * + * Runs all registered data erasers for the email address associated with the + * request, then marks the request as completed. + * + * ## OPTIONS + * + * + * : The ID of the remove_personal_data privacy request to process. + * + * ## EXAMPLES + * + * # Erase personal data for request 1. + * $ wp user privacy-request erase 1 + * Success: Erased personal data for request 1. + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of associative arguments. + */ + public function erase( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + list( $request_id ) = $args; + $request_id = (int) $request_id; + + $request = $this->get_request_check( $request_id ); + + if ( 'remove_personal_data' !== $request->action_name ) { + WP_CLI::error( "Request {$request_id} is not a 'remove_personal_data' request." ); + } + + $email_address = $request->email; + $erasers = apply_filters( 'wp_privacy_personal_data_erasers', [] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $messages = []; + + foreach ( $erasers as $eraser_key => $eraser ) { + if ( ! isset( $eraser['callback'] ) || ! is_callable( $eraser['callback'] ) ) { + WP_CLI::warning( "Eraser '{$eraser_key}' does not have a valid callback." ); + continue; + } + + $page = 1; + do { + $response = call_user_func( $eraser['callback'], $email_address, $page ); + + if ( ! is_array( $response ) ) { + WP_CLI::warning( "Eraser '{$eraser_key}' returned an invalid response." ); + break; + } + + if ( ! empty( $response['messages'] ) ) { + $messages = array_merge( $messages, (array) $response['messages'] ); + } + + $done = ! empty( $response['done'] ); + ++$page; + } while ( ! $done ); + } + + wp_update_post( + [ + 'ID' => $request_id, + 'post_status' => 'request-completed', + ] + ); + update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); + + foreach ( $messages as $message ) { + WP_CLI::log( (string) $message ); // @phpstan-ignore cast.string + } + + WP_CLI::success( "Erased personal data for request {$request_id}." ); + } + + /** + * Exports personal data for a given privacy request. + * + * Runs all registered data exporters for the email address associated with + * the request, generates a ZIP file containing the data, then marks the + * request as completed. + * + * ## OPTIONS + * + * + * : The ID of the export_personal_data privacy request to process. + * + * ## EXAMPLES + * + * # Export personal data for request 1. + * $ wp user privacy-request export 1 + * Success: Exported personal data to: /var/www/html/wp-content/uploads/wp-personal-data-exports/wp-personal-data-export-bob-example-com-1.zip + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of associative arguments. + */ + public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + list( $request_id ) = $args; + $request_id = (int) $request_id; + + $request = $this->get_request_check( $request_id ); + + if ( 'export_personal_data' !== $request->action_name ) { + WP_CLI::error( "Request {$request_id} is not an 'export_personal_data' request." ); + } + + $email_address = $request->email; + $exporters = apply_filters( 'wp_privacy_personal_data_exporters', [] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $export_data = []; + + foreach ( $exporters as $exporter_key => $exporter ) { + if ( ! isset( $exporter['callback'] ) || ! is_callable( $exporter['callback'] ) ) { + WP_CLI::warning( "Exporter '{$exporter_key}' does not have a valid callback." ); + continue; + } + + $page = 1; + do { + $response = call_user_func( $exporter['callback'], $email_address, $page ); + + if ( ! is_array( $response ) ) { + WP_CLI::warning( "Exporter '{$exporter_key}' returned an invalid response." ); + break; + } + + if ( ! empty( $response['data'] ) ) { + if ( ! isset( $export_data[ $exporter_key ] ) ) { + $export_data[ $exporter_key ] = []; + } + $export_data[ $exporter_key ] = array_merge( $export_data[ $exporter_key ], (array) $response['data'] ); + } + + $done = ! empty( $response['done'] ); + ++$page; + } while ( ! $done ); + } + + update_post_meta( $request_id, '_export_data_grouped', $export_data ); + wp_privacy_generate_personal_data_export_file( $request_id ); + + $file_path = get_post_meta( $request_id, '_export_file_path', true ); + + wp_update_post( + [ + 'ID' => $request_id, + 'post_status' => 'request-completed', + ] + ); + update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); + + WP_CLI::success( 'Exported personal data to: ' . (string) $file_path ); // @phpstan-ignore cast.string + } + + /** + * Marks one or more privacy requests as completed. + * + * ## OPTIONS + * + * ... + * : One or more IDs of the privacy requests to complete. + * + * ## EXAMPLES + * + * # Mark request 1 as completed. + * $ wp user privacy-request complete 1 + * Privacy request 1 completed. + * Success: Completed 1 of 1 privacy requests. + * + * # Mark multiple requests as completed. + * $ wp user privacy-request complete 1 2 + * Privacy request 1 completed. + * Privacy request 2 completed. + * Success: Completed 2 of 2 privacy requests. + * + * @param array $args Indexed array of positional arguments. + * @param array $assoc_args Associative array of associative arguments. + */ + public function complete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + $successes = 0; + $errors = 0; + + foreach ( $args as $request_id ) { + $request_id = (int) $request_id; + $request = $this->get_request( $request_id ); + + if ( ! $request ) { + WP_CLI::warning( "Could not find privacy request with ID {$request_id}." ); + ++$errors; + continue; + } + + $result = wp_update_post( + [ + 'ID' => $request_id, + 'post_status' => 'request-completed', + ], + true + ); + + if ( is_wp_error( $result ) ) { + WP_CLI::warning( "Failed completing privacy request {$request_id}: " . $result->get_error_message() ); + ++$errors; + } else { + update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); + WP_CLI::log( "Privacy request {$request_id} completed." ); + ++$successes; + } + } + + $count = count( $args ); + Utils\report_batch_operation_results( 'privacy request', 'complete', $count, $successes, $errors ); + } + + /** + * Gets a user privacy request object. + * + * @param int $request_id Request ID. + * @return WP_User_Request|false The request if found; false otherwise. + */ + private function get_request( $request_id ) { + return wp_get_user_request( $request_id ); + } + + /** + * Gets a user privacy request object or exits with an error. + * + * @param int $request_id Request ID. + * @return WP_User_Request The request. + */ + private function get_request_check( $request_id ) { + $request = $this->get_request( $request_id ); + + if ( ! $request ) { + WP_CLI::error( "Could not find privacy request with ID {$request_id}." ); + } + + return $request; + } + + /** + * Converts a WP_Post (user_request post type) to an associative array for display. + * + * @param WP_Post $post Post object of type user_request. + * @return array|false Array of request data, or false on failure. + */ + private function get_request_data( $post ) { + $request = wp_get_user_request( $post->ID ); + + if ( ! $request ) { + return false; + } + + return [ + 'ID' => $request->ID, + 'user_id' => $request->user_id, + 'user_email' => $request->email, + 'action_name' => $request->action_name, + 'status' => $request->status, + 'created_timestamp' => $request->created_timestamp, + 'confirmed_timestamp' => $request->confirmed_timestamp, + 'completed_timestamp' => $request->completed_timestamp, + ]; + } +} From f1aa6e1904442dbc2de4d45007742fbe18b55250 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:07:37 +0000 Subject: [PATCH 03/10] Address code review feedback: improve type safety and error handling in export and erase commands Agent-Logs-Url: https://github.com/wp-cli/entity-command/sessions/945cce56-1ac8-48a6-adc2-32f7ea452224 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/User_Privacy_Request_Command.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index eb916a208..810ee5053 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -293,6 +293,7 @@ public function delete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly continue; } + // Function exists since WP 4.9.6 but is absent from the wordpress-stubs library. $result = wp_delete_user_request( $request_id ); // @phpstan-ignore function.notFound if ( is_wp_error( $result ) ) { @@ -375,7 +376,9 @@ public function erase( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalys update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); foreach ( $messages as $message ) { - WP_CLI::log( (string) $message ); // @phpstan-ignore cast.string + if ( is_scalar( $message ) ) { + WP_CLI::log( (string) $message ); + } } WP_CLI::success( "Erased personal data for request {$request_id}." ); @@ -448,6 +451,10 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly $file_path = get_post_meta( $request_id, '_export_file_path', true ); + if ( ! is_string( $file_path ) || '' === $file_path ) { + WP_CLI::error( 'Failed to generate the personal data export file.' ); + } + wp_update_post( [ 'ID' => $request_id, @@ -456,7 +463,7 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly ); update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); - WP_CLI::success( 'Exported personal data to: ' . (string) $file_path ); // @phpstan-ignore cast.string + WP_CLI::success( "Exported personal data to: {$file_path}" ); } /** From 3cbdd2f81d5bebeb1900bd7b1b185a275fc4b5ac Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Apr 2026 17:50:54 +0200 Subject: [PATCH 04/10] Fix tests and phpstan --- features/user-privacy-request.feature | 5 +- src/User_Privacy_Request_Command.php | 84 ++++++++++++++++----------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/features/user-privacy-request.feature b/features/user-privacy-request.feature index 736aa757a..eb76a8d2e 100644 --- a/features/user-privacy-request.feature +++ b/features/user-privacy-request.feature @@ -225,7 +225,7 @@ Feature: Manage user privacy requests When I try `wp user privacy-request create admin@example.com invalid_action` Then STDERR should contain: """ - Error: Invalid action type 'invalid_action'. + Error: Invalid value specified for positional arg. """ @require-wp-4.9 @@ -235,5 +235,6 @@ Feature: Manage user privacy requests When I try `wp user privacy-request create admin@example.com export_personal_data --status=invalid` Then STDERR should contain: """ - Error: Invalid status 'invalid'. + Error: Parameter errors: + Invalid value specified for 'status' (The initial status of the request.) """ diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index 810ee5053..a2d52f285 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -133,8 +133,8 @@ final class User_Privacy_Request_Command { * * @subcommand list * - * @param array $args Indexed array of positional arguments. - * @param array $assoc_args Associative array of associative arguments. + * @param list $args Positional arguments. + * @param array{action-type?: string, status?: string, field?: string, fields?: string, format?: string} $assoc_args Associative arguments. */ public function list_( $args, $assoc_args ) { $query_args = [ @@ -147,7 +147,7 @@ public function list_( $args, $assoc_args ) { $action_type = Utils\get_flag_value( $assoc_args, 'action-type' ); if ( $action_type ) { - $query_args['name'] = sanitize_key( $action_type ); + $query_args['post_name__in'] = [ sanitize_key( $action_type ) ]; } $status = Utils\get_flag_value( $assoc_args, 'status' ); @@ -219,22 +219,13 @@ public function list_( $args, $assoc_args ) { * $ wp user privacy-request create bob@example.com export_personal_data --porcelain * 3 * - * @param array $args Indexed array of positional arguments. - * @param array $assoc_args Associative array of associative arguments. + * @param array{string, string} $args Positional arguments. + * @param array{status?: string, send-email?: bool, porcelain?: bool} $assoc_args Associative arguments. */ public function create( $args, $assoc_args ) { list( $email_address, $action_name ) = $args; - $valid_actions = [ 'export_personal_data', 'remove_personal_data' ]; - if ( ! in_array( $action_name, $valid_actions, true ) ) { - WP_CLI::error( "Invalid action type '{$action_name}'. Use 'export_personal_data' or 'remove_personal_data'." ); - } - - $status = Utils\get_flag_value( $assoc_args, 'status', 'pending' ); - $valid_statuses = [ 'pending', 'confirmed' ]; - if ( ! in_array( $status, $valid_statuses, true ) ) { - WP_CLI::error( "Invalid status '{$status}'. Use 'pending' or 'confirmed'." ); - } + $status = Utils\get_flag_value( $assoc_args, 'status', 'pending' ); $request_id = wp_create_user_request( $email_address, $action_name, [], $status ); @@ -276,8 +267,8 @@ public function create( $args, $assoc_args ) { * Privacy request 3 deleted. * Success: Deleted 3 of 3 privacy requests. * - * @param array $args Indexed array of positional arguments. - * @param array $assoc_args Associative array of associative arguments. + * @param list $args Positional arguments. + * @param array{} $assoc_args Associative arguments. */ public function delete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $successes = 0; @@ -293,8 +284,7 @@ public function delete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly continue; } - // Function exists since WP 4.9.6 but is absent from the wordpress-stubs library. - $result = wp_delete_user_request( $request_id ); // @phpstan-ignore function.notFound + $result = wp_delete_post( $request_id, true ); if ( is_wp_error( $result ) ) { WP_CLI::warning( "Failed deleting privacy request {$request_id}: " . $result->get_error_message() ); @@ -326,8 +316,8 @@ public function delete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly * $ wp user privacy-request erase 1 * Success: Erased personal data for request 1. * - * @param array $args Indexed array of positional arguments. - * @param array $assoc_args Associative array of associative arguments. + * @param array{string} $args Positional arguments. + * @param array{} $assoc_args Associative arguments. */ public function erase( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed list( $request_id ) = $args; @@ -402,8 +392,8 @@ public function erase( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalys * $ wp user privacy-request export 1 * Success: Exported personal data to: /var/www/html/wp-content/uploads/wp-personal-data-exports/wp-personal-data-export-bob-example-com-1.zip * - * @param array $args Indexed array of positional arguments. - * @param array $assoc_args Associative array of associative arguments. + * @param array{string} $args Positional arguments. + * @param array{} $assoc_args Associative arguments. */ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed list( $request_id ) = $args; @@ -417,7 +407,7 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly $email_address = $request->email; $exporters = apply_filters( 'wp_privacy_personal_data_exporters', [] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound - $export_data = []; + $groups = []; foreach ( $exporters as $exporter_key => $exporter ) { if ( ! isset( $exporter['callback'] ) || ! is_callable( $exporter['callback'] ) ) { @@ -434,11 +424,34 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly break; } - if ( ! empty( $response['data'] ) ) { - if ( ! isset( $export_data[ $exporter_key ] ) ) { - $export_data[ $exporter_key ] = []; + if ( ! empty( $response['data'] ) && is_array( $response['data'] ) ) { + foreach ( $response['data'] as $export_datum ) { + if ( ! is_array( $export_datum ) ) { + continue; + } + if ( ! isset( $export_datum['group_id'], $export_datum['item_id'] ) ) { + continue; + } + if ( ! is_scalar( $export_datum['group_id'] ) || ! is_scalar( $export_datum['item_id'] ) ) { + continue; + } + $group_id = (string) $export_datum['group_id']; + $item_id = (string) $export_datum['item_id']; + + if ( ! isset( $groups[ $group_id ] ) ) { + $groups[ $group_id ] = [ + 'group_label' => isset( $export_datum['group_label'] ) && is_scalar( $export_datum['group_label'] ) ? (string) $export_datum['group_label'] : '', + 'group_description' => isset( $export_datum['group_description'] ) && is_scalar( $export_datum['group_description'] ) ? (string) $export_datum['group_description'] : '', + 'items' => [], + ]; + } + if ( ! isset( $groups[ $group_id ]['items'][ $item_id ] ) ) { + $groups[ $group_id ]['items'][ $item_id ] = []; + } + if ( isset( $export_datum['data'] ) && is_array( $export_datum['data'] ) ) { + $groups[ $group_id ]['items'][ $item_id ] = array_merge( $groups[ $group_id ]['items'][ $item_id ], $export_datum['data'] ); + } } - $export_data[ $exporter_key ] = array_merge( $export_data[ $exporter_key ], (array) $response['data'] ); } $done = ! empty( $response['done'] ); @@ -446,15 +459,20 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly } while ( ! $done ); } - update_post_meta( $request_id, '_export_data_grouped', $export_data ); + update_post_meta( $request_id, '_export_data_grouped', $groups ); + + require_once ABSPATH . 'wp-admin/includes/privacy-tools.php'; wp_privacy_generate_personal_data_export_file( $request_id ); - $file_path = get_post_meta( $request_id, '_export_file_path', true ); + $file_name = get_post_meta( $request_id, '_export_file_name', true ); - if ( ! is_string( $file_path ) || '' === $file_path ) { + if ( ! is_string( $file_name ) || '' === $file_name ) { WP_CLI::error( 'Failed to generate the personal data export file.' ); } + $exports_dir = wp_privacy_exports_dir(); + $file_path = $exports_dir . $file_name; + wp_update_post( [ 'ID' => $request_id, @@ -487,8 +505,8 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly * Privacy request 2 completed. * Success: Completed 2 of 2 privacy requests. * - * @param array $args Indexed array of positional arguments. - * @param array $assoc_args Associative array of associative arguments. + * @param list $args Positional arguments. + * @param array{} $assoc_args Associative arguments. */ public function complete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $successes = 0; From ac9b5e5956aaaec180ee90c6b7c613e9b4cbf566 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Apr 2026 21:54:04 +0200 Subject: [PATCH 05/10] more precise tags --- features/user-privacy-request.feature | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/features/user-privacy-request.feature b/features/user-privacy-request.feature index eb76a8d2e..14620e5a4 100644 --- a/features/user-privacy-request.feature +++ b/features/user-privacy-request.feature @@ -1,6 +1,6 @@ Feature: Manage user privacy requests - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Create and list privacy requests Given a WP install @@ -26,7 +26,7 @@ Feature: Manage user privacy requests 1 """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Create requests with confirmed status Given a WP install @@ -40,7 +40,7 @@ Feature: Manage user privacy requests {REQUEST_ID},request-confirmed """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Filter privacy request list by action type Given a WP install @@ -70,7 +70,7 @@ Feature: Manage user privacy requests {EXPORT_ID} """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Filter privacy request list by status Given a WP install @@ -100,7 +100,7 @@ Feature: Manage user privacy requests {CONFIRMED_ID} """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Delete privacy requests Given a WP install @@ -125,7 +125,7 @@ Feature: Manage user privacy requests Warning: Could not find privacy request with ID 9999. """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Complete privacy requests Given a WP install @@ -150,7 +150,7 @@ Feature: Manage user privacy requests Warning: Could not find privacy request with ID 9999. """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Erase personal data for a request Given a WP install @@ -169,7 +169,7 @@ Feature: Manage user privacy requests {REQUEST_ID} """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Erase command fails for non-erasure requests Given a WP install @@ -182,7 +182,7 @@ Feature: Manage user privacy requests Error: Request {REQUEST_ID} is not a 'remove_personal_data' request. """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Export personal data for a request Given a WP install @@ -205,7 +205,7 @@ Feature: Manage user privacy requests {REQUEST_ID} """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Export command fails for non-export requests Given a WP install @@ -218,7 +218,7 @@ Feature: Manage user privacy requests Error: Request {REQUEST_ID} is not an 'export_personal_data' request. """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Create request with invalid action type Given a WP install @@ -228,7 +228,7 @@ Feature: Manage user privacy requests Error: Invalid value specified for positional arg. """ - @require-wp-4.9 + @require-wp-4.9.6 Scenario: Create request with invalid status Given a WP install From a29449ca66d4c82386231d3c8972ac8505dec2eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:34:03 +0000 Subject: [PATCH 06/10] Fall back to wp_get_user_request_data() for WP < 5.4 compatibility Agent-Logs-Url: https://github.com/wp-cli/entity-command/sessions/b837075e-3a1d-486d-8781-8d174feb9cfa Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/User_Privacy_Request_Command.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index a2d52f285..31875eac2 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -551,7 +551,11 @@ public function complete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAna * @return WP_User_Request|false The request if found; false otherwise. */ private function get_request( $request_id ) { - return wp_get_user_request( $request_id ); + if ( function_exists( 'wp_get_user_request' ) ) { + return wp_get_user_request( $request_id ); + } + + return wp_get_user_request_data( $request_id ); // phpcs:ignore WordPress.WP.DeprecatedFunctions.wp_get_user_request_dataFound -- Fallback for WP < 5.4. // @phpstan-ignore function.deprecated } /** @@ -577,7 +581,11 @@ private function get_request_check( $request_id ) { * @return array|false Array of request data, or false on failure. */ private function get_request_data( $post ) { - $request = wp_get_user_request( $post->ID ); + if ( function_exists( 'wp_get_user_request' ) ) { + $request = wp_get_user_request( $post->ID ); + } else { + $request = wp_get_user_request_data( $post->ID ); // phpcs:ignore WordPress.WP.DeprecatedFunctions.wp_get_user_request_dataFound -- Fallback for WP < 5.4. // @phpstan-ignore function.deprecated + } if ( ! $request ) { return false; From de9bc03ad1fd0be6c662e48a6eed7ca8d8674f83 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 27 Apr 2026 10:00:18 +0200 Subject: [PATCH 07/10] Add file_exists check --- src/User_Privacy_Request_Command.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index 31875eac2..5b97b6f9a 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -461,7 +461,13 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly update_post_meta( $request_id, '_export_data_grouped', $groups ); - require_once ABSPATH . 'wp-admin/includes/privacy-tools.php'; + // Files were moved in WP 5.3.0, see https://core.trac.wordpress.org/ticket/43895. + if ( file_exists( ABSPATH . 'wp-admin/includes/privacy-tools.php' ) ) { + require_once ABSPATH . 'wp-admin/includes/privacy-tools.php'; + } else { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + wp_privacy_generate_personal_data_export_file( $request_id ); $file_name = get_post_meta( $request_id, '_export_file_name', true ); From 842f7f1bf435f91af57ae280dfa9a725fcecf76d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:37:51 +0000 Subject: [PATCH 08/10] Fix --status=confirmed compat for WP < 5.7 in create() Agent-Logs-Url: https://github.com/wp-cli/entity-command/sessions/23bdadcf-1412-48b9-a8b9-e2c3a8c6adb7 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/User_Privacy_Request_Command.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index 5b97b6f9a..8515ebc37 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -233,6 +233,17 @@ public function create( $args, $assoc_args ) { WP_CLI::error( $request_id ); } + // The $status parameter for wp_create_user_request() was added in WP 5.7.0. + // For older versions, manually update the post status when 'confirmed' was requested. + if ( 'confirmed' === $status ) { + wp_update_post( + [ + 'ID' => $request_id, + 'post_status' => 'request-confirmed', + ] + ); + } + if ( Utils\get_flag_value( $assoc_args, 'send-email', false ) ) { wp_send_user_request( $request_id ); } From 305de2e68fe6ce93ccb1ea515fda7dc73dbf5f46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:22:31 +0000 Subject: [PATCH 09/10] Fix export file path resolution for WP < 5.5 (_export_file_path vs _export_file_name) Agent-Logs-Url: https://github.com/wp-cli/entity-command/sessions/76bb4546-13cf-4c5e-90d1-e8679fd9b477 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/User_Privacy_Request_Command.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index 8515ebc37..448fa13a1 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -481,15 +481,23 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly wp_privacy_generate_personal_data_export_file( $request_id ); - $file_name = get_post_meta( $request_id, '_export_file_name', true ); + // WP 5.5+ stores only the basename in '_export_file_name'; older versions + // store the full absolute path in '_export_file_path'. + $raw_path = get_post_meta( $request_id, '_export_file_path', true ); + $file_path = is_string( $raw_path ) ? $raw_path : ''; + + if ( '' === $file_path ) { + $raw_name = get_post_meta( $request_id, '_export_file_name', true ); + $file_name = is_string( $raw_name ) ? $raw_name : ''; + if ( '' !== $file_name ) { + $file_path = wp_privacy_exports_dir() . $file_name; + } + } - if ( ! is_string( $file_name ) || '' === $file_name ) { + if ( '' === $file_path ) { WP_CLI::error( 'Failed to generate the personal data export file.' ); } - $exports_dir = wp_privacy_exports_dir(); - $file_path = $exports_dir . $file_name; - wp_update_post( [ 'ID' => $request_id, @@ -498,7 +506,7 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly ); update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); - WP_CLI::success( "Exported personal data to: {$file_path}" ); + WP_CLI::success( 'Exported personal data to: ' . $file_path ); } /** From 93150c18fc020ffa19164674aa08f5d142cccb02 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Apr 2026 12:55:04 +0200 Subject: [PATCH 10/10] Apply feedback from code review --- entity-command.php | 12 +++- src/User_Privacy_Request_Command.php | 90 ++++++++++++++++++---------- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/entity-command.php b/entity-command.php index 9bcb0068e..52bc0a4cd 100644 --- a/entity-command.php +++ b/entity-command.php @@ -95,7 +95,17 @@ ) ); WP_CLI::add_command( 'user meta', 'User_Meta_Command' ); -WP_CLI::add_command( 'user privacy-request', 'User_Privacy_Request_Command' ); +WP_CLI::add_command( + 'user privacy-request', + 'User_Privacy_Request_Command', + array( + 'before_invoke' => function () { + if ( Utils\wp_version_compare( '4.9.6', '<' ) ) { + WP_CLI::error( 'Requires WordPress 4.9.6 or greater.' ); + } + }, + ) +); WP_CLI::add_command( 'user session', 'User_Session_Command' ); WP_CLI::add_command( 'user term', 'User_Term_Command' ); diff --git a/src/User_Privacy_Request_Command.php b/src/User_Privacy_Request_Command.php index 448fa13a1..a45ef0abe 100644 --- a/src/User_Privacy_Request_Command.php +++ b/src/User_Privacy_Request_Command.php @@ -168,7 +168,10 @@ public function list_( $args, $assoc_args ) { $formatter = new Formatter( $assoc_args, self::REQUEST_FIELDS ); if ( 'ids' === $format ) { - WP_CLI::line( implode( ' ', wp_list_pluck( $requests, 'ID' ) ) ); + $ids = wp_list_pluck( $requests, 'ID' ); + if ( ! empty( $ids ) ) { + WP_CLI::line( implode( ' ', $ids ) ); + } } else { $formatter->display_items( $requests ); } @@ -245,7 +248,10 @@ public function create( $args, $assoc_args ) { } if ( Utils\get_flag_value( $assoc_args, 'send-email', false ) ) { - wp_send_user_request( $request_id ); + $send_result = wp_send_user_request( $request_id ); + if ( is_wp_error( $send_result ) ) { + WP_CLI::error( $send_result ); + } } if ( Utils\get_flag_value( $assoc_args, 'porcelain', false ) ) { @@ -297,8 +303,8 @@ public function delete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly $result = wp_delete_post( $request_id, true ); - if ( is_wp_error( $result ) ) { - WP_CLI::warning( "Failed deleting privacy request {$request_id}: " . $result->get_error_message() ); + if ( ! $result ) { + WP_CLI::warning( "Failed deleting privacy request {$request_id}." ); ++$errors; } else { WP_CLI::log( "Privacy request {$request_id} deleted." ); @@ -355,26 +361,43 @@ public function erase( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalys $response = call_user_func( $eraser['callback'], $email_address, $page ); if ( ! is_array( $response ) ) { - WP_CLI::warning( "Eraser '{$eraser_key}' returned an invalid response." ); - break; + WP_CLI::error( "Eraser '{$eraser_key}' did not return an array." ); + } + + if ( ! array_key_exists( 'items_removed', $response ) ) { + WP_CLI::error( "Expected items_removed key in response array from '{$eraser_key}' eraser." ); + } + + if ( ! array_key_exists( 'items_retained', $response ) ) { + WP_CLI::error( "Expected items_retained key in response array from '{$eraser_key}' eraser." ); + } + + if ( ! array_key_exists( 'messages', $response ) ) { + WP_CLI::error( "Expected messages key in response array from '{$eraser_key}' eraser." ); + } + + if ( ! is_array( $response['messages'] ) ) { + WP_CLI::error( "Expected messages key to reference an array in response array from '{$eraser_key}' eraser." ); + } + + if ( ! array_key_exists( 'done', $response ) ) { + WP_CLI::error( "Expected done flag in response array from '{$eraser_key}' eraser." ); } if ( ! empty( $response['messages'] ) ) { - $messages = array_merge( $messages, (array) $response['messages'] ); + $messages = array_merge( $messages, $response['messages'] ); } - $done = ! empty( $response['done'] ); + $done = (bool) $response['done']; ++$page; } while ( ! $done ); } - wp_update_post( - [ - 'ID' => $request_id, - 'post_status' => 'request-completed', - ] - ); - update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); + $result = _wp_privacy_completed_request( $request_id ); + + if ( is_wp_error( $result ) ) { + WP_CLI::error( "Failed completing privacy request {$request_id}: " . $result->get_error_message() ); + } foreach ( $messages as $message ) { if ( is_scalar( $message ) ) { @@ -431,8 +454,19 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly $response = call_user_func( $exporter['callback'], $email_address, $page ); if ( ! is_array( $response ) ) { - WP_CLI::warning( "Exporter '{$exporter_key}' returned an invalid response." ); - break; + WP_CLI::error( "Exporter '{$exporter_key}' did not return an array." ); + } + + if ( ! array_key_exists( 'data', $response ) ) { + WP_CLI::error( "Expected data in response array from exporter '{$exporter_key}'." ); + } + + if ( ! is_array( $response['data'] ) ) { + WP_CLI::error( "Expected data array in response array from exporter '{$exporter_key}'." ); + } + + if ( ! array_key_exists( 'done', $response ) ) { + WP_CLI::error( "Expected done (boolean) in response array from exporter '{$exporter_key}'." ); } if ( ! empty( $response['data'] ) && is_array( $response['data'] ) ) { @@ -465,7 +499,7 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly } } - $done = ! empty( $response['done'] ); + $done = (bool) $response['done']; ++$page; } while ( ! $done ); } @@ -498,13 +532,11 @@ public function export( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly WP_CLI::error( 'Failed to generate the personal data export file.' ); } - wp_update_post( - [ - 'ID' => $request_id, - 'post_status' => 'request-completed', - ] - ); - update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() ); + $result = _wp_privacy_completed_request( $request_id ); + + if ( is_wp_error( $result ) ) { + WP_CLI::error( "Failed completing privacy request {$request_id}: " . $result->get_error_message() ); + } WP_CLI::success( 'Exported personal data to: ' . $file_path ); } @@ -547,13 +579,7 @@ public function complete( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAna continue; } - $result = wp_update_post( - [ - 'ID' => $request_id, - 'post_status' => 'request-completed', - ], - true - ); + $result = _wp_privacy_completed_request( $request_id ); if ( is_wp_error( $result ) ) { WP_CLI::warning( "Failed completing privacy request {$request_id}: " . $result->get_error_message() );