esc_html__( 'Firewall - Alert', 'defender-security' ), 'slug' => self::SLUG, 'status' => self::STATUS_DISABLED, 'description' => esc_html__( 'Get email when a user or IP is locked out for trying to access your login area.', 'defender-security' ), // @since 3.0.0 Fix 'Guest'-line. 'in_house_recipients' => $this->get_default_user(), 'out_house_recipients' => array(), 'type' => 'notification', 'dry_run' => false, 'configs' => array( 'threshold' => 3, 'cool_off' => 24, 'login_lockout' => true, 'nf_lockout' => true, ), ); $this->import( $default ); } /** * Checks whether the notification options are enabled for the specified lockout log. * * @param Lockout_Log $model The lockout log model. * * @return bool True if notification options are enabled; otherwise, false. */ public function check_options( Lockout_Log $model ): bool { if ( self::STATUS_ACTIVE !== $this->status ) { return false; } // Check 'Login Protection Lockout'. if ( Lockout_Log::AUTH_LOCK === $model->type ) { if ( true === wd_di()->get( Login_Lockout::class )->enabled ) { return true; } return false; } // Check '404 Protection Lockout'. if ( in_array( $model->type, Lockout_Log::get_404_lockout_types(), true ) ) { if ( true === wd_di()->get( Notfound_Lockout::class )->enabled ) { return true; } return false; } // Check 'User Agent Lockout' (includes Fake Bot and Malicious Bot lockouts). if ( in_array( $model->type, Lockout_Log::get_ua_lockout_types(), true ) ) { if ( true === wd_di()->get( User_Agent_Lockout::class )->enabled ) { return true; } return false; } return false; } /** * Sends the notification for a lockout when it is enabled for that type. * * Firewall-time detectors call this directly because the 'defender_notify' hook * is not registered yet at that stage, keeping the eligibility rules in this class. * * @param Lockout_Log $model The lockout log model. * * @return void */ public function notify_lockout( Lockout_Log $model ): void { if ( $this->check_options( $model ) ) { $this->send( $model ); } } /** * Constructs and sends the firewall notification email based on the lockout log. * * @param Lockout_Log $model The lockout log model. * * @return void */ public function send( Lockout_Log $model ): void { if ( ! $this->check_options( $model ) ) { return; } $template = $this->get_template( $model ); $service = wd_di()->get( Notification::class ); $network_site_url = network_site_url(); $email_data = $this->get_email_data( $model, $template, $network_site_url ); $headers = wd_di()->get( Mail::class )->get_headers( defender_noreply_email( 'wd_lockout_noreply_email' ), self::SLUG ); foreach ( $this->in_house_recipients as $user ) { if ( self::USER_SUBSCRIBED !== $user['status'] ) { continue; } $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service ); } foreach ( $this->out_house_recipients as $user ) { if ( self::USER_SUBSCRIBED !== $user['status'] ) { continue; } $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service ); } } /** * Gets the email template for the lockout log. * * @param Lockout_Log $model The lockout log model. * * @return string The email template key. */ private function get_template( Lockout_Log $model ): string { if ( Lockout_Log::AUTH_LOCK === $model->type ) { return 'login-lockout'; } if ( in_array( $model->type, Lockout_Log::get_ua_lockout_types(), true ) ) { return 'ua-lockout'; } return 'lockout-404'; } /** * Constructs and sends the email to the specified recipient. * * @param string $email The recipient's email address. * @param string $name The recipient's name. * @param array $email_data Pre-computed email subject and body text. * @param array $headers Pre-computed mail headers. * @param object $service The notification service object. * * @return void */ private function send_to_user( string $email, string $name, array $email_data, array $headers, object $service ): void { if ( $this->is_email_limit_reached( $email ) ) { return; } $content = $this->render_email_content( $name, $email_data['text'], $email, $service ); $ret = wp_mail( $email, $email_data['subject'], $content, $headers ); if ( $ret ) { $this->save_log( $email ); } } /** * Checks whether the recipient has reached the email notification limit. * * @param string $email The recipient's email address. * * @return bool True when the email limit has been reached. */ private function is_email_limit_reached( string $email ): bool { // Handle cool_off period in minutes and hours. // The minutes values are in fractional hours, e.g., 0.25 = 15 minutes, 0.5 = 30 minutes. $cool_off_seconds = (int) round( (float) $this->configs['cool_off'] * HOUR_IN_SECONDS ); $count = Email_Track::count( $this->slug, $email, time() - $cool_off_seconds, time() ); return $count >= $this->configs['threshold']; } /** * Gets email subject and body text for a lockout template. * * @param Lockout_Log $model The lockout log model. * @param string $template The email template to use. * @param string $network_site_url The network site URL. * * @return array Email subject and body text. */ private function get_email_data( Lockout_Log $model, string $template, string $network_site_url ): array { if ( 'login-lockout' === $template ) { return $this->get_login_lockout_email_data( $model, $network_site_url ); } if ( 'ua-lockout' === $template ) { return $this->get_ua_lockout_email_data( $model, $network_site_url ); } return $this->get_404_lockout_email_data( $model, $network_site_url ); } /** * Gets email data for login lockouts. * * @param Lockout_Log $model The lockout log model. * @param string $network_site_url The network site URL. * * @return array Email subject and body text. */ private function get_login_lockout_email_data( Lockout_Log $model, string $network_site_url ): array { /* translators: %s: Site URL. */ $subject = sprintf( __( 'Login lockout alert for %s', 'defender-security' ), $network_site_url ); $subject = wp_specialchars_decode( $subject, ENT_QUOTES ); // If the log is made from the 2FA module, then we get the settings from it, otherwise from Login_Lockout. $settings = wd_di()->get( Login_Lockout::class ); if ( false !== strpos( $model->log, '2fa attempts' ) ) { $component = wd_di()->get( Two_Fa::class ); $attempt_limit = $component->get_attempt_limit(); $time_limit = $component->get_time_limit() . esc_html__( ' seconds', 'defender-security' ); $type = '2fa'; } else { $attempt_limit = $settings->attempt; $time_limit = $settings->duration . ' ' . $settings->duration_unit; $type = 'login'; } // $text & $string will be escaped at src\view\email\login-lockout.php. /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4. Lockout type, 5: Translation string. */ $text = __( 'The host %1$s has been locked out of %2$s due to more than %3$s failed %4$s attempts. %5$s', 'defender-security' ); $text = sprintf( $text, '' . $model->ip . '', '' . $network_site_url . '', '' . $attempt_limit . '', $type, $this->get_lockout_duration_text( $settings->lockout_type, $time_limit ) ); return compact( 'subject', 'text' ); } /** * Gets email data for user agent lockouts. * * @param Lockout_Log $model The lockout log model. * @param string $network_site_url The network site URL. * * @return array Email subject and body text. */ private function get_ua_lockout_email_data( Lockout_Log $model, string $network_site_url ): array { $subject = sprintf( /* translators: %s: Site URL. */ __( 'User Agent lockout alert for %s', 'defender-security' ), $network_site_url ); $subject = wp_specialchars_decode( $subject, ENT_QUOTES ); $text = sprintf( /* translators: 1: User agent, 2: Site URL */ __( 'The %1$s has been locked out of %2$s.', 'defender-security' ), '' . $model->user_agent . '', '' . $network_site_url . '', ); return compact( 'subject', 'text' ); } /** * Gets email data for 404 lockouts. * * @param Lockout_Log $model The lockout log model. * @param string $network_site_url The network site URL. * * @return array Email subject and body text. */ private function get_404_lockout_email_data( Lockout_Log $model, string $network_site_url ): array { /* translators: %s: Site URL. */ $subject = sprintf( __( '404 lockout alert for %s', 'defender-security' ), $network_site_url ); $subject = wp_specialchars_decode( $subject, ENT_QUOTES ); $settings = wd_di()->get( Notfound_Lockout::class ); /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4: Tried, 5. Translation string. */ $text = __( 'The host %1$s has been locked out of %2$s due to more than %3$s 404 requests for the file %4$s. %5$s', 'defender-security' ); $text = sprintf( $text, '' . $model->ip . '', '' . $network_site_url . '', '' . $settings->attempt . '', '' . $model->tried . '', $this->get_lockout_duration_text( $settings->lockout_type, $settings->duration . ' ' . $settings->duration_unit ) ); return compact( 'subject', 'text' ); } /** * Gets the lockout duration text used in notification emails. * * @param string $lockout_type The lockout type. * @param string $duration The lockout duration. * * @return string The lockout duration text. */ private function get_lockout_duration_text( string $lockout_type, string $duration ): string { if ( 'permanent' === $lockout_type ) { return esc_html__( 'Accordingly, the host has been permanently banned.', 'defender-security' ); } return sprintf( /* translators: %s: Duration. */ esc_html__( 'They have been locked out for %s.', 'defender-security' ), '' . esc_html( $duration ) . '' ); } /** * Renders the complete email content. * * @param string $name The recipient's name. * @param string $text The email body text. * @param string $email The recipient's email address. * @param object $service The notification service object. * * @return string The rendered email content. */ private function render_email_content( string $name, string $text, string $email, object $service ): string { $logs_url = network_admin_url( 'admin.php?page=wdf-ip-lockout&view=logs' ); // Need for activated Mask Login feature. $logs_url = apply_filters( 'report_email_logs_link', $logs_url, $email ); // We don't call the Firewall controller to avoid cyclic dependency. It's a workaround with the simplest controller. $controller = wd_di()->get( Blocklist_Monitor::class ); $content_body = $controller->render_partial( 'email/login-lockout', array( 'name' => $name, // It's escaped value. 'text' => $text, 'logs_url' => $logs_url, ), false ); $unsubscribe_link = $service->create_unsubscribe_url( $this->slug, $email ); return $controller->render_partial( 'email/index', array( 'title' => esc_html__( 'Firewall', 'defender-security' ), 'content_body' => $content_body, 'unsubscribe_link' => $unsubscribe_link, ), false ); } /** * Define labels. * * @return array The array of settings labels. */ public function labels(): array { return array( 'notification' => esc_html__( 'Firewall - Alert', 'defender-security' ), 'login_lockout_notification' => esc_html__( 'Login Protection Lockout', 'defender-security' ), 'ip_lockout_notification' => esc_html__( '404 Detection Lockout', 'defender-security' ), 'ua_lockout_notification' => esc_html__( 'User Agent Lockout', 'defender-security' ), 'notification_subscribers' => esc_html__( 'Recipients', 'defender-security' ), 'cooldown_enabled' => esc_html__( 'Limit email alerts for repeat lockouts', 'defender-security' ), 'cooldown_number_lockout' => esc_html__( 'Repeat Lockouts Threshold', 'defender-security' ), 'cooldown_period' => esc_html__( 'Repeat Lockouts Period', 'defender-security' ), ); } /** * Additional converting rules. * * @param array $configs The configuration data. * * @return array The type-casted configuration data. * @since 3.1.0 */ public function type_casting( array $configs ): array { if ( ! isset( $configs['threshold'] ) ) { $configs['threshold'] = $this->configs['threshold'] ?? 3; } if ( ! isset( $configs['cool_off'] ) ) { $configs['cool_off'] = $this->configs['cool_off'] ?? 24; } return $configs; } /** * Returns lockout notification flags for the Hub. * * @return array */ public function get_hub_data(): array { if ( self::STATUS_ACTIVE !== $this->status ) { return array( 'login_lockout' => false, '404_lockout' => false, 'ua_lockout' => false, ); } return array( 'login_lockout' => wd_di()->get( Login_Lockout::class )->enabled, '404_lockout' => wd_di()->get( Notfound_Lockout::class )->enabled, 'ua_lockout' => wd_di()->get( User_Agent_Lockout::class )->enabled, ); } /** * Normalizes firewall notification data after loading. */ protected function after_load(): void { parent::after_load(); $this->title = esc_html__( 'Firewall alert', 'defender-security' ); } }