WordPress.org

Make WordPress Core

Ticket #43443: 43443.5.patch

File 43443.5.patch, 38.4 KB (added by azaozz, 3 years ago)
  • src/wp-admin/includes/admin-filters.php

     
    4646add_action( 'admin_head', '_ipad_meta' );
    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
    5251// Prerendering.
  • src/wp-admin/includes/ajax-actions.php

     
    44644464
    44654465        // Find the request CPT
    44664466        $request = get_post( $request_id );
    4467         if ( 'user_remove_request' !== $request->post_type ) {
     4467        if ( 'remove_personal_data' !== $request->post_title ) {
    44684468                wp_send_json_error( __( 'Error: Invalid request ID.' ) );
    44694469        }
    44704470
    4471         $email_address = get_post_meta( $request_id, '_user_email', true );
     4471        $email_address = get_post_meta( $request_id, '_wp_user_request_user_email', true );
    44724472
    44734473        if ( ! is_email( $email_address ) ) {
    44744474                wp_send_json_error( __( 'Error: Invalid email address in request.' ) );
  • src/wp-admin/includes/user.php

     
    581581}
    582582
    583583/**
    584  * Get action description from the name.
    585  *
    586  * @since 4.9.6
    587  * @access private
    588  *
    589  * @return string
    590  */
    591 function _wp_privacy_action_description( $request_type ) {
    592         switch ( $request_type ) {
    593                 case 'user_export_request':
    594                         return __( 'Export Personal Data' );
    595                 case 'user_remove_request':
    596                         return __( 'Remove Personal Data' );
    597         }
    598 }
    599 
    600 /**
    601  * Log a request and send to the user.
    602  *
    603  * @since 4.9.6
    604  * @access private
    605  *
    606  * @param string $email_address Email address sending the request to.
    607  * @param string $action Action being requested.
    608  * @param string $description Description of request.
    609  * @return bool|WP_Error depending on success.
    610  */
    611 function _wp_privacy_create_request( $email_address, $action, $description ) {
    612         $user_id = 0;
    613         $user    = get_user_by( 'email', $email_address );
    614 
    615         if ( $user ) {
    616                 $user_id = $user->ID;
    617         }
    618 
    619         $privacy_request_id = wp_insert_post( array(
    620                 'post_author'   => $user_id,
    621                 'post_status'   => 'request-pending',
    622                 'post_type'     => $action,
    623                 'post_date'     => current_time( 'mysql', false ),
    624                 'post_date_gmt' => current_time( 'mysql', true ),
    625         ), true );
    626 
    627         if ( is_wp_error( $privacy_request_id ) ) {
    628                 return $privacy_request_id;
    629         }
    630 
    631         update_post_meta( $privacy_request_id, '_user_email', $email_address );
    632         update_post_meta( $privacy_request_id, '_action_name', $action );
    633         update_post_meta( $privacy_request_id, '_confirmed_timestamp', false );
    634 
    635         return wp_send_account_verification_key( $email_address, $action, $description, array(
    636                 'privacy_request_id' => $privacy_request_id,
    637         ) );
    638 }
    639 
    640 /**
    641584 * Resend an existing request and return the result.
    642585 *
    643586 * @since 4.9.6
    644587 * @access private
    645588 *
    646  * @param int $privacy_request_id Request ID.
     589 * @param int $request_id Request ID.
    647590 * @return bool|WP_Error
    648591 */
    649 function _wp_privacy_resend_request( $privacy_request_id ) {
    650         $privacy_request_id = absint( $privacy_request_id );
    651         $privacy_request    = get_post( $privacy_request_id );
     592function _wp_privacy_resend_request( $request_id ) {
     593        $request_id = absint( $request_id );
     594        $request    = get_post( $request_id );
    652595
    653         if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
     596        if ( ! $request || 'user_request' !== $request->post_type ) {
    654597                return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
    655598        }
    656599
    657         $email_address = get_post_meta( $privacy_request_id, '_user_email', true );
    658         $action        = get_post_meta( $privacy_request_id, '_action_name', true );
    659         $description   = _wp_privacy_action_description( $action );
    660         $result        = wp_send_account_verification_key( $email_address, $action, $description, array(
    661                 'privacy_request_id' => $privacy_request_id,
    662         ) );
     600        $result = wp_send_user_request( $request_id );
    663601
    664602        if ( is_wp_error( $result ) ) {
    665603                return $result;
     
    667605                return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation request.' ) );
    668606        }
    669607
    670         wp_update_post( array(
    671                 'ID'            => $privacy_request_id,
    672                 'post_status'   => 'request-pending',
    673                 'post_date'     => current_time( 'mysql', false ),
    674                 'post_date_gmt' => current_time( 'mysql', true ),
    675         ) );
    676 
    677608        return true;
    678609}
    679610
     
    683614 * @since 4.9.6
    684615 * @access private
    685616 *
    686  * @param int $privacy_request_id Request ID.
     617 * @param int $request_id Request ID.
    687618 * @return bool|WP_Error
    688619 */
    689 function _wp_privacy_completed_request( $privacy_request_id ) {
    690         $privacy_request_id = absint( $privacy_request_id );
    691         $privacy_request    = get_post( $privacy_request_id );
     620function _wp_privacy_completed_request( $request_id ) {
     621        $request_id   = absint( $request_id );
     622        $request_data = wp_get_user_request_data( $request_id );
    692623
    693         if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
    694                 return new WP_Error( 'privacy_request_error', __( 'Invalid request.' ) );
     624        if ( ! $request_data ) {
     625                return;
    695626        }
    696627
     628        update_post_meta( $privacy_request_id, '_wp_user_request_confirmed_timestamp', time() );
    697629        wp_update_post( array(
    698                 'ID'          => $privacy_request_id,
    699                 'post_status' => 'request-completed',
     630                'ID'          => $request_data['request_id'],
     631                'post_status' => 'request-confirmed',
    700632        ) );
    701 
    702         update_post_meta( $privacy_request_id, '_completed_timestamp', time() );
    703633}
    704634
    705635/**
     
    803733                                        $email_address = $username_or_email_address;
    804734                                }
    805735
    806                                 if ( ! empty( $email_address ) ) {
    807                                         $result = _wp_privacy_create_request( $email_address, $action_type, _wp_privacy_action_description( $action_type ) );
     736                                if ( empty( $email_address ) ) {
     737                                        break;
     738                                }
    808739
    809                                         if ( is_wp_error( $result ) ) {
    810                                                 add_settings_error(
    811                                                         'username_or_email_to_export',
    812                                                         'username_or_email_to_export',
    813                                                         $result->get_error_message(),
    814                                                         'error'
    815                                                 );
    816                                         } elseif ( ! $result ) {
    817                                                 add_settings_error(
    818                                                         'username_or_email_to_export',
    819                                                         'username_or_email_to_export',
    820                                                         __( 'Unable to initiate confirmation request.' ),
    821                                                         'error'
    822                                                 );
    823                                         } else {
    824                                                 add_settings_error(
    825                                                         'username_or_email_to_export',
    826                                                         'username_or_email_to_export',
    827                                                         __( 'Confirmation request initiated successfully.' ),
    828                                                         'updated'
    829                                                 );
    830                                         }
     740                                $request_id = wp_create_user_request( $email_address, $action_type );
     741
     742                                if ( is_wp_error( $request_id ) ) {
     743                                        add_settings_error(
     744                                                'username_or_email_to_export',
     745                                                'username_or_email_to_export',
     746                                                $request_id->get_error_message(),
     747                                                'error'
     748                                        );
     749                                        break;
     750                                } elseif ( ! $request_id ) {
     751                                        add_settings_error(
     752                                                'username_or_email_to_export',
     753                                                'username_or_email_to_export',
     754                                                __( 'Unable to initiate confirmation request.' ),
     755                                                'error'
     756                                        );
     757                                        break;
    831758                                }
     759
     760                                wp_send_user_request( $request_id );
     761
     762                                add_settings_error(
     763                                        'username_or_email_to_export',
     764                                        'username_or_email_to_export',
     765                                        __( 'Confirmation request initiated successfully.' ),
     766                                        'updated'
     767                                );
    832768                                break;
    833769                }
    834770        }
     
    871807                        </div>
    872808                        <?php wp_nonce_field( 'personal-data-request' ); ?>
    873809                        <input type="hidden" name="action" value="add_export_personal_data_request" />
    874                         <input type="hidden" name="type_of_action" value="user_export_request" />
     810                        <input type="hidden" name="type_of_action" value="export_personal_data" />
    875811                </form>
    876812                <hr />
    877813
     
    937873                        </div>
    938874                        <?php wp_nonce_field( 'personal-data-request' ); ?>
    939875                        <input type="hidden" name="action" value="add_remove_personal_data_request" />
    940                         <input type="hidden" name="type_of_action" value="user_remove_request" />
     876                        <input type="hidden" name="type_of_action" value="remove_personal_data" />
    941877                </form>
    942878                <hr />
    943879
     
    1011947         */
    1012948        public function get_columns() {
    1013949                $columns = array(
    1014                         'cb'         => '<input type="checkbox" />',
    1015                         'email'      => __( 'Requester' ),
    1016                         'status'     => __( 'Status' ),
    1017                         'requested' => __( 'Requested' ),
    1018                         'next_steps' => __( 'Next Steps' ),
     950                        'cb'                  => '<input type="checkbox" />',
     951                        'email'               => __( 'Requester' ),
     952                        'status'              => __( 'Status' ),
     953                        'requested_timestamp' => __( 'Requested' ),
     954                        'next_steps'          => __( 'Next Steps' ),
    1019955                );
    1020956                return $columns;
    1021957        }
     
    1043979        }
    1044980
    1045981        /**
     982         * Count number of requests for each status.
     983         *
     984         * @since 4.9.6
     985         *
     986         * @return object Number of posts for each status.
     987         */
     988        protected function get_request_counts() {
     989                global $wpdb;
     990
     991                $cache_key = $this->post_type . '-' . $this->request_type;
     992                $counts    = wp_cache_get( $cache_key, 'counts' );
     993
     994                if ( false !== $counts ) {
     995                        return $counts;
     996                }
     997
     998                $query = "
     999                        SELECT post_status, COUNT( * ) AS num_posts
     1000                        FROM {$wpdb->posts}
     1001                        WHERE post_type = %s
     1002                        AND post_title = %s
     1003                        GROUP BY post_status";
     1004
     1005                $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $this->post_type, $this->request_type ), ARRAY_A );
     1006                $counts  = array_fill_keys( get_post_stati(), 0 );
     1007
     1008                foreach ( $results as $row ) {
     1009                        $counts[ $row['post_status'] ] = $row['num_posts'];
     1010                }
     1011
     1012                $counts = (object) $counts;
     1013                wp_cache_set( $cache_key, $counts, 'counts' );
     1014
     1015                return $counts;
     1016        }
     1017
     1018        /**
    10461019         * Get an associative array ( id => link ) with the list
    10471020         * of views available on this table.
    10481021         *
     
    10551028                $statuses       = _wp_privacy_statuses();
    10561029                $views          = array();
    10571030                $admin_url      = admin_url( 'tools.php?page=' . $this->request_type );
    1058                 $counts         = wp_count_posts( $this->post_type );
     1031                $counts         = $this->get_request_counts();
    10591032
    10601033                $current_link_attributes = empty( $current_status ) ? ' class="current" aria-current="page"' : '';
    10611034                $views['all']            = '<a href="' . esc_url( $admin_url ) . "\" $current_link_attributes>" . esc_html__( 'All' ) . ' <span class="count">(' . absint( array_sum( (array) $counts ) ) . ')</span></a>';
     
    11511124                $posts_per_page = 20;
    11521125                $args           = array(
    11531126                        'post_type'      => $this->post_type,
     1127                        'title'          => $this->request_type,
    11541128                        'posts_per_page' => $posts_per_page,
    11551129                        'offset'         => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page: 0,
    11561130                        'post_status'    => 'any',
     
    11661140                                $name_query,
    11671141                                'relation'  => 'AND',
    11681142                                array(
    1169                                         'key'     => '_user_email',
     1143                                        'key'     => '_wp_user_request_user_email',
    11701144                                        'value'   => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ): '',
    1171                                         'compare' => 'LIKE'
     1145                                        'compare' => 'LIKE',
    11721146                                ),
    11731147                        );
    11741148                }
    11751149
    1176                 $privacy_requests_query = new WP_Query( $args );
    1177                 $privacy_requests       = $privacy_requests_query->posts;
     1150                $requests_query = new WP_Query( $args );
     1151                $requests       = $requests_query->posts;
    11781152
    1179                 foreach ( $privacy_requests as $privacy_request ) {
    1180                         $this->items[] = array(
    1181                                 'request_id' => $privacy_request->ID,
    1182                                 'user_id'    => $privacy_request->post_author,
    1183                                 'email'      => get_post_meta( $privacy_request->ID, '_user_email', true ),
    1184                                 'action'     => get_post_meta( $privacy_request->ID, '_action_name', true ),
    1185                                 'requested'  => strtotime( $privacy_request->post_date_gmt ),
    1186                                 'confirmed'  => get_post_meta( $privacy_request->ID, '_confirmed_timestamp', true ),
    1187                                 'completed'  => get_post_meta( $privacy_request->ID, '_completed_timestamp', true ),
    1188                         );
     1153                foreach ( $requests as $request ) {
     1154                        $this->items[] = wp_get_user_request_data( $request->ID );
    11891155                }
    11901156
    11911157                $this->set_pagination_args(
    11921158                        array(
    1193                                 'total_items' => $privacy_requests_query->found_posts,
     1159                                'total_items' => $requests_query->found_posts,
    11941160                                'per_page'    => $posts_per_page,
    11951161                        )
    11961162                );
     
    12281194
    12291195                switch ( $status ) {
    12301196                        case 'request-confirmed':
    1231                                 $timestamp = $item['confirmed'];
     1197                                $timestamp = $item['confirmed_timestamp'];
    12321198                                break;
    12331199                        case 'request-completed':
    1234                                 $timestamp = $item['completed'];
     1200                                $timestamp = $item['completed_timestamp'];
    12351201                                break;
    12361202                }
    12371203
     
    12791245        public function column_default( $item, $column_name ) {
    12801246                $cell_value = $item[ $column_name ];
    12811247
    1282                 if ( in_array( $column_name, array( 'requested' ), true ) ) {
     1248                if ( in_array( $column_name, array( 'requested_timestamp' ), true ) ) {
    12831249                        return $this->get_timestamp_as_date( $cell_value );
    12841250                }
    12851251
     
    13521318         *
    13531319         * @var string $post_type The post type.
    13541320         */
    1355         protected $post_type = 'user_export_request';
     1321        protected $post_type = 'user_request';
    13561322
    13571323        /**
    13581324         * Actions column.
     
    14371403         *
    14381404         * @var string $post_type The post type.
    14391405         */
    1440         protected $post_type = 'user_remove_request';
     1406        protected $post_type = 'user_request';
    14411407
    14421408        /**
    14431409         * Actions column.
  • src/wp-includes/default-filters.php

     
    328328add_action( 'do_pings', 'do_all_pings', 10, 1 );
    329329add_action( 'do_robots', 'do_robots' );
    330330add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 );
    331 add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter', 10 );
    332 add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser', 10 );
    333331add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' );
    334332add_action( 'admin_print_scripts', 'print_emoji_detection_script' );
    335333add_action( 'admin_print_scripts', 'print_head_scripts', 20 );
     
    349347add_action( 'admin_init', 'send_frame_options_header', 10, 0 );
    350348add_action( 'welcome_panel', 'wp_welcome_panel' );
    351349
     350// Privacy
     351add_action( 'user_request_action_confirmed', '_wp_privacy_account_request_confirmed' );
     352add_filter( 'user_request_action_confirmed_message', '_wp_privacy_account_request_confirmed_message', 10, 2 );
     353add_filter( 'wp_privacy_personal_data_exporters', 'wp_register_comment_personal_data_exporter' );
     354add_filter( 'wp_privacy_personal_data_erasers', 'wp_register_comment_personal_data_eraser' );
     355
    352356// Cron tasks
    353357add_action( 'wp_scheduled_delete', 'wp_scheduled_delete' );
    354358add_action( 'wp_scheduled_auto_draft_delete', 'wp_delete_auto_drafts' );
  • src/wp-includes/post.php

     
    227227        );
    228228
    229229        register_post_type(
    230                 'user_export_request', array(
     230                'user_request', array(
    231231                        'labels'           => array(
    232                                 'name'          => __( 'Export Personal Data Requests' ),
    233                                 'singular_name' => __( 'Export Personal Data Request' ),
     232                                'name'          => __( 'User Requests' ),
     233                                'singular_name' => __( 'User Request' ),
    234234                        ),
    235235                        'public'           => false,
    236236                        '_builtin'         => true, /* internal use only. don't use this when registering your own post type. */
     
    239239                        'query_var'        => false,
    240240                        'can_export'       => false,
    241241                        'delete_with_user' => false,
     242                        'supports'         => array(),
    242243                )
    243244        );
    244245
    245         register_post_type(
    246                 'user_remove_request', array(
    247                         'labels'           => array(
    248                                 'name'          => __( 'Remove Personal Data Requests' ),
    249                                 'singular_name' => __( 'Remove Personal Data Request' ),
    250                         ),
    251                         'public'           => false,
    252                         '_builtin'         => true, /* internal use only. don't use this when registering your own post type. */
    253                         'hierarchical'     => false,
    254                         'rewrite'          => false,
    255                         'query_var'        => false,
    256                         'can_export'       => false,
    257                         'delete_with_user' => false,
    258                 )
    259         );
    260 
    261246        register_post_status(
    262247                'publish', array(
    263248                        'label'       => _x( 'Published', 'post status' ),
  • src/wp-includes/user.php

     
    28132813/**
    28142814 * Get all user privacy request types.
    28152815 *
    2816  * @since 5.0.0
     2816 * @since 4.9.6
    28172817 * @access private
    28182818 *
    28192819 * @return array
     
    28202820 */
    28212821function _wp_privacy_action_request_types() {
    28222822        return array(
    2823                 'user_export_request',
    2824                 'user_remove_request',
     2823                'export_personal_data',
     2824                'remove_personal_data',
    28252825        );
    28262826}
    28272827
     
    28282828/**
    28292829 * Update log when privacy request is confirmed.
    28302830 *
    2831  * @since 5.0.0
     2831 * @since 4.9.6
    28322832 * @access private
    28332833 *
    2834  * @param array $result Result of the request from the user.
     2834 * @param int $request_id ID of the request.
    28352835 */
    2836 function _wp_privacy_account_request_confirmed( $result ) {
    2837         if ( isset( $result['action'], $result['request_data'], $result['request_data']['privacy_request_id'] ) && in_array( $result['action'], _wp_privacy_action_request_types(), true ) ) {
    2838                 $privacy_request_id = absint( $result['request_data']['privacy_request_id'] );
    2839                 $privacy_request    = get_post( $privacy_request_id );
     2836function _wp_privacy_account_request_confirmed( $request_id ) {
     2837        $request_data = wp_get_user_request_data( $request_id );
    28402838
    2841                 if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
    2842                         return;
    2843                 }
     2839        if ( ! $request_data ) {
     2840                return;
     2841        }
    28442842
    2845                 update_post_meta( $privacy_request_id, '_confirmed_timestamp', time() );
    2846                 wp_update_post( array(
    2847                         'ID'          => $privacy_request_id,
    2848                         'post_status' => 'request-confirmed',
    2849                 ) );
     2843        if ( ! in_array( $request_data['status'], array( 'request-pending', 'request-failed' ), true ) ) {
     2844                return;
    28502845        }
     2846
     2847        update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() );
     2848        wp_update_post( array(
     2849                'ID'          => $request_data['request_id'],
     2850                'post_status' => 'request-confirmed',
     2851        ) );
    28512852}
    2852 add_action( 'account_action_confirmed', '_wp_privacy_account_request_confirmed' );
    28532853
    28542854/**
    2855  * Update log when privacy request fails.
     2855 * Return request confirmation message HTML.
    28562856 *
    2857  * @since 5.0.0
     2857 * @since 4.9.6
    28582858 * @access private
    28592859 *
    2860  * @param array $result Result of the request from the user.
     2860 * @return string $message The confirmation message.
    28612861 */
    2862 function _wp_privacy_account_request_failed( $result ) {
    2863         if ( isset( $result['action'], $result['request_data'], $result['request_data']['privacy_request_id'] ) &&
    2864                 in_array( $result['action'], _wp_privacy_action_request_types(), true ) ) {
     2862function _wp_privacy_account_request_confirmed_message( $message, $request_id ) {
     2863        $request = wp_get_user_request_data( $request_id );
    28652864
    2866                 $privacy_request_id = absint( $result['request_data']['privacy_request_id'] );
    2867                 $privacy_request    = get_post( $privacy_request_id );
     2865        if ( $request && in_array( $request['action'], _wp_privacy_action_request_types(), true ) ) {
     2866                $message = '<p class="message">' . __( 'Action has been confirmed.' ) . '</p>';
     2867                $message .= __( 'The site administrator has been notified and will fulfill your request as soon as possible.' );
     2868        }
    28682869
    2869                 if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
    2870                         return;
    2871                 }
    2872 
    2873                 wp_update_post( array(
    2874                         'ID'          => $privacy_request_id,
    2875                         'post_status' => 'request-failed',
    2876                 ) );
    2877         }
     2870        return $message;
    28782871}
    28792872
    28802873/**
    2881  * Send a confirmation request email to confirm an action.
     2874 * Create and log a user request to perform a specific action.
    28822875 *
    2883  * @since 5.0.0
     2876 * Requests are stored inside a post type named `user_request` since they can apply to both
     2877 * users on the site, or guests without a user account.
    28842878 *
    2885  * @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.
    2886  * @param string $action_name        Name of the action that is being confirmed. Defaults to 'confirm_email'.
    2887  * @param string $action_description User facing description of the action they will be confirming. Defaults to "confirm your email address".
    2888  * @param array  $request_data       Misc data you want to send with the verification request and pass to the actions once the request is confirmed.
    2889  * @return WP_Error|bool Will return true/false based on the success of sending the email, or a WP_Error object.
     2879 * @since 4.9.6
     2880 *
     2881 * @param string $email_address User email address. This can be the address of a registered or non-registered user.
     2882 * @param string $action_name   Name of the action that is being confirmed. Required.
     2883 * @param array  $request_data  Misc data you want to send with the verification request and pass to the actions once the request is confirmed.
     2884 * @return int|WP_Error Returns the request ID if successful, or a WP_Error object on failure.
    28902885 */
    2891 function wp_send_account_verification_key( $email = '', $action_name = '', $action_description = '', $request_data = array() ) {
    2892         if ( ! function_exists( 'wp_get_current_user' ) ) {
    2893                 return new WP_Error( 'invalid', __( 'This function cannot be used before init.' ) );
    2894         }
     2886function wp_create_user_request( $email_address = '', $action_name = '', $request_data = array() ) {
     2887        $email_address = sanitize_email( $email_address );
     2888        $action_name   = sanitize_key( $action_name );
    28952889
    2896         $action_name        = sanitize_key( $action_name );
    2897         $action_description = wp_kses_post( $action_description );
    2898 
    2899         if ( empty( $action_name ) ) {
    2900                 $action_name = 'confirm_email';
     2890        if ( ! is_email( $email_address ) ) {
     2891                return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
    29012892        }
    29022893
    2903         if ( empty( $action_description ) ) {
    2904                 $action_description = __( 'Confirm your email address.' );
     2894        if ( ! $action_name ) {
     2895                return new WP_Error( 'invalid_action', __( 'Invalid action name' ) );
    29052896        }
    29062897
    2907         if ( empty( $email ) ) {
    2908                 $user  = wp_get_current_user();
    2909                 $email = $user->ID ? $user->user_email : '';
    2910         } else {
    2911                 $user = false;
    2912         }
     2898        $user    = get_user_by( 'email', $email_address );
     2899        $user_id = $user && ! is_wp_error( $user ) ? $user->ID: 0;
    29132900
    2914         $email = sanitize_email( $email );
     2901        // Check for duplicates.
     2902        $requests_query = new WP_Query( array(
     2903                'post_type'   => 'user_request',
     2904                'title'       => $action_name,
     2905                'post_status' => 'any',
     2906                'fields'      => 'ids',
     2907                'meta_query'  => array(
     2908                        array(
     2909                                'key'     => '_wp_user_request_user_email',
     2910                                'value'   => $email_address,
     2911                        ),
     2912                ),
     2913        ) );
    29152914
    2916         if ( ! is_email( $email ) ) {
    2917                 return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
     2915        if ( $requests_query->found_posts ) {
     2916                return new WP_Error( 'duplicate_request', __( 'A request for this email address already exists.' ) );
    29182917        }
    29192918
    2920         if ( ! $user ) {
    2921                 $user = get_user_by( 'email', $email );
     2919        $request_id = wp_insert_post( array(
     2920                'post_author'   => $user_id,
     2921                'post_title'    => $action_name,
     2922                'post_content'  => wp_json_encode( $request_data ),
     2923                'post_status'   => 'request-pending',
     2924                'post_type'     => 'user_request',
     2925                'post_date'     => current_time( 'mysql', false ),
     2926                'post_date_gmt' => current_time( 'mysql', true ),
     2927        ), true );
     2928
     2929        if ( is_wp_error( $request_id ) ) {
     2930                return $request_id;
    29222931        }
    29232932
    2924         $confirm_key = wp_get_account_verification_key( $email, $action_name, $request_data );
     2933        update_post_meta( $request_id, '_wp_user_request_user_email', $email_address );
     2934        update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', false );
    29252935
    2926         if ( is_wp_error( $confirm_key ) ) {
    2927                 return $confirm_key;
     2936        return $request_id;
     2937}
     2938
     2939/**
     2940 * Get action description from the name and return a string.
     2941 *
     2942 * @since 4.9.6
     2943 *
     2944 * @param string $action_name Action name of the request.
     2945 * @return string
     2946 */
     2947function wp_user_request_action_description( $action_name ) {
     2948        switch ( $action_name ) {
     2949                case 'export_personal_data':
     2950                        $description = __( 'Export Personal Data' );
     2951                        break;
     2952                case 'remove_personal_data':
     2953                        $description = __( 'Remove Personal Data' );
     2954                        break;
     2955                default:
     2956                        /* translators: %s: action name */
     2957                        $description = sprintf( __( 'Confirm the "%s" action' ), $action_name );
     2958                        break;
    29282959        }
    29292960
    2930         // We could be dealing with a registered user account, or a visitor.
    2931         $is_registered_user = $user && ! is_wp_error( $user );
     2961        /**
     2962         * Filters the user action description.
     2963         *
     2964         * @param string $description The default description.
     2965         * @param string $action_name The name of the request.
     2966         */                             
     2967        return apply_filters( 'user_request_action_description', $description, $action_name );
     2968}
    29322969
    2933         if ( $is_registered_user ) {
    2934                 $uid = $user->ID;
    2935         } else {
    2936                 // 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.
    2937                 $uid = function_exists( 'hash' ) ? hash( 'sha256', $email ) : sha1( $email );
     2970/**
     2971 * Send a confirmation request email to confirm an action.
     2972 *
     2973 * If the request is not already pending, it will be updated.
     2974 *
     2975 * @since 4.9.6
     2976 *
     2977 * @param string $request_id ID of the request created via wp_create_user_request().
     2978 * @return WP_Error|bool Will return true/false based on the success of sending the email, or a WP_Error object.
     2979 */
     2980function wp_send_user_request( $request_id ) {
     2981        $request_id = absint( $request_id );
     2982        $request    = get_post( $request_id );
     2983
     2984        if ( ! $request || 'user_request' !== $request->post_type ) {
     2985                return new WP_Error( 'user_request_error', __( 'Invalid request.' ) );
    29382986        }
    29392987
     2988        if ( 'request-pending' !== $request->post_status ) {
     2989                wp_update_post( array(
     2990                        'ID'            => $request_id,
     2991                        'post_status'   => 'request-pending',
     2992                        'post_date'     => current_time( 'mysql', false ),
     2993                        'post_date_gmt' => current_time( 'mysql', true ),
     2994                ) );
     2995        }
     2996
     2997        $email_data = array(
     2998                'action_name' => $request->post_title,
     2999                'email'       => get_post_meta( $request->ID, '_wp_user_request_user_email', true ),
     3000                'description' => wp_user_request_action_description( $request->post_title ),
     3001                'confirm_url' => add_query_arg( array(
     3002                        'action'      => 'confirmaction',
     3003                        'request_id'  => $request_id,
     3004                        'confirm_key' => wp_generate_user_request_key( $request_id ),
     3005                ), site_url( 'wp-login.php' ) ),
     3006                'sitename'    => is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ),
     3007                'siteurl'     => network_home_url(),
     3008        );
     3009
    29403010        /* translators: Do not translate DESCRIPTION, CONFIRM_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
    29413011        $email_text = __(
    29423012                'Howdy,
     
    29583028###SITEURL###'
    29593029        );
    29603030
    2961         $email_data = array(
    2962                 'action_name' => $action_name,
    2963                 'email'       => $email,
    2964                 'description' => $action_description,
    2965                 'confirm_url' => add_query_arg( array(
    2966                         'action'         => 'verifyaccount',
    2967                         'confirm_action' => $action_name,
    2968                         'uid'            => $uid,
    2969                         'confirm_key'    => $confirm_key,
    2970                 ), site_url( 'wp-login.php' ) ),
    2971                 'sitename'    => is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ),
    2972                 'siteurl'     => network_home_url(),
    2973         );
    2974 
    29753031        /**
    29763032         * Filters the text of the email sent when an account action is attempted.
    29773033         *
     
    29833039         * ###SITENAME###           The name of the site.
    29843040         * ###SITEURL###            The URL to the site.
    29853041         *
    2986          * @since 5.0.0
     3042         * @since 4.9.6
    29873043         *
    29883044         * @param string $email_text     Text in the email.
    29893045         * @param array  $email_data {
     
    29973053         *     @type string $siteurl     The site URL sending the mail.
    29983054         * }
    29993055         */
    3000         $content = apply_filters( 'account_verification_email_content', $email_text, $email_data );
     3056        $content = apply_filters( 'user_request_action_email_content', $email_text, $email_data );
    30013057
    30023058        $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content );
    30033059        $content = str_replace( '###CONFIRM_URL###', esc_url_raw( $email_data['confirm_url'] ), $content );
     
    30103066}
    30113067
    30123068/**
    3013  * Creates, stores, then returns a confirmation key for an account action.
     3069 * Returns a confirmation key for a user action and stores the hashed version.
    30143070 *
    3015  * @since 5.0.0
     3071 * @since 4.9.6
    30163072 *
    3017  * @param string $email        User email address. This can be the address of a registered or non-registered user.
    3018  * @param string $action_name  Name of the action this key is being generated for.
    3019  * @param array  $request_data Misc data you want to send with the verification request and pass to the actions once the request is confirmed.
    3020  * @return string|WP_Error Confirmation key on success. WP_Error on error.
     3073 * @param int $request_id Request ID.
     3074 * @return string Confirmation key.
    30213075 */
    3022 function wp_get_account_verification_key( $email, $action_name, $request_data = array() ) {
     3076function wp_generate_user_request_key( $request_id ) {
    30233077        global $wp_hasher;
    30243078
    3025         if ( ! is_email( $email ) ) {
    3026                 return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
    3027         }
    3028 
    3029         if ( empty( $action_name ) ) {
    3030                 return new WP_Error( 'invalid_action', __( 'Invalid action' ) );
    3031         }
    3032 
    3033         $user = get_user_by( 'email', $email );
    3034 
    3035         // We could be dealing with a registered user account, or a visitor.
    3036         $is_registered_user = $user && ! is_wp_error( $user );
    3037 
    30383079        // Generate something random for a confirmation key.
    30393080        $key = wp_generate_password( 20, false );
    30403081
    3041         // Now insert the key, hashed, into the DB.
     3082        // Return the key, hashed.
    30423083        if ( empty( $wp_hasher ) ) {
    30433084                require_once ABSPATH . WPINC . '/class-phpass.php';
    30443085                $wp_hasher = new PasswordHash( 8, true );
    30453086        }
    30463087
    3047         $hashed_key = $wp_hasher->HashPassword( $key );
    3048         $value      = array(
    3049                 'action'       => $action_name,
    3050                 'time'         => time(),
    3051                 'hash'         => $hashed_key,
    3052                 'email'        => $email,
    3053                 'request_data' => $request_data,
    3054         );
     3088        update_post_meta( $request_id, '_wp_user_request_confirm_key', $wp_hasher->HashPassword( $key ) );
     3089        update_post_meta( $request_id, '_wp_user_request_confirm_key_timestamp', time() );
    30553090
    3056         if ( $is_registered_user ) {
    3057                 $key_saved = (bool) update_user_meta( $user->ID, '_verify_action_' . $action_name, wp_json_encode( $value ) );
    3058         } else {
    3059                 $uid       = function_exists( 'hash' ) ? hash( 'sha256', $email ) : sha1( $email );
    3060                 $key_saved = (bool) update_site_option( '_verify_action_' . $action_name . '_' . $uid, wp_json_encode( $value ) );
    3061         }
    3062 
    3063         if ( false === $key_saved ) {
    3064                 return new WP_Error( 'no_account_verification_key_update', __( 'Could not save confirm account action key to database.' ) );
    3065         }
    3066 
    30673091        return $key;
    30683092}
    30693093
    30703094/**
    3071  * Checks if a key is valid and handles the action based on this.
     3095 * Valdate a user request by comparing the key with the request's key.
    30723096 *
    3073  * @since 5.0.0
     3097 * @since 4.9.6
    30743098 *
    3075  * @param string $key         Key to confirm.
    3076  * @param string $uid         Email hash or user ID.
    3077  * @param string $action_name Name of the action this key is being generated for.
    3078  * @return array|WP_Error WP_Error on failure, action name and user email address on success.
     3099 * @param string $request_id ID of the request being confirmed.
     3100 * @param string $key        Provided key to validate.
     3101 * @return bool|WP_Error WP_Error on failure, true on success.
    30793102 */
    3080 function wp_check_account_verification_key( $key, $uid, $action_name ) {
     3103function wp_validate_user_request_key( $request_id, $key ) {
    30813104        global $wp_hasher;
    30823105
    3083         if ( empty( $action_name ) || empty( $key ) || empty( $uid ) ) {
    3084                 return new WP_Error( 'invalid_key', __( 'Invalid key' ) );
     3106        $request_id = absint( $request_id );
     3107        $request    = wp_get_user_request_data( $request_id );
     3108
     3109        if ( ! $request ) {
     3110                return new WP_Error( 'user_request_error', __( 'Invalid request.' ) );
    30853111        }
    30863112
    3087         $user = false;
     3113        if ( ! in_array( $request['status'], array( 'request-pending', 'request-failed' ), true ) ) {
     3114                return __( 'This link has expired.' );
     3115        }
    30883116
    3089         if ( is_numeric( $uid ) ) {
    3090                 $user = get_user_by( 'id', absint( $uid ) );
     3117        if ( empty( $key ) ) {
     3118                return new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    30913119        }
    30923120
    3093         // We could be dealing with a registered user account, or a visitor.
    3094         $is_registered_user = ( $user && ! is_wp_error( $user ) );
    3095         $key_request_time   = '';
    3096         $saved_key          = '';
    3097         $email              = '';
    3098 
    30993121        if ( empty( $wp_hasher ) ) {
    31003122                require_once ABSPATH . WPINC . '/class-phpass.php';
    31013123                $wp_hasher = new PasswordHash( 8, true );
    31023124        }
    31033125
    3104         // Get the saved key from the database.
    3105         if ( $is_registered_user ) {
    3106                 $raw_data = get_user_meta( $user->ID, '_verify_action_' . $action_name, true );
    3107                 $email    = $user->user_email;
     3126        $key_request_time = $request['confirm_key_timestamp'];
     3127        $saved_key        = $request['confirm_key'];
    31083128
    3109                 if ( false !== strpos( $raw_data, ':' ) ) {
    3110                         list( $key_request_time, $saved_key ) = explode( ':', $raw_data, 2 );
    3111                 }
    3112         } else {
    3113                 $raw_data = get_site_option( '_verify_action_' . $action_name . '_' . $uid, '' );
    3114 
    3115                 if ( false !== strpos( $raw_data, ':' ) ) {
    3116                         list( $key_request_time, $saved_key, $email ) = explode( ':', $raw_data, 3 );
    3117                 }
    3118         }
    3119 
    3120         $data             = json_decode( $raw_data, true );
    3121         $key_request_time = (int) isset( $data['time'] ) ? $data['time'] : 0;
    3122         $saved_key        = isset( $data['hash'] ) ? $data['hash'] : '';
    3123         $email            = sanitize_email( isset( $data['email'] ) ? $data['email'] : '' );
    3124         $request_data     = isset( $data['request_data'] ) ? $data['request_data'] : array();
    3125 
    31263129        if ( ! $saved_key ) {
    31273130                return new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    31283131        }
    31293132
    3130         if ( ! $key_request_time || ! $email ) {
     3133        if ( ! $key_request_time ) {
    31313134                return new WP_Error( 'invalid_key', __( 'Invalid action' ) );
    31323135        }
    31333136
     
    31343137        /**
    31353138         * Filters the expiration time of confirm keys.
    31363139         *
    3137          * @since 5.0.0
     3140         * @since 4.9.6
    31383141         *
    31393142         * @param int $expiration The expiration time in seconds.
    31403143         */
    3141         $expiration_duration = apply_filters( 'account_verification_expiration', DAY_IN_SECONDS );
     3144        $expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
    31423145        $expiration_time     = $key_request_time + $expiration_duration;
    31433146
    31443147        if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) {
     
    31453148                return new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    31463149        }
    31473150
    3148         if ( $expiration_time && time() < $expiration_time ) {
    3149                 $return = array(
    3150                         'action'       => $action_name,
    3151                         'email'        => $email,
    3152                         'request_data' => $request_data,
    3153                 );
    3154         } else {
     3151        if ( ! $expiration_time || time() > $expiration_time ) {
    31553152                $return = new WP_Error( 'expired_key', __( 'The confirmation email has expired.' ) );
    31563153        }
    31573154
    3158         // Clean up stored keys.
    3159         if ( $is_registered_user ) {
    3160                 delete_user_meta( $user->ID, '_verify_action_' . $action_name );
    3161         } else {
    3162                 delete_site_option( '_verify_action_' . $action_name . '_' . $uid );
     3155        return true;
     3156}
     3157
     3158/**
     3159 * Return data about a user request.
     3160 *
     3161 * @since 4.9.6
     3162 *
     3163 * @param int $request_id Request ID to get data about.
     3164 * @return array|false
     3165 */
     3166function wp_get_user_request_data( $request_id ) {
     3167        $request_id = absint( $request_id );
     3168        $request    = get_post( $request_id );
     3169
     3170        if ( ! $request || 'user_request' !== $request->post_type ) {
     3171                return false;
    31633172        }
    31643173
    3165         return $return;
     3174        return array(
     3175                'request_id'            => $request->ID,
     3176                'user_id'               => $request->post_author,
     3177                'email'                 => get_post_meta( $request->ID, '_wp_user_request_user_email', true ),
     3178                'action'                => $request->post_title,
     3179                'requested_timestamp'   => strtotime( $request->post_date_gmt ),
     3180                'confirmed_timestamp'   => get_post_meta( $request->ID, '_wp_user_request_confirmed_timestamp', true ),
     3181                'completed_timestamp'   => get_post_meta( $request->ID, '_wp_user_request_completed_timestamp', true ),
     3182                'request_data'          => json_decode( $request->post_content, true ),
     3183                'status'                => $request->post_status,
     3184                'confirm_key'           => get_post_meta( $request_id, '_wp_user_request_confirm_key', true ),
     3185                'confirm_key_timestamp' => get_post_meta( $request_id, '_wp_user_request_confirm_key_timestamp', true ),
     3186        );
    31663187}
  • src/wp-login.php

     
    427427}
    428428
    429429// validate action so as to default to the login screen
    430 if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'verifyaccount' ), true ) && false === has_filter( 'login_form_' . $action ) ) {
     430if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'confirmaction' ), true ) && false === has_filter( 'login_form_' . $action ) ) {
    431431        $action = 'login';
    432432}
    433433
     
    858858
    859859                break;
    860860
    861         case 'verifyaccount' :
    862                 if ( isset( $_GET['confirm_action'], $_GET['confirm_key'], $_GET['uid'] ) ) {
    863                         $key         = sanitize_text_field( wp_unslash( $_GET['confirm_key'] ) );
    864                         $uid         = sanitize_text_field( wp_unslash( $_GET['uid'] ) );
    865                         $action_name = sanitize_key( wp_unslash( $_GET['confirm_action'] ) );
    866                         $result      = wp_check_account_verification_key( $key, $uid, $action_name );
     861        case 'confirmaction' :
     862                if ( ! isset( $_GET['request_id'] ) ) {
     863                        wp_die( __( 'Invalid request' ) );
     864                }
     865
     866                $request_id = (int) $_GET['request_id'];
     867
     868                if ( isset( $_GET['confirm_key'] ) ) {
     869                        $key    = sanitize_text_field( wp_unslash( $_GET['confirm_key'] ) );
     870                        $result = wp_validate_user_request_key( $request_id, $key );
    867871                } else {
    868872                        $result = new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    869873                }
    870874
    871875                if ( is_wp_error( $result ) ) {
    872                         /**
    873                          * Fires an action hook when the account action was not confirmed.
    874                          *
    875                          * After running this action hook the page will die.
    876                          *
    877                          * @param WP_Error $result Error object.
    878                          */
    879                         do_action( 'account_action_failed', $result );
    880 
    881876                        wp_die( $result );
    882877                }
    883878
     
    890885                 * After firing this action hook the page will redirect to wp-login a callback
    891886                 * redirects or exits first.
    892887                 *
    893                  * @param array $result {
    894                  *     Data about the action which was confirmed.
    895                  *
    896                  *     @type string $action Name of the action that was confirmed.
    897                  *     @type string $email  Email of the user who confirmed the action.
    898                  * }
     888                 * @param int $request_id Request ID.
    899889                 */
    900                 do_action( 'account_action_confirmed', $result );
     890                do_action( 'user_request_action_confirmed', $request_id );
    901891
    902                 $message = '<p class="message">' . __( 'Action has been confirmed.' ) . '</p>';
    903                 login_header( '', $message );
     892                $message = apply_filters( 'user_request_action_confirmed_message', '<p class="message">' . __( 'Action has been confirmed.' ) . '</p>', $request_id );
     893
     894                login_header( __( 'User action confirmed.' ), $message );
    904895                login_footer();
    905896                exit;
    906897