ailed () { global $um_limit_login_failed; do_action( 'login_errors', '' ); $um_limit_login_failed = true; } /** * For plugin MemberPress * Triggers authenticate filter to allow Limit Login Attempts Reloaded * to track credentials and check lockouts before MemberPress validates the password * This enables the plugin to display remaining attempts messages * * @param array $errors Array of existing errors (MemberPress passes validate_login output first). * @param array $params Login parameters (log, pwd) * @return array Errors for MemberPress; when LLAR blocks login, returns that message as first error. */ public function mepr_validate_login_handler( $errors, $params = array() ) { if ( ! isset( $_POST['log'] ) || ! isset( $_POST['pwd'] ) ) { return $errors; } $log = sanitize_text_field( wp_unslash( $_POST['log'] ) ); $pwd = isset( $_POST['pwd'] ) ? $_POST['pwd'] : ''; // Password should not be sanitized // Trigger authenticate filter to track credentials and check lockouts. $auth_result = apply_filters( 'authenticate', null, $log, $pwd ); if ( is_wp_error( $auth_result ) ) { $codes = $auth_result->get_error_codes(); if ( in_array( 'too_many_retries', $codes, true ) ) { return array( $auth_result->get_error_message( 'too_many_retries' ) ); } if ( in_array( 'username_blacklisted', $codes, true ) ) { return array( $auth_result->get_error_message( 'username_blacklisted' ) ); } } if ( ! $this->is_limit_login_ok( $log ) ) { return array( $this->error_msg( $log ) ); } return $errors; } /** * Run MFA flow on login: handshake, save session, redirect to MFA app. * Exits on successful redirect. Call only after password verification. * * @param string $username Login username. */ public function limit_login_failed( $username ) { $this->local_lockout->limit_login_failed( $username ); } /** * Handle notification in event of lockout * * @param $user * @return bool|void */ public function notify( $user ) { $this->local_lockout->notify( $user ); } /** * Email notification of lockout to admin (if configured) * * @param $user */ public function notify_email( $user ) { $this->local_lockout->notify_email( $user ); } /** * Logging of lockout (if configured) * * @param $user_login * * @internal param $user */ public function notify_log( $user_login ) { $this->local_lockout->notify_log( $user_login ); } /** * Check if IP is whitelisted. * * This function allow external ip whitelisting using a filter. Note that it can * be called multiple times during the login process. * * Note that retries and statistics are still counted and notifications * done as usual for whitelisted ips , but no lockout is done. * * Example: * function my_ip_whitelist($allow, $ip) { * return ($ip == 'my-ip') ? true : $allow; * } * add_filter('limit_login_whitelist_ip', 'my_ip_whitelist', 10, 2); * * @param null $ip * * @return bool */ public function is_ip_whitelisted( $ip = null ) { return $this->ip_resolver->is_ip_whitelisted( $ip ); } public function is_username_whitelisted( $username ) { return $this->local_lockout->is_username_whitelisted( $username ); } public function is_ip_blacklisted( $ip = null ) { return $this->ip_resolver->is_ip_blacklisted( $ip ); } public function is_username_blacklisted( $username ) { return $this->local_lockout->is_username_blacklisted( $username ); } /** * Filter: allow login attempt? (called from wp_authenticate()) * * @param $user WP_User * @param $password * * @return WP_Error|WP_User */ public function wp_authenticate_user( $user, $password ) { return $this->auth_handler->wp_authenticate_user( $user, $password ); } /** * Filter: add this failure to login page "Shake it!" * * @param $error_codes * * @return array */ public function failure_shake( $error_codes ) { $error_codes[] = 'too_many_retries'; $error_codes[] = 'username_blacklisted'; return $error_codes; } /** * Keep track of if user or password are empty, to filter errors correctly * * @param $user * @param $username * @param $password */ public function track_credentials( $user, $username, $password ) { return $this->auth_handler->track_credentials( $user, $username, $password ); } /** * Construct informative error message * * @param string $username Optional username from the auth hook. * @return string * @throws Exception */ public function error_msg( $username = '' ) { return $this->error_presenter->error_msg( $username ); } /** * When returning from MFA with llar_mfa_error, inject an error so WordPress outputs the red #login_error block. * * @param \WP_Error $errors WP_Error object passed to login_header(). * @param string $redirect_to Redirect URL. * @return \WP_Error */ public function inject_mfa_return_login_error( $errors, $redirect_to ) { return $this->error_presenter->inject_mfa_return_login_error( $errors, $redirect_to ); } /** * Fix up the error message before showing it * * @param $content * * @return string */ public function fixup_error_messages( $content ) { return $this->error_presenter->fixup_error_messages( $content ); } public function fixup_error_messages_wc( \WP_Error $error ) { return $this->error_presenter->fixup_error_messages_wc( $error ); } /** * Get correct remote address * * @return string * */ public function get_address() { return $this->ip_resolver->get_address(); } /** * Clean up old lockouts and retries, and save supplied arrays * * @param null $retries * @param null $lockouts * @param null $valid */ public function cleanup( $retries = null, $lockouts = null, $valid = null ) { $this->local_lockout->cleanup( $retries, $lockouts, $valid ); } /** * Render admin options page */ public function options_page() { $this->admin_ui->options_page(); } /** * Render an admin notice view by key (e.g. 'auto-update', 'mfa-no-ssl'). * * @param string $notice_key Notice identifier. * @param array $args Variables to pass to the notice view. * @return void */ public function render_admin_notice( $notice_key, array $args = array() ) { if ( null === $this->admin_notices_controller ) { $this->admin_notices_controller = new AdminNoticesController(); } $this->admin_notices_controller->render( $notice_key, $args ); } /** * Show warning when MFA is enabled and rescue links need attention: no rescue payload transients, * or latest payload expiry is within RESCUE_NOTICE_THRESHOLD. Uses a short-lived cache for the * max-expiry query to avoid scanning wp_options on every admin page load. * * @return bool */ public function should_show_mfa_recovery_links_expired_notice() { if ( ! (bool) Config::get( 'mfa_enabled' ) ) { return false; } $seconds_left = $this->mfa_controller->get_rescue_links_seconds_left(); if ( null === $seconds_left ) { return true; } return $seconds_left <= MfaConstants::RESCUE_NOTICE_THRESHOLD; } public function show_message( $msg, $is_error = false ) { $this->pending_admin_message = array( 'msg' => $msg, 'is_error' => $is_error, ); } private function plan_name_match( $plan = 'default' ) { if ( ! array_key_exists( $plan, $this->plans ) ) { $plan = 'default'; } return $this->plans[ $plan ]['name']; } public function array_name_plans() { $plans = []; foreach ( $this->plans as $plan ) { $plans[ $plan['name'] ] = $plan['rate']; } return $plans; } private function info() { if ( self::$cloud_app ) { $this->info_data = self::$cloud_app->info(); } return $this->info_data; } public function info_is_exhausted() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } return isset( $this->info_data['requests']['exhausted'] ) ? filter_var( $this->info_data['requests']['exhausted'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false; } /** * Whether /info reports the Micro Cloud quota as almost exhausted. * * @return bool */ public function info_is_almost_exhausted() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } return isset( $this->info_data['requests']['almost_exhausted'] ) ? filter_var( $this->info_data['requests']['almost_exhausted'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false; } /** * Whether /info returned usable quota and plan data for the dashboard UI. * * @return bool */ public function info_has_valid_data() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } if ( empty( $this->info_data ) || ! is_array( $this->info_data ) ) { return false; } if ( empty( $this->info_data['requests'] ) || ! is_array( $this->info_data['requests'] ) ) { return false; } return array_key_exists( 'quota', $this->info_data['requests'] ) && '' !== (string) $this->info_data['requests']['quota']; } /** * Cloud API responded to /info but access is denied (e.g. quota exhausted or unpaid domain). * * @return bool */ public function info_is_cloud_unavailable() { if ( ! self::$cloud_app ) { return false; } if ( $this->info_has_valid_data() ) { return false; } return ! self::$cloud_app->is_info_network_failure(); } public function info_requests() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } return ( ! empty( $this->info_data ) && ! empty( $this->info_data['requests'] ) ) ? $this->info_data['requests'] : ''; } public function info_sub_group() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } $data = ( ! empty( $this->info_data ) && ! empty( $this->info_data['sub_group'] ) ) ? $this->info_data['sub_group'] : ''; return $this->plan_name_match( $data ); } public function info_upgrade_url() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } return ( ! empty( $this->info_data ) && ! empty( $this->info_data['upgrade_url'] ) ) ? $this->info_data['upgrade_url'] : ''; } public function info_block_by_country() { if ( empty( $this->info_data ) ) { $this->info_data = $this->info(); } return ( ! empty( $this->info_data ) && ! empty( $this->info_data['block_by_country'] ) ) ? $this->info_data['block_by_country'] : ''; } /** * Public wrapper for llar_api_response to allow integrations to use it * Only allows calls from integration classes within this plugin * * @param string $user_data User data to check * @param BaseIntegration|null $integration Integration instance (optional, for security validation) * @return array API response */ public function check_registration_api( $user_data, $integration = null ) { return $this->registration_limiter->check_registration_api( $user_data, $integration ); } /** * Register new user standard WP */ public function llar_submit_login_form_register() { $this->registration_limiter->llar_submit_login_form_register(); } /** * Correcting errors in the presence of a registration prohibition marker * @param $errors * @param $sanitized_user_login * @param $user_email * * @return mixed */ public function llar_submit_registration_errors( $errors, $sanitized_user_login, $user_email ) { return $this->registration_limiter->llar_submit_registration_errors( $errors, $sanitized_user_login, $user_email ); } /** * Debug tab: foreign authenticate filter callbacks. * * @return array */ public static function get_foreign_authenticate_hooks() { return AuthenticateHooksInspector::get_foreign_authenticate_hooks(); } /** * Admin notice: leave a review (dashboard/plugins/LLAR screens). * * @return void */ public function render_leave_review_admin_notice() { $screen = get_current_screen(); if ( isset( $_COOKIE['llar_review_notice_shown'] ) ) { Config::update( 'review_notice_shown', true ); @setcookie( 'llar_review_notice_shown', '', time() - 3600, '/' ); } if ( ! $this->has_capability || Config::get( 'review_notice_shown' ) || ! $screen || ! in_array( $screen->base, array( 'dashboard', 'plugins', 'toplevel_page_limit-login-attempts' ), true ) ) { return; } $activation_timestamp = Config::get( 'activation_timestamp' ); if ( ! $activation_timestamp || $activation_timestamp >= strtotime( '-1 month' ) ) { return; } $this->admin_notices_controller->render( 'leave-review' ); } }