(bool) $ok, 'code' => (string) $code, 'navigate' => $navigate, 'reload' => (bool) $reload, 'text' => '', 'field' => '', 'step' => '', 'token' => '', ); } /** * Result for a field error. The specific text is only sent in AJAX * responses; URLs carry the generic code. * * @param WP_Error $error Error. * @return array */ private static function field_result( $error ) { $data = (array) $error->get_error_data(); $code = in_array( $error->get_error_code(), array( 'field_required', 'field_invalid' ), true ) ? $error->get_error_code() : 'field_invalid'; $result = self::result( false, $code ); $result['text'] = isset( $data['text'] ) ? (string) $data['text'] : ''; $result['field'] = isset( $data['field'] ) ? sanitize_key( $data['field'] ) : ''; return $result; } /** * Run one action. * * @param array $data Raw (slashed) request data, as in $_POST. * @return array */ public static function process( $data ) { $action = isset( $data['logvia_action'] ) ? sanitize_key( wp_unslash( $data['logvia_action'] ) ) : ''; $nonce = isset( $data['_logvia_nonce'] ) ? sanitize_text_field( wp_unslash( $data['_logvia_nonce'] ) ) : ''; if ( ! $action || ! wp_verify_nonce( $nonce, 'logvia_' . $action ) ) { return self::result( false, 'session_expired', null, true ); } if ( in_array( $action, array( 'login', 'register', 'forgot' ), true ) && ! Logvia_Captcha::verify( $action, $data ) ) { return self::result( false, 'captcha_failed' ); } switch ( $action ) { case 'login': return self::login( $data ); case 'register': return self::register( $data ); case 'verify': return self::verify( $data ); case 'resend': return self::resend( $data ); case 'forgot': return self::forgot( $data ); case 'reset': return self::reset( $data ); case 'reset_code': return self::reset_with_code( $data ); } return self::result( false, 'session_expired', null, true ); } /** * Requested redirect and the page the form was on. * * @param array $data Request data. * @return array [redirect_to, from] */ private static function targets( $data ) { return array( isset( $data['redirect_to'] ) ? sanitize_url( wp_unslash( $data['redirect_to'] ) ) : '', isset( $data['logvia_from'] ) ? sanitize_url( wp_unslash( $data['logvia_from'] ) ) : '', ); } /* ------------------------------------------------------------------ * Login * ------------------------------------------------------------------ */ /** * Login. * * Passwords are passed to WordPress exactly as wp-login.php does (slashed), * so a password set here also works on wp-login.php and vice versa. * * @param array $data Request data. * @return array */ private static function login( $data ) { $login = trim( sanitize_text_field( wp_unslash( isset( $data['log'] ) ? $data['log'] : '' ) ) ); $password = isset( $data['pwd'] ) ? (string) $data['pwd'] : ''; $ip = Logvia_Rate_Limiter::ip(); $window = self::window(); if ( '' === $login || '' === $password ) { return self::result( false, 'login_failed' ); } if ( Logvia_Rate_Limiter::exceeded( 'login_ip', $ip, self::limit( 'login_ip' ) ) || Logvia_Rate_Limiter::exceeded( 'login_user', $login, self::limit( 'login_user' ) ) ) { return self::result( false, 'rate_limited' ); } $user = wp_signon( array( 'user_login' => $login, 'user_password' => $password, 'remember' => ! empty( $data['rememberme'] ), ), is_ssl() ); if ( is_wp_error( $user ) ) { if ( 'logvia_pending' === $user->get_error_code() ) { return self::result( false, 'account_pending' ); } if ( 'logvia_rejected' === $user->get_error_code() ) { return self::result( false, 'account_rejected' ); } Logvia_Rate_Limiter::hit( 'login_ip', $ip, $window ); Logvia_Rate_Limiter::hit( 'login_user', $login, $window ); return self::result( false, 'login_failed' ); } Logvia_Rate_Limiter::clear( 'login_user', $login ); wp_set_current_user( $user->ID ); list( $requested, $from ) = self::targets( $data ); return self::result( true, '', Logvia_Plugin::login_redirect( $user, $requested, $from ) ); } /* ------------------------------------------------------------------ * Registration * ------------------------------------------------------------------ */ /** * Registration step 1: validate. Then either email a code (no user is * created yet) or, when verification is off, create the account now. * * @param array $data Request data. * @return array */ private static function register( $data ) { if ( ! Logvia_Settings::get( 'registration' ) ) { return self::result( false, 'registration_closed' ); } $email = sanitize_email( wp_unslash( isset( $data['email'] ) ? $data['email'] : '' ) ); /** * Supply a password instead of asking for one (for add-ons that let * customers sign up without a password field). * * @param string $password Generated password, or '' to use the form's. * @param array $data Request data. */ $preset = (string) apply_filters( 'logvia_registration_password', '', $data ); $generate = '' !== $preset; $password = $generate ? $preset : ( isset( $data['pwd'] ) ? (string) $data['pwd'] : '' ); $confirm = isset( $data['pwd_confirm'] ) ? (string) $data['pwd_confirm'] : ''; $min = max( 6, (int) Logvia_Settings::get( 'min_password' ) ); $verify_on = (bool) Logvia_Settings::get( 'email_verification' ); if ( ! is_email( $email ) ) { return self::result( false, 'invalid_email' ); } if ( email_exists( $email ) ) { return self::result( false, 'email_exists' ); } if ( ! $generate ) { if ( mb_strlen( wp_unslash( $password ) ) < $min ) { return self::result( false, 'password_short' ); } if ( ! Logvia_Password::strong_enough( wp_unslash( $password ), array( strstr( $email, '@', true ) ) ) ) { return self::result( false, 'password_weak' ); } if ( Logvia_Forms::confirm_enabled() && ! hash_equals( $password, $confirm ) ) { return self::result( false, 'password_mismatch' ); } } $values = Logvia_Forms::validate( $data ); if ( is_wp_error( $values ) ) { return self::field_result( $values ); } /** * Validate a registration before it continues. Return a WP_Error whose * code is a Logvia message code to stop it. * * @param true|WP_Error $valid Validation result. * @param string $email Email. * @param array $data Request data. */ $valid = apply_filters( 'logvia_validate_registration', true, $email, $data ); if ( is_wp_error( $valid ) ) { return self::result( false, $valid->get_error_code() ); } if ( ! self::allow_email( $email ) ) { return self::result( false, 'rate_limited' ); } /** * Extra data to carry through verification (for example uploaded * files). Return a WP_Error with data[field] and data[text] to stop. * * @param array|WP_Error $extra Extra data. * @param array $data Request data. */ $extra = apply_filters( 'logvia_registration_extra', array(), $data ); if ( is_wp_error( $extra ) ) { return self::field_result( $extra ); } list( $requested, $from ) = self::targets( $data ); $pending = array( 'email' => $email, // Only a hash of the password is ever stored. 'pass_hash' => wp_hash_password( $password ), 'generated' => $generate, 'redirect_to' => $requested, 'from' => $from, 'fields' => $values, 'extra' => $extra, ); if ( ! $verify_on ) { return self::finish_registration( $pending ); } $code = self::new_code(); $token = bin2hex( random_bytes( 24 ) ); $pending['code_hash'] = wp_hash_password( $code ); $pending['attempts'] = 0; $pending['resends'] = 0; $pending['sent_at'] = time(); set_transient( self::pending_key( $token ), $pending, self::PENDING_TTL ); if ( ! Logvia_Emails::send( 'verify', $email, array( 'code' => $code ) ) ) { delete_transient( self::pending_key( $token ) ); do_action( 'logvia_registration_discarded', $pending ); return self::result( false, 'email_failed' ); } $result = self::result( true, 'code_sent', Logvia_Plugin::url( 'verify', array( 'token' => $token, 'logvia_msg' => 'code_sent', ) ) ); $result['step'] = 'verify'; $result['token'] = $token; return $result; } /** * Registration step 2: check the code, then create the account. * * @param array $data Request data. * @return array */ private static function verify( $data ) { $token = self::clean_token( isset( $data['token'] ) ? $data['token'] : '' ); $pending = $token ? get_transient( self::pending_key( $token ) ) : false; if ( ! is_array( $pending ) ) { return self::result( false, 'verify_expired', Logvia_Plugin::url( 'register', array( 'logvia_msg' => 'verify_expired' ) ) ); } $code = preg_replace( '/\D/', '', (string) wp_unslash( isset( $data['code'] ) ? $data['code'] : '' ) ); if ( 6 !== strlen( $code ) || ! wp_check_password( $code, $pending['code_hash'] ) ) { ++$pending['attempts']; if ( $pending['attempts'] >= self::MAX_CODE_TRIES ) { delete_transient( self::pending_key( $token ) ); do_action( 'logvia_registration_discarded', $pending ); return self::result( false, 'too_many_attempts', Logvia_Plugin::url( 'register', array( 'logvia_msg' => 'too_many_attempts' ) ) ); } set_transient( self::pending_key( $token ), $pending, self::PENDING_TTL ); return self::result( false, 'code_incorrect' ); } delete_transient( self::pending_key( $token ) ); return self::finish_registration( $pending ); } /** * Create the account from a validated registration, then log in, or put * it on hold for approval. * * @param array $p Pending registration. * @return array */ private static function finish_registration( $p ) { if ( email_exists( $p['email'] ) ) { do_action( 'logvia_registration_discarded', $p ); return self::result( false, 'email_exists', Logvia_Plugin::url( 'login', array( 'logvia_msg' => 'email_exists' ) ) ); } $values = isset( $p['fields'] ) ? (array) $p['fields'] : array(); /** * Username for the new account ('' = generated from the email). * * @param string $username Username. * @param array $values Validated form values. */ $username = (string) apply_filters( 'logvia_new_username', '', $values ); $user_id = self::create_customer( $p['email'], $p['pass_hash'], array(), array( 'username' => $username ) ); if ( is_wp_error( $user_id ) ) { do_action( 'logvia_registration_discarded', $p ); return self::result( false, 'account_failed', Logvia_Plugin::url( 'register', array( 'logvia_msg' => 'account_failed' ) ) ); } Logvia_Forms::save_user_values( $user_id, $values ); /** * Fires once a registration has become an account. * * @param int $user_id User ID. * @param array $p Registration (email, fields, extra, generated...). */ do_action( 'logvia_registration_complete', $user_id, $p ); $user = get_user_by( 'id', $user_id ); /** * Keep a new account from logging in straight away (for example to * wait for approval). Return true to hold it. * * @param bool $hold Hold. * @param int $user_id User ID. */ if ( apply_filters( 'logvia_hold_new_account', false, $user_id ) ) { return self::result( true, 'account_pending', Logvia_Plugin::url( 'login', array( 'logvia_msg' => 'account_pending' ) ) ); } self::log_in( $user, true ); return self::result( true, '', Logvia_Plugin::register_redirect( $user, isset( $p['redirect_to'] ) ? $p['redirect_to'] : '', isset( $p['from'] ) ? $p['from'] : '' ) ); } /** * Send a fresh verification code. * * @param array $data Request data. * @return array */ private static function resend( $data ) { $token = self::clean_token( isset( $data['token'] ) ? $data['token'] : '' ); $pending = $token ? get_transient( self::pending_key( $token ) ) : false; if ( ! is_array( $pending ) ) { return self::result( false, 'verify_expired', Logvia_Plugin::url( 'register', array( 'logvia_msg' => 'verify_expired' ) ) ); } if ( time() - (int) $pending['sent_at'] < self::RESEND_COOLDOWN ) { return self::result( false, 'resend_wait' ); } if ( (int) $pending['resends'] >= self::MAX_RESENDS ) { delete_transient( self::pending_key( $token ) ); return self::result( false, 'resend_limit', Logvia_Plugin::url( 'register', array( 'logvia_msg' => 'resend_limit' ) ) ); } if ( ! self::allow_email( $pending['email'] ) ) { return self::result( false, 'rate_limited' ); } $code = self::new_code(); $pending['code_hash'] = wp_hash_password( $code ); $pending['attempts'] = 0; $pending['sent_at'] = time(); ++$pending['resends']; set_transient( self::pending_key( $token ), $pending, self::PENDING_TTL ); if ( ! Logvia_Emails::send( 'verify', $pending['email'], array( 'code' => $code ) ) ) { return self::result( false, 'email_failed' ); } return self::result( true, 'code_resent' ); } /* ------------------------------------------------------------------ * Password reset * ------------------------------------------------------------------ */ /** * Lost password: always the same answer, so it never reveals whether an * account exists. * * @param array $data Request data. * @return array */ private static function forgot( $data ) { $login = trim( sanitize_text_field( wp_unslash( isset( $data['email'] ) ? $data['email'] : '' ) ) ); if ( '' === $login ) { return self::result( false, 'invalid_email' ); } if ( ! self::allow_email( $login ) ) { return self::result( false, 'rate_limited' ); } $user = is_email( $login ) ? get_user_by( 'email', $login ) : get_user_by( 'login', $login ); if ( 'code' !== Logvia_Settings::get( 'reset_method' ) ) { if ( $user ) { self::send_reset( $user ); } return self::result( true, 'reset_sent' ); } // Code method: unknown accounts get an identical-looking session. $code = self::new_code(); $token = bin2hex( random_bytes( 24 ) ); set_transient( self::reset_key( $token ), array( 'user_id' => $user ? (int) $user->ID : 0, 'code_hash' => wp_hash_password( $code ), 'attempts' => 0, ), self::PENDING_TTL ); if ( $user ) { Logvia_Emails::send( 'reset_code', $user->user_email, array( 'code' => $code, 'name' => $user->display_name, ) ); } $result = self::result( true, 'reset_code_sent', Logvia_Plugin::url( 'reset', array( 'rtoken' => $token, 'logvia_msg' => 'reset_code_sent', ) ) ); $result['step'] = 'reset_code'; $result['token'] = $token; return $result; } /** * Check a new password. * * @param string $password Slashed password. * @param string $confirm Slashed confirmation. * @param WP_User $user User. * @return string Message code, or '' when fine. */ private static function password_problem( $password, $confirm, $user ) { $min = max( 6, (int) Logvia_Settings::get( 'min_password' ) ); if ( mb_strlen( wp_unslash( $password ) ) < $min ) { return 'password_short'; } if ( ! Logvia_Password::strong_enough( wp_unslash( $password ), array( $user->user_login, strstr( $user->user_email, '@', true ) ) ) ) { return 'password_weak'; } if ( ! hash_equals( $password, $confirm ) ) { return 'password_mismatch'; } return ''; } /** * Set a new password from a reset link. * * @param array $data Request data. * @return array */ private static function reset( $data ) { $login = sanitize_user( wp_unslash( isset( $data['login'] ) ? $data['login'] : '' ), false ); $key = sanitize_text_field( wp_unslash( isset( $data['key'] ) ? $data['key'] : '' ) ); $password = isset( $data['pwd'] ) ? (string) $data['pwd'] : ''; $confirm = isset( $data['pwd_confirm'] ) ? (string) $data['pwd_confirm'] : ''; $user = check_password_reset_key( $key, $login ); if ( is_wp_error( $user ) ) { return self::result( false, 'reset_invalid', Logvia_Plugin::url( 'forgot', array( 'logvia_msg' => 'reset_invalid' ) ) ); } $problem = self::password_problem( $password, $confirm, $user ); if ( $problem ) { return self::result( false, $problem ); } reset_password( $user, $password ); self::after_reset( $user ); return self::result( true, 'password_changed', Logvia_Plugin::url( 'login', array( 'logvia_msg' => 'password_changed' ) ) ); } /** * Set a new password with an emailed code. * * @param array $data Request data. * @return array */ private static function reset_with_code( $data ) { $token = self::clean_token( isset( $data['rtoken'] ) ? $data['rtoken'] : '' ); $session = $token ? get_transient( self::reset_key( $token ) ) : false; if ( ! is_array( $session ) ) { return self::result( false, 'reset_invalid', Logvia_Plugin::url( 'forgot', array( 'logvia_msg' => 'reset_invalid' ) ) ); } $code = preg_replace( '/\D/', '', (string) wp_unslash( isset( $data['code'] ) ? $data['code'] : '' ) ); $user = $session['user_id'] ? get_user_by( 'id', $session['user_id'] ) : false; if ( ! $user || 6 !== strlen( $code ) || ! wp_check_password( $code, $session['code_hash'] ) ) { ++$session['attempts']; if ( $session['attempts'] >= self::MAX_CODE_TRIES ) { delete_transient( self::reset_key( $token ) ); return self::result( false, 'too_many_attempts', Logvia_Plugin::url( 'forgot', array( 'logvia_msg' => 'too_many_attempts' ) ) ); } set_transient( self::reset_key( $token ), $session, self::PENDING_TTL ); return self::result( false, 'code_incorrect' ); } $password = isset( $data['pwd'] ) ? (string) $data['pwd'] : ''; $confirm = isset( $data['pwd_confirm'] ) ? (string) $data['pwd_confirm'] : ''; $problem = self::password_problem( $password, $confirm, $user ); if ( $problem ) { return self::result( false, $problem ); } delete_transient( self::reset_key( $token ) ); reset_password( $user, $password ); self::after_reset( $user ); return self::result( true, 'password_changed', Logvia_Plugin::url( 'login', array( 'logvia_msg' => 'password_changed' ) ) ); } /** * Clear lockouts after a successful reset. * * @param WP_User $user User. */ private static function after_reset( $user ) { Logvia_Rate_Limiter::clear( 'login_user', $user->user_login ); Logvia_Rate_Limiter::clear( 'login_user', $user->user_email ); } /* ------------------------------------------------------------------ * Request handlers * ------------------------------------------------------------------ */ /** * Verify the nonce of a submitted Logvia form. Each form action has its own nonce. * * @return bool */ private static function verify_request_nonce() { $action = isset( $_POST['logvia_action'] ) ? sanitize_key( wp_unslash( $_POST['logvia_action'] ) ) : ''; $nonce = isset( $_POST['_logvia_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_logvia_nonce'] ) ) : ''; return $action && in_array( $action, self::ACTIONS, true ) && false !== wp_verify_nonce( $nonce, 'logvia_' . $action ); } /** * Classic POST fallback (no JavaScript): redirect after POST. */ public static function handle_post() { if ( empty( $_POST['logvia_action'] ) || wp_doing_ajax() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only checks whether this is a Logvia form; the nonce is verified on the next line. return; } if ( ! self::verify_request_nonce() ) { Logvia_Plugin::redirect( 'login', array( 'logvia_msg' => 'session_expired' ) ); } $data = $_POST; // Nonce verified above. $result = self::process( $data ); if ( ! empty( $result['navigate'] ) ) { wp_safe_redirect( $result['navigate'] ); exit; } $action = sanitize_key( wp_unslash( $data['logvia_action'] ) ); $map = array( 'resend' => 'verify', 'reset_code' => 'reset', ); $screen = isset( $map[ $action ] ) ? $map[ $action ] : $action; $screen = in_array( $screen, Logvia_Plugin::$screens, true ) ? $screen : 'login'; $args = array( 'logvia_msg' => $result['code'] ); if ( 'verify' === $screen && ! empty( $data['token'] ) ) { $args['token'] = self::clean_token( $data['token'] ); } if ( 'reset' === $screen ) { if ( ! empty( $data['rtoken'] ) ) { $args['rtoken'] = self::clean_token( $data['rtoken'] ); } else { $args['login'] = sanitize_user( wp_unslash( isset( $data['login'] ) ? $data['login'] : '' ), false ); $args['key'] = sanitize_text_field( wp_unslash( isset( $data['key'] ) ? $data['key'] : '' ) ); } } if ( ! empty( $data['redirect_to'] ) ) { $args['redirect_to'] = sanitize_url( wp_unslash( $data['redirect_to'] ) ); } Logvia_Plugin::redirect( $screen, $args ); } /** * AJAX endpoint: the page shows the result in place. */ public static function ajax() { if ( ! self::verify_request_nonce() ) { wp_send_json( array( 'ok' => false, 'code' => 'session_expired', 'type' => Logvia_Messages::type( 'session_expired' ), 'message' => Logvia_Messages::text( 'session_expired' ), 'reload' => true, ), 403 ); } $result = self::process( $_POST ); // Nonce verified above. wp_send_json( array( 'ok' => $result['ok'], 'code' => $result['code'], 'type' => $result['code'] ? Logvia_Messages::type( $result['code'] ) : 'success', 'message' => '' !== $result['text'] ? $result['text'] : ( $result['code'] ? Logvia_Messages::text( $result['code'] ) : '' ), 'field' => $result['field'], 'step' => $result['step'], 'token' => $result['token'], 'navigate' => $result['navigate'] ? esc_url_raw( $result['navigate'] ) : null, 'reload' => $result['reload'], ) ); } /* ------------------------------------------------------------------ * Helpers * ------------------------------------------------------------------ */ /** * Role given to new accounts. * * @return string */ public static function default_role() { $role = (string) Logvia_Settings::get( 'default_role' ); $assignable = Logvia_Forms::assignable_roles(); if ( $role && isset( $assignable[ $role ] ) ) { return $role; } if ( class_exists( 'WooCommerce' ) && isset( $assignable['customer'] ) ) { return 'customer'; } $fallback = get_option( 'default_role', 'subscriber' ); return isset( $assignable[ $fallback ] ) ? $fallback : 'subscriber'; } /** * Create a customer account from an already-hashed password. * Uses WooCommerce's customer creation when available so WooCommerce * emails and hooks behave as usual. * * @param string $email Email. * @param string $pass_hash Password hash (or '' for a random password). * @param array $profile Optional first_name, last_name, display_name. * @param array $opts Optional username. * @return int|WP_Error */ public static function create_customer( $email, $pass_hash = '', $profile = array(), $opts = array() ) { $username = ! empty( $opts['username'] ) && ! username_exists( $opts['username'] ) ? $opts['username'] : self::unique_username( $email ); $random = wp_generate_password( 32, true, true ); $role = self::default_role(); if ( function_exists( 'wc_create_new_customer' ) ) { $user_id = wc_create_new_customer( $email, $username, $random, array( 'source' => 'logvia', 'role' => $role, ) ); } else { $user_id = wp_insert_user( array( 'user_login' => $username, 'user_email' => $email, 'user_pass' => $random, 'role' => $role, ) ); } if ( is_wp_error( $user_id ) ) { return $user_id; } $user = get_user_by( 'id', $user_id ); if ( $user && ! in_array( $role, (array) $user->roles, true ) ) { $user->set_role( $role ); } if ( $pass_hash ) { global $wpdb; // Store the hash created at registration; the plain password is never kept. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Same write wp_set_password() performs. $wpdb->update( $wpdb->users, array( 'user_pass' => $pass_hash ), array( 'ID' => $user_id ) ); clean_user_cache( $user_id ); } $profile = array_filter( array_intersect_key( (array) $profile, array_flip( array( 'first_name', 'last_name', 'display_name' ) ) ) ); if ( $profile ) { $profile['ID'] = $user_id; wp_update_user( array_map( 'sanitize_text_field', $profile ) ); } /** * Fires after Logvia creates an account. * * @param int $user_id User ID. * @param string $email Email. */ do_action( 'logvia_customer_created', $user_id, $email ); return $user_id; } /** * Log a user in without a password (after email or Google verification). * * @param WP_User $user User. * @param bool $remember Remember me. */ public static function log_in( $user, $remember = true ) { wp_set_current_user( $user->ID ); wp_set_auth_cookie( $user->ID, $remember, is_ssl() ); // Lets WooCommerce merge the guest cart and other plugins react to the login. do_action( 'wp_login', $user->user_login, $user ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core action, fired as wp_signon() does. } /** * Unique username from the email's local part. * * @param string $email Email. * @return string */ private static function unique_username( $email ) { $base = sanitize_user( (string) strstr( $email, '@', true ), true ); $base = $base ? substr( $base, 0, 50 ) : 'customer'; $name = $base; $n = 2; while ( username_exists( $name ) ) { $name = $base . $n; ++$n; } return $name; } /** * Lockout window in seconds. * * @return int */ private static function window() { return max( 1, (int) Logvia_Settings::get( 'limit_window' ) ) * MINUTE_IN_SECONDS; } /** * Rate limits: failed logins per IP / per account in the lockout window, * emails per IP / per address in an hour. * * @param string $name login_ip | login_user | mail_ip | mail_to. * @return int */ private static function limit( $name ) { $per_user = max( 1, (int) Logvia_Settings::get( 'limit_attempts' ) ); /** * Filter Logvia's rate limits. * * @param array $limits Limits. */ $limits = apply_filters( 'logvia_rate_limits', array( 'login_ip' => max( 20, $per_user * 4 ), 'login_user' => $per_user, 'mail_ip' => 10, 'mail_to' => 5, ) ); return isset( $limits[ $name ] ) ? max( 1, (int) $limits[ $name ] ) : 5; } /** * Email-sending limits per recipient and per IP. * * @param string $email Recipient. * @return bool */ private static function allow_email( $email ) { $ip = Logvia_Rate_Limiter::ip(); if ( Logvia_Rate_Limiter::exceeded( 'mail_ip', $ip, self::limit( 'mail_ip' ) ) || Logvia_Rate_Limiter::exceeded( 'mail_to', $email, self::limit( 'mail_to' ) ) ) { return false; } Logvia_Rate_Limiter::hit( 'mail_ip', $ip, HOUR_IN_SECONDS ); Logvia_Rate_Limiter::hit( 'mail_to', $email, HOUR_IN_SECONDS ); return true; } /** * Random six-digit code. * * @return string */ private static function new_code() { return (string) random_int( 100000, 999999 ); } /** * Transient key for a pending registration. * * @param string $token Token. * @return string */ private static function pending_key( $token ) { return 'logvia_pending_' . substr( hash( 'sha256', $token ), 0, 40 ); } /** * Transient key for a reset-by-code session. * * @param string $token Token. * @return string */ private static function reset_key( $token ) { return 'logvia_reset_' . substr( hash( 'sha256', $token ), 0, 40 ); } /** * Keep only hex characters. * * @param mixed $token Raw token. * @return string */ public static function clean_token( $token ) { return substr( preg_replace( '/[^a-f0-9]/', '', strtolower( (string) wp_unslash( $token ) ) ), 0, 64 ); } /** * Email a password reset link that opens the Logvia reset screen. * * @param WP_User $user User. * @return bool */ public static function send_reset( $user ) { $key = get_password_reset_key( $user ); if ( is_wp_error( $key ) ) { return false; } return Logvia_Emails::send( 'reset', $user->user_email, array( 'link' => Logvia_Plugin::url( 'reset', array( 'key' => $key, 'login' => $user->user_login, ) ), 'name' => $user->display_name, ) ); } }