Make WordPress Core

Changeset 43083


Ignore:
Timestamp:
05/02/2018 01:00:46 AM (8 years ago)
Author:
SergeyBiryukov
Message:

Privacy: update the method to confirm user requests by email. Use a single CPT to store the requests and to allow logging/audit trail.

Props mikejolley.
Merges [43008] to the 4.9 branch.
See #43443.

Location:
branches/4.9
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • branches/4.9

  • branches/4.9/src/wp-admin/includes/admin-filters.php

    r43071 r43083  
    4747
    4848// Privacy tools
    49 add_action( 'account_action_failed', '_wp_privacy_account_request_failed' );
    5049add_action( 'admin_menu', '_wp_privacy_hook_requests_page' );
    5150
  • branches/4.9/src/wp-admin/includes/ajax-actions.php

    r43075 r43083  
    41564156        // Find the request CPT
    41574157        $request = get_post( $request_id );
    4158         if ( 'user_remove_request' !== $request->post_type ) {
     4158        if ( 'remove_personal_data' !== $request->post_title ) {
    41594159                wp_send_json_error( __( 'Error: Invalid request ID.' ) );
    41604160        }
    41614161
    4162         $email_address = get_post_meta( $request_id, '_user_email', true );
     4162        $email_address = get_post_meta( $request_id, '_wp_user_request_user_email', true );
    41634163
    41644164        if ( ! is_email( $email_address ) ) {
  • branches/4.9/src/wp-admin/includes/user.php

    r43075 r43083  
    541541
    542542/**
    543  * Get action description from the name.
     543 * Resend an existing request and return the result.
    544544 *
    545545 * @since 4.9.6
    546546 * @access private
    547547 *
    548  * @return string
    549  */
    550 function _wp_privacy_action_description( $request_type ) {
    551         switch ( $request_type ) {
    552                 case 'user_export_request':
    553                         return __( 'Export Personal Data' );
    554                 case 'user_remove_request':
    555                         return __( 'Remove Personal Data' );
    556         }
    557 }
    558 
    559 /**
    560  * Log a request and send to the user.
    561  *
    562  * @since 4.9.6
    563  * @access private
    564  *
    565  * @param string $email_address Email address sending the request to.
    566  * @param string $action Action being requested.
    567  * @param string $description Description of request.
    568  * @return bool|WP_Error depending on success.
    569  */
    570 function _wp_privacy_create_request( $email_address, $action, $description ) {
    571         $user_id = 0;
    572         $user    = get_user_by( 'email', $email_address );
    573 
    574         if ( $user ) {
    575                 $user_id = $user->ID;
    576         }
    577 
    578         $privacy_request_id = wp_insert_post( array(
    579                 'post_author'   => $user_id,
    580                 'post_status'   => 'request-pending',
    581                 'post_type'     => $action,
    582                 'post_date'     => current_time( 'mysql', false ),
    583                 'post_date_gmt' => current_time( 'mysql', true ),
    584         ), true );
    585 
    586         if ( is_wp_error( $privacy_request_id ) ) {
    587                 return $privacy_request_id;
    588         }
    589 
    590         update_post_meta( $privacy_request_id, '_user_email', $email_address );
    591         update_post_meta( $privacy_request_id, '_action_name', $action );
    592         update_post_meta( $privacy_request_id, '_confirmed_timestamp', false );
    593 
    594         return wp_send_account_verification_key( $email_address, $action, $description, array(
    595                 'privacy_request_id' => $privacy_request_id,
    596         ) );
    597 }
    598 
    599 /**
    600  * Resend an existing request and return the result.
    601  *
    602  * @since 4.9.6
    603  * @access private
    604  *
    605  * @param int $privacy_request_id Request ID.
     548 * @param int $request_id Request ID.
    606549 * @return bool|WP_Error
    607550 */
    608 function _wp_privacy_resend_request( $privacy_request_id ) {
    609         $privacy_request_id = absint( $privacy_request_id );
    610         $privacy_request    = get_post( $privacy_request_id );
    611 
    612         if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
     551function _wp_privacy_resend_request( $request_id ) {
     552        $request_id = absint( $request_id );
     553        $request    = get_post( $request_id );
     554
     555        if ( ! $request || 'user_request' !== $request->post_type ) {
    613556                return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
    614557        }
    615558
    616         $email_address = get_post_meta( $privacy_request_id, '_user_email', true );
    617         $action        = get_post_meta( $privacy_request_id, '_action_name', true );
    618         $description   = _wp_privacy_action_description( $action );
    619         $result        = wp_send_account_verification_key( $email_address, $action, $description, array(
    620                 'privacy_request_id' => $privacy_request_id,
    621         ) );
     559        $result = wp_send_user_request( $request_id );
    622560
    623561        if ( is_wp_error( $result ) ) {
    … …  
    627565        }
    628566
    629         wp_update_post( array(
    630                 'ID'            => $privacy_request_id,
    631                 'post_status'   => 'request-pending',
    632                 'post_date'     => current_time( 'mysql', false ),
    633                 'post_date_gmt' => current_time( 'mysql', true ),
    634         ) );
    635 
    636567        return true;
    637568}
    … …  
    643574 * @access private
    644575 *
    645  * @param int $privacy_request_id Request ID.
    646  * @return bool|WP_Error
    647  */
    648 function _wp_privacy_completed_request( $privacy_request_id ) {
    649         $privacy_request_id = absint( $privacy_request_id );
    650         $privacy_request    = get_post( $privacy_request_id );
    651 
    652         if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
     576 * @param int $request_id Request ID.
     577 * @return int|WP_Error Request ID on succes or WP_Error.
     578 */
     579function _wp_privacy_completed_request( $request_id ) {
     580        $request_id   = absint( $request_id );
     581        $request_data = wp_get_user_request_data( $request_id );
     582
     583        if ( ! $request_data ) {
    653584                return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
    654585        }
    655586
    656         wp_update_post( array(
    657                 'ID'          => $privacy_request_id,
    658                 'post_status' => 'request-completed',
     587        update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() );
     588        $request = wp_update_post( array(
     589                'ID'          => $request_data['request_id'],
     590                'post_status' => 'request-confirmed',
    659591        ) );
    660 
    661         update_post_meta( $privacy_request_id, '_completed_timestamp', time() );
     592        return $request;
    662593}
    663594
    … …  
    763694                                }
    764695
    765                                 if ( ! empty( $email_address ) ) {
    766                                         $result = _wp_privacy_create_request( $email_address, $action_type, _wp_privacy_action_description( $action_type ) );
    767 
    768                                         if ( is_wp_error( $result ) ) {
    769                                                 add_settings_error(
    770                                                         'username_or_email_to_export',
    771                                                         'username_or_email_to_export',
    772                                                         $result->get_error_message(),
    773                                                         'error'
    774                                                 );
    775                                         } elseif ( ! $result ) {
    776                                                 add_settings_error(
    777                                                         'username_or_email_to_export',
    778                                                         'username_or_email_to_export',
    779                                                         __( 'Unable to initiate confirmation request.' ),
    780                                                         'error'
    781                                                 );
    782                                         } else {
    783                                                 add_settings_error(
    784                                                         'username_or_email_to_export',
    785                                                         'username_or_email_to_export',
    786                                                         __( 'Confirmation request initiated successfully.' ),
    787                                                         'updated'
    788                                                 );
    789                                         }
     696                                if ( empty( $email_address ) ) {
     697                                        break;
    790698                                }
     699
     700                                $request_id = wp_create_user_request( $email_address, $action_type );
     701
     702                                if ( is_wp_error( $request_id ) ) {
     703                                        add_settings_error(
     704                                                'username_or_email_to_export',
     705                                                'username_or_email_to_export',
     706                                                $request_id->get_error_message(),
     707                                                'error'
     708                                        );
     709                                        break;
     710                                } elseif ( ! $request_id ) {
     711                                        add_settings_error(
     712                                                'username_or_email_to_export',
     713                                                'username_or_email_to_export',
     714                                                __( 'Unable to initiate confirmation request.' ),
     715                                                'error'
     716                                        );
     717                                        break;
     718                                }
     719
     720                                wp_send_user_request( $request_id );
     721
     722                                add_settings_error(
     723                                        'username_or_email_to_export',
     724                                        'username_or_email_to_export',
     725                                        __( 'Confirmation request initiated successfully.' ),
     726                                        'updated'
     727                                );
    791728                                break;
    792729                }
    … …  
    831768                        <?php wp_nonce_field( 'personal-data-request' ); ?>
    832769                        <input type="hidden" name="action" value="add_export_personal_data_request" />
    833                         <input type="hidden" name="type_of_action" value="user_export_request" />
     770                        <input type="hidden" name="type_of_action" value="export_personal_data" />
    834771                </form>
    835772                <hr />
    … …  
    897834                        <?php wp_nonce_field( 'personal-data-request' ); ?>
    898835                        <input type="hidden" name="action" value="add_remove_personal_data_request" />
    899                         <input type="hidden" name="type_of_action" value="user_remove_request" />
     836                        <input type="hidden" name="type_of_action" value="remove_personal_data" />
    900837                </form>
    901838                <hr />
    … …  
    971908        public function get_columns() {
    972909                $columns = array(
    973                         'cb'         => '<input type="checkbox" />',
    974                         'email'      => __( 'Requester' ),
    975                         'status'     => __( 'Status' ),
    976                         'requested' => __( 'Requested' ),
    977                         'next_steps' => __( 'Next Steps' ),
     910                        'cb'                  => '<input type="checkbox" />',
     911                        'email'               => __( 'Requester' ),
     912                        'status'              => __( 'Status' ),
     913                        'requested_timestamp' => __( 'Requested' ),
     914                        'next_steps'          => __( 'Next Steps' ),
    978915                );
    979916                return $columns;
    … …  
    1000937        protected function get_default_primary_column_name() {
    1001938                return 'email';
     939        }
     940
     941        /**
     942         * Count number of requests for each status.
     943         *
     944         * @since 4.9.6
     945         *
     946         * @return object Number of posts for each status.
     947         */
     948        protected function get_request_counts() {
     949                global $wpdb;
     950
     951                $cache_key = $this->post_type . '-' . $this->request_type;
     952                $counts    = wp_cache_get( $cache_key, 'counts' );
     953
     954                if ( false !== $counts ) {
     955                        return $counts;
     956                }
     957
     958                $query = "
     959                        SELECT post_status, COUNT( * ) AS num_posts
     960                        FROM {$wpdb->posts}
     961                        WHERE post_type = %s
     962                        AND post_title = %s
     963                        GROUP BY post_status";
     964
     965                $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $this->post_type, $this->request_type ), ARRAY_A );
     966                $counts  = array_fill_keys( get_post_stati(), 0 );
     967
     968                foreach ( $results as $row ) {
     969                        $counts[ $row['post_status'] ] = $row['num_posts'];
     970                }
     971
     972                $counts = (object) $counts;
     973                wp_cache_set( $cache_key, $counts, 'counts' );
     974
     975                return $counts;
    1002976        }
    1003977
    … …  
    1015989                $views          = array();
    1016990                $admin_url      = admin_url( 'tools.php?page=' . $this->request_type );
    1017                 $counts         = wp_count_posts( $this->post_type );
     991                $counts         = $this->get_request_counts();
    1018992
    1019993                $current_link_attributes = empty( $current_status ) ? ' class="current" aria-current="page"' : '';
    … …  
    10501024                $action      = $this->current_action();
    10511025                $request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array(); // WPCS: input var ok, CSRF ok.
     1026                $count = 0;
    10521027
    10531028                if ( $request_ids ) {
    … …  
    10571032                switch ( $action ) {
    10581033                        case 'delete':
    1059                                 $count = 0;
    1060 
    10611034                                foreach ( $request_ids as $request_id ) {
    10621035                                        if ( wp_delete_post( $request_id, true ) ) {
    … …  
    10731046                                break;
    10741047                        case 'resend':
    1075                                 $count = 0;
    1076 
    10771048                                foreach ( $request_ids as $request_id ) {
    1078                                         if ( _wp_privacy_resend_request( $request_id ) ) {
    1079                                                 $count ++;
     1049                                        $resend = _wp_privacy_resend_request( $request_id );
     1050                                       
     1051                                        if ( $resend && ! is_wp_error( $resend ) ) {
     1052                                                $count++;
    10801053                                        }
    10811054                                }
    … …  
    11111084                $args           = array(
    11121085                        'post_type'      => $this->post_type,
     1086                        'title'          => $this->request_type,
    11131087                        'posts_per_page' => $posts_per_page,
    11141088                        'offset'         => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page: 0,
    … …  
    11261100                                'relation'  => 'AND',
    11271101                                array(
    1128                                         'key'     => '_user_email',
     1102                                        'key'     => '_wp_user_request_user_email',
    11291103                                        'value'   => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ): '',
    1130                                         'compare' => 'LIKE'
     1104                                        'compare' => 'LIKE',
    11311105                                ),
    11321106                        );
    11331107                }
    11341108
    1135                 $privacy_requests_query = new WP_Query( $args );
    1136                 $privacy_requests       = $privacy_requests_query->posts;
    1137 
    1138                 foreach ( $privacy_requests as $privacy_request ) {
    1139                         $this->items[] = array(
    1140                                 'request_id' => $privacy_request->ID,
    1141                                 'user_id'    => $privacy_request->post_author,
    1142                                 'email'      => get_post_meta( $privacy_request->ID, '_user_email', true ),
    1143                                 'action'     => get_post_meta( $privacy_request->ID, '_action_name', true ),
    1144                                 'requested'  => strtotime( $privacy_request->post_date_gmt ),
    1145                                 'confirmed'  => get_post_meta( $privacy_request->ID, '_confirmed_timestamp', true ),
    1146                                 'completed'  => get_post_meta( $privacy_request->ID, '_completed_timestamp', true ),
    1147                         );
     1109                $requests_query = new WP_Query( $args );
     1110                $requests       = $requests_query->posts;
     1111
     1112                foreach ( $requests as $request ) {
     1113                        $this->items[] = wp_get_user_request_data( $request->ID );
    11481114                }
    11491115
    11501116                $this->set_pagination_args(
    11511117                        array(
    1152                                 'total_items' => $privacy_requests_query->found_posts,
     1118                                'total_items' => $requests_query->found_posts,
    11531119                                'per_page'    => $posts_per_page,
    11541120                        )
    … …  
    11881154                switch ( $status ) {
    11891155                        case 'request-confirmed':
    1190                                 $timestamp = $item['confirmed'];
     1156                                $timestamp = $item['confirmed_timestamp'];
    11911157                                break;
    11921158                        case 'request-completed':
    1193                                 $timestamp = $item['completed'];
     1159                                $timestamp = $item['completed_timestamp'];
    11941160                                break;
    11951161                }
    … …  
    12391205                $cell_value = $item[ $column_name ];
    12401206
    1241                 if ( in_array( $column_name, array( 'requested' ), true ) ) {
     1207                if ( in_array( $column_name, array( 'requested_timestamp' ), true ) ) {
    12421208                        return $this->get_timestamp_as_date( $cell_value );
    12431209                }
    … …  
    13121278         * @var string $post_type The post type.
    13131279         */
    1314         protected $post_type = 'user_export_request';
     1280        protected $post_type = 'user_request';
    13151281
    13161282        /**
    … …  
    13971363         * @var string $post_type The post type.
    13981364         */
    1399         protected $post_type = 'user_remove_request';
     1365        protected $post_type = 'user_request';
    14001366
    14011367        /**
  • branches/4.9/src/wp-includes/default-filters.php

    r43080 r43083  
    302302add_action( 'do_robots',                  'do_robots'                                      );
    303303add_action( 'set_comment_cookies',        'wp_set_comment_cookies',                  10, 2 );
    304 add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter', 10 );
    305 add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser', 10 );
    306304add_action( 'sanitize_comment_cookies',   'sanitize_comment_cookies'                       );
    307305add_action( 'admin_print_scripts',        'print_emoji_detection_script'                   );
    … …  
    323321add_action( 'welcome_panel',              'wp_welcome_panel'                               );
    324322
     323// Privacy
     324add_action( 'user_request_action_confirmed', '_wp_privacy_account_request_confirmed' );
     325add_filter( 'user_request_action_confirmed_message', '_wp_privacy_account_request_confirmed_message', 10, 2 );
     326add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter' );
     327add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser' );
     328
    325329// Cron tasks
    326330add_action( 'wp_scheduled_delete',            'wp_scheduled_delete'       );
  • branches/4.9/src/wp-includes/post.php

    r43071 r43083  
    211211        ) );
    212212
    213         register_post_type( 'user_export_request', array(
     213        register_post_type( 'user_request', array(
    214214                'labels'           => array(
    215                         'name'          => __( 'Export Personal Data Requests' ),
    216                         'singular_name' => __( 'Export Personal Data Request' ),
     215                        'name'          => __( 'User Requests' ),
     216                        'singular_name' => __( 'User Request' ),
    217217                ),
    218218                'public'           => false,
    … …  
    223223                'can_export'       => false,
    224224                'delete_with_user' => false,
    225         ) );
    226 
    227         register_post_type( 'user_remove_request', array(
    228                 'labels'           => array(
    229                         'name'          => __( 'Remove Personal Data Requests' ),
    230                         'singular_name' => __( 'Remove Personal Data Request' ),
    231                 ),
    232                 'public'           => false,
    233                 '_builtin'         => true, /* internal use only. don't use this when registering your own post type. */
    234                 'hierarchical'     => false,
    235                 'rewrite'          => false,
    236                 'query_var'        => false,
    237                 'can_export'       => false,
    238                 'delete_with_user' => false,
     225                'supports'         => array(),
    239226        ) );
    240227
  • branches/4.9/src/wp-includes/user.php

    r43071 r43083  
    27362736 * Get all user privacy request types.
    27372737 *
    2738  * @since 5.0.0
     2738 * @since 4.9.6
    27392739 * @access private
    27402740 *
    … …  
    27432743function _wp_privacy_action_request_types() {
    27442744        return array(
    2745                 'user_export_request',
    2746                 'user_remove_request',
     2745                'export_personal_data',
     2746                'remove_personal_data',
    27472747        );
    27482748}
    … …  
    27512751 * Update log when privacy request is confirmed.
    27522752 *
    2753  * @since 5.0.0
     2753 * @since 4.9.6
    27542754 * @access private
    27552755 *
    2756  * @param array $result Result of the request from the user.
    2757  */
    2758 function _wp_privacy_account_request_confirmed( $result ) {
    2759         if ( isset( $result['action'], $result['request_data'], $result['request_data']['privacy_request_id'] ) && in_array( $result['action'], _wp_privacy_action_request_types(), true ) ) {
    2760                 $privacy_request_id = absint( $result['request_data']['privacy_request_id'] );
    2761                 $privacy_request    = get_post( $privacy_request_id );
    2762 
    2763                 if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
    2764                         return;
    2765                 }
    2766 
    2767                 update_post_meta( $privacy_request_id, '_confirmed_timestamp', time() );
     2756 * @param int $request_id ID of the request.
     2757 */
     2758function _wp_privacy_account_request_confirmed( $request_id ) {
     2759        $request_data = wp_get_user_request_data( $request_id );
     2760
     2761        if ( ! $request_data ) {
     2762                return;
     2763        }
     2764
     2765        if ( ! in_array( $request_data['status'], array( 'request-pending', 'request-failed' ), true ) ) {
     2766                return;
     2767        }
     2768
     2769        update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() );
     2770        wp_update_post( array(
     2771                'ID'          => $request_data['request_id'],
     2772                'post_status' => 'request-confirmed',
     2773        ) );
     2774}
     2775
     2776/**
     2777 * Return request confirmation message HTML.
     2778 *
     2779 * @since 4.9.6
     2780 * @access private
     2781 *
     2782 * @return string $message The confirmation message.
     2783 */
     2784function _wp_privacy_account_request_confirmed_message( $message, $request_id ) {
     2785        $request = wp_get_user_request_data( $request_id );
     2786
     2787        if ( $request && in_array( $request['action'], _wp_privacy_action_request_types(), true ) ) {
     2788                $message = '<p class="message">' . __( 'Action has been confirmed.' ) . '</p>';
     2789                $message .= __( 'The site administrator has been notified and will fulfill your request as soon as possible.' );
     2790        }
     2791
     2792        return $message;
     2793}
     2794
     2795/**
     2796 * Create and log a user request to perform a specific action.
     2797 *
     2798 * Requests are stored inside a post type named `user_request` since they can apply to both
     2799 * users on the site, or guests without a user account.
     2800 *
     2801 * @since 4.9.6
     2802 *
     2803 * @param string $email_address User email address. This can be the address of a registered or non-registered user.
     2804 * @param string $action_name   Name of the action that is being confirmed. Required.
     2805 * @param array  $request_data  Misc data you want to send with the verification request and pass to the actions once the request is confirmed.
     2806 * @return int|WP_Error Returns the request ID if successful, or a WP_Error object on failure.
     2807 */
     2808function wp_create_user_request( $email_address = '', $action_name = '', $request_data = array() ) {
     2809        $email_address = sanitize_email( $email_address );
     2810        $action_name   = sanitize_key( $action_name );
     2811
     2812        if ( ! is_email( $email_address ) ) {
     2813                return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
     2814        }
     2815
     2816        if ( ! $action_name ) {
     2817                return new WP_Error( 'invalid_action', __( 'Invalid action name' ) );
     2818        }
     2819
     2820        $user    = get_user_by( 'email', $email_address );
     2821        $user_id = $user && ! is_wp_error( $user ) ? $user->ID: 0;
     2822
     2823        // Check for duplicates.
     2824        $requests_query = new WP_Query( array(
     2825                'post_type'   => 'user_request',
     2826                'title'       => $action_name,
     2827                'post_status' => 'any',
     2828                'fields'      => 'ids',
     2829                'meta_query'  => array(
     2830                        array(
     2831                                'key'     => '_wp_user_request_user_email',
     2832                                'value'   => $email_address,
     2833                        ),
     2834                ),
     2835        ) );
     2836
     2837        if ( $requests_query->found_posts ) {
     2838                return new WP_Error( 'duplicate_request', __( 'A request for this email address already exists.' ) );
     2839        }
     2840
     2841        $request_id = wp_insert_post( array(
     2842                'post_author'   => $user_id,
     2843                'post_title'    => $action_name,
     2844                'post_content'  => wp_json_encode( $request_data ),
     2845                'post_status'   => 'request-pending',
     2846                'post_type'     => 'user_request',
     2847                'post_date'     => current_time( 'mysql', false ),
     2848                'post_date_gmt' => current_time( 'mysql', true ),
     2849        ), true );
     2850
     2851        if ( is_wp_error( $request_id ) ) {
     2852                return $request_id;
     2853        }
     2854
     2855        update_post_meta( $request_id, '_wp_user_request_user_email', $email_address );
     2856        update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', false );
     2857
     2858        return $request_id;
     2859}
     2860
     2861/**
     2862 * Get action description from the name and return a string.
     2863 *
     2864 * @since 4.9.6
     2865 *
     2866 * @param string $action_name Action name of the request.
     2867 * @return string
     2868 */
     2869function wp_user_request_action_description( $action_name ) {
     2870        switch ( $action_name ) {
     2871                case 'export_personal_data':
     2872                        $description = __( 'Export Personal Data' );
     2873                        break;
     2874                case 'remove_personal_data':
     2875                        $description = __( 'Remove Personal Data' );
     2876                        break;
     2877                default:
     2878                        /* translators: %s: action name */
     2879                        $description = sprintf( __( 'Confirm the "%s" action' ), $action_name );
     2880                        break;
     2881        }
     2882
     2883        /**
     2884         * Filters the user action description.
     2885         *
     2886         * @param string $description The default description.
     2887         * @param string $action_name The name of the request.
     2888         */                             
     2889        return apply_filters( 'user_request_action_description', $description, $action_name );
     2890}
     2891
     2892/**
     2893 * Send a confirmation request email to confirm an action.
     2894 *
     2895 * If the request is not already pending, it will be updated.
     2896 *
     2897 * @since 4.9.6
     2898 *
     2899 * @param string $request_id ID of the request created via wp_create_user_request().
     2900 * @return WP_Error|bool Will return true/false based on the success of sending the email, or a WP_Error object.
     2901 */
     2902function wp_send_user_request( $request_id ) {
     2903        $request_id = absint( $request_id );
     2904        $request    = get_post( $request_id );
     2905
     2906        if ( ! $request || 'user_request' !== $request->post_type ) {
     2907                return new WP_Error( 'user_request_error', __( 'Invalid request.' ) );
     2908        }
     2909
     2910        if ( 'request-pending' !== $request->post_status ) {
    27682911                wp_update_post( array(
    2769                         'ID'          => $privacy_request_id,
    2770                         'post_status' => 'request-confirmed',
     2912                        'ID'            => $request_id,
     2913                        'post_status'   => 'request-pending',
     2914                        'post_date'     => current_time( 'mysql', false ),
     2915                        'post_date_gmt' => current_time( 'mysql', true ),
    27712916                ) );
    27722917        }
    2773 }
    2774 add_action( 'account_action_confirmed', '_wp_privacy_account_request_confirmed' );
    2775 
    2776 /**
    2777  * Update log when privacy request fails.
    2778  *
    2779  * @since 5.0.0
    2780  * @access private
    2781  *
    2782  * @param array $result Result of the request from the user.
    2783  */
    2784 function _wp_privacy_account_request_failed( $result ) {
    2785         if ( isset( $result['action'], $result['request_data'], $result['request_data']['privacy_request_id'] ) &&
    2786                 in_array( $result['action'], _wp_privacy_action_request_types(), true ) ) {
    2787 
    2788                 $privacy_request_id = absint( $result['request_data']['privacy_request_id'] );
    2789                 $privacy_request    = get_post( $privacy_request_id );
    2790 
    2791                 if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
    2792                         return;
    2793                 }
    2794 
    2795                 wp_update_post( array(
    2796                         'ID'          => $privacy_request_id,
    2797                         'post_status' => 'request-failed',
    2798                 ) );
    2799         }
    2800 }
    2801 
    2802 /**
    2803  * Send a confirmation request email to confirm an action.
    2804  *
    2805  * @since 5.0.0
    2806  *
    2807  * @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.
    2808  * @param string $action_name        Name of the action that is being confirmed. Defaults to 'confirm_email'.
    2809  * @param string $action_description User facing description of the action they will be confirming. Defaults to "confirm your email address".
    2810  * @param array  $request_data       Misc data you want to send with the verification request and pass to the actions once the request is confirmed.
    2811  * @return WP_Error|bool Will return true/false based on the success of sending the email, or a WP_Error object.
    2812  */
    2813 function wp_send_account_verification_key( $email = '', $action_name = '', $action_description = '', $request_data = array() ) {
    2814         if ( ! function_exists( 'wp_get_current_user' ) ) {
    2815                 return new WP_Error( 'invalid', __( 'This function cannot be used before init.' ) );
    2816         }
    2817 
    2818         $action_name        = sanitize_key( $action_name );
    2819         $action_description = wp_kses_post( $action_description );
    2820 
    2821         if ( empty( $action_name ) ) {
    2822                 $action_name = 'confirm_email';
    2823         }
    2824 
    2825         if ( empty( $action_description ) ) {
    2826                 $action_description = __( 'Confirm your email address.' );
    2827         }
    2828 
    2829         if ( empty( $email ) ) {
    2830                 $user  = wp_get_current_user();
    2831                 $email = $user->ID ? $user->user_email : '';
    2832         } else {
    2833                 $user = false;
    2834         }
    2835 
    2836         $email = sanitize_email( $email );
    2837 
    2838         if ( ! is_email( $email ) ) {
    2839                 return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
    2840         }
    2841 
    2842         if ( ! $user ) {
    2843                 $user = get_user_by( 'email', $email );
    2844         }
    2845 
    2846         $confirm_key = wp_get_account_verification_key( $email, $action_name, $request_data );
    2847 
    2848         if ( is_wp_error( $confirm_key ) ) {
    2849                 return $confirm_key;
    2850         }
    2851 
    2852         // We could be dealing with a registered user account, or a visitor.
    2853         $is_registered_user = $user && ! is_wp_error( $user );
    2854 
    2855         if ( $is_registered_user ) {
    2856                 $uid = $user->ID;
    2857         } else {
    2858                 // Generate a UID for this email address so we don't send the actual email in the query string. Hash is not supported on all systems.
    2859                 $uid = function_exists( 'hash' ) ? hash( 'sha256', $email ) : sha1( $email );
    2860         }
     2918
     2919        $email_data = array(
     2920                'action_name' => $request->post_title,
     2921                'email'       => get_post_meta( $request->ID, '_wp_user_request_user_email', true ),
     2922                'description' => wp_user_request_action_description( $request->post_title ),
     2923                'confirm_url' => add_query_arg( array(
     2924                        'action'      => 'confirmaction',
     2925                        'request_id'  => $request_id,
     2926                        'confirm_key' => wp_generate_user_request_key( $request_id ),
     2927                ), site_url( 'wp-login.php' ) ),
     2928                'sitename'    => is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ),
     2929                'siteurl'     => network_home_url(),
     2930        );
    28612931
    28622932        /* translators: Do not translate DESCRIPTION, CONFIRM_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
    … …  
    28792949All at ###SITENAME###
    28802950###SITEURL###'
    2881         );
    2882 
    2883         $email_data = array(
    2884                 'action_name' => $action_name,
    2885                 'email'       => $email,
    2886                 'description' => $action_description,
    2887                 'confirm_url' => add_query_arg( array(
    2888                         'action'         => 'verifyaccount',
    2889                         'confirm_action' => $action_name,
    2890                         'uid'            => $uid,
    2891                         'confirm_key'    => $confirm_key,
    2892                 ), site_url( 'wp-login.php' ) ),
    2893                 'sitename'    => is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ),
    2894                 'siteurl'     => network_home_url(),
    28952951        );
    28962952
    … …  
    29062962         * ###SITEURL###            The URL to the site.
    29072963         *
    2908          * @since 5.0.0
     2964         * @since 4.9.6
    29092965         *
    29102966         * @param string $email_text     Text in the email.
    … …  
    29202976         * }
    29212977         */
    2922         $content = apply_filters( 'account_verification_email_content', $email_text, $email_data );
     2978        $content = apply_filters( 'user_request_action_email_content', $email_text, $email_data );
    29232979
    29242980        $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content );
    … …  
    29332989
    29342990/**
    2935  * Creates, stores, then returns a confirmation key for an account action.
    2936  *
    2937  * @since 5.0.0
    2938  *
    2939  * @param string $email        User email address. This can be the address of a registered or non-registered user.
    2940  * @param string $action_name  Name of the action this key is being generated for.
    2941  * @param array  $request_data Misc data you want to send with the verification request and pass to the actions once the request is confirmed.
    2942  * @return string|WP_Error Confirmation key on success. WP_Error on error.
    2943  */
    2944 function wp_get_account_verification_key( $email, $action_name, $request_data = array() ) {
     2991 * Returns a confirmation key for a user action and stores the hashed version.
     2992 *
     2993 * @since 4.9.6
     2994 *
     2995 * @param int $request_id Request ID.
     2996 * @return string Confirmation key.
     2997 */
     2998function wp_generate_user_request_key( $request_id ) {
    29452999        global $wp_hasher;
    2946 
    2947         if ( ! is_email( $email ) ) {
    2948                 return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
    2949         }
    2950 
    2951         if ( empty( $action_name ) ) {
    2952                 return new WP_Error( 'invalid_action', __( 'Invalid action' ) );
    2953         }
    2954 
    2955         $user = get_user_by( 'email', $email );
    2956 
    2957         // We could be dealing with a registered user account, or a visitor.
    2958         $is_registered_user = $user && ! is_wp_error( $user );
    29593000
    29603001        // Generate something random for a confirmation key.
    29613002        $key = wp_generate_password( 20, false );
    29623003
    2963         // Now insert the key, hashed, into the DB.
     3004        // Return the key, hashed.
    29643005        if ( empty( $wp_hasher ) ) {
    29653006                require_once ABSPATH . WPINC . '/class-phpass.php';
    … …  
    29673008        }
    29683009
    2969         $hashed_key = $wp_hasher->HashPassword( $key );
    2970         $value      = array(
    2971                 'action'       => $action_name,
    2972                 'time'         => time(),
    2973                 'hash'         => $hashed_key,
    2974                 'email'        => $email,
    2975                 'request_data' => $request_data,
    2976         );
    2977 
    2978         if ( $is_registered_user ) {
    2979                 $key_saved = (bool) update_user_meta( $user->ID, '_verify_action_' . $action_name, wp_json_encode( $value ) );
    2980         } else {
    2981                 $uid       = function_exists( 'hash' ) ? hash( 'sha256', $email ) : sha1( $email );
    2982                 $key_saved = (bool) update_site_option( '_verify_action_' . $action_name . '_' . $uid, wp_json_encode( $value ) );
    2983         }
    2984 
    2985         if ( false === $key_saved ) {
    2986                 return new WP_Error( 'no_account_verification_key_update', __( 'Could not save confirm account action key to database.' ) );
    2987         }
     3010        update_post_meta( $request_id, '_wp_user_request_confirm_key', $wp_hasher->HashPassword( $key ) );
     3011        update_post_meta( $request_id, '_wp_user_request_confirm_key_timestamp', time() );
    29883012
    29893013        return $key;
    … …  
    29913015
    29923016/**
    2993  * Checks if a key is valid and handles the action based on this.
    2994  *
    2995  * @since 5.0.0
    2996  *
    2997  * @param string $key         Key to confirm.
    2998  * @param string $uid         Email hash or user ID.
    2999  * @param string $action_name Name of the action this key is being generated for.
    3000  * @return array|WP_Error WP_Error on failure, action name and user email address on success.
    3001  */
    3002 function wp_check_account_verification_key( $key, $uid, $action_name ) {
     3017 * Valdate a user request by comparing the key with the request's key.
     3018 *
     3019 * @since 4.9.6
     3020 *
     3021 * @param string $request_id ID of the request being confirmed.
     3022 * @param string $key        Provided key to validate.
     3023 * @return bool|WP_Error WP_Error on failure, true on success.
     3024 */
     3025function wp_validate_user_request_key( $request_id, $key ) {
    30033026        global $wp_hasher;
    30043027
    3005         if ( empty( $action_name ) || empty( $key ) || empty( $uid ) ) {
     3028        $request_id = absint( $request_id );
     3029        $request    = wp_get_user_request_data( $request_id );
     3030
     3031        if ( ! $request ) {
     3032                return new WP_Error( 'user_request_error', __( 'Invalid request.' ) );
     3033        }
     3034
     3035        if ( ! in_array( $request['status'], array( 'request-pending', 'request-failed' ), true ) ) {
     3036                return __( 'This link has expired.' );
     3037        }
     3038
     3039        if ( empty( $key ) ) {
    30063040                return new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    30073041        }
    3008 
    3009         $user = false;
    3010 
    3011         if ( is_numeric( $uid ) ) {
    3012                 $user = get_user_by( 'id', absint( $uid ) );
    3013         }
    3014 
    3015         // We could be dealing with a registered user account, or a visitor.
    3016         $is_registered_user = ( $user && ! is_wp_error( $user ) );
    3017         $key_request_time   = '';
    3018         $saved_key          = '';
    3019         $email              = '';
    30203042
    30213043        if ( empty( $wp_hasher ) ) {
    … …  
    30243046        }
    30253047
    3026         // Get the saved key from the database.
    3027         if ( $is_registered_user ) {
    3028                 $raw_data = get_user_meta( $user->ID, '_verify_action_' . $action_name, true );
    3029                 $email    = $user->user_email;
    3030 
    3031                 if ( false !== strpos( $raw_data, ':' ) ) {
    3032                         list( $key_request_time, $saved_key ) = explode( ':', $raw_data, 2 );
    3033                 }
    3034         } else {
    3035                 $raw_data = get_site_option( '_verify_action_' . $action_name . '_' . $uid, '' );
    3036 
    3037                 if ( false !== strpos( $raw_data, ':' ) ) {
    3038                         list( $key_request_time, $saved_key, $email ) = explode( ':', $raw_data, 3 );
    3039                 }
    3040         }
    3041 
    3042         $data             = json_decode( $raw_data, true );
    3043         $key_request_time = (int) isset( $data['time'] ) ? $data['time'] : 0;
    3044         $saved_key        = isset( $data['hash'] ) ? $data['hash'] : '';
    3045         $email            = sanitize_email( isset( $data['email'] ) ? $data['email'] : '' );
    3046         $request_data     = isset( $data['request_data'] ) ? $data['request_data'] : array();
     3048        $key_request_time = $request['confirm_key_timestamp'];
     3049        $saved_key        = $request['confirm_key'];
    30473050
    30483051        if ( ! $saved_key ) {
    … …  
    30503053        }
    30513054
    3052         if ( ! $key_request_time || ! $email ) {
     3055        if ( ! $key_request_time ) {
    30533056                return new WP_Error( 'invalid_key', __( 'Invalid action' ) );
    30543057        }
    … …  
    30573060         * Filters the expiration time of confirm keys.
    30583061         *
    3059          * @since 5.0.0
     3062         * @since 4.9.6
    30603063         *
    30613064         * @param int $expiration The expiration time in seconds.
    30623065         */
    3063         $expiration_duration = apply_filters( 'account_verification_expiration', DAY_IN_SECONDS );
     3066        $expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
    30643067        $expiration_time     = $key_request_time + $expiration_duration;
    30653068
    … …  
    30683071        }
    30693072
    3070         if ( $expiration_time && time() < $expiration_time ) {
    3071                 $return = array(
    3072                         'action'       => $action_name,
    3073                         'email'        => $email,
    3074                         'request_data' => $request_data,
    3075                 );
    3076         } else {
     3073        if ( ! $expiration_time || time() > $expiration_time ) {
    30773074                $return = new WP_Error( 'expired_key', __( 'The confirmation email has expired.' ) );
    30783075        }
    30793076
    3080         // Clean up stored keys.
    3081         if ( $is_registered_user ) {
    3082                 delete_user_meta( $user->ID, '_verify_action_' . $action_name );
    3083         } else {
    3084                 delete_site_option( '_verify_action_' . $action_name . '_' . $uid );
    3085         }
    3086 
    3087         return $return;
    3088 }
     3077        return true;
     3078}
     3079
     3080/**
     3081 * Return data about a user request.
     3082 *
     3083 * @since 4.9.6
     3084 *
     3085 * @param int $request_id Request ID to get data about.
     3086 * @return array|false
     3087 */
     3088function wp_get_user_request_data( $request_id ) {
     3089        $request_id = absint( $request_id );
     3090        $request    = get_post( $request_id );
     3091
     3092        if ( ! $request || 'user_request' !== $request->post_type ) {
     3093                return false;
     3094        }
     3095
     3096        return array(
     3097                'request_id'            => $request->ID,
     3098                'user_id'               => $request->post_author,
     3099                'email'                 => get_post_meta( $request->ID, '_wp_user_request_user_email', true ),
     3100                'action'                => $request->post_title,
     3101                'requested_timestamp'   => strtotime( $request->post_date_gmt ),
     3102                'confirmed_timestamp'   => get_post_meta( $request->ID, '_wp_user_request_confirmed_timestamp', true ),
     3103                'completed_timestamp'   => get_post_meta( $request->ID, '_wp_user_request_completed_timestamp', true ),
     3104                'request_data'          => json_decode( $request->post_content, true ),
     3105                'status'                => $request->post_status,
     3106                'confirm_key'           => get_post_meta( $request_id, '_wp_user_request_confirm_key', true ),
     3107                'confirm_key_timestamp' => get_post_meta( $request_id, '_wp_user_request_confirm_key_timestamp', true ),
     3108        );
     3109}
  • branches/4.9/src/wp-login.php

    r43070 r43083  
    414414
    415415// validate action so as to default to the login screen
    416 if ( !in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'verifyaccount' ), true ) && false === has_filter( 'login_form_' . $action ) )
     416if ( !in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction' ), true ) && false === has_filter( 'login_form_' . $action ) )
    417417        $action = 'login';
    418418
    … …  
    839839break;
    840840
    841 case 'verifyaccount' :
    842         if ( isset( $_GET['confirm_action'], $_GET['confirm_key'], $_GET['uid'] ) ) {
    843                 $key         = sanitize_text_field( wp_unslash( $_GET['confirm_key'] ) );
    844                 $uid         = sanitize_text_field( wp_unslash( $_GET['uid'] ) );
    845                 $action_name = sanitize_key( wp_unslash( $_GET['confirm_action'] ) );
    846                 $result      = wp_check_account_verification_key( $key, $uid, $action_name );
     841case 'confirmaction' :
     842        if ( ! isset( $_GET['request_id'] ) ) {
     843                wp_die( __( 'Invalid request' ) );
     844        }
     845
     846        $request_id = (int) $_GET['request_id'];
     847
     848        if ( isset( $_GET['confirm_key'] ) ) {
     849                $key    = sanitize_text_field( wp_unslash( $_GET['confirm_key'] ) );
     850                $result = wp_validate_user_request_key( $request_id, $key );
    847851        } else {
    848852                $result = new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    … …  
    850854
    851855        if ( is_wp_error( $result ) ) {
    852                 /**
    853                  * Fires an action hook when the account action was not confirmed.
    854                  *
    855                  * After running this action hook the page will die.
    856                  *
    857                  * @param WP_Error $result Error object.
    858                  */
    859                 do_action( 'account_action_failed', $result );
    860 
    861856                wp_die( $result );
    862857        }
    … …  
    868863         * clicking on the link in the confirmation email.
    869864         *
    870          * After firing this action hook the page will redirect to wp-login a callback 
     865         * After firing this action hook the page will redirect to wp-login a callback
    871866         * redirects or exits first.
    872          *
    873          * @param array $result {
    874          *     Data about the action which was confirmed.
    875          *
    876          *     @type string $action Name of the action that was confirmed.
    877          *     @type string $email  Email of the user who confirmed the action.
    878          * }
    879          */
    880         do_action( 'account_action_confirmed', $result );
    881 
    882         $message = '<p class="message">' . __( 'Action has been confirmed.' ) . '</p>';
    883         login_header( '', $message );
     867         *
     868         * @param int $request_id Request ID.
     869         */
     870        do_action( 'user_request_action_confirmed', $request_id );
     871
     872        $message = apply_filters( 'user_request_action_confirmed_message', '<p class="message">' . __( 'Action has been confirmed.' ) . '</p>', $request_id );
     873
     874        login_header( __( 'User action confirmed.' ), $message );
    884875        login_footer();
    885876        exit;
Note: See TracChangeset for help on using the changeset viewer.