From 14d25f6094bbde10e47fb49b56cdf1fac0ab302e Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 2 May 2018 01:04:26 +0000 Subject: [PATCH] Privacy: update and enhance the method to confirm user requests by email. Introduce WP_User_Request to hold all request vars similarly to WP_Post. Props mikejolley, desrosj. Merges [43011] and [43014] to the 4.9 branch. See #43443. Built from https://develop.svn.wordpress.org/branches/4.9@43084 git-svn-id: http://core.svn.wordpress.org/branches/4.9@42913 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/ajax-actions.php | 7 +- wp-admin/includes/user.php | 135 +++++++++++-------- wp-includes/post.php | 2 +- wp-includes/user.php | 208 ++++++++++++++++++++--------- wp-includes/version.php | 2 +- 5 files changed, 232 insertions(+), 122 deletions(-) diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index 8049f4abc5..6f522fe347 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -4154,12 +4154,13 @@ function wp_ajax_wp_privacy_erase_personal_data() { check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' ); // Find the request CPT - $request = get_post( $request_id ); - if ( 'remove_personal_data' !== $request->post_title ) { + $request = wp_get_user_request_data( $request_id ); + + if ( ! $request || 'remove_personal_data' !== $request->action_name ) { wp_send_json_error( __( 'Error: Invalid request ID.' ) ); } - $email_address = get_post_meta( $request_id, '_wp_user_request_user_email', true ); + $email_address = $request->email; if ( ! is_email( $email_address ) ) { wp_send_json_error( __( 'Error: Invalid email address in request.' ) ); diff --git a/wp-admin/includes/user.php b/wp-admin/includes/user.php index 6c7c5011c8..a0184906fb 100644 --- a/wp-admin/includes/user.php +++ b/wp-admin/includes/user.php @@ -585,10 +585,12 @@ function _wp_privacy_completed_request( $request_id ) { } update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() ); + $request = wp_update_post( array( - 'ID' => $request_data['request_id'], + 'ID' => $request_id, 'post_status' => 'request-confirmed', ) ); + return $request; } @@ -730,6 +732,38 @@ function _wp_personal_data_handle_actions() { } } +/** + * Cleans up failed and expired requests before displaying the list table. + * + * @since 4.9.6 + * @access private + */ +function _wp_personal_data_cleanup_requests() { + $expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS ); + $requests_query = new WP_Query( array( + 'post_type' => 'user_request', + 'posts_per_page' => -1, + 'post_status' => 'request-pending', + 'fields' => 'ids', + 'date_query' => array( + array( + 'column' => 'post_modified_gmt', + 'before' => $expires . ' seconds ago', + ), + ), + ) ); + + $request_ids = $requests_query->posts; + + foreach ( $request_ids as $request_id ) { + wp_update_post( array( + 'ID' => $request_id, + 'post_status' => 'request-failed', + 'post_password' => '', + ) ); + } +} + /** * Personal data export. * @@ -742,6 +776,7 @@ function _wp_personal_data_export_page() { } _wp_personal_data_handle_actions(); + _wp_personal_data_cleanup_requests(); $requests_table = new WP_Privacy_Data_Export_Requests_Table( array( 'plural' => 'privacy_requests', @@ -803,6 +838,7 @@ function _wp_personal_data_removal_page() { } _wp_personal_data_handle_actions(); + _wp_personal_data_cleanup_requests(); // "Borrow" xfn.js for now so we don't have to create new files. wp_enqueue_script( 'xfn' ); @@ -841,7 +877,7 @@ function _wp_personal_data_removal_page() {
search_box( __( 'Search Requests' ), 'requests' ); ?> - + @@ -907,11 +943,11 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { */ public function get_columns() { $columns = array( - 'cb' => '', - 'email' => __( 'Requester' ), - 'status' => __( 'Status' ), - 'requested_timestamp' => __( 'Requested' ), - 'next_steps' => __( 'Next Steps' ), + 'cb' => '', + 'email' => __( 'Requester' ), + 'status' => __( 'Status' ), + 'created_timestamp' => __( 'Requested' ), + 'next_steps' => __( 'Next Steps' ), ); return $columns; } @@ -959,7 +995,7 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { SELECT post_status, COUNT( * ) AS num_posts FROM {$wpdb->posts} WHERE post_type = %s - AND post_title = %s + AND post_name = %s GROUP BY post_status"; $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $this->post_type, $this->request_type ), ARRAY_A ); @@ -1047,7 +1083,7 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { case 'resend': foreach ( $request_ids as $request_id ) { $resend = _wp_privacy_resend_request( $request_id ); - + if ( $resend && ! is_wp_error( $resend ) ) { $count++; } @@ -1083,10 +1119,11 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { $posts_per_page = 20; $args = array( 'post_type' => $this->post_type, - 'title' => $this->request_type, + 'post_name__in' => array( $this->request_type ), 'posts_per_page' => $posts_per_page, 'offset' => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page: 0, 'post_status' => 'any', + 's' => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '', ); if ( ! empty( $_REQUEST['filter-status'] ) ) { @@ -1094,18 +1131,6 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { $args['post_status'] = $filter_status; } - if ( ! empty( $_REQUEST['s'] ) ) { - $args['meta_query'] = array( - $name_query, - 'relation' => 'AND', - array( - 'key' => '_wp_user_request_user_email', - 'value' => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ): '', - 'compare' => 'LIKE', - ), - ); - } - $requests_query = new WP_Query( $args ); $requests = $requests_query->posts; @@ -1113,6 +1138,8 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { $this->items[] = wp_get_user_request_data( $request->ID ); } + $this->items = array_filter( $this->items ); + $this->set_pagination_args( array( 'total_items' => $requests_query->found_posts, @@ -1126,11 +1153,11 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. * @return string */ public function column_cb( $item ) { - return sprintf( '', esc_attr( $item['request_id'] ) ); + return sprintf( '', esc_attr( $item->ID ) ); } /** @@ -1138,11 +1165,11 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. * @return string */ public function column_status( $item ) { - $status = get_post_status( $item['request_id'] ); + $status = get_post_status( $item->ID ); $status_object = get_post_status_object( $status ); if ( ! $status_object || empty( $status_object->label ) ) { @@ -1153,10 +1180,10 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { switch ( $status ) { case 'request-confirmed': - $timestamp = $item['confirmed_timestamp']; + $timestamp = $item->confirmed_timestamp; break; case 'request-completed': - $timestamp = $item['completed_timestamp']; + $timestamp = $item->completed_timestamp; break; } @@ -1197,14 +1224,14 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { * * @since 4.9.6 * - * @param array $item Item being shown. - * @param string $column_name Name of column being shown. + * @param WP_User_Request $item Item being shown. + * @param string $column_name Name of column being shown. * @return string */ public function column_default( $item, $column_name ) { - $cell_value = $item[ $column_name ]; + $cell_value = $item->$column_name; - if ( in_array( $column_name, array( 'requested_timestamp' ), true ) ) { + if ( in_array( $column_name, array( 'created_timestamp' ), true ) ) { return $this->get_timestamp_as_date( $cell_value ); } @@ -1216,11 +1243,11 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. * @return string */ public function column_email( $item ) { - return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( array() ) ); + return sprintf( '%1$s %2$s', $item->email, $this->row_actions( array() ) ); } /** @@ -1228,7 +1255,7 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. */ public function column_next_steps( $item ) {} @@ -1237,10 +1264,10 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table { * * @since 4.9.6 * - * @param object $item The current item + * @param WP_User_Request $item The current item */ public function single_row( $item ) { - $status = get_post_status( $item['request_id'] ); + $status = $item->status; echo ''; $this->single_row_columns( $item ); @@ -1284,13 +1311,13 @@ class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Requests_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. * @return string */ public function column_email( $item ) { $exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() ); $exporters_count = count( $exporters ); - $request_id = $item['request_id']; + $request_id = $item->ID; $nonce = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id ); $download_data_markup = '
$download_data_markup, ); - return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( $row_actions ) ); + return sprintf( '%1$s %2$s', $item->email, $this->row_actions( $row_actions ) ); } /** @@ -1315,10 +1342,10 @@ class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Requests_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. */ public function column_next_steps( $item ) { - $status = get_post_status( $item['request_id'] ); + $status = $item->status; switch ( $status ) { case 'request-pending': @@ -1328,12 +1355,12 @@ class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Requests_Table { // TODO Complete in follow on patch. break; case 'request-failed': - submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item['request_id'] . ']', false ); + submit_button( __( 'Retry' ), 'secondary', 'privacy_action_email_retry[' . $item->ID . ']', false ); break; case 'request-completed': echo '' . esc_html__( 'Remove request' ) . ''; break; } @@ -1369,18 +1396,18 @@ class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Requests_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. * @return string */ public function column_email( $item ) { $row_actions = array(); - // Allow the administrator to "force remove" the personal data even if confirmation has not yet been received - $status = get_post_status( $item['request_id'] ); + // Allow the administrator to "force remove" the personal data even if confirmation has not yet been received. + $status = $item->status; if ( 'request-confirmed' !== $status ) { $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() ); $erasers_count = count( $erasers ); - $request_id = $item['request_id']; + $request_id = $item->ID; $nonce = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id ); $remove_data_markup = '
row_actions( $row_actions ) ); + return sprintf( '%1$s %2$s', $item->email, $this->row_actions( $row_actions ) ); } /** @@ -1406,10 +1433,10 @@ class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Requests_Table { * * @since 4.9.6 * - * @param array $item Item being shown. + * @param WP_User_Request $item Item being shown. */ public function column_next_steps( $item ) { - $status = get_post_status( $item['request_id'] ); + $status = $item->status; switch ( $status ) { case 'request-pending': @@ -1418,7 +1445,7 @@ class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Requests_Table { case 'request-confirmed': $erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() ); $erasers_count = count( $erasers ); - $request_id = $item['request_id']; + $request_id = $item->ID; $nonce = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id ); echo '
ID . ']', false ); break; case 'request-completed': echo '' . esc_html__( 'Remove request' ) . ''; break; } diff --git a/wp-includes/post.php b/wp-includes/post.php index 48d4267d07..d69f5a839d 100644 --- a/wp-includes/post.php +++ b/wp-includes/post.php @@ -3800,7 +3800,7 @@ function check_and_publish_future_post( $post_id ) { * @return string Unique slug for the post, based on $post_name (with a -1, -2, etc. suffix) */ function wp_unique_post_slug( $slug, $post_ID, $post_status, $post_type, $post_parent ) { - if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ) ) || ( 'inherit' == $post_status && 'revision' == $post_type ) ) + if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ) ) || ( 'inherit' == $post_status && 'revision' == $post_type ) || 'user_request' === $post_type ) return $slug; global $wpdb, $wp_rewrite; diff --git a/wp-includes/user.php b/wp-includes/user.php index f3c76a9070..968f56386a 100644 --- a/wp-includes/user.php +++ b/wp-includes/user.php @@ -2762,13 +2762,13 @@ function _wp_privacy_account_request_confirmed( $request_id ) { return; } - if ( ! in_array( $request_data['status'], array( 'request-pending', 'request-failed' ), true ) ) { + if ( ! in_array( $request_data->status, array( 'request-pending', 'request-failed' ), true ) ) { return; } update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() ); wp_update_post( array( - 'ID' => $request_data['request_id'], + 'ID' => $request_id, 'post_status' => 'request-confirmed', ) ); } @@ -2784,7 +2784,7 @@ function _wp_privacy_account_request_confirmed( $request_id ) { function _wp_privacy_account_request_confirmed_message( $message, $request_id ) { $request = wp_get_user_request_data( $request_id ); - if ( $request && in_array( $request['action'], _wp_privacy_action_request_types(), true ) ) { + if ( $request && in_array( $request->action_name, _wp_privacy_action_request_types(), true ) ) { $message = '

' . __( 'Action has been confirmed.' ) . '

'; $message .= __( 'The site administrator has been notified and will fulfill your request as soon as possible.' ); } @@ -2822,16 +2822,11 @@ function wp_create_user_request( $email_address = '', $action_name = '', $reques // Check for duplicates. $requests_query = new WP_Query( array( - 'post_type' => 'user_request', - 'title' => $action_name, - 'post_status' => 'any', - 'fields' => 'ids', - 'meta_query' => array( - array( - 'key' => '_wp_user_request_user_email', - 'value' => $email_address, - ), - ), + 'post_type' => 'user_request', + 'post_name__in' => array( $action_name ), // Action name stored in post_name column. + 'title' => $email_address, // Email address stored in post_title column. + 'post_status' => 'any', + 'fields' => 'ids', ) ); if ( $requests_query->found_posts ) { @@ -2840,7 +2835,8 @@ function wp_create_user_request( $email_address = '', $action_name = '', $reques $request_id = wp_insert_post( array( 'post_author' => $user_id, - 'post_title' => $action_name, + 'post_name' => $action_name, + 'post_title' => $email_address, 'post_content' => wp_json_encode( $request_data ), 'post_status' => 'request-pending', 'post_type' => 'user_request', @@ -2848,13 +2844,6 @@ function wp_create_user_request( $email_address = '', $action_name = '', $reques 'post_date_gmt' => current_time( 'mysql', true ), ), true ); - if ( is_wp_error( $request_id ) ) { - return $request_id; - } - - update_post_meta( $request_id, '_wp_user_request_user_email', $email_address ); - update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', false ); - return $request_id; } @@ -2883,9 +2872,11 @@ function wp_user_request_action_description( $action_name ) { /** * Filters the user action description. * + * @since 4.9.6 + * * @param string $description The default description. * @param string $action_name The name of the request. - */ + */ return apply_filters( 'user_request_action_description', $description, $action_name ); } @@ -2901,25 +2892,15 @@ function wp_user_request_action_description( $action_name ) { */ function wp_send_user_request( $request_id ) { $request_id = absint( $request_id ); - $request = get_post( $request_id ); + $request = wp_get_user_request_data( $request_id ); - if ( ! $request || 'user_request' !== $request->post_type ) { + if ( ! $request ) { return new WP_Error( 'user_request_error', __( 'Invalid request.' ) ); } - if ( 'request-pending' !== $request->post_status ) { - wp_update_post( array( - 'ID' => $request_id, - 'post_status' => 'request-pending', - 'post_date' => current_time( 'mysql', false ), - 'post_date_gmt' => current_time( 'mysql', true ), - ) ); - } - $email_data = array( - 'action_name' => $request->post_title, - 'email' => get_post_meta( $request->ID, '_wp_user_request_user_email', true ), - 'description' => wp_user_request_action_description( $request->post_title ), + 'email' => $request->email, + 'description' => wp_user_request_action_description( $request->action_name ), 'confirm_url' => add_query_arg( array( 'action' => 'confirmaction', 'request_id' => $request_id, @@ -2967,12 +2948,12 @@ All at ###SITENAME### * @param array $email_data { * Data relating to the account action email. * - * @type string $action_name Name of the action being performed. - * @type string $email The email address this is being sent to. - * @type string $description Description of the action being performed so the user knows what the email is for. - * @type string $confirm_url The link to click on to confirm the account action. - * @type string $sitename The site name sending the mail. - * @type string $siteurl The site URL sending the mail. + * @type WP_User_Request $request User request object. + * @type string $email The email address this is being sent to. + * @type string $description Description of the action being performed so the user knows what the email is for. + * @type string $confirm_url The link to click on to confirm the account action. + * @type string $sitename The site name sending the mail. + * @type string $siteurl The site URL sending the mail. * } */ $content = apply_filters( 'user_request_action_email_content', $email_text, $email_data ); @@ -2988,7 +2969,7 @@ All at ###SITENAME### } /** - * Returns a confirmation key for a user action and stores the hashed version. + * Returns a confirmation key for a user action and stores the hashed version for future comparison. * * @since 4.9.6 * @@ -3007,8 +2988,13 @@ function wp_generate_user_request_key( $request_id ) { $wp_hasher = new PasswordHash( 8, true ); } - update_post_meta( $request_id, '_wp_user_request_confirm_key', $wp_hasher->HashPassword( $key ) ); - update_post_meta( $request_id, '_wp_user_request_confirm_key_timestamp', time() ); + wp_update_post( array( + 'ID' => $request_id, + 'post_status' => 'request-pending', + 'post_password' => $wp_hasher->HashPassword( $key ), + 'post_modified' => current_time( 'mysql', false ), + 'post_modified_gmt' => current_time( 'mysql', true ), + ) ); return $key; } @@ -3032,7 +3018,7 @@ function wp_validate_user_request_key( $request_id, $key ) { return new WP_Error( 'user_request_error', __( 'Invalid request.' ) ); } - if ( ! in_array( $request['status'], array( 'request-pending', 'request-failed' ), true ) ) { + if ( ! in_array( $request->status, array( 'request-pending', 'request-failed' ), true ) ) { return __( 'This link has expired.' ); } @@ -3045,8 +3031,8 @@ function wp_validate_user_request_key( $request_id, $key ) { $wp_hasher = new PasswordHash( 8, true ); } - $key_request_time = $request['confirm_key_timestamp']; - $saved_key = $request['confirm_key']; + $key_request_time = $request->modified_timestamp; + $saved_key = $request->confirm_key; if ( ! $saved_key ) { return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); @@ -3087,23 +3073,119 @@ function wp_validate_user_request_key( $request_id, $key ) { */ function wp_get_user_request_data( $request_id ) { $request_id = absint( $request_id ); - $request = get_post( $request_id ); + $post = get_post( $request_id ); - if ( ! $request || 'user_request' !== $request->post_type ) { + if ( ! $post || 'user_request' !== $post->post_type ) { return false; } - return array( - 'request_id' => $request->ID, - 'user_id' => $request->post_author, - 'email' => get_post_meta( $request->ID, '_wp_user_request_user_email', true ), - 'action' => $request->post_title, - 'requested_timestamp' => strtotime( $request->post_date_gmt ), - 'confirmed_timestamp' => get_post_meta( $request->ID, '_wp_user_request_confirmed_timestamp', true ), - 'completed_timestamp' => get_post_meta( $request->ID, '_wp_user_request_completed_timestamp', true ), - 'request_data' => json_decode( $request->post_content, true ), - 'status' => $request->post_status, - 'confirm_key' => get_post_meta( $request_id, '_wp_user_request_confirm_key', true ), - 'confirm_key_timestamp' => get_post_meta( $request_id, '_wp_user_request_confirm_key_timestamp', true ), - ); + return new WP_User_Request( $post ); +} + +/** + * WP_User_Request class. + * + * Represents user request data loaded from a WP_Post object. + * + * @since 4.9.6 + */ +final class WP_User_Request { + /** + * Request ID. + * + * @var int + */ + public $ID = 0; + + /** + * User ID. + * + * @var int + */ + + public $user_id = 0; + + /** + * User email. + * + * @var int + */ + public $email = ''; + + /** + * Action name. + * + * @var string + */ + public $action_name = ''; + + /** + * Current status. + * + * @var string + */ + public $status = ''; + + /** + * Timestamp this request was created. + * + * @var int|null + */ + public $created_timestamp = null; + + /** + * Timestamp this request was last modified. + * + * @var int|null + */ + public $modified_timestamp = null; + + /** + * Timestamp this request was confirmed. + * + * @var int + */ + public $confirmed_timestamp = null; + + /** + * Timestamp this request was completed. + * + * @var int + */ + public $completed_timestamp = null; + + /** + * Misc data assigned to this request. + * + * @var array + */ + public $request_data = array(); + + /** + * Key used to confirm this request. + * + * @var string + */ + public $confirm_key = ''; + + /** + * Constructor. + * + * @since 4.9.6 + * + * @param WP_Post|object $post Post object. + */ + public function __construct( $post ) { + $this->ID = $post->ID; + $this->user_id = $post->post_author; + $this->email = $post->post_title; + $this->action_name = $post->post_name; + $this->status = $post->post_status; + $this->created_timestamp = strtotime( $post->post_date_gmt ); + $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->request_data = json_decode( $post->post_content, true ); + $this->confirm_key = $post->post_password; + } } diff --git a/wp-includes/version.php b/wp-includes/version.php index c3ab177b27..939e908d3a 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.9.6-alpha-43083'; +$wp_version = '4.9.6-alpha-43084'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.