*/ private static $errors = array(); /** * Email posted in this request (kept in the form after an error). * * @var string */ private static $email = ''; /** * Hooks. * * @return void */ public static function init() { add_shortcode( 'paygrab_login', array( __CLASS__, 'shortcode' ) ); add_action( 'template_redirect', array( __CLASS__, 'handle' ), 5 ); add_action( 'template_redirect', array( __CLASS__, 'checkout_needs_login' ), 4 ); add_action( 'login_init', array( __CLASS__, 'from_wp_login' ), 1 ); add_filter( 'login_url', array( __CLASS__, 'login_url' ), 20, 2 ); add_filter( 'register_url', array( __CLASS__, 'register_url' ), 20 ); add_filter( 'lostpassword_url', array( __CLASS__, 'lostpassword_url' ), 20, 2 ); add_filter( 'login_redirect', array( __CLASS__, 'after_wp_login' ), 20, 3 ); add_action( 'admin_init', array( __CLASS__, 'keep_out_of_admin' ), 1 ); // The lock after failed logins applies to every way in (PAYGRAB's page, wp-login.php, XML-RPC …). add_filter( 'authenticate', array( __CLASS__, 'locked' ), 99, 2 ); add_action( 'wp_login_failed', array( __CLASS__, 'count_failure' ) ); add_action( 'wp_login', array( __CLASS__, 'forget_failures' ), 10, 2 ); add_filter( 'show_admin_bar', array( __CLASS__, 'admin_bar' ), 20 ); } /** * Whether a user is a member (no admin work), who is kept on the front of the site. * * @param WP_User|null $user User (current user when null). * @return bool */ public static function is_member( $user = null ) { $user = $user ? $user : wp_get_current_user(); $is = $user && $user->exists() && ! user_can( $user, 'edit_posts' ); /** * Filters whether a user is kept out of the admin and away from wp-login.php. * * @param bool $is Member. * @param WP_User $user User. */ return (bool) apply_filters( 'paygrab_is_member', $is, $user ); } /** * URL of PAYGRAB's login page ('' when the page is not set). * * @param array $args Query arguments. * @return string */ private static function page_url( $args = array() ) { $url = paygrab_page_url( 'login' ); return $url ? add_query_arg( $args, $url ) : ''; } /** * Visitors who are not logged in go from the checkout to the login page, then back to the article. * * @return void */ public static function checkout_needs_login() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- navigation only. if ( is_user_logged_in() || ! paygrab_is_page( 'checkout' ) || empty( $_GET['product'] ) || 'GET' !== paygrab_request_method() ) { return; } $product = absint( $_GET['product'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $source = paygrab_request_source(); wp_safe_redirect( paygrab_login_url( Paygrab_Checkout::url( $product, $source ), array( 'source' => $source, 'product' => $product ) ) ); exit; } /** * Handle the login, "forgot password" and "new password" forms. * * @return void */ public static function handle() { if ( ! paygrab_is_page( 'login' ) ) { return; } if ( 'POST' !== paygrab_request_method() ) { // A member who is already logged in does not need the login page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( is_user_logged_in() && ! isset( $_GET['paygrab_reset'] ) ) { wp_safe_redirect( paygrab_request_return() ); exit; } return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- each form checks its own nonce. $action = isset( $_POST['paygrab_action'] ) ? sanitize_key( wp_unslash( $_POST['paygrab_action'] ) ) : ''; if ( 'login' === $action ) { self::login(); } elseif ( 'lost_password' === $action ) { self::lost_password(); } elseif ( 'reset_password' === $action ) { self::reset_password(); } } /** * Key that counts failed logins for one address from one place. * * @param string $email Email. * @return string */ private static function fail_key( $email ) { return 'paygrab_login_fail_' . md5( strtolower( $email ) . '|' . paygrab_client_ip() ); } /** * Failure key for what was typed as the login (user name or email). * * @param string $login User name or email. * @return string */ private static function fail_key_for( $login ) { $login = trim( (string) $login ); $user = is_email( $login ) ? get_user_by( 'email', $login ) : get_user_by( 'login', $login ); return self::fail_key( $user ? $user->user_email : $login ); } /** * Refuse the login while it is locked, whatever the password. * * @param WP_User|WP_Error|null $user Result so far. * @param string $username User name or email. * @return WP_User|WP_Error|null */ public static function locked( $user, $username ) { if ( '' !== trim( (string) $username ) && self::lock_applies( $username ) && (int) get_transient( self::fail_key_for( $username ) ) >= self::MAX_FAILS ) { return new WP_Error( 'paygrab_locked', __( 'You entered a wrong password too many times. Please wait 15 minutes and try again.', 'paygrab' ) ); } return $user; } /** * Count a failed login. * * @param string $username User name or email. * @return void */ public static function count_failure( $username ) { if ( ! self::lock_applies( $username ) ) { return; } $key = self::fail_key_for( $username ); $fails = (int) get_transient( $key ); // Tries while locked are not counted, so the 15 minutes run from the fifth failure and do not start over. if ( $fails < self::MAX_FAILS ) { set_transient( $key, $fails + 1, self::LOCK ); } } /** * The lock is for members (and unknown addresses). Staff accounts keep WordPress's own behaviour, so * nobody can lock an administrator out by typing wrong passwords from a shared address (a CDN or proxy). * * @param string $login User name or email. * @return bool */ private static function lock_applies( $login ) { $login = trim( (string) $login ); $user = is_email( $login ) ? get_user_by( 'email', $login ) : get_user_by( 'login', $login ); return ! $user || self::is_member( $user ); } /** * A successful login clears the count. * * @param string $login User login. * @param WP_User $user User. * @return void */ public static function forget_failures( $login, $user ) { if ( $user instanceof WP_User ) { delete_transient( self::fail_key( $user->user_email ) ); } } /** * Log in. * * @return void */ private static function login() { $email = isset( $_POST['paygrab_email'] ) ? sanitize_email( wp_unslash( $_POST['paygrab_email'] ) ) : ''; $pass = isset( $_POST['paygrab_password'] ) ? (string) wp_unslash( $_POST['paygrab_password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- passwords are checked, never shown. $remember = ! empty( $_POST['paygrab_remember'] ); self::$email = $email; if ( ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_login' ) ) { self::$errors['form'] = __( 'The page expired. Please try again.', 'paygrab' ); return; } $user = is_email( $email ) ? get_user_by( 'email', $email ) : false; if ( $user ) { // wp_signon() runs the lock (authenticate) and counts failures (wp_login_failed). $result = wp_signon( array( 'user_login' => $user->user_login, 'user_password' => $pass, 'remember' => $remember, ), is_ssl() ); } else { $result = self::locked( null, $email ); if ( ! is_wp_error( $result ) ) { self::count_failure( $email ); $result = new WP_Error( 'paygrab_login' ); } } if ( is_wp_error( $result ) ) { // The message never says which one was wrong, so nobody can find out who is registered. $locked = 'paygrab_locked' === $result->get_error_code() || (int) get_transient( self::fail_key( $email ) ) >= self::MAX_FAILS; self::$errors['form'] = $locked ? __( 'You entered a wrong password too many times. Please wait 15 minutes and try again.', 'paygrab' ) : __( 'The email address or the password is wrong.', 'paygrab' ); self::$errors['hint'] = $locked ? '' : '1'; return; } $to = paygrab_request_return(); if ( ! self::is_member( $result ) && paygrab_mypage_url() === $to ) { $to = admin_url(); } wp_safe_redirect( $to ); exit; } /** * Send the "set a new password" email. The answer is the same whether the address is registered or not. * * @return void */ private static function lost_password() { $email = isset( $_POST['paygrab_email'] ) ? sanitize_email( wp_unslash( $_POST['paygrab_email'] ) ) : ''; if ( ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_lost_password' ) ) { self::$errors['form'] = __( 'The page expired. Please try again.', 'paygrab' ); return; } if ( ! is_email( $email ) ) { self::$email = $email; self::$errors['email'] = __( 'Enter a valid email address.', 'paygrab' ); return; } $user = get_user_by( 'email', $email ); if ( $user && ! paygrab_rate_limited( 'lost_password', 5, HOUR_IN_SECONDS ) ) { $key = get_password_reset_key( $user ); if ( ! is_wp_error( $key ) ) { Paygrab_Mailer::password_reset( $user, self::page_url( array( 'paygrab_reset' => $key, 'paygrab_login' => rawurlencode( $user->user_login ) ) ) ); } } wp_safe_redirect( self::page_url( array( 'paygrab_lost' => 'sent' ) ) ); exit; } /** * Check a reset link (valid for an hour). * * @param string $key Key. * @param string $login User login. * @return WP_User|WP_Error */ private static function check_reset( $key, $login ) { $short = static fn() => self::RESET_TTL; add_filter( 'password_reset_expiration', $short ); $user = check_password_reset_key( $key, $login ); remove_filter( 'password_reset_expiration', $short ); return $user; } /** * Set the new password, log in and go to My page. * * @return void */ private static function reset_password() { $key = isset( $_POST['paygrab_reset'] ) ? sanitize_text_field( wp_unslash( $_POST['paygrab_reset'] ) ) : ''; $login = isset( $_POST['paygrab_login'] ) ? sanitize_user( wp_unslash( $_POST['paygrab_login'] ) ) : ''; $pass = isset( $_POST['paygrab_password'] ) ? (string) wp_unslash( $_POST['paygrab_password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- passwords are hashed, never shown. if ( ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_reset_password' ) ) { self::$errors['form'] = __( 'The page expired. Please try again.', 'paygrab' ); return; } $user = self::check_reset( $key, $login ); if ( is_wp_error( $user ) ) { wp_safe_redirect( self::page_url( array( 'paygrab_lost' => 'expired' ) ) ); exit; } if ( strlen( $pass ) < 8 ) { self::$errors['password'] = __( 'Use at least 8 characters for the password.', 'paygrab' ); return; } reset_password( $user, $pass ); delete_transient( self::fail_key( $user->user_email ) ); wp_set_current_user( $user->ID ); wp_set_auth_cookie( $user->ID, false, is_ssl() ); wp_safe_redirect( add_query_arg( 'paygrab_notice', 'password', paygrab_mypage_url() ) ); exit; } /** * [paygrab_login] shortcode. * * @return string */ public static function shortcode() { return self::render(); } /** * The login screen (also shown by My page to visitors who are not logged in). * * @param string $return Where to go after logging in (default: from the request). * @return string */ public static function render( $return = '' ) { Paygrab_Frontend::enqueue(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- screen selection only. $lost = isset( $_GET['paygrab_lost'] ) ? sanitize_key( wp_unslash( $_GET['paygrab_lost'] ) ) : ''; $reset = isset( $_GET['paygrab_reset'] ) ? sanitize_text_field( wp_unslash( $_GET['paygrab_reset'] ) ) : ''; $login = isset( $_GET['paygrab_login'] ) ? sanitize_user( wp_unslash( $_GET['paygrab_login'] ) ) : ''; // phpcs:enable if ( '' !== $reset ) { $user = self::check_reset( $reset, $login ); if ( is_wp_error( $user ) ) { return paygrab_get_template( 'login-lost', array( 'state' => 'expired', 'errors' => array(), 'email' => '' ) ); } return paygrab_get_template( 'login-reset', array( 'key' => $reset, 'login' => $login, 'errors' => self::$errors ) ); } if ( '' !== $lost ) { return paygrab_get_template( 'login-lost', array( 'state' => in_array( $lost, array( 'sent', 'expired' ), true ) ? $lost : 'form', 'errors' => self::$errors, 'email' => self::$email ) ); } $source = paygrab_request_source(); $product = paygrab_request_product(); $args = array( 'source' => $source, 'product' => $product ? $product->ID : 0, ); return paygrab_get_template( 'login', array( 'errors' => self::$errors, 'email' => self::$email, 'product' => $product, 'return' => '' !== $return ? $return : ( $source ? '' : paygrab_request_return() ), 'source' => $source, 'register_url' => paygrab_register_url( '' !== $return ? $return : ( $source ? '' : paygrab_request_return() ), $args ), 'lost_url' => paygrab_page_url( 'login' ) ? self::page_url( array( 'paygrab_lost' => 'form' ) ) : wp_lostpassword_url(), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- message selection only. 'left' => isset( $_GET['paygrab_left'] ), ) ); } /** * wp-login.php: members use PAYGRAB's page instead. * * Bare wp-login.php and links into the admin keep WordPress's screen, so administrators log in as before. * * @return void */ public static function from_wp_login() { if ( ! paygrab_page_url( 'login' ) || 'GET' !== paygrab_request_method() || paygrab_yomeru_signup() ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- navigation only. $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : 'login'; $redirect = isset( $_GET['redirect_to'] ) ? wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ), '' ) : ''; // phpcs:enable if ( 'logout' === $action || 'rp' === $action || 'resetpass' === $action || 'confirmaction' === $action || 'postpass' === $action ) { return; } if ( is_user_logged_in() ) { if ( self::is_member() && 'login' === $action ) { wp_safe_redirect( paygrab_mypage_url() ); exit; } return; } $front = '' !== $redirect && ! str_starts_with( $redirect, admin_url() ); if ( 'register' === $action ) { wp_safe_redirect( paygrab_register_url( $redirect ) ); exit; } if ( 'lostpassword' === $action && $front ) { wp_safe_redirect( self::page_url( array( 'paygrab_lost' => 'form' ) ) ); exit; } if ( 'login' === $action && $front ) { wp_safe_redirect( paygrab_login_url( $redirect ) ); exit; } } /** * Login links that come back to the front of the site point at PAYGRAB's page. * * @param string $url Login URL. * @param string $redirect Where to go after logging in. * @return string */ public static function login_url( $url, $redirect ) { if ( ! paygrab_page_url( 'login' ) || paygrab_yomeru_signup() || '' === (string) $redirect || str_starts_with( (string) $redirect, admin_url() ) ) { return $url; } return add_query_arg( 'redirect_to', rawurlencode( $redirect ), paygrab_page_url( 'login' ) ); } /** * Sign-up links point at PAYGRAB's sign-up page. * * @param string $url Registration URL. * @return string */ public static function register_url( $url ) { $page = paygrab_page_url( 'register' ); return $page && ! paygrab_yomeru_signup() ? $page : $url; } /** * "Lost your password?" links on the front point at PAYGRAB's page. * * @param string $url URL. * @param string $redirect Redirect. * @return string */ public static function lostpassword_url( $url, $redirect ) { if ( is_admin() || ! paygrab_page_url( 'login' ) || paygrab_yomeru_signup() || ( '' !== (string) $redirect && str_starts_with( (string) $redirect, admin_url() ) ) ) { return $url; } return self::page_url( array( 'paygrab_lost' => 'form' ) ); } /** * Members who still log in on wp-login.php go to My page instead of their admin profile. * * @param string $to Destination. * @param string $requested Requested destination. * @param WP_User|WP_Error $user User. * @return string */ public static function after_wp_login( $to, $requested, $user ) { if ( $user instanceof WP_User && self::is_member( $user ) && ( '' === (string) $requested || str_starts_with( (string) $to, admin_url() ) ) ) { return paygrab_mypage_url(); } return $to; } /** * Members cannot open the admin (Ajax and admin-post keep working for plugins). * * @return void */ public static function keep_out_of_admin() { if ( wp_doing_ajax() || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ! is_user_logged_in() || ! self::is_member() ) { return; } $script = isset( $_SERVER['SCRIPT_NAME'] ) ? basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_NAME'] ) ) ) : ''; if ( in_array( $script, array( 'admin-post.php', 'admin-ajax.php' ), true ) ) { return; } wp_safe_redirect( paygrab_mypage_url() ); exit; } /** * No admin bar for members. * * @param bool $show Show. * @return bool */ public static function admin_bar( $show ) { return is_user_logged_in() && self::is_member() ? false : $show; } }