Files
Mykyta Synelnikov bcfdbb954d Refactor email placeholders and deprecate obsolete methods
Streamline placeholder handling for emails by introducing universal patterns and replacements, and update email dispatch functions for efficiency. Deprecated various redundant user and password-related methods, consolidating logic into common utility classes for better maintainability.
2025-05-19 14:26:39 +03:00

722 lines
23 KiB
PHP

<?php
namespace um\core;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'um\core\Password' ) ) {
/**
* Class Password
* @package um\core
*/
class Password {
/**
* @var bool
*/
private $change_password = false;
/**
* Password constructor.
*/
public function __construct() {
add_shortcode( 'ultimatemember_password', array( &$this, 'ultimatemember_password' ) );
add_action( 'template_redirect', array( &$this, 'form_init' ), 10001 );
add_action( 'um_reset_password_errors_hook', array( &$this, 'um_reset_password_errors_hook' ) );
add_action( 'um_reset_password_process_hook', array( &$this, 'um_reset_password_process_hook' ) );
add_action( 'um_change_password_errors_hook', array( &$this, 'um_change_password_errors_hook' ) );
add_action( 'um_change_password_process_hook', array( &$this, 'um_change_password_process_hook' ) );
}
/**
* Get Reset URL
*
* @param int|null $user_id
*
* @return string
*/
public function reset_url( $user_id = null ) {
static $reset_key = null;
if ( is_null( $user_id ) ) {
$user_id = um_user( 'ID' );
}
delete_option( "um_cache_userdata_{$user_id}" );
// New reset password key via WordPress native field. It maybe already exists here but generated twice to make sure that emailed with a proper and fresh hash.
// But doing that only once in 1 request using static variable. Different email placeholders can use reset_url() and we have to use 1 time generated to avoid invalid keys.
$user_data = get_userdata( $user_id );
if ( empty( $user_data ) ) {
return '';
}
if ( empty( $reset_key ) ) {
$reset_key = UM()->common()->users()->maybe_generate_password_reset_key( $user_data );
}
// This link looks like WordPress native link e.g. wp-login.php?action=rp&key={hash}&login={user_login}
return add_query_arg(
array(
'act' => 'reset_password',
'hash' => $reset_key,
'login' => rawurlencode( $user_data->user_login ),
),
um_get_core_page( 'password-reset' )
);
}
/**
* Add class based on shortcode
*
* @param string $mode
*
* @return string
*/
function get_class( $mode ) {
$classes = 'um-'.$mode;
if ( is_admin() ) {
$classes .= ' um-in-admin';
}
if ( true === UM()->fields()->editing ) {
$classes .= ' um-editing';
}
if ( true === UM()->fields()->viewing ) {
$classes .= ' um-viewing';
}
/**
* UM hook
*
* @type filter
* @title um_form_official_classes__hook
* @description Change form additional classes
* @input_vars
* [{"var":"$classes","type":"string","desc":"Form additional classes"}]
* @change_log
* ["Since: 2.0"]
* @usage
* <?php add_filter( 'um_form_official_classes__hook', 'function_name', 10, 1 ); ?>
* @example
* <?php
* add_filter( 'um_form_official_classes__hook', 'my_form_official_classes', 10, 1 );
* function my_form_official_classes( $classes ) {
* // your code here
* return $classes;
* }
* ?>
*/
$classes = apply_filters( 'um_form_official_classes__hook', $classes );
return $classes;
}
/**
* Shortcode
*
* @param array $args
*
* @return string
*/
public function ultimatemember_password( $args = array() ) {
/** There is possible to use 'shortcode_atts_ultimatemember_password' filter for getting customized $atts. This filter is documented in wp-includes/shortcodes.php "shortcode_atts_{$shortcode}" */
$args = shortcode_atts(
array(
'template' => 'password-reset',
'mode' => 'password',
'form_id' => 'um_password_id',
'max_width' => '450px',
'align' => 'center',
),
$args,
'ultimatemember_password'
);
if ( empty( $args['use_custom_settings'] ) ) {
$args = array_merge( $args, UM()->shortcodes()->get_css_args( $args ) );
} else {
$args = array_merge( UM()->shortcodes()->get_css_args( $args ), $args );
}
/**
* Filters extend Reset Password Arguments
*
* @since 1.3.x
* @hook um_reset_password_shortcode_args_filter
*
* @param {array} $args Shortcode arguments.
*
* @return {array} Shortcode arguments.
*
* @example <caption>Extend Reset Password Arguments.</caption>
* function my_reset_password_shortcode_args( $args ) {
* // your code here
* return $args;
* }
* add_filter( 'um_reset_password_shortcode_args_filter', 'my_reset_password_shortcode_args', 10, 1 );
*/
$args = apply_filters( 'um_reset_password_shortcode_args_filter', $args );
if ( false !== $this->change_password ) {
// then COOKIE are valid then get data from them and populate hidden fields for the password reset form
$args['rp_mode'] = 'pw_change';
$args['template'] = 'password-change';
$args['rp_key'] = '';
$rp_cookie = 'wp-resetpass-' . COOKIEHASH;
if ( isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) {
list( $rp_login, $rp_key ) = explode( ':', wp_unslash( $_COOKIE[ $rp_cookie ] ), 2 );
$args['login'] = $rp_login;
$args['rp_key'] = $rp_key;
$rp_user_obj = get_user_by( 'login', $rp_login );
if ( false !== $rp_user_obj ) {
$set_password_required = get_user_meta( $rp_user_obj->ID, 'um_set_password_required', true );
if ( ! empty( $set_password_required ) ) {
$args['rp_mode'] = 'pw_set';
}
}
}
}
if ( empty( $args['mode'] ) || empty( $args['template'] ) ) {
return '';
}
UM()->fields()->set_id = absint( $args['form_id'] );
ob_start();
/** This filter is documented in includes/core/class-shortcodes.php */
do_action( "um_pre_{$args['mode']}_shortcode", $args );
/** This filter is documented in includes/core/class-shortcodes.php */
do_action( 'um_before_form_is_loaded', $args );
/** This filter is documented in includes/core/class-shortcodes.php */
do_action( "um_before_{$args['mode']}_form_is_loaded", $args );
UM()->shortcodes()->template_load( $args['template'], $args );
if ( ! is_admin() && ! defined( 'DOING_AJAX' ) ) {
UM()->shortcodes()->dynamic_css( $args );
}
return ob_get_clean();
}
/**
* Check if a legitimate password reset request is in action
*
* @return bool
*/
public function is_reset_request() {
// phpcs:ignore WordPress.Security.NonceVerification -- already verified here
return ! empty( $_POST['_um_password_reset'] );
}
/**
* Check if a legitimate password change request is in action
*
* works both for the Account > Password form and the Reset Password shortcode form
*
* @return bool
*/
public function is_change_request() {
// phpcs:ignore WordPress.Security.NonceVerification -- already verified here
if ( ! empty( $_POST['_um_account'] ) && isset( $_POST['_um_account_tab'] ) && 'password' === sanitize_key( $_POST['_um_account_tab'] ) ) {
return true;
}
if ( ! empty( $_POST['_um_password_change'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- already verified here
return true;
}
return false;
}
/**
* Password page form
*/
public function form_init() {
if ( um_is_core_page( 'password-reset' ) ) {
UM()->fields()->set_mode = 'password';
}
// validate $rp_cookie and hash via check_password_reset_key
if ( um_is_core_page( 'password-reset' ) && isset( $_REQUEST['act'] ) && 'reset_password' === sanitize_key( $_REQUEST['act'] ) ) {
wp_fix_server_vars();
$rp_cookie = 'wp-resetpass-' . COOKIEHASH;
if ( isset( $_GET['hash'] ) && isset( $_GET['login'] ) ) {
$value = sprintf( '%s:%s', wp_unslash( $_GET['login'] ), wp_unslash( $_GET['hash'] ) );
$this->setcookie( $rp_cookie, $value );
// Not `um_safe_redirect()` because password-reset page is predefined page and is situated on the same host.
wp_safe_redirect( remove_query_arg( array( 'hash', 'login' ) ) );
exit;
}
if ( isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) {
list( $rp_login, $rp_key ) = explode( ':', wp_unslash( $_COOKIE[ $rp_cookie ] ), 2 );
$user = check_password_reset_key( $rp_key, $rp_login );
if ( isset( $_POST['user_password'] ) && ! hash_equals( $rp_key, $_POST['rp_key'] ) ) {
$user = false;
}
} else {
$user = false;
}
if ( ! $user || is_wp_error( $user ) ) {
$this->setcookie( $rp_cookie, false );
if ( $user && 'expired_key' === $user->get_error_code() ) {
wp_redirect( add_query_arg( array( 'updated' => 'expiredkey' ), um_get_core_page( 'password-reset' ) ) );
} else {
wp_redirect( add_query_arg( array( 'updated' => 'invalidkey' ), um_get_core_page( 'password-reset' ) ) );
}
exit;
}
// this variable is used for populating the reset password form via the hash and login
$this->change_password = true;
}
if ( $this->is_reset_request() ) {
$form_data = array(
'mode' => 'password',
);
UM()->form()->post_form = wp_unslash( $_POST );
if ( empty( UM()->form()->post_form['mode'] ) ) {
UM()->form()->post_form['mode'] = 'password';
}
/**
* Fires for handle validate errors on the reset password form submit.
*
* @since 1.3.x
* @since 2.6.8 Added $form_data attribute.
*
* @hook um_reset_password_errors_hook
*
* @param {array} $submission_data Form submitted data.
* @param {array} $form_data Form data. Since 2.6.8
*
* @example <caption>Make any custom validation on password reset form.</caption>
* function my_reset_password_errors( $submission_data, $form_data ) {
* // your code here
* }
* add_action( 'um_reset_password_errors_hook', 'my_reset_password_errors', 10, 2 );
*/
do_action( 'um_reset_password_errors_hook', UM()->form()->post_form, $form_data );
if ( ! isset( UM()->form()->errors ) ) {
/**
* Fires for handle the reset password form when submitted data is valid.
*
* @since 1.3.x
* @since 2.6.8 Added $form_data attribute.
*
* @hook um_reset_password_process_hook
*
* @param {array} $submission_data Form submitted data.
* @param {array} $form_data Form data. Since 2.6.8
*
* @example <caption>Make any custom action when password reset form is submitted.</caption>
* function my_reset_password_process( $submission_data, $form_data ) {
* // your code here
* }
* add_action( 'um_reset_password_process_hook', 'my_reset_password_process', 10, 2 );
*/
do_action( 'um_reset_password_process_hook', UM()->form()->post_form, $form_data );
}
}
if ( $this->is_change_request() ) {
$formdata = wp_unslash( $_POST );
// Don't un-slash passwords in manner of WordPress native password field.
$fields_map = array(
'user_password',
'confirm_user_password',
);
$formdata = UM()->form()::ignore_formdata_unslash( $formdata, $fields_map );
UM()->form()->post_form = $formdata;
/**
* UM hook
*
* @type action
* @title um_change_password_errors_hook
* @description Action on change password submit form
* @input_vars
* [{"var":"$post","type":"array","desc":"Form submitted"}]
* @change_log
* ["Since: 2.0"]
* @usage add_action( 'um_change_password_errors_hook', 'function_name', 10, 1 );
* @example
* <?php
* add_action( 'um_change_password_errors_hook', 'my_change_password_errors', 10, 1 );
* function my_change_password_errors( $post ) {
* // your code here
* }
* ?>
*/
do_action( 'um_change_password_errors_hook', UM()->form()->post_form );
if ( ! isset( UM()->form()->errors ) ) {
/**
* UM hook
*
* @type action
* @title um_change_password_process_hook
* @description Action on change password success submit form
* @input_vars
* [{"var":"$post","type":"array","desc":"Form submitted"}]
* @change_log
* ["Since: 2.0"]
* @usage add_action( 'um_change_password_process_hook', 'function_name', 10, 1 );
* @example
* <?php
* add_action( 'um_change_password_process_hook', 'my_change_password_process', 10, 1 );
* function my_change_password_process( $post ) {
* // your code here
* }
* ?>
*/
do_action( 'um_change_password_process_hook', UM()->form()->post_form );
}
}
}
/**
* Error handler: reset password
*
* @param $args
*/
public function um_reset_password_errors_hook( $args ) {
if ( isset( $args[ UM()->honeypot ] ) && '' !== $args[ UM()->honeypot ] ) {
wp_die( esc_html__( 'Hello, spam bot!', 'ultimate-member' ) );
}
$user = '';
foreach ( $args as $key => $val ) {
if ( strstr( $key, 'username_b' ) ) {
$user = trim( sanitize_text_field( $val ) );
}
}
if ( empty( $user ) ) {
UM()->form()->add_error( 'username_b', __( 'Please provide your username or email', 'ultimate-member' ) );
}
if ( ( ! is_email( $user ) && username_exists( $user ) ) || ( is_email( $user ) && email_exists( $user ) ) ) {
if ( is_email( $user ) ) {
$user_id = email_exists( $user );
} else {
$user_id = username_exists( $user );
}
$attempts = (int) get_user_meta( $user_id, 'password_rst_attempts', true );
$is_admin = user_can( absint( $user_id ), 'manage_options' );
if ( UM()->options()->get( 'enable_reset_password_limit' ) ) { // if reset password limit is set
if ( ! ( UM()->options()->get( 'disable_admin_reset_password_limit' ) && $is_admin ) ) {
// Doesn't trigger this when a user has admin capabilities and when reset password limit is disabled for admins
$limit = UM()->options()->get( 'reset_password_limit_number' );
if ( $attempts >= $limit ) {
UM()->form()->add_error( 'username_b', __( 'You have reached the limit for requesting password change for this user already. Contact support if you cannot open the email', 'ultimate-member' ) );
} else {
update_user_meta( $user_id, 'password_rst_attempts', $attempts + 1 );
}
}
}
}
}
/**
* Process a new request
*
* @param $args
*/
public function um_reset_password_process_hook( $args ) {
$user = null;
foreach ( $args as $key => $val ) {
if ( strstr( $key, 'username_b' ) ) {
$user = trim( sanitize_text_field( $val ) );
}
}
if ( username_exists( $user ) ) {
$data = get_user_by( 'login', $user );
} elseif ( email_exists( $user ) ) {
$data = get_user_by( 'email', $user );
}
if ( isset( $data ) && is_a( $data, '\WP_User' ) ) {
um_fetch_user( $data->ID );
if ( false === UM()->options()->get( 'only_approved_user_reset_password' ) || UM()->common()->users()->has_status( $data->ID, 'approved' ) ) {
UM()->user()->password_reset( $data->ID );
}
}
wp_safe_redirect( um_get_core_page( 'password-reset', 'checkemail' ) );
exit;
}
/**
* Error handler: changing password
*
* It works both for the Reset Password Shortcode and Account > Change password form
*
* @param $args
*/
public function um_change_password_errors_hook( $args ) {
if ( isset( $args[ UM()->honeypot ] ) && '' !== $args[ UM()->honeypot ] ) {
wp_die( esc_html__( 'Hello, spam bot!', 'ultimate-member' ) );
}
if ( ! empty( $args['_um_account'] ) && isset( $args['_um_account_tab'] ) && 'password' === sanitize_key( $args['_um_account_tab'] ) ) {
// validate for security on the account change password page
if ( ! is_user_logged_in() ) {
wp_die( esc_html__( 'This is not possible for security reasons.', 'ultimate-member' ) );
}
}
if ( ! empty( $args['user_password'] ) && UM()->options()->get( 'change_password_request_limit' ) && is_user_logged_in() ) {
$transient_id = '_um_change_password_rate_limit__' . um_user( 'ID' );
$last_request = get_transient( $transient_id );
$request_limit_time = apply_filters( 'um_change_password_attempt_limit_interval', 30 * MINUTE_IN_SECONDS );
if ( ! $last_request ) {
set_transient( $transient_id, time(), $request_limit_time );
} else {
UM()->form()->add_error( 'user_password', __( 'Unable to change password because of password change limit. Please try again later.', 'ultimate-member' ) );
return;
}
}
if ( isset( $args['user_password'] ) && empty( $args['user_password'] ) ) {
UM()->form()->add_error( 'user_password', __( 'You must enter a new password', 'ultimate-member' ) );
return;
}
if ( isset( $args['user_password'] ) ) {
$args['user_password'] = trim( $args['user_password'] );
}
if ( isset( $args['confirm_user_password'] ) ) {
$args['confirm_user_password'] = trim( $args['confirm_user_password'] );
}
// Check for "\" in password.
if ( false !== strpos( wp_unslash( $args['user_password'] ), '\\' ) ) {
UM()->form()->add_error( 'user_password', __( 'Passwords may not contain the character "\\".', 'ultimate-member' ) );
}
if ( UM()->options()->get( 'require_strongpass' ) ) {
wp_fix_server_vars();
$rp_cookie = 'wp-resetpass-' . COOKIEHASH;
if ( ! is_user_logged_in() && isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) {
list( $rp_login, $rp_key ) = explode( ':', wp_unslash( $_COOKIE[ $rp_cookie ] ), 2 );
$user = check_password_reset_key( $rp_key, $rp_login );
um_fetch_user( $user->ID );
} elseif ( is_user_logged_in() ) {
um_fetch_user( get_current_user_id() );
}
$min_length = UM()->options()->get( 'password_min_chars' );
$min_length = ! empty( $min_length ) ? $min_length : 8;
$max_length = UM()->options()->get( 'password_max_chars' );
$max_length = ! empty( $max_length ) ? $max_length : 30;
$user_login = um_user( 'user_login' );
$user_email = um_user( 'user_email' );
if ( mb_strlen( wp_unslash( $args['user_password'] ) ) < $min_length ) {
// translators: %s: min length.
UM()->form()->add_error( 'user_password', sprintf( __( 'Your password must contain at least %d characters', 'ultimate-member' ), $min_length ) );
}
if ( mb_strlen( wp_unslash( $args['user_password'] ) ) > $max_length ) {
// translators: %s: max length.
UM()->form()->add_error( 'user_password', sprintf( __( 'Your password must contain less than %d characters', 'ultimate-member' ), $max_length ) );
}
if ( strpos( strtolower( $user_login ), strtolower( $args['user_password'] ) ) > -1 ) {
UM()->form()->add_error( 'user_password', __( 'Your password cannot contain the part of your username', 'ultimate-member' ) );
}
if ( strpos( strtolower( $user_email ), strtolower( $args['user_password'] ) ) > -1 ) {
UM()->form()->add_error( 'user_password', __( 'Your password cannot contain the part of your email address', 'ultimate-member' ) );
}
if ( ! UM()->validation()->strong_pass( $args['user_password'] ) ) {
UM()->form()->add_error( 'user_password', __( 'Your password must contain at least one lowercase letter, one capital letter and one number', 'ultimate-member' ) );
}
}
if ( isset( $args['confirm_user_password'] ) && empty( $args['confirm_user_password'] ) ) {
UM()->form()->add_error( 'confirm_user_password', __( 'You must confirm your new password', 'ultimate-member' ) );
}
if ( isset( $args['user_password'] ) && isset( $args['confirm_user_password'] ) && $args['user_password'] !== $args['confirm_user_password'] ) {
UM()->form()->add_error( 'confirm_user_password', __( 'Your passwords do not match', 'ultimate-member' ) );
}
}
/**
* Process a change request
*
* @param $args
*/
public function um_change_password_process_hook( $args ) {
if ( isset( $args['_um_password_change'] ) && $args['_um_password_change'] == 1 ) {
// it only works on the Password Reset Shortcode form
$rp_cookie = 'wp-resetpass-' . COOKIEHASH;
if ( isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) {
list( $rp_login, $rp_key ) = explode( ':', wp_unslash( $_COOKIE[ $rp_cookie ] ), 2 );
$user = check_password_reset_key( $rp_key, $rp_login );
if ( isset( $args['user_password'] ) && ! hash_equals( $rp_key, $args['rp_key'] ) ) {
$user = false;
}
} else {
$user = false;
}
if ( ! $user || is_wp_error( $user ) ) {
$this->setcookie( $rp_cookie, false );
if ( $user && 'expired_key' === $user->get_error_code() ) {
wp_redirect( add_query_arg( array( 'updated' => 'expiredkey' ), um_get_core_page( 'password-reset' ) ) );
} else {
wp_redirect( add_query_arg( array( 'updated' => 'invalidkey' ), um_get_core_page( 'password-reset' ) ) );
}
exit;
}
$errors = new \WP_Error();
/** This action is documented in wp-login.php */
do_action( 'validate_password_reset', $errors, $user );
if ( ( ! $errors->get_error_code() ) ) {
reset_password( $user, trim( $args['user_password'] ) );
// send the Password Changed Email
UM()->user()->password_changed( $user->ID );
// clear temporary data
$attempts = (int) get_user_meta( $user->ID, 'password_rst_attempts', true );
if ( $attempts ) {
update_user_meta( $user->ID, 'password_rst_attempts', 0 );
}
$this->setcookie( $rp_cookie, false );
$set_password_required = get_user_meta( $user->ID, 'um_set_password_required', true );
if ( ! empty( $set_password_required ) ) {
delete_user_meta( $user->ID, 'um_set_password_required' );
}
/**
* UM hook
*
* @type action
* @title um_after_changing_user_password
* @description Hook that runs after user change their password
* @input_vars
* [{"var":"$user_id","type":"int","desc":"User ID"}]
* @change_log
* ["Since: 2.0"]
* @usage add_action( 'um_after_changing_user_password', 'function_name', 10, 1 );
* @example
* <?php
* add_action( 'um_after_changing_user_password', 'my_after_changing_user_password', 10, 1 );
* function my_user_login_extra( $user_id ) {
* // your code here
* }
* ?>
*/
do_action( 'um_after_changing_user_password', $user->ID );
if ( ! is_user_logged_in() ) {
$url = um_get_core_page( 'login', 'password_changed' );
} else {
$url = um_get_core_page( 'password-reset', 'password_changed' );
}
wp_redirect( $url );
exit;
}
}
}
/**
* Disable page caching and set or clear cookie
*
* @param string $name
* @param string $value
* @param int $expire
* @param string $path
*/
public function setcookie( $name, $value = '', $expire = 0, $path = '' ) {
if ( empty( $value ) ) {
$expire = time() - YEAR_IN_SECONDS;
}
if ( empty( $path ) ) {
list( $path ) = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) );
}
$levels = ob_get_level();
for ( $i = 0; $i < $levels; $i++ ) {
@ob_end_clean();
}
nocache_headers();
setcookie( $name, $value, $expire, $path, COOKIE_DOMAIN, is_ssl(), true );
}
/**
* UM Placeholders for reset password
* @depreated 2.10.5
* @param $placeholders
*
* @return array
*/
public function add_placeholder( $placeholders ) {
_deprecated_function( __METHOD__, '2.10.5' );
return $placeholders;
}
/**
* UM Replace Placeholders for reset password
* @depreated 2.10.5
* @param $replace_placeholders
*
* @return array
*/
public function add_replace_placeholder( $replace_placeholders ) {
_deprecated_function( __METHOD__, '2.10.5' );
return $replace_placeholders;
}
}
}