Make WordPress Core


Ignore:
Timestamp:
04/27/2018 10:12:01 AM (7 years ago)
Author:
azaozz
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.
See #43443.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/user.php

    r42967 r43008  
    28142814 * Get all user privacy request types.
    28152815 *
    2816  * @since 5.0.0
     2816 * @since 4.9.6
    28172817 * @access private
    28182818 *
     
    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}
     
    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.
    2835  */
    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 );
    2840 
    2841         if ( ! $privacy_request || ! in_array( $privacy_request->post_type, _wp_privacy_action_request_types(), true ) ) {
    2842             return;
    2843         }
    2844 
    2845         update_post_meta( $privacy_request_id, '_confirmed_timestamp', time() );
     2834 * @param int $request_id ID of the request.
     2835 */
     2836function _wp_privacy_account_request_confirmed( $request_id ) {
     2837    $request_data = wp_get_user_request_data( $request_id );
     2838
     2839    if ( ! $request_data ) {
     2840        return;
     2841    }
     2842
     2843    if ( ! in_array( $request_data['status'], array( 'request-pending', 'request-failed' ), true ) ) {
     2844        return;
     2845    }
     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    ) );
     2852}
     2853
     2854/**
     2855 * Return request confirmation message HTML.
     2856 *
     2857 * @since 4.9.6
     2858 * @access private
     2859 *
     2860 * @return string $message The confirmation message.
     2861 */
     2862function _wp_privacy_account_request_confirmed_message( $message, $request_id ) {
     2863    $request = wp_get_user_request_data( $request_id );
     2864
     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    }
     2869
     2870    return $message;
     2871}
     2872
     2873/**
     2874 * Create and log a user request to perform a specific action.
     2875 *
     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.
     2878 *
     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.
     2885 */
     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 );
     2889
     2890    if ( ! is_email( $email_address ) ) {
     2891        return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
     2892    }
     2893
     2894    if ( ! $action_name ) {
     2895        return new WP_Error( 'invalid_action', __( 'Invalid action name' ) );
     2896    }
     2897
     2898    $user    = get_user_by( 'email', $email_address );
     2899    $user_id = $user && ! is_wp_error( $user ) ? $user->ID: 0;
     2900
     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    ) );
     2914
     2915    if ( $requests_query->found_posts ) {
     2916        return new WP_Error( 'duplicate_request', __( 'A request for this email address already exists.' ) );
     2917    }
     2918
     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;
     2931    }
     2932
     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 );
     2935
     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;
     2959    }
     2960
     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}
     2969
     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.' ) );
     2986    }
     2987
     2988    if ( 'request-pending' !== $request->post_status ) {
    28462989        wp_update_post( array(
    2847             'ID'          => $privacy_request_id,
    2848             'post_status' => 'request-confirmed',
     2990            'ID'            => $request_id,
     2991            'post_status'   => 'request-pending',
     2992            'post_date'     => current_time( 'mysql', false ),
     2993            'post_date_gmt' => current_time( 'mysql', true ),
    28492994        ) );
    28502995    }
    2851 }
    2852 add_action( 'account_action_confirmed', '_wp_privacy_account_request_confirmed' );
    2853 
    2854 /**
    2855  * Update log when privacy request fails.
    2856  *
    2857  * @since 5.0.0
    2858  * @access private
    2859  *
    2860  * @param array $result Result of the request from the user.
    2861  */
    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 ) ) {
    2865 
    2866         $privacy_request_id = absint( $result['request_data']['privacy_request_id'] );
    2867         $privacy_request    = get_post( $privacy_request_id );
    2868 
    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     }
    2878 }
    2879 
    2880 /**
    2881  * Send a confirmation request email to confirm an action.
    2882  *
    2883  * @since 5.0.0
    2884  *
    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.
    2890  */
    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     }
    2895 
    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';
    2901     }
    2902 
    2903     if ( empty( $action_description ) ) {
    2904         $action_description = __( 'Confirm your email address.' );
    2905     }
    2906 
    2907     if ( empty( $email ) ) {
    2908         $user  = wp_get_current_user();
    2909         $email = $user->ID ? $user->user_email : '';
    2910     } else {
    2911         $user = false;
    2912     }
    2913 
    2914     $email = sanitize_email( $email );
    2915 
    2916     if ( ! is_email( $email ) ) {
    2917         return new WP_Error( 'invalid_email', __( 'Invalid email address' ) );
    2918     }
    2919 
    2920     if ( ! $user ) {
    2921         $user = get_user_by( 'email', $email );
    2922     }
    2923 
    2924     $confirm_key = wp_get_account_verification_key( $email, $action_name, $request_data );
    2925 
    2926     if ( is_wp_error( $confirm_key ) ) {
    2927         return $confirm_key;
    2928     }
    2929 
    2930     // We could be dealing with a registered user account, or a visitor.
    2931     $is_registered_user = $user && ! is_wp_error( $user );
    2932 
    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 );
    2938     }
     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    );
    29393009
    29403010    /* translators: Do not translate DESCRIPTION, CONFIRM_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
     
    29573027All at ###SITENAME###
    29583028###SITEURL###'
    2959     );
    2960 
    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(),
    29733029    );
    29743030
     
    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.
     
    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 );
     
    30113067
    30123068/**
    3013  * Creates, stores, then returns a confirmation key for an account action.
    3014  *
    3015  * @since 5.0.0
    3016  *
    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.
    3021  */
    3022 function wp_get_account_verification_key( $email, $action_name, $request_data = array() ) {
     3069 * Returns a confirmation key for a user action and stores the hashed version.
     3070 *
     3071 * @since 4.9.6
     3072 *
     3073 * @param int $request_id Request ID.
     3074 * @return string Confirmation key.
     3075 */
     3076function wp_generate_user_request_key( $request_id ) {
    30233077    global $wp_hasher;
    3024 
    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 );
    30373078
    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';
     
    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     );
    3055 
    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     }
     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() );
    30663090
    30673091    return $key;
     
    30693093
    30703094/**
    3071  * Checks if a key is valid and handles the action based on this.
    3072  *
    3073  * @since 5.0.0
    3074  *
    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.
    3079  */
    3080 function wp_check_account_verification_key( $key, $uid, $action_name ) {
     3095 * Valdate a user request by comparing the key with the request's key.
     3096 *
     3097 * @since 4.9.6
     3098 *
     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.
     3102 */
     3103function wp_validate_user_request_key( $request_id, $key ) {
    30813104    global $wp_hasher;
    30823105
    3083     if ( empty( $action_name ) || empty( $key ) || empty( $uid ) ) {
     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.' ) );
     3111    }
     3112
     3113    if ( ! in_array( $request['status'], array( 'request-pending', 'request-failed' ), true ) ) {
     3114        return __( 'This link has expired.' );
     3115    }
     3116
     3117    if ( empty( $key ) ) {
    30843118        return new WP_Error( 'invalid_key', __( 'Invalid key' ) );
    30853119    }
    3086 
    3087     $user = false;
    3088 
    3089     if ( is_numeric( $uid ) ) {
    3090         $user = get_user_by( 'id', absint( $uid ) );
    3091     }
    3092 
    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              = '';
    30983120
    30993121    if ( empty( $wp_hasher ) ) {
     
    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;
    3108 
    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();
     3126    $key_request_time = $request['confirm_key_timestamp'];
     3127    $saved_key        = $request['confirm_key'];
    31253128
    31263129    if ( ! $saved_key ) {
     
    31283131    }
    31293132
    3130     if ( ! $key_request_time || ! $email ) {
     3133    if ( ! $key_request_time ) {
    31313134        return new WP_Error( 'invalid_key', __( 'Invalid action' ) );
    31323135    }
     
    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
     
    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 );
    3163     }
    3164 
    3165     return $return;
    3166 }
     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;
     3172    }
     3173
     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    );
     3187}
Note: See TracChangeset for help on using the changeset viewer.