| 2812 | |
| 2813 | /** |
| 2814 | * Send a confirmation request email to confirm an action. |
| 2815 | * |
| 2816 | * @since 5.0.0 |
| 2817 | * |
| 2818 | * @param string $action_name Name of the action that is being confirmed. |
| 2819 | * @param string $action_description User facing description of the action they will be confirming. |
| 2820 | * @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. |
| 2821 | * |
| 2822 | * @return WP_ERROR|bool Will return true/false based on the success of sending the email, or a WP_Error object. |
| 2823 | */ |
| 2824 | function send_confirm_account_action_email( $action_name, $action_description = '', $email = '' ) { |
| 2825 | $action_name = sanitize_key( $action_name ); |
| 2826 | $action_description = wp_kses_post( $action_description ); |
| 2827 | |
| 2828 | if ( empty( $action_name ) ) { |
| 2829 | return new WP_Error( 'invalid_action', __( 'Invalid action' ) ); |
| 2830 | } |
| 2831 | |
| 2832 | if ( empty( $email ) ) { |
| 2833 | $user = wp_get_current_user(); |
| 2834 | $email = $user->ID ? $user->user_email : ''; |
| 2835 | } else { |
| 2836 | $user = false; |
| 2837 | } |
| 2838 | |
| 2839 | $email = sanitize_email( $email ); |
| 2840 | |
| 2841 | if ( ! is_email( $email ) ) { |
| 2842 | return new WP_Error( 'invalid_email', __( 'Invalid email address' ) ); |
| 2843 | } |
| 2844 | |
| 2845 | if ( ! $user ) { |
| 2846 | $user = get_user_by( 'email', $email ); |
| 2847 | } |
| 2848 | |
| 2849 | // We could be dealing with a registered user account, or a visitor. |
| 2850 | $is_registered_user = $user && ! is_wp_error( $user ); |
| 2851 | $uid = $is_registered_user ? $user->ID : hash( 'sha256', $email ); |
| 2852 | $confirm_key = get_confirm_account_action_key( $action_name, $email ); |
| 2853 | |
| 2854 | if ( is_wp_error( $confirm_key ) ) { |
| 2855 | return $confirm_key; |
| 2856 | } |
| 2857 | |
| 2858 | // Prepare the email content. |
| 2859 | if ( ! $action_description ) { |
| 2860 | $action_description = $action_name; |
| 2861 | } |
| 2862 | |
| 2863 | /* translators: Do not translate DESCRIPTION, CONFIRM_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */ |
| 2864 | $email_text = __( |
| 2865 | 'Howdy, |
| 2866 | |
| 2867 | An account linked to your email address has requested to perform |
| 2868 | the following action: |
| 2869 | |
| 2870 | ###DESCRIPTION### |
| 2871 | |
| 2872 | To confirm this action, please click on the following link: |
| 2873 | ###CONFIRM_URL### |
| 2874 | |
| 2875 | You can safely ignore and delete this email if you do not want to |
| 2876 | take this action. |
| 2877 | |
| 2878 | This email has been sent to ###EMAIL###. |
| 2879 | |
| 2880 | Regards, |
| 2881 | All at ###SITENAME### |
| 2882 | ###SITEURL###' |
| 2883 | ); |
| 2884 | |
| 2885 | $email_data = array( |
| 2886 | 'action_name' => $action_name, |
| 2887 | 'email' => $email, |
| 2888 | 'description' => $action_description, |
| 2889 | 'confirm_url' => add_query_arg( array( |
| 2890 | 'action' => 'emailconfirm', |
| 2891 | 'confirm_action' => $action_name, |
| 2892 | 'uid' => $uid, |
| 2893 | 'confirm_key' => $confirm_key, |
| 2894 | ), site_url( 'wp-login.php' ) ), |
| 2895 | 'sitename' => is_multisite() ? get_site_option( 'site_name' ) : get_option( 'blogname' ), |
| 2896 | 'siteurl' => network_home_url(), |
| 2897 | ); |
| 2898 | |
| 2899 | /** |
| 2900 | * Filters the text of the email sent when an account action is attempted. |
| 2901 | * |
| 2902 | * The following strings have a special meaning and will get replaced dynamically: |
| 2903 | * ###USERNAME### The user's username, if the user has an account. Prefixed with single space. Otherwise left blank. |
| 2904 | * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. |
| 2905 | * ###CONFIRM_URL### The link to click on to confirm the account action. |
| 2906 | * ###EMAIL### The email we are sending to. |
| 2907 | * ###SITENAME### The name of the site. |
| 2908 | * ###SITEURL### The URL to the site. |
| 2909 | * |
| 2910 | * @param string $email_text Text in the email. |
| 2911 | * @param array $email_data { |
| 2912 | * Data relating to the account action email. |
| 2913 | * |
| 2914 | * @type string $action_name Name of the action being performed. |
| 2915 | * @type string $email The email address this is being sent to. |
| 2916 | * @type string $description Description of the action being performed so the user knows what the email is for. |
| 2917 | * @type string $confirm_url The link to click on to confirm the account action. |
| 2918 | * @type string $sitename The site name sending the mail. |
| 2919 | * @type string $siteurl The site URL sending the mail. |
| 2920 | * } |
| 2921 | */ |
| 2922 | $content = apply_filters( 'confirm_account_action_email_content', $email_text, $email_data ); |
| 2923 | |
| 2924 | $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content ); |
| 2925 | $content = str_replace( '###CONFIRM_URL###', esc_url_raw( $email_data['confirm_url'] ), $content ); |
| 2926 | $content = str_replace( '###EMAIL###', $email_data['email'], $content ); |
| 2927 | $content = str_replace( '###SITENAME###', wp_specialchars_decode( $email_data['sitename'], ENT_QUOTES ), $content ); |
| 2928 | $content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content ); |
| 2929 | |
| 2930 | /* translators: %s Site name. */ |
| 2931 | return wp_mail( $email_data['email'], sprintf( __( '[%s] Confirm Account Action' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ), $content ); |
| 2932 | } |
| 2933 | |
| 2934 | /** |
| 2935 | * Creates, stores, then returns a confirmation key for an account action. |
| 2936 | * |
| 2937 | * @since 5.0.0 |
| 2938 | * |
| 2939 | * @param string $action_name Name of the action this key is being generated for. |
| 2940 | * @param string $email User email address. This can be the address of a registered or non-registered user. |
| 2941 | * |
| 2942 | * @return string|WP_Error Confirmation key on success. WP_Error on error. |
| 2943 | */ |
| 2944 | function get_confirm_account_action_key( $action_name, $email ) { |
| 2945 | 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 ); |
| 2959 | |
| 2960 | // Generate something random for a confirmation key. |
| 2961 | $key = wp_generate_password( 20, false ); |
| 2962 | |
| 2963 | // Now insert the key, hashed, into the DB. |
| 2964 | if ( empty( $wp_hasher ) ) { |
| 2965 | require_once ABSPATH . WPINC . '/class-phpass.php'; |
| 2966 | $wp_hasher = new PasswordHash( 8, true ); |
| 2967 | } |
| 2968 | |
| 2969 | $hashed_key = $wp_hasher->HashPassword( $key ); |
| 2970 | |
| 2971 | if ( $is_registered_user ) { |
| 2972 | $key_saved = (bool) update_user_meta( $user->ID, '_account_action_' . $action_name, implode( ':', array( time(), $hashed_key ) ) ); |
| 2973 | } else { |
| 2974 | $key_saved = (bool) update_site_option( '_account_action_' . hash( 'sha256', $email ) . '_' . $action_name, implode( ':', array( time(), $hashed_key, $email ) ) ); |
| 2975 | } |
| 2976 | |
| 2977 | if ( false === $key_saved ) { |
| 2978 | return new WP_Error( 'no_confirm_account_action_key_update', __( 'Could not save confirm account action key to database.' ) ); |
| 2979 | } |
| 2980 | |
| 2981 | return $key; |
| 2982 | } |
| 2983 | |
| 2984 | /** |
| 2985 | * Checks if a key is valid and handles the action based on this. |
| 2986 | * |
| 2987 | * @since 5.0.0 |
| 2988 | * |
| 2989 | * @param string $action_name Name of the action this key is being generated for. |
| 2990 | * @param string $key Key to confirm. |
| 2991 | * @param string $uid Email hash or user ID. |
| 2992 | * |
| 2993 | * @return array|WP_Error WP_Error on failure, action name and user email address on success. |
| 2994 | */ |
| 2995 | function check_confirm_account_action_key( $action_name, $key, $uid ) { |
| 2996 | global $wp_hasher; |
| 2997 | |
| 2998 | if ( ! empty( $action_name ) && ! empty( $key ) && ! empty( $uid ) ) { |
| 2999 | $user = false; |
| 3000 | |
| 3001 | if ( is_numeric( $uid ) ) { |
| 3002 | $user = get_user_by( 'id', absint( $uid ) ); |
| 3003 | } |
| 3004 | |
| 3005 | // We could be dealing with a registered user account, or a visitor. |
| 3006 | $is_registered_user = $user && ! is_wp_error( $user ); |
| 3007 | $key_request_time = ''; |
| 3008 | $saved_key = ''; |
| 3009 | $email = ''; |
| 3010 | |
| 3011 | if ( empty( $wp_hasher ) ) { |
| 3012 | require_once ABSPATH . WPINC . '/class-phpass.php'; |
| 3013 | $wp_hasher = new PasswordHash( 8, true ); |
| 3014 | } |
| 3015 | |
| 3016 | // Get the saved key from the database. |
| 3017 | if ( $is_registered_user ) { |
| 3018 | $confirm_action_data = get_user_meta( $user->ID, '_account_action_' . $action_name, true ); |
| 3019 | $email = $user->user_email; |
| 3020 | |
| 3021 | if ( false !== strpos( $confirm_action_data, ':' ) ) { |
| 3022 | list( $key_request_time, $saved_key ) = explode( ':', $confirm_action_data, 2 ); |
| 3023 | } |
| 3024 | } else { |
| 3025 | $confirm_action_data = get_site_option( '_account_action_' . $uid . '_' . $action_name, '' ); |
| 3026 | |
| 3027 | if ( false !== strpos( $confirm_action_data, ':' ) ) { |
| 3028 | list( $key_request_time, $email, $saved_key ) = explode( ':', $confirm_action_data, 3 ); |
| 3029 | } |
| 3030 | } |
| 3031 | |
| 3032 | if ( ! $saved_key ) { |
| 3033 | return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); |
| 3034 | } |
| 3035 | |
| 3036 | /** |
| 3037 | * Filters the expiration time of confirm keys. |
| 3038 | * |
| 3039 | * @param int $expiration The expiration time in seconds. |
| 3040 | */ |
| 3041 | $expiration_duration = apply_filters( 'account_action_expiration', DAY_IN_SECONDS ); |
| 3042 | $expiration_time = $key_request_time + $expiration_duration; |
| 3043 | |
| 3044 | $hash_is_correct = $wp_hasher->CheckPassword( $key, $saved_key ); |
| 3045 | |
| 3046 | if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) { |
| 3047 | // Clean up stored keys. |
| 3048 | if ( $is_registered_user ) { |
| 3049 | delete_user_meta( $user->ID, '_account_action_' . $action_name ); |
| 3050 | } else { |
| 3051 | delete_site_option( '_account_action_' . $uid . '_' . $action_name ); |
| 3052 | } |
| 3053 | return array( |
| 3054 | 'action' => $action_name, |
| 3055 | 'email' => $email, |
| 3056 | ); |
| 3057 | } elseif ( $expiration_time ) { |
| 3058 | return new WP_Error( 'expired_key', __( 'Invalid key' ) ); |
| 3059 | } |
| 3060 | } |
| 3061 | return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); |
| 3062 | } |
| 3063 | No newline at end of file |