From 33979450aca7de46e784eb2d422b8772b24ebda3 Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Tue, 10 Apr 2018 18:01:20 +0000 Subject: [PATCH] Privacy: add new wp-admin screens for exporting and removing of personal data. Props @melchoyce, @mikejolley, @allendav, @xkon. See #43481. git-svn-id: https://develop.svn.wordpress.org/trunk@42967 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/forms.css | 100 +++ src/wp-admin/includes/admin-filters.php | 4 + src/wp-admin/includes/user.php | 851 ++++++++++++++++++++++++ src/wp-includes/post.php | 84 +++ src/wp-includes/user.php | 83 ++- 5 files changed, 1114 insertions(+), 8 deletions(-) diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 0f93e6bd81..97e8180ee0 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -1403,3 +1403,103 @@ table.form-table td .updated p { margin-right: 0.5em; } } + + +/* Privacy */ + +.privacy_requests .column-email { + width: 40%; +} +.privacy_requests .column-type { + text-align: center; +} +.privacy_requests thead td:first-child, +.privacy_requests tfoot td:first-child { + border-left: 4px solid #fff; +} +.privacy_requests tbody th { + border-left: 4px solid #fff; + background: #fff; + box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1); +} +.privacy_requests tbody td { + background: #fff; + box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1); +} +.privacy_requests .status-request-confirmed th, +.privacy_requests .status-request-confirmed td { + background-color: #f7fcfe; + border-left-color: #00a0d2; +} +.privacy_requests .status-request-failed th, +.privacy_requests .status-request-failed td { + background-color: #fef7f1; + border-left-color: #d64d21; +} +.status-label { + font-weight: bold; +} +.status-label.status-request-pending { + font-weight: normal; + font-style: italic; + color: #6c7781; +} +.status-label.status-request-failed { + color: #aa0000; + font-weight: bold; +} +.wp-privacy-request-form { + clear: both; +} +.wp-privacy-request-form-field { + margin: 1.5em 0; +} +.wp-privacy-request-form label { + font-weight: bold; + line-height: 1.5; + padding-bottom: .5em; + display: block; +} +.wp-privacy-request-form input { + line-height: 1.5; + margin: 0; +} +.email-personal-data::before { + display: inline-block; + font: normal 20px/1 dashicons; + margin: 3px 5px 0 -2px; + speak: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + vertical-align: top; +} +.email-personal-data--sending::before { + color: #f56e28; + content: "\f463"; + -webkit-animation: rotation 2s infinite linear; + animation: rotation 2s infinite linear; +} +.email-personal-data--sent::before { + color: #79ba49; + content: "\f147"; +} +@-webkit-keyframes rotation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes rotation { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 62e6c4cfc4..b9435862ce 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -45,6 +45,10 @@ add_action( 'admin_head', 'wp_color_scheme_settings' ); add_action( 'admin_head', 'wp_site_icon' ); add_action( 'admin_head', '_ipad_meta' ); +// Privacy tools +add_action( 'account_action_failed', '_wp_privacy_account_request_failed' ); +add_action( 'admin_menu', '_wp_privacy_hook_requests_page' ); + // Prerendering. if ( ! is_customize_preview() ) { add_filter( 'admin_print_styles', 'wp_resource_hints', 1 ); diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index 28ca05c341..13c0c9b8da 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -579,3 +579,854 @@ Please click the following link to activate your user account: ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), home_url(), wp_specialchars_decode( translate_user_role( $role['name'] ) ) ); } + +/** + * Get action description from the name. + * + * @since 5.0.0 + * @access private + * + * @return string + */ +function _wp_privacy_action_description( $request_type ) { + switch ( $request_type ) { + case 'user_export_request': + return __( 'Export Personal Data' ); + case 'user_remove_request': + return __( 'Remove Personal Data' ); + } +} + +/** + * Log a request and send to the user. + * + * @since 5.0.0 + * @access private + * + * @param string $email_address Email address sending the request to. + * @param string $action Action being requested. + * @param string $description Description of request. + * @return bool|WP_Error depending on success. + */ +function _wp_privacy_create_request( $email_address, $action, $description ) { + $user_id = 0; + $user = get_user_by( 'email', $email_address ); + + if ( $user ) { + $user_id = $user->ID; + } + + $privacy_request_id = wp_insert_post( array( + 'post_author' => $user_id, + 'post_status' => 'request-pending', + 'post_type' => $action, + 'post_date' => current_time( 'mysql', false ), + 'post_date_gmt' => current_time( 'mysql', true ), + ), true ); + + if ( is_wp_error( $privacy_request_id ) ) { + return $privacy_request_id; + } + + update_post_meta( $privacy_request_id, '_user_email', $email_address ); + update_post_meta( $privacy_request_id, '_action_name', $action ); + update_post_meta( $privacy_request_id, '_confirmed_timestamp', false ); + + return wp_send_account_verification_key( $email_address, $action, $description, array( + 'privacy_request_id' => $privacy_request_id, + ) ); +} + +/** + * Resend an existing request and return the result. + * + * @since 5.0.0 + * @access private + * + * @param int $privacy_request_id Request ID. + * @return bool|WP_Error + */ +function _wp_privacy_resend_request( $privacy_request_id ) { + $privacy_request_id = absint( $privacy_request_id ); + $privacy_request = get_post( $privacy_request_id ); + + if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) { + return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) ); + } + + $email_address = get_post_meta( $privacy_request_id, '_user_email', true ); + $action = get_post_meta( $privacy_request_id, '_action_name', true ); + $description = _wp_privacy_action_description( $action ); + $result = wp_send_account_verification_key( $email_address, $action, $description, array( + 'privacy_request_id' => $privacy_request_id, + ) ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation request.' ) ); + } + + wp_update_post( array( + 'ID' => $privacy_request_id, + 'post_status' => 'request-pending', + 'post_date' => current_time( 'mysql', false ), + 'post_date_gmt' => current_time( 'mysql', true ), + ) ); + + return true; +} + +/** + * Marks a request as completed by the admin and logs the datetime. + * + * @since 5.0.0 + * @access private + * + * @param int $privacy_request_id Request ID. + * @return bool|WP_Error + */ +function _wp_privacy_completed_request( $privacy_request_id ) { + $privacy_request_id = absint( $privacy_request_id ); + $privacy_request = get_post( $privacy_request_id ); + + if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) { + return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) ); + } + + wp_update_post( array( + 'ID' => $privacy_request_id, + 'post_status' => 'request-completed', + ) ); + + update_post_meta( $privacy_request_id, '_completed_timestamp', time() ); +} + +/** + * Handle list table actions. + * + * @since 5.0.0 + * @access private + */ +function _wp_personal_data_handle_actions() { + if ( isset( $_POST['export_personal_data_email_retry'] ) ) { // WPCS: input var ok. + check_admin_referer( 'bulk-privacy_requests' ); + + $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['export_personal_data_email_retry'] ) ) ) ); // WPCS: input var ok, sanitization ok. + $result = _wp_privacy_resend_request( $request_id ); + + if ( is_wp_error( $result ) ) { + add_settings_error( + 'export_personal_data_email_retry', + 'export_personal_data_email_retry', + $result->get_error_message(), + 'error' + ); + } else { + add_settings_error( + 'export_personal_data_email_retry', + 'export_personal_data_email_retry', + __( 'Confirmation request re-resent successfully.' ), + 'updated' + ); + } + + } elseif ( isset( $_POST['export_personal_data_email_send'] ) ) { // WPCS: input var ok. + check_admin_referer( 'bulk-privacy_requests' ); + + $request_id = absint( current( array_keys( (array) wp_unslash( $_POST['export_personal_data_email_send'] ) ) ) ); // WPCS: input var ok, sanitization ok. + $result = false; + + /** + * TODO: Email the data to the user here. + */ + + if ( is_wp_error( $result ) ) { + add_settings_error( + 'export_personal_data_email_send', + 'export_personal_data_email_send', + $result->get_error_message(), + 'error' + ); + } else { + _wp_privacy_completed_request( $request_id ); + add_settings_error( + 'export_personal_data_email_send', + 'export_personal_data_email_send', + __( 'Personal data was sent to the user successfully.' ), + 'updated' + ); + } + + } elseif ( isset( $_POST['action'] ) ) { + $action = isset( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : ''; // WPCS: input var ok, CSRF ok. + + switch ( $action ) { + case 'add_export_personal_data_request': + case 'add_remove_personal_data_request': + check_admin_referer( 'personal-data-request' ); + + if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_to_export'] ) ) { // WPCS: input var ok. + add_settings_error( + 'action_type', + 'action_type', + __( 'Invalid action.' ), + 'error' + ); + } + $action_type = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) ); // WPCS: input var ok. + $username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_to_export'] ) ); // WPCS: input var ok. + $email_address = ''; + + if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) { + add_settings_error( + 'action_type', + 'action_type', + __( 'Invalid action.' ), + 'error' + ); + } + + if ( ! is_email( $username_or_email_address ) ) { + $user = get_user_by( 'login', $username_or_email_address ); + if ( ! $user instanceof WP_User ) { + add_settings_error( + 'username_or_email_to_export', + 'username_or_email_to_export', + __( 'Unable to add export request. A valid email address or username must be supplied.' ), + 'error' + ); + } else { + $email_address = $user->user_email; + } + } else { + $email_address = $username_or_email_address; + } + + if ( ! empty( $email_address ) ) { + $result = _wp_privacy_create_request( $email_address, $action_type, _wp_privacy_action_description( $action_type ) ); + + if ( is_wp_error( $result ) ) { + add_settings_error( + 'username_or_email_to_export', + 'username_or_email_to_export', + $result->get_error_message(), + 'error' + ); + } elseif ( ! $result ) { + add_settings_error( + 'username_or_email_to_export', + 'username_or_email_to_export', + __( 'Unable to initiate confirmation request.' ), + 'error' + ); + } else { + add_settings_error( + 'username_or_email_to_export', + 'username_or_email_to_export', + __( 'Confirmation request initiated successfully.' ), + 'updated' + ); + } + } + break; + } + } +} + +/** + * Personal data export. + * + * @since 5.0.0 + * @access private + */ +function _wp_personal_data_export_page() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'Sorry, you are not allowed to manage privacy on this site.' ) ); + } + + _wp_personal_data_handle_actions(); + + $requests_table = new WP_Privacy_Data_Export_Requests_Table( array( + 'plural' => 'privacy_requests', + 'singular' => 'privacy_request', + ) ); + $requests_table->process_bulk_action(); + $requests_table->prepare_items(); + ?> +
+

+
+ + + +
+

+

+ +
+ + + +
+ + + +
+
+ + views(); ?> + +
+ search_box( __( 'Search Requests' ), 'requests' ); ?> + + + + +
+ +
+ display(); + $requests_table->embed_scripts(); + ?> +
+
+ 'privacy_requests', + 'singular' => 'privacy_request', + ) ); + $requests_table->process_bulk_action(); + $requests_table->prepare_items(); + ?> +
+

+
+ + + +
+

+

+ +
+ + + +
+ + + +
+
+ + views(); ?> + +
+ search_box( __( 'Search Requests' ), 'requests' ); ?> + + + + +
+ +
+ display(); + $requests_table->embed_scripts(); + ?> +
+
+ '', + 'email' => __( 'Requester' ), + 'status' => __( 'Status' ), + 'requested' => __( 'Requested' ), + 'next_steps' => __( 'Next Steps' ), + ); + return $columns; + } + + /** + * Get a list of sortable columns. + * + * @since 5.0.0 + * + * @return array + */ + protected function get_sortable_columns() { + return array(); + } + + /** + * Default primary column. + * + * @since 5.0.0 + * + * @return string + */ + protected function get_default_primary_column_name() { + return 'email'; + } + + /** + * Get an associative array ( id => link ) with the list + * of views available on this table. + * + * @since 5.0.0 + * + * @return array + */ + protected function get_views() { + $current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ): ''; + $statuses = _wp_privacy_statuses(); + $views = array(); + $admin_url = admin_url( 'tools.php?page=' . $this->request_type ); + $counts = wp_count_posts( $this->request_type ); + + $current_link_attributes = empty( $current_status ) ? ' class="current" aria-current="page"' : ''; + $views['all'] = '" . esc_html__( 'All' ) . ' (' . absint( array_sum( (array) $counts ) ) . ')'; + + foreach ( $statuses as $status => $label ) { + $current_link_attributes = $status === $current_status ? ' class="current" aria-current="page"' : ''; + $views[ $status ] = '" . esc_html( $label ) . ' (' . absint( $counts->$status ) . ')'; + } + + return $views; + } + + /** + * Get bulk actions. + * + * @since 5.0.0 + * + * @return array + */ + protected function get_bulk_actions() { + return array( + 'delete' => __( 'Remove' ), + 'resend' => __( 'Resend email' ), + ); + } + + /** + * Process bulk actions. + * + * @since 5.0.0 + */ + public function process_bulk_action() { + $action = $this->current_action(); + $request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array(); // WPCS: input var ok, CSRF ok. + + if ( $request_ids ) { + check_admin_referer( 'bulk-privacy_requests' ); + } + + switch ( $action ) { + case 'delete': + $count = 0; + + foreach ( $request_ids as $request_id ) { + if ( wp_delete_post( $request_id, true ) ) { + $count ++; + } + } + + add_settings_error( + 'bulk_action', + 'bulk_action', + sprintf( _n( 'Deleted %d request', 'Deleted %d requests', $count ), $count ), + 'updated' + ); + break; + case 'resend': + $count = 0; + + foreach ( $request_ids as $request_id ) { + if ( _wp_privacy_resend_request( $request_id ) ) { + $count ++; + } + } + + add_settings_error( + 'bulk_action', + 'bulk_action', + sprintf( _n( 'Re-sent %d request', 'Re-sent %d requests', $count ), $count ), + 'updated' + ); + break; + } + } + + /** + * Prepare items to output. + * + * @since 5.0.0 + */ + public function prepare_items() { + global $wpdb; + + $primary = $this->get_primary_column_name(); + $this->_column_headers = array( + $this->get_columns(), + array(), + $this->get_sortable_columns(), + $primary, + ); + + $this->items = array(); + $posts_per_page = 20; + $args = array( + 'post_type' => $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', + ); + + if ( ! empty( $_REQUEST['filter-status'] ) ) { + $filter_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : ''; + $args['post_status'] = $filter_status; + } + + if ( ! empty( $_REQUEST['s'] ) ) { + $args['meta_query'] = array( + $name_query, + 'relation' => 'AND', + array( + 'key' => '_user_email', + 'value' => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ): '', + 'compare' => 'LIKE' + ), + ); + } + + $privacy_requests_query = new WP_Query( $args ); + $privacy_requests = $privacy_requests_query->posts; + + foreach ( $privacy_requests as $privacy_request ) { + $this->items[] = array( + 'request_id' => $privacy_request->ID, + 'user_id' => $privacy_request->post_author, + 'email' => get_post_meta( $privacy_request->ID, '_user_email', true ), + 'action' => get_post_meta( $privacy_request->ID, '_action_name', true ), + 'requested' => strtotime( $privacy_request->post_date_gmt ), + 'confirmed' => get_post_meta( $privacy_request->ID, '_confirmed_timestamp', true ), + 'completed' => get_post_meta( $privacy_request->ID, '_completed_timestamp', true ), + ); + } + + $this->set_pagination_args( + array( + 'total_items' => $privacy_requests_query->found_posts, + 'per_page' => $posts_per_page, + ) + ); + } + + /** + * Checkbox column. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + * @return string + */ + public function column_cb( $item ) { + return sprintf( '', esc_attr( $item['request_id'] ) ); + } + + /** + * Status column. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + * @return string + */ + public function column_status( $item ) { + $status = get_post_status( $item['request_id'] ); + $status_object = get_post_status_object( $status ); + + if ( ! $status_object || empty( $status_object->label ) ) { + return '-'; + } + + $timestamp = false; + + switch ( $status ) { + case 'request-confirmed': + $timestamp = $item['confirmed']; + break; + case 'request-completed': + $timestamp = $item['completed']; + break; + } + + echo ''; + echo esc_html( $status_object->label ); + + if ( $timestamp ) { + echo ' (' . $this->get_timestamp_as_date( $timestamp ) . ')'; + } + + echo ''; + } + + /** + * Convert timestamp for display. + * + * @since 5.0.0 + * + * @param int $timestamp Event timestamp. + * @return string + */ + protected function get_timestamp_as_date( $timestamp ) { + if ( empty( $timestamp ) ) { + return ''; + } + + $time_diff = current_time( 'timestamp', true ) - $timestamp; + + if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) { + return sprintf( __( '%s ago' ), human_time_diff( $timestamp ) ); + } + + return date_i18n( get_option( 'date_format' ), $timestamp ); + } + + /** + * Default column handler. + * + * @since 5.0.0 + * + * @param array $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 ]; + + if ( in_array( $column_name, array( 'requested' ), true ) ) { + return $this->get_timestamp_as_date( $cell_value ); + } + + return $cell_value; + } + + /** + * Actions column. Overriden by children. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + * @return string + */ + public function column_email( $item ) { + return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( array() ) ); + } + + /** + * Next steps column. Overriden by children. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + */ + public function column_next_steps( $item ) {} + + /** + * Generates content for a single row of the table + * + * @since 5.0.0 + * + * @param object $item The current item + */ + public function single_row( $item ) { + $status = get_post_status( $item['request_id'] ); + + echo ''; + $this->single_row_columns( $item ); + echo ''; + } + + /** + * Embed scripts used to perform actions. Overriden by children. + * + * @since 5.0.0 + */ + public function embed_scripts() {} +} + +/** + * WP_Privacy_Data_Export_Requests_Table class. + * + * @since 5.0.0 + */ +class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Requests_Table { + /** + * Action name for the requests this table will work with. Classes + * which inherit from WP_Privacy_Requests_Table should define this. + * e.g. 'user_export_request' + * + * @since 5.0.0 + * + * @var string $request_type Name of action. + */ + protected $request_type = 'user_export_request'; + + /** + * Actions column. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + * @return string + */ + public function column_email( $item ) { + $row_actions = array( + 'download_data' => __( 'Download Personal Data' ), + ); + + return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( $row_actions ) ); + } + + /** + * Next steps column. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + */ + public function column_next_steps( $item ) { + $status = get_post_status( $item['request_id'] ); + + switch ( $status ) { + case 'request-pending': + esc_html_e( 'Waiting for confirmation' ); + break; + case 'request-confirmed': + // TODO Complete in follow on patch. + break; + case 'request-failed': + submit_button( __( 'Retry' ), 'secondary', 'export_personal_data_email_retry[' . $item['request_id'] . ']', false ); + break; + case 'request-completed': + echo '' . esc_html__( 'Remove request' ) . ''; + break; + } + } +} + +/** + * WP_Privacy_Data_Removal_Requests_Table class. + * + * @since 5.0.0 + */ +class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Requests_Table { + /** + * Action name for the requests this table will work with. Classes + * which inherit from WP_Privacy_Requests_Table should define this. + * e.g. 'user_remove_request' + * + * @since 5.0.0 + * + * @var string $request_type Name of action. + */ + protected $request_type = 'user_remove_request'; + + /** + * Actions column. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + * @return string + */ + public function column_email( $item ) { + $row_actions = array( + // TODO Complete in follow on patch. + 'remove_data' => __( 'Remove Personal Data' ), + ); + + // If we have a user ID, include a delete user action. + if ( ! empty( $item['user_id'] ) ) { + // TODO Complete in follow on patch. + $row_actions['delete_user'] = __( 'Delete User' ); + } + + return sprintf( '%1$s %2$s', $item['email'], $this->row_actions( $row_actions ) ); + } + + /** + * Next steps column. + * + * @since 5.0.0 + * + * @param array $item Item being shown. + */ + public function column_next_steps( $item ) { + } + +} diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index e46f949891..cb5025f607 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -226,6 +226,38 @@ function create_initial_post_types() { ) ); + register_post_type( + 'user_export_request', array( + 'labels' => array( + 'name' => __( 'Export Personal Data Requests' ), + 'singular_name' => __( 'Export Personal Data Request' ), + ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'can_export' => false, + 'delete_with_user' => false, + ) + ); + + register_post_type( + 'user_remove_request', array( + 'labels' => array( + 'name' => __( 'Remove Personal Data Requests' ), + 'singular_name' => __( 'Remove Personal Data Request' ), + ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'can_export' => false, + 'delete_with_user' => false, + ) + ); + register_post_status( 'publish', array( 'label' => _x( 'Published', 'post status' ), @@ -297,6 +329,42 @@ function create_initial_post_types() { 'exclude_from_search' => false, ) ); + + register_post_status( + 'request-pending', array( + 'label' => _x( 'Pending', 'request status' ), + 'internal' => true, + '_builtin' => true, /* internal use only. */ + 'exclude_from_search' => false, + ) + ); + + register_post_status( + 'request-confirmed', array( + 'label' => _x( 'Confirmed', 'request status' ), + 'internal' => true, + '_builtin' => true, /* internal use only. */ + 'exclude_from_search' => false, + ) + ); + + register_post_status( + 'request-failed', array( + 'label' => _x( 'Failed', 'request status' ), + 'internal' => true, + '_builtin' => true, /* internal use only. */ + 'exclude_from_search' => false, + ) + ); + + register_post_status( + 'request-completed', array( + 'label' => _x( 'Completed', 'request status' ), + 'internal' => true, + '_builtin' => true, /* internal use only. */ + 'exclude_from_search' => false, + ) + ); } /** @@ -782,6 +850,22 @@ function get_page_statuses() { return $status; } +/** + * Return statuses for privacy requests. + * + * @since 5.0.0 + * + * @return array + */ +function _wp_privacy_statuses() { + return array( + 'request-pending' => __( 'Pending' ), // Pending confirmation from user. + 'request-confirmed' => __( 'Confirmed' ), // User has confirmed the action. + 'request-failed' => __( 'Failed' ), // User failed to confirm the action. + 'request-completed' => __( 'Completed' ), // Admin has handled the request. + ); +} + /** * Register a post status. Do not use before init. * diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 2c38690252..270363c8c8 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -2810,12 +2810,79 @@ function new_user_email_admin_notice() { } } +/** + * Get all user privacy request types. + * + * @since 5.0.0 + * @access private + * + * @return array + */ +function _wp_privacy_action_request_types() { + return array( + 'user_export_request', + 'user_remove_request', + ); +} + +/** + * Update log when privacy request is confirmed. + * + * @since 5.0.0 + * @access private + * + * @param array $result Result of the request from the user. + */ +function _wp_privacy_account_request_confirmed( $result ) { + if ( isset( $result['action'], $result['request_data'], $result['request_data']['privacy_request_id'] ) && in_array( $result['action'], _wp_privacy_action_request_types(), true ) ) { + $privacy_request_id = absint( $result['request_data']['privacy_request_id'] ); + $privacy_request = get_post( $privacy_request_id ); + + if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) { + return; + } + + update_post_meta( $privacy_request_id, '_confirmed_timestamp', time() ); + wp_update_post( array( + 'ID' => $privacy_request_id, + 'post_status' => 'request-confirmed', + ) ); + } +} +add_action( 'account_action_confirmed', '_wp_privacy_account_request_confirmed' ); + +/** + * Update log when privacy request fails. + * + * @since 5.0.0 + * @access private + * + * @param array $result Result of the request from the user. + */ +function _wp_privacy_account_request_failed( $result ) { + if ( isset( $result['action'], $result['request_data'], $result['request_data']['privacy_request_id'] ) && + in_array( $result['action'], _wp_privacy_action_request_types(), true ) ) { + + $privacy_request_id = absint( $result['request_data']['privacy_request_id'] ); + $privacy_request = get_post( $privacy_request_id ); + + if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) { + return; + } + + wp_update_post( array( + 'ID' => $privacy_request_id, + 'post_status' => 'request-failed', + ) ); + } +} + /** * Send a confirmation request email to confirm an action. * * @since 5.0.0 * - * @param string $email User email address. This can be the address of a registered or non-registered user. Defaults to logged in user email address. + * @param string $email User email address. This can be the address of a registered or non-registered user. Defaults to logged in user email address. * @param string $action_name Name of the action that is being confirmed. Defaults to 'confirm_email'. * @param string $action_description User facing description of the action they will be confirming. Defaults to "confirm your email address". * @param array $request_data Misc data you want to send with the verification request and pass to the actions once the request is confirmed. @@ -2917,7 +2984,7 @@ All at ###SITENAME### * ###SITEURL### The URL to the site. * * @since 5.0.0 - * + * * @param string $email_text Text in the email. * @param array $email_data { * Data relating to the account action email. @@ -3039,14 +3106,14 @@ function wp_check_account_verification_key( $key, $uid, $action_name ) { $raw_data = get_user_meta( $user->ID, '_verify_action_' . $action_name, true ); $email = $user->user_email; - if ( false !== strpos( $confirm_action_data, ':' ) ) { - list( $key_request_time, $saved_key ) = explode( ':', $confirm_action_data, 2 ); + if ( false !== strpos( $raw_data, ':' ) ) { + list( $key_request_time, $saved_key ) = explode( ':', $raw_data, 2 ); } } else { $raw_data = get_site_option( '_verify_action_' . $action_name . '_' . $uid, '' ); - if ( false !== strpos( $confirm_action_data, ':' ) ) { - list( $key_request_time, $saved_key, $email ) = explode( ':', $confirm_action_data, 3 ); + if ( false !== strpos( $raw_data, ':' ) ) { + list( $key_request_time, $saved_key, $email ) = explode( ':', $raw_data, 3 ); } } @@ -3068,7 +3135,7 @@ function wp_check_account_verification_key( $key, $uid, $action_name ) { * Filters the expiration time of confirm keys. * * @since 5.0.0 - * + * * @param int $expiration The expiration time in seconds. */ $expiration_duration = apply_filters( 'account_verification_expiration', DAY_IN_SECONDS ); @@ -3096,4 +3163,4 @@ function wp_check_account_verification_key( $key, $uid, $action_name ) { } return $return; -} \ No newline at end of file +}