Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<span>';
$complete_request_markup .= sprintf(
'<a href="%s" class="complete-request" aria-label="%s">%s</a>',
Expand Down Expand Up @@ -142,18 +142,41 @@ public function column_next_steps( $item ) {
echo '<button type="submit" class="button-link" name="privacy_action_email_retry[' . $item->ID . ']" id="privacy_action_email_retry[' . $item->ID . ']">' . __( 'Retry' ) . '</button>';
break;
case 'request-completed':
echo '<a href="' . esc_url(
wp_nonce_url(
add_query_arg(
array(
'action' => 'delete',
'request_id' => array( $item->ID ),
),
admin_url( 'export-personal-data.php' )
),
'bulk-privacy_requests'
)
) . '">' . esc_html__( 'Remove request' ) . '</a>';
printf(
'<a href="%s" aria-label="%s">%s</a>',
esc_url( $this->get_request_action_url( 'archive', $item->ID ) ),
esc_attr(
sprintf(
/* translators: %s: Request email. */
__( 'Archive request for &#8220;%s&#8221;' ),
$item->email
)
),
esc_html__( 'Archive request' )
);
break;
case 'request-archived':
printf(
'<a href="%s" aria-label="%s">%s</a> | <a href="%s" class="submitdelete" aria-label="%s">%s</a>',
esc_url( $this->get_request_action_url( 'unarchive', $item->ID ) ),
esc_attr(
sprintf(
/* translators: %s: Request email. */
__( 'Restore request for &#8220;%s&#8221;' ),
$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 &#8220;%s&#8221; permanently' ),
$item->email
)
),
esc_html__( 'Delete permanently' )
);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<span>';
$complete_request_markup .= sprintf(
'<a href="%s" class="complete-request" aria-label="%s">%s</a>',
Expand Down Expand Up @@ -149,18 +149,41 @@ public function column_next_steps( $item ) {
echo '<button type="submit" class="button-link" name="privacy_action_email_retry[' . $item->ID . ']" id="privacy_action_email_retry[' . $item->ID . ']">' . __( 'Retry' ) . '</button>';
break;
case 'request-completed':
echo '<a href="' . esc_url(
wp_nonce_url(
add_query_arg(
array(
'action' => 'delete',
'request_id' => array( $item->ID ),
),
admin_url( 'erase-personal-data.php' )
),
'bulk-privacy_requests'
)
) . '">' . esc_html__( 'Remove request' ) . '</a>';
printf(
'<a href="%s" aria-label="%s">%s</a>',
esc_url( $this->get_request_action_url( 'archive', $item->ID ) ),
esc_attr(
sprintf(
/* translators: %s: Request email. */
__( 'Archive request for &#8220;%s&#8221;' ),
$item->email
)
),
esc_html__( 'Archive request' )
);
break;
case 'request-archived':
printf(
'<a href="%s" aria-label="%s">%s</a> | <a href="%s" class="submitdelete" aria-label="%s">%s</a>',
esc_url( $this->get_request_action_url( 'unarchive', $item->ID ) ),
esc_attr(
sprintf(
/* translators: %s: Request email. */
__( 'Restore request for &#8220;%s&#8221;' ),
$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 &#8220;%s&#8221; permanently' ),
$item->email
)
),
esc_html__( 'Delete permanently' )
);
break;
}
}
Expand Down
142 changes: 141 additions & 1 deletion src/wp-admin/includes/class-wp-privacy-requests-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 ) ) {
Expand Down Expand Up @@ -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 '<span class="status-label status-' . esc_attr( $status ) . '">';
Expand Down
80 changes: 80 additions & 0 deletions src/wp-admin/includes/privacy-tools.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading
Loading