WordPress.org

Make WordPress Core


Ignore:
Timestamp:
05/02/2018 01:00:46 AM (22 months 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:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/4.9

  • 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}
Note: See TracChangeset for help on using the changeset viewer.