<?php

namespace WPSecurityNinja\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

define( 'WF_SN_CF_OPTIONS_KEY', 'wf_sn_cf' ); // @important - if changed, this is used various places, 2FA, etc.
define( 'WF_SN_CF_VALIDATED_CRAWLERS', 'wf_sn_cf_validated_crawlers' );

class Wf_sn_cf {

	/**
	 * Cached visitor IP for the current request.
	 *
	 * @var string|null
	 */
	private static $cached_ip = null;

	/**
	 * Cloud Firewall options.
	 *
	 * @var array<string, mixed>|null
	 */
	public static $options = null;

	/**
	 * Locally banned IPs (IP => expiry timestamp), cached per request.
	 *
	 * @var array<string, int>|null
	 */
	public static $banned_ips = null;

	/**
	 * Per-request cache for get_satellite_soften_info() keyed by IP (avoids duplicate ASN lookups).
	 *
	 * @var array<string, array{applies: bool, asn: int, as_label: string}>
	 */
	private static $satellite_soften_info_cache = array();

	/**
	 * Per-request keys for satellite soften event logging (md5 of IP + rule).
	 *
	 * @var array<string, bool>
	 */
	private static $satellite_soften_logged = array();

	/**
	 * Per-request cache for get_visitor_hostname() keyed by IP.
	 *
	 * @var array<string, string>
	 */
	private static $visitor_hostname_cache = array();

	/**
	 * Per-request cache for cloud IP blocklist option (wf_sn_cf_ips).
	 *
	 * @var array|null|false null = not loaded; false = missing/invalid; array = decoded option.
	 */
	private static $cloud_ips_cache = null;

	public static $central_api_url = 'https://api.securityninjawp.com/wp-json/secnin/v1/';

	public static function init() {
		// Load options if not already loaded
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		// Conditionally load 404 Guard class only if enabled
		// $firewall_options = self::get_options();

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			require_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-wf-sn-cf-ip-management.php';
			if ( self::is_active() && ! empty( self::$options['404guard_enabled'] ) ) {
				require_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-404-guard.php';
				SN_404_Guard::init();
			}
		}

		// update geolocation database via SN_Geolocation in class-sn-geolocation.php

		// setup_theme seems to be earliest hook - because of Freemius API - Future - add as mu-plugin - plugins_loaded earliest possible hook
		if ( self::$options['active'] ) {
			add_action( 'template_redirect', array( __CLASS__, 'check_visitor' ), 1 );
			add_action( 'login_head', array( __NAMESPACE__ . '\\wf_sn_cf', 'check_visitor' ), 1 );

			if ( secnin_fs()->can_use_premium_code__premium_only() ) {

				add_action( 'secnin_update_geoip', array( __NAMESPACE__ . '\\wf_sn_cf', 'update_geoip_database__premium_only' ) );
				add_action( 'secnin_update_geoip', array( __NAMESPACE__ . '\\wf_sn_cf', 'update_geoip_database__premium_only' ) );

				add_action( 'secnin_update_cloud_firewall', array( __NAMESPACE__ . '\\wf_sn_cf', 'update_cloud_ips__premium_only' ) );

				add_action( 'secnin_prune_visitor_log', array( __NAMESPACE__ . '\\wf_sn_cf', 'prune_visitor_log' ) );

				add_action( 'secnin_prune_banned', array( __NAMESPACE__ . '\\wf_sn_cf', 'prune_banned' ) );

				add_action( 'secnin_update_blocked_ips', array( __NAMESPACE__ . '\\wf_sn_cf', 'action_update_blocked_ips__premium_only' ) );

				add_action( 'init', array( __NAMESPACE__ . '\\wf_sn_cf', 'schedule_cron_jobs' ) );
			}
		}

		add_action( 'init', array( __NAMESPACE__ . '\\wf_sn_cf', 'do_init_action' ), 1 );

		// Basic login/failed login logging for all versions
		add_action( 'wp_login', array( __NAMESPACE__ . '\\wf_sn_cf', 'log_successful_login' ), 10, 2 );
		add_action( 'wp_login_failed', array( __NAMESPACE__ . '\\wf_sn_cf', 'failed_login' ) );

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			add_action( 'wp_login', array( __NAMESPACE__ . '\\wf_sn_cf', 'set_login_timestamp' ), 10, 2 );
			add_action( 'login_init', array( __NAMESPACE__ . '\\wf_sn_cf', 'form_init_check' ) );
			add_filter( 'authenticate', array( __NAMESPACE__ . '\\wf_sn_cf', 'login_filter' ), 10, 3 );
			add_filter( 'login_message', array( __NAMESPACE__ . '\\wf_sn_cf', 'login_message' ) );
			add_filter( 'login_errors', array( __NAMESPACE__ . '\\wf_sn_cf', 'process_login_errors' ) );
			add_filter( 'woocommerce_login_form_start', array( __NAMESPACE__ . '\\wf_sn_cf', 'process_woocommerce_login_form_start' ) );

			// Lost password protection
			add_action( 'lostpassword_post', array( __NAMESPACE__ . '\\wf_sn_cf', 'check_lost_password_attempt' ) );

		}

		if ( is_admin() ) {
			// add tab to Security Ninja tabs
			add_filter( 'sn_tabs', array( __NAMESPACE__ . '\wf_sn_cf', 'sn_tabs' ) );

			// Register settings
			add_action( 'admin_init', array( __NAMESPACE__ . '\\wf_sn_cf', 'register_settings' ) );

			// Register AJAX actions
			add_action( 'wp_ajax_sn_enable_firewall', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_enable_firewall' ) );
			add_action( 'wp_ajax_sn_disable_firewall', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_disable_firewall' ) );
			add_action( 'wp_ajax_sn_send_unblock_email', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_send_unblock_email' ) );

			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				add_action( 'wp_ajax_sn_test_ip', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_test_ip' ) );
				add_action( 'wp_ajax_sn_cf_add_ip_rule', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_add_ip_rule' ) );
				add_action( 'wp_ajax_sn_cf_edit_ip_rule', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_edit_ip_rule' ) );
				add_action( 'wp_ajax_sn_cf_remove_ip_rule', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_remove_ip_rule' ) );
				add_action( 'wp_ajax_sn_cf_bulk_add_ip_rules', array( __NAMESPACE__ . '\\wf_sn_cf', 'ajax_bulk_add_ip_rules' ) );
			}

			add_action( 'admin_post_sn_save_login_settings', array( __NAMESPACE__ . '\\wf_sn_cf', 'save_login_settings' ) );

			// Enqueue scripts and styles
			add_action( 'admin_enqueue_scripts', array( __NAMESPACE__ . '\\wf_sn_cf', 'enqueue_scripts' ) );
		}
	}
	/**
	 * check_user_login.
	 *
	 * @since v0.0.1
	 * @access public
	 * @param string $user_login Login name.
	 * @return string
	 */
	public static function check_user_login__premium_only( $user_login ) {
		self::get_options();

		$protect_login_form = self::$options['protect_login_form'];
		if ( ! $protect_login_form ) {
			return $user_login;
		}
		self::check_visitor();
		return $user_login;
	}

	/**
	 * check_register_post.
	 *
	 * @since v0.0.1
	 * @access public
	 * @param string               $sanitized_user_login Sanitized username.
	 * @param string               $user_email           User email.
	 * @param \WP_Error            $errors               Registration errors.
	 * @return \WP_Error
	 */
	public static function check_register_post__premium_only( $sanitized_user_login, $user_email, $errors ) {

		self::get_options();

		$protect_login_form = self::$options['protect_login_form'];
		if ( ! $protect_login_form ) {
			return $errors;
		}

		self::check_visitor();

		return $errors;
	}


	/**
	 * Process IP only early.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, May 21st, 2024.
	 * @version v1.0.1  Monday, January 13th, 2025 - Added REST API protection
	 * @access  public static
	 * @return  void
	 */
	public static function do_init_action() {
		// Load options if not already loaded
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		// Check for safe URL unlock parameter BEFORE checking bans
		// This allows blocked visitors to whitelist their IP using the secret access URL
		if ( isset( $_REQUEST['snf'] ) && sanitize_key( wp_unslash( $_REQUEST['snf'] ) ) === self::$options['unblock_url'] ) {
			$current_user_ip = self::get_user_ip();
			$local_whitelist = self::$options['whitelist'];
			if ( ! is_array( $local_whitelist ) ) {
				$local_whitelist = array();
			}
			// Use CIDR-aware check so secret URL does not append a duplicate IP when already covered by a range.
			if ( ! Wf_sn_cf_Utils::is_whitelisted( $current_user_ip, $local_whitelist ) ) {
				self::$options['whitelist'][] = $current_user_ip;
				wf_sn_el_modules::log_event( 'security_ninja', 'unblocked_ip', 'New IP added to the whitelist using the secret access URL.', '' );
				update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );
			}
			// Return early - don't check bans or kill the request
			return;
		}

		// Enhanced REST API protection - check for wp-json in the URL
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
		if ( strpos( $request_uri, '/wp-json/' ) !== false ) {
			return;
		}

		// Additional protection for REST API requests that might not have /wp-json/ in the path
		// but are still legitimate REST API calls (e.g. when using rest_route parameter)
		if ( wp_is_json_request() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || isset( $_GET['rest_route'] ) ) {
			return;
		}

		if ( wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
			return;
		}

		// Kill-switch: no ban enforcement or query filtering when firewall is disabled.
		// Secret unlock URL above still works so users can whitelist their IP.
		if ( 1 !== self::is_active() ) {
			return;
		}

		$current_user_ip = self::get_user_ip();

		// Skip expensive ban checks for logged-in admins in wp-admin / admin-ajax.
		// (Secret unlock, REST, cron/CLI, and inactive firewall already returned above.)
		$skip_ban_enforcement = ( is_admin() || wp_doing_ajax() ) && current_user_can( 'manage_options' );
		$reason               = false;
		if ( ! $skip_ban_enforcement ) {
			$reason = self::is_banned_ip( $current_user_ip );
		}

		if ( $reason ) {
			// Login-only bypass: country bans use countryblock_loginonly; IP/cloud bans use global.
			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				if ( self::should_limit_ban_to_login_only__premium_only( $reason ) ) {
					if ( ! self::is_login_related_request__premium_only() ) {
						return;
					}
				}
			}

			self::update_blocked_count( $current_user_ip );

			$ua_string = '';
			if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
				$ua_string = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
			}
			$data = array(
				'user_agent' => $ua_string,
				'ip'         => $current_user_ip,
				'reason'     => $reason,
			);
			wf_sn_el_modules::log_event( 'security_ninja', 'do_init_action', 'Blocked.', $data, null, $current_user_ip );
			wp_clear_auth_cookie();
			self::kill_request( $current_user_ip, $reason, null, true );
			exit();
		}
		// Only check if firewall is active
		if ( ( 1 === (int) self::$options['active'] ) && ( 1 === (int) self::$options['filterqueries'] ) ) {
			// Determine if user is whitelisted (but still check for suspicious requests)
			$whitelisted_user = false;

			if ( current_user_can( 'manage_options' ) ) {
				$whitelisted_user = true;
			}
			$local_whitelist = self::$options['whitelist'];
			if ( ! is_array( $local_whitelist ) ) {
				$local_whitelist = array();
			}
			if ( Wf_sn_cf_Utils::is_whitelisted( $current_user_ip, $local_whitelist ) ) {
				$whitelisted_user = true;
			}

			if ( self::is_whitelisted_service( $current_user_ip ) ) {
				$whitelisted_user = true;
			}

			$validated_crawlers = get_option( WF_SN_CF_VALIDATED_CRAWLERS );
			if ( is_array( $validated_crawlers ) && in_array( $current_user_ip, $validated_crawlers, true ) ) {
				$whitelisted_user = true;
			}

			// Always check for bad queries (even for whitelisted users), but skip AJAX, cron, and admin requests
			if ( ! wp_doing_ajax() && ! wp_doing_cron() && ! is_admin() ) {
				// Skip security checks for temporary login links
				if ( self::is_temporary_login_link() ) {
					return; // Allow temporary login links through
				}
				$bad_query = self::check_bad_queries();

				if ( $bad_query !== false ) {
					// Detects if we are importing
					if ( defined( 'WP_IMPORTING' ) && $bad_query ) {
						if ( secnin_fs()->can_use_premium_code__premium_only() ) {
							$ua_string            = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '';
							$current_user_country = '';
							include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geolocation.php';
							if ( class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
								$geolocate_ip = \WPSecurityNinja\Plugin\SN_Geolocation::geolocate_ip( $current_user_ip, true );
								if ( $geolocate_ip ) {
									$current_user_country = $geolocate_ip['country'];
								}
							}
							self::log_visitor__premium_only(
								array(
									'ip'          => $current_user_ip,
									'user_agent'  => $ua_string,
									'country'     => $current_user_country,
									'banned'      => 1,
									'ban_reason'  => 'login_denied_banned_ip',
									'description' => __( 'Suspicious data detected during import', 'security-ninja' ),
									'raw_data'    => esc_sql( $bad_query ),
								)
							);
						}
						// set the query to false, not going to block but we left a notice
						$bad_query = false;
					}

					if ( $bad_query ) {
						$extramessage = '';
						$extraarr     = array(
							'ban_type' => '',
						);

						if ( isset( $bad_query['request_uri'] ) ) {
							$extraarr['ban_reason'] = $bad_query['request_uri'];
							$extraarr['ban_type']   = 'request_uri';
							$extramessage           = 'request_uri';
						}

						if ( isset( $bad_query['query_string'] ) ) {
							$extraarr['ban_type']   = 'query_string';
							$extraarr['ban_reason'] = $bad_query['query_string'];
						}

						if ( isset( $bad_query['http_user_agent'] ) ) {
							$extraarr['ban_type']   = 'http_user_agent';
							$extraarr['ban_reason'] = $bad_query['http_user_agent'];
						}

						if ( isset( $bad_query['referrer'] ) ) {
							$extraarr['ban_type']   = 'referrer';
							$extraarr['ban_reason'] = $bad_query['referrer'];
						}

						if ( isset( $bad_query['blocked_host'] ) ) {
							$extraarr['ban_type']   = 'blocked_host';
							$extraarr['ban_reason'] = $bad_query['visitor_host'];
						}

						$extraarr = array_merge( $extraarr, $bad_query );

						$extraarr['ip']         = $current_user_ip;
						$ua_string              = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '';
						$extraarr['user_agent'] = $ua_string;

						if ( secnin_fs()->can_use_premium_code__premium_only() ) {
							$current_user_country = '';
							include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geolocation.php';
							if ( class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
								$geolocate_ip = \WPSecurityNinja\Plugin\SN_Geolocation::geolocate_ip( $current_user_ip, true );
								if ( $geolocate_ip ) {
									$current_user_country = $geolocate_ip['country'];
								}
							}
							$extraarr['country'] = $current_user_country;
						}

						$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url( $_SERVER['REQUEST_URI'] ) : '';
						if ( ! empty( $request_uri ) ) {
							$extraarr['request_uri'] = $request_uri;
						}

						$query_string = isset( $_SERVER['QUERY_STRING'] ) ? esc_url( $_SERVER['QUERY_STRING'] ) : '';
						if ( ! empty( $query_string ) ) {
							$extraarr['query_string'] = $query_string;
						}

						$http_referer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url( $_SERVER['HTTP_REFERER'] ) : '';
						if ( ! empty( $http_referer ) ) {
							$extraarr['http_referer'] = $http_referer;
						}

						$blockedmessage = __( 'Suspicious Request', 'security-ninja' );
						if ( isset( $extramessage ) ) {
							$blockedmessage .= ' ' . $extramessage;
						}

						// Always log suspicious requests, even for whitelisted users
						$log_action  = $whitelisted_user ? 'suspicious_request_whitelisted' : 'blocked_ip_suspicious_request';
						$log_message = $whitelisted_user
							? __( 'Suspicious Request (Whitelisted - Not Blocked)', 'security-ninja' ) . ( $extramessage ? ' ' . $extramessage : '' )
							: $blockedmessage;

						wf_sn_el_modules::log_event( 'security_ninja', $log_action, $log_message, $extraarr );

						if ( secnin_fs()->can_use_premium_code__premium_only() ) {
							self::log_visitor__premium_only(
								array(
									'ip'          => $current_user_ip,
									'user_agent'  => $ua_string,
									'country'     => isset( $current_user_country ) ? $current_user_country : '',
									'banned'      => $whitelisted_user ? 0 : 1, // Don't mark as banned if whitelisted
									'ban_reason'  => 'blocked_ip_suspicious_request_' . esc_attr( $extraarr['ban_type'] ),
									'description' => $whitelisted_user
										? __( 'Suspicious request detected (whitelisted user - allowed)', 'security-ninja' ) . ' ' . esc_attr( $extraarr['ban_type'] )
										: __( 'Blocked', 'security-ninja' ) . ' ' . esc_attr( $extraarr['ban_type'] ),
									'raw_data'    => $extraarr,
								)
							);

							// Only report to network for non-whitelisted users
							if ( ! $whitelisted_user ) {
								$submitargs = array(
									'ban_type'   => $extraarr['ban_type'],
									'ban_reason' => $extraarr['ban_reason'],
								);
								self::network_reportip__premium_only( $current_user_ip, $submitargs );
							}
						}

						// Only block non-whitelisted users
						if ( ! $whitelisted_user ) {
							// Update blocked count
							self::update_blocked_count( $current_user_ip );

							// Block the request
							wp_clear_auth_cookie();
							self::kill_request( $current_user_ip, $extraarr['ban_reason'], 1 * HOUR_IN_SECONDS, true );
							exit();
						}
						// For whitelisted users, log but allow the request to continue
					}
				}
			}
		}
	}

	/**
	 * Basic logging for successful login (free version)
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, January 14th, 2025.
	 * @param   mixed   $user_login
	 * @param   mixed   $user
	 * @return  void
	 */
	public static function log_successful_login( $user_login, $user ) {
		if ( ! is_object( $user ) || ! isset( $user->ID ) ) {
			return;
		}
		$current_user_ip = self::get_user_ip();
		$ua_string       = '';
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			$ua_string = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
		}

		// Check if login was via temporary login plugin
		$temp_login_plugin = self::get_temporary_login_plugin_name();

		if ( $temp_login_plugin ) {
			$description = sprintf(
				/* translators: 1: User login name, 2: IP address, 3: Temporary login plugin name */
				__( '%1$s logged in successfully from %2$s via temporary login (%3$s)', 'security-ninja' ),
				esc_html( $user_login ),
				esc_html( $current_user_ip ),
				esc_html( $temp_login_plugin )
			);
		} else {
			$description = sprintf(
				/* translators: 1: User login name, 2: IP address */
				__( '%1$s logged in successfully from %2$s', 'security-ninja' ),
				esc_html( $user_login ),
				esc_html( $current_user_ip )
			);
		}

		$event_data = array(
			'ip'         => $current_user_ip,
			'username'   => $user_login,
			'user_id'    => $user->ID,
			'user_agent' => $ua_string,
		);

		// Add temporary login plugin info if detected
		if ( $temp_login_plugin ) {
			$event_data['temporary_login_plugin'] = $temp_login_plugin;
		}

		wf_sn_el_modules::log_event( 'security_ninja', 'wp_login', $description, $event_data, $user->ID );
	}


	/**
	 * Sets a timestamp for the user when successfully logging in.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, February 6th, 2024.
	 * @param   mixed   $user_login
	 * @param   mixed   $user
	 * @return  void
	 */
	public static function set_login_timestamp( $user_login, $user ) {
		if ( ! is_object( $user ) || ! isset( $user->ID ) ) {
			return;
		}
		update_user_meta( $user->ID, 'sn_last_login', current_time( 'mysql' ) );
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			// If user is an administrator, add their IP to whitelist
			if ( user_can( $user->ID, 'manage_options' ) ) {
				$current_user_ip = self::get_user_ip();
				if ( $current_user_ip ) {
					self::add_ip_to_whitelist( $current_user_ip );
				}
			}
		}
	}


	/**
	 * Reverse DNS hostname for an IP with per-request and transient caching.
	 *
	 * @author  Lars Koudal
	 * @since   5.289
	 * @param   string $ip IP address.
	 * @return  string Lowercase hostname, or the IP on lookup failure (matches gethostbyaddr behavior).
	 */
	private static function get_visitor_hostname( $ip ) {
		if ( ! is_string( $ip ) || '' === $ip ) {
			return '';
		}

		if ( isset( self::$visitor_hostname_cache[ $ip ] ) ) {
			return self::$visitor_hostname_cache[ $ip ];
		}

		$transient_key = 'wf_sn_cf_rdns_' . md5( $ip );
		$cached        = get_transient( $transient_key );
		if ( false !== $cached && is_string( $cached ) ) {
			self::$visitor_hostname_cache[ $ip ] = $cached;
			return $cached;
		}

		$hostname = gethostbyaddr( $ip );
		if ( ! is_string( $hostname ) || '' === $hostname ) {
			$hostname = $ip;
		}
		$hostname = strtolower( $hostname );

		set_transient( $transient_key, $hostname, WEEK_IN_SECONDS );
		self::$visitor_hostname_cache[ $ip ] = $hostname;

		return $hostname;
	}


	/**
	 * Validate a crawlers IP against the hostname
	 *
	 * Also validates known AI crawlers (OpenAI, Perplexity) when User-Agent matches and the IP is in
	 * published provider ranges (cached; non-blocking refresh). Anthropic is not included: no stable
	 * published IP ranges for verification. Google-Extended is a robots policy token, not a crawler UA.
	 *
	 * @author  Lars Koudal
	 * @since   v5.123
	 * @version v1.0.0  Monday, August 30th, 2021.
	 * @version v1.0.1  Monday, June 3rd, 2024.
	 * @access  private static
	 * @param   mixed       $testip Client IP.
	 * @param   string|null $ua     User-Agent string; null reads HTTP_USER_AGENT when present.
	 * @return  boolean
	 */
	public static function validate_crawler_ip( $testip, $ua = null ) {
		// Lets check if the IP has already been validated
		$validated_crawlers = get_option( WF_SN_CF_VALIDATED_CRAWLERS );
		if ( $validated_crawlers ) {
			if ( in_array( $testip, $validated_crawlers, true ) ) {
				return true;
			}
		} else {
			$validated_crawlers = array();
		}

		$hostname         = self::get_visitor_hostname( $testip ); //"crawl-66-249-66-1.googlebot.com"
		$valid_host_names = array(
			'.crawl.baidu.com',
			'.crawl.baidu.jp',
			'.search.msn.com',
			'.google.com',
			'.googlebot.com',
			'.crawl.yahoo.net',
			'.yandex.ru',
			'.yandex.net',
			'.yandex.com',
			'.search.msn.com',
			'.petalsearch.com',
			'applebot.apple.com',
			'.ahrefs.com',  // Added Ahrefs
			'.semrush.com',
			'.duckduckgo.com',
			'facebookexternalhit.com',
			'.commoncrawl.org',
			'.googleother.com',
			'.google-inspectiontool.com',
			'.swiftype.com',
			'.sogou.com',
			'.yahoo.com',
			'.bing.com',
		);

		foreach ( $valid_host_names as $valid_host ) {
			if ( Wf_sn_cf_Utils::string_ends_with( $hostname, $valid_host ) ) {
				$returned_ip = gethostbyname( $hostname );
				if ( $returned_ip === $testip ) {
					$validated_crawlers[] = $testip;
					update_option( WF_SN_CF_VALIDATED_CRAWLERS, $validated_crawlers, false );

					wf_sn_el_modules::log_event( 'security_ninja', 'validated_crawler_ip', 'Valid Crawler' . esc_attr( $hostname ), '', '', esc_attr( $testip ) );
					return true;
				}
			}
		}

		// Verified AI crawlers: UA token + IP in provider-published ranges (cached).
		if ( null === $ua ) {
			$ua = '';
			if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
				$ua = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) );
			}
		}

		$ai_feed_urls = Wf_sn_cf_Utils::get_ai_crawler_feed_urls_for_ua( $ua );
		if ( ! empty( $ai_feed_urls ) ) {
			foreach ( $ai_feed_urls as $feed_url ) {
				$cidrs = Wf_sn_cf_Utils::get_ai_crawler_cidrs_for_feed_url( $feed_url );
				foreach ( $cidrs as $cidr ) {
					if ( Wf_sn_cf_Utils::ipCIDRMatch( $testip, $cidr ) ) {
						$validated_crawlers[] = $testip;
						update_option( WF_SN_CF_VALIDATED_CRAWLERS, $validated_crawlers, false );
						wf_sn_el_modules::log_event(
							'security_ninja',
							'validated_crawler_ip',
							__( 'Valid AI crawler (published IP ranges)', 'security-ninja' ),
							array(
								'feed' => $feed_url,
								'ua'   => $ua,
							),
							null,
							is_string( $testip ) ? $testip : null
						);
						return true;
					}
				}
			}
		}

		return false;
	}



	/**
	 * Checks if an IP is from a service that has been enabled
	 *
	 * This method checks if the given IP address is whitelisted for services such as Broken Link Checker, GetTerms, WP Rocket, ManageWP, UptimeRobot, WP Compress, and MonSpark.
	 * It also checks for IP ranges (CIDR) in the whitelist.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Wednesday, May 8th, 2024.
	 * @access  public static
	 * @param   mixed   $current_user_ip    The IP address to check against the whitelist.
	 * @return  boolean                 Returns true if the IP is whitelisted, false otherwise.
	 */
	public static function is_whitelisted_service( $current_user_ip ) {
		$whitelist_brokenlink = array(
			'94.231.107.9', // Broken Link Checker
			'54.191.137.17', // Broken Link Checker
		);

		// GetTerms cookie scanner — always whitelisted (getterms.io).
		$whitelist_getterms = array(
			'45.55.125.144', // GetTerms cookie scanner
		);
		$whitelist_wprocket = array();
		if ( isset( self::$options['whitelist_wprocket'] ) && self::$options['whitelist_wprocket'] ) {
			$whitelist_wprocket = array(
				'109.234.160.58', // WP Rocket - Load CSS async
				'51.83.15.135', // WP Rocket - Load CSS async
				'51.210.39.196', // WP Rocket - Load CSS async
				'146.59.192.120', // WP Rocket - license validation
				'135.125.83.227', // WP Rocket - Remove unused CSS
				'146.59.251.59', // WP Rocket - RocketCDN
			);
		}

		$extra_whitelist = array(
			// Divi Dash
			'67.227.164.200',
			'67.227.164.201',
			'67.227.164.202',
			// Fastpixel
			'23.88.6.90',
			'136.243.103.55',
			'176.9.77.187',
			'5.161.48.189',
			// WPMU Dev
			'18.204.159.253',
			'54.227.51.40',
			'18.219.56.14',
			'45.55.78.242',
			'35.171.56.101',
			'34.196.51.17',
			'35.157.144.199',
			'165.227.251.117',
			'165.227.251.120',
			'140.82.60.49',
			'45.63.10.140',
			'18.219.161.157',
			'165.227.127.103',
			'64.176.196.23',
			'144.202.86.106',
			'3.93.131.0',
			'167.71.93.101',
			'167.71.179.192',
		);

		/*
		@todo -
		IP addresses used by wpcompress.com resolve to rDNS names in the format api.wpcompress.com. To simplify firewall configurations and ensure you're whitelisting the correct IP addresses, you can whitelist IPs based on the domain *.wpcompress.com by resolving the rDNS of our IPs.

		*/
		$whitelist_wpcompress = array(
			'168.119.147.46',
			'71.19.240.35',
			'216.52.183.178',
			'167.160.91.242',
			'51.79.230.163',
			'51.161.208.134',
			'213.133.103.23',
			'162.55.161.208',
			'213.239.197.231',
			'88.99.209.68',
			'109.224.229.186',
			'109.224.229.188',
			'109.224.229.192',
			'109.224.229.241',
			'109.224.230.12',
			'109.224.230.19',
			'2a01:4f8:251:a11::/64',
			'2605:9f80:c000:240::2/64',
			'2605:9f80:1000:461::2/64',
			'2402:1f00:8001:11a3::/64',
			'2402:1f00:8201:486::/64',
			'2a01:4f8:a0:90d5::/64',
			'2a01:4f8:c012:bb07::/64',
			'2a01:4f8:222:1059::/64',
			'2a01:4f8:10a:3a47::/64',
		);

		// Hardcoded, not an option - always whitelisted.
		$whitelist_modulards = array(
			'54.220.170.248/32',
			'34.249.165.39/32',
			'63.34.51.157/32',
			'54.73.153.210/32',
			'52.210.126.224/32',
		);

		// MonSpark uptime monitoring — always whitelisted (see docs.monspark.com IP whitelisting).
		$whitelist_monspark = array(
			'51.15.238.191',   // EU
			'51.15.192.231',   // EU
			'85.111.66.59',    // EU
			'15.235.41.103',   // US
			'148.113.138.118', // US
			'51.79.143.61',    // APAC
			'135.125.246.37',  // EU (legacy)
			'57.129.32.158',   // EU (legacy)
		);

		$whitelist_managewp = array();
		if ( isset( self::$options['whitelist_managewp'] ) && self::$options['whitelist_managewp'] ) {
			$whitelist_managewp = self::load_ip_whitelist( 'whitelist-managewp.json' );
		}

		$whitelist_uptimia = array();
		if ( isset( self::$options['whitelist_uptimia'] ) && self::$options['whitelist_uptimia'] ) {
			$whitelist_uptimia = self::load_ip_whitelist( 'whitelist-uptimia.json' );
		}

		$whitelist_uptimerobot = array();
		if ( isset( self::$options['whitelist_uptimerobot'] ) && self::$options['whitelist_uptimerobot'] ) {
			$whitelist_uptimerobot = self::load_ip_whitelist( 'whitelist-uptimerobot.json' );
		}

		$whitelist = array_merge( $whitelist_brokenlink, $whitelist_getterms, $whitelist_wprocket, $whitelist_managewp, $whitelist_uptimia, $whitelist_wpcompress, $extra_whitelist, $whitelist_uptimerobot, $whitelist_modulards, $whitelist_monspark );

		foreach ( $whitelist as $whitelist_item ) {
			// Check if the current whitelist item is an IP range (CIDR)
			if ( strpos( $whitelist_item, '/' ) !== false ) {
				// Use the proper CIDR matching function
				if ( Wf_sn_cf_Utils::ipCIDRMatch( $current_user_ip, $whitelist_item ) ) {
					return true; // IP is whitelisted
				}
			} elseif ( $current_user_ip === $whitelist_item ) {
					return true; // IP is whitelisted

			}
		}
		return false; // IP is not whitelisted
	}



	/**
	 * Loads an IP whitelist from a bundled JSON data file.
	 *
	 * The service whitelist data files (ManageWP, UptimeRobot, Uptimia) contain a
	 * plain JSON array of IP addresses and CIDR ranges. They are shipped as JSON
	 * data rather than executable PHP so that security scanners do not mistake a
	 * static list of IPs for obfuscated/concealed code.
	 *
	 * @author  Lars Koudal
	 * @since   v5.290
	 * @access  private static
	 * @param   string $filename  Filename of the JSON file inside modules/cloud-firewall/.
	 * @return  array             Array of IP/CIDR strings, or empty array on failure.
	 */
	private static function load_ip_whitelist( $filename ) {
		static $cache = array();

		$filename = basename( $filename );
		if ( isset( $cache[ $filename ] ) ) {
			return $cache[ $filename ];
		}

		$path = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/' . $filename;

		if ( ! is_readable( $path ) ) {
			$cache[ $filename ] = array();
			return $cache[ $filename ];
		}

		$contents = file_get_contents( $path );
		if ( false === $contents || '' === $contents ) {
			$cache[ $filename ] = array();
			return $cache[ $filename ];
		}

		$decoded = json_decode( $contents, true );
		if ( ! is_array( $decoded ) ) {
			$cache[ $filename ] = array();
			return $cache[ $filename ];
		}

		$ips = array();
		foreach ( $decoded as $entry ) {
			if ( ! is_string( $entry ) ) {
				continue;
			}
			$entry = trim( $entry );
			if ( '' !== $entry ) {
				$ips[] = $entry;
			}
		}

		$cache[ $filename ] = $ips;
		return $cache[ $filename ];
	}

	/**
	 * Resolve this server's IP once per request (and cache 1 hour across requests).
	 *
	 * Used to avoid treating the server itself as a banned visitor.
	 *
	 * @return string Server IP or empty string on failure.
	 */
	private static function get_server_ip() {
		static $server_ip = null;
		if ( null !== $server_ip ) {
			return $server_ip;
		}

		$cached = get_transient( 'wf_sn_cf_server_ip' );
		if ( is_string( $cached ) && filter_var( $cached, FILTER_VALIDATE_IP ) ) {
			$server_ip = $cached;
			return $server_ip;
		}

		$host     = gethostname();
		$resolved = ( $host ) ? gethostbyname( $host ) : '';
		if ( is_string( $resolved ) && filter_var( $resolved, FILTER_VALIDATE_IP ) ) {
			$server_ip = $resolved;
			set_transient( 'wf_sn_cf_server_ip', $server_ip, HOUR_IN_SECONDS );
		} else {
			$server_ip = '';
		}

		return $server_ip;
	}

	/**
	 * Cloud IP blocklist option, cached for the current request.
	 *
	 * @return array|false Decoded option array, or false if missing/invalid.
	 */
	private static function get_cloud_ips_data() {
		if ( null !== self::$cloud_ips_cache ) {
			return self::$cloud_ips_cache;
		}

		$ips                   = get_option( 'wf_sn_cf_ips' );
		self::$cloud_ips_cache = is_array( $ips ) ? $ips : false;
		return self::$cloud_ips_cache;
	}

	/**
	 * Clear the per-request cloud IP cache after the option is updated.
	 *
	 * @return void
	 */
	private static function clear_cloud_ips_cache() {
		self::$cloud_ips_cache = null;
	}

	/**
	 * Check visitor and apply firewall rules
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Monday, January 13th, 2025 - Added additional REST API protection
	 * @access  public static
	 * @return  void
	 */
	public static function check_visitor() {
		global $wpdb;

		// Enhanced REST API protection - check for wp-json in the URL
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
		if ( strpos( $request_uri, '/wp-json/' ) !== false ) {
			return;
		}

		// Additional protection for REST API requests that might not have /wp-json/ in the path
		// but are still legitimate REST API calls (e.g. when using rest_route parameter)
		if ( wp_is_json_request() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || isset( $_GET['rest_route'] ) ) {
			return;
		}

		// Filter out AJAX, cron and admin related requests
		if ( wp_doing_ajax() || wp_doing_cron() || is_admin() ) {
			return;
		}

		$server_ip = self::get_server_ip();

		$whitelisted_user = false;
		$administrator    = false; // @todo next linie - implementer egen løsning med bonus for at finde land hvis slået til
		$visit_logged     = false;

		$current_user_ip = self::get_user_ip();

		if ( $server_ip === $current_user_ip ) {
			return;
		}

		if ( current_user_can( 'manage_options' ) ) {
			// A user with admin privileges
			$administrator    = true;
			$whitelisted_user = true;
		}

		// Prevents user from being blocked even from a blocked country if IP is whitelisted (exact IP or CIDR range).
		$local_whitelist = self::$options['whitelist'];
		if ( ! is_array( $local_whitelist ) ) {
			$local_whitelist = array();
		}
		if ( Wf_sn_cf_Utils::is_whitelisted( $current_user_ip, $local_whitelist ) ) {
			$whitelisted_user = true;
		}

		if ( self::is_whitelisted_service( $current_user_ip ) ) {
			$whitelisted_user = true;
		}

		if ( in_array( $current_user_ip, array( '::1', '127.0.0.1' ), true ) ) {
			// Do not auto-trust localhost for WAF/logging unless explicitly whitelisted.
			if ( ! Wf_sn_cf_Utils::is_whitelisted( $current_user_ip, $local_whitelist ) ) {
				$whitelisted_user = false;
				$administrator    = false;
			}
		}

		$ua_string = '';
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			$ua_string = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
		}

		$current_user_country = '';

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {

			include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geolocation.php';
			if ( class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
				$geolocate_ip = \WPSecurityNinja\Plugin\SN_Geolocation::geolocate_ip( $current_user_ip, true );
				if ( $geolocate_ip ) {
					$current_user_country = $geolocate_ip['country'];
				}
			}

			// Check IP against blacklist
			$blacklist = self::$options['blacklist'];
			if ( ( ! $whitelisted_user ) && ( is_array( $blacklist ) ) ) {
				foreach ( $blacklist as $bl ) {
					if ( trim( $bl ) === $current_user_ip ) {
						wf_sn_el_modules::log_event( 'security_ninja', 'blacklisted_IP', $current_user_ip . ' IP is blacklisted locally.', '' );

						self::update_blocked_count( $current_user_ip );
						self::kill_request( $current_user_ip );
					}
				}
			}
		}
		// Processing
		// Note: Secret access URL check is handled by do_init_action() for all users

			// Checks if IP is from a known crawler
		if ( ( ! $whitelisted_user ) && ( self::validate_crawler_ip( $current_user_ip ) ) ) {
			// Validated crawler
			$whitelisted_user = true;
		}

			// Check if an IP is banned and blocks
		if ( ( ! $whitelisted_user ) && ( 1 === (int) self::$options['active'] ) ) {
			$ban_reason = self::is_banned_ip( $current_user_ip );
			if ( $ban_reason ) {
				// Login-only bypass: country bans use countryblock_loginonly; IP/cloud bans use global.
				// Must not nest under !global — country login-only must work when global is ON.
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					if ( self::should_limit_ban_to_login_only__premium_only( $ban_reason ) ) {
						if ( ! self::is_login_related_request__premium_only() ) {
							$extraarr = array(
								'ip'          => $current_user_ip,
								'user_agent'  => $ua_string,
								'request_uri' => sanitize_text_field( $_SERVER['REQUEST_URI'] ),
								'country'     => $current_user_country,
								'ban_reason'  => 'banned_ip_allowed',
								'description' => $ban_reason . ' (allowed - login-only mode)',
							);
							self::log_visitor__premium_only( $extraarr );
							$visit_logged = true;
							return;
						}
					}
				}

				// Free build: when global is OFF, block here (free has no login-only helper).
				// Premium continues below for login pages or full-site bans.
				if ( ! self::$options['global'] ) {
					$is_premium_build = false;
					if ( secnin_fs()->can_use_premium_code__premium_only() ) {
						$is_premium_build = true;
					}
					if ( ! $is_premium_build ) {
						wf_sn_el_modules::log_event(
							'security_ninja',
							'blocked_ip_banned',
							__( 'IP is blocked.', 'security-ninja' ),
							array(
								'ip'         => $current_user_ip,
								'ban_reason' => $ban_reason,
							)
						);
						self::update_blocked_count( $current_user_ip );
						self::kill_request();
						return;
					}
				}

				// Login-related request in login-only mode, or full-site ban — proceed with blocking.
				$premium_logged = false;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$extraarr = array(
						'ip'          => $current_user_ip,
						'user_agent'  => $ua_string,
						'request_uri' => sanitize_text_field( $_SERVER['REQUEST_URI'] ),
						'country'     => $current_user_country,
						'ban_reason'  => 'banned_ip',
						'description' => $ban_reason,
					);

					self::log_visitor__premium_only( $extraarr );
					$visit_logged = true;

					wf_sn_el_modules::log_event( 'security_ninja', 'blocked_ip_banned', __( 'IP is blocked.', 'security-ninja' ), $extraarr );
					$premium_logged = true;
				}
				if ( ! $premium_logged ) {
					wf_sn_el_modules::log_event(
						'security_ninja',
						'blocked_ip_banned',
						__( 'IP is blocked.', 'security-ninja' ),
						array(
							'ip'         => $current_user_ip,
							'ban_reason' => $ban_reason,
						)
					);
				}
				self::update_blocked_count( $current_user_ip );
				self::kill_request();
			}
		}

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			$blocked_country_code = self::is_banned_country__premium_only( $current_user_ip );
			if ( ( ! $whitelisted_user ) && $blocked_country_code ) {

				// Check if country blocking is restricted to login functionality only
				if ( isset( self::$options['countryblock_loginonly'] ) && self::$options['countryblock_loginonly'] ) {
					// If login-only is enabled, only block from login-related pages
					if ( ! self::is_login_related_request__premium_only() ) {
						// Allow access but log the visit
						self::log_visitor__premium_only(
							array(
								'ip'          => $current_user_ip,
								'user_agent'  => $ua_string,
								'request_uri' => sanitize_text_field( $_SERVER['REQUEST_URI'] ),
								'country'     => $blocked_country_code,
								'banned'      => 0,
								'ban_reason'  => 'country_banned_allowed',
								'description' => 'Allowed visit from blocked country - ' . $blocked_country_code . ' (login-only mode)',
							)
						);
						$visit_logged = true;
						return; // Allow the request to continue
					}
				}
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					self::log_visitor__premium_only(
						array(
							'ip'          => $current_user_ip,
							'user_agent'  => $ua_string,
							'request_uri' => sanitize_text_field( $_SERVER['REQUEST_URI'] ),
							'country'     => $blocked_country_code,
							'banned'      => 1,
							'ban_reason'  => 'country_banned_' . esc_attr( $blocked_country_code ),
							'description' => 'Blocked visit, country banned - ' . $blocked_country_code,
						)
					);
				}
				$visit_logged = true;

				wf_sn_el_modules::log_event( 'security_ninja', 'blocked_ip_country_ban', $blocked_country_code . ' is blocked.', '' );

				self::update_blocked_count( $current_user_ip );
				self::kill_request( $current_user_ip, 'blocked_ip_country_ban', 1 * HOUR_IN_SECONDS, true );
			}
		}
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( ! $visit_logged ) {
				// LARS - vi tracker besøgende uanset hvad
				self::log_visitor__premium_only(
					array(
						'ip'         => $current_user_ip,
						'user_agent' => $ua_string,
						'country'    => $current_user_country,
						'banned'     => 0,
					)
				);
			}
		}
	}




	/**
	 * Checks for bad queries - CBQ - Taken with a little shame from BBQ - thank you for the superfast firewall
	 * Based on 8G Firewall by Jeff Starr - https://perishablepress.com/8g-blacklist/
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  array
	 */
	/**
	 * Check for suspicious queries and patterns
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Monday, January 13th, 2025 - Added REST API protection
	 * @version v1.0.2  Tuesday, January 14th, 2025 - Improved REST API protection for booking plugins
	 * @access  public static
	 * @return  array|false Array with match details or false if no match
	 */
	public static function check_bad_queries() {
		// Enhanced REST API protection - check for wp-json in the URL
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
		if ( strpos( $request_uri, '/wp-json/' ) !== false ) {
			return false;
		}

		// Additional protection for REST API requests that might not have /wp-json/ in the path
		// but are still legitimate REST API calls (e.g. when using rest_route parameter)
		if ( wp_is_json_request() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || isset( $_GET['rest_route'] ) ) {
			return false;
		}

		$request_uri_array = apply_filters(
			'request_uri_items',
			array(
				'3commas'             => ',{3,}', // Matches three or more commas
				'7_or_more_hyphens'   => '-{7,}', // Matches seven or more hyphens
				'caret_backtick'      => '[\^`<>\\|]', // Matches caret, backtick, less than, greater than, backslash, or pipe characters
				'alnum_long'          => '[a-z0-9]{2000,}', // Matches any string with 2000 or more alphanumeric characters
				'eq_backslash'        => '=?\\\\(\'|%27)/?\\.', // Matches = followed optionally by a backslash or single quote and a dot
				'special_chars'       => '/(\*|"|\'|\.|,|&|&amp;?)/?$', // Matches / followed by *, ", ', ., ,, &, optionally &amp;, and optionally ending with /
				'php_file'            => '\.(php)(\()?([0-9]+)(\))?(/)?$', // Matches .php optionally followed by (), a number, and optionally ending with /
				'header_cookie'       => '/(.*)(header:|set-cookie:)(.*)=', // Matches any string containing header: or set-cookie: followed by =
				'ftp_config_files'    => '\.(s?ftp-?)config|(s?ftp-?)config\.', // Matches .config or config. with optional sftp- prefix
				'file_editors'        => '/(f?ckfinder|fck/|f?ckeditor|fullclick)', // Matches paths related to file editors or ckfinder
				'download_framework'  => '/((force-)?download|framework/main)(\.php)', // Matches specific PHP files or paths indicating file download or framework access
				'misc_chars'          => '\{0\}|"0"="0|/\(/\|\.\.\.|\+\+\+|\\\\"', // Matches {0}, "0"="0, /(/, ..., +++, or \"
				'vbulletin'           => '/(vbull(etin)?|boards|vbforum|vbweb|webvb)(/)?', // Matches paths related to vBulletin or similar forum software
				'permalink'           => '(\.|20)(get|the)(_)(permalink|posts_page_url)\(', // Matches specific patterns that could indicate an attempt to exploit or probe
				'complex_injections'  => '///|\?\?|/&&|/\*(.*)\*/|/:/|\\\\|0x00|%00|%0d%0a', // Matches sequences that could be part of complex injections
				'alfa_cgi'            => '(/)(cgi_?)?alfa(_?cgiapi|_?data|_?v[0-9]+)?(\\.php)', // Matches specific CGI or PHP files with "alfa" in the name
				'thumbnail_php'       => '(thumbs?(_editor|open)?|tim(thumbs?)?)((\\.|%2e)php)', // Matches thumbnail editor or opener PHP files
				'admin_php'           => '(/)((boot)?_?admin(er|istrator|s)(_events)?)(\\.php)', // Matches admin-related PHP files with optional prefixes
				'sensitive_user_dirs' => '(/%7e)(root|ftp|bin|nobody|named|guest|logs|sshd)(/)', // Matches user directories for sensitive usernames
				'backup_files'        => '(archive|backup|db|master|sql|wp|www|wwwroot)\\.(gz|zip)', // Matches backup or archive files
				'shell_scripts'       => '(/)(\\.?mad|alpha|c99|php|web)?sh(3|e)ll([0-9]+|\\w)(\\.php)', // Matches shell scripts, including common names and versions
				'upload_scripts'      => '(/)(admin-?|file-?)(upload)(bg|_?file|ify|svu|ye)?(\\.php)', // Matches various upload handler scripts
				'sensitive_dirs'      => '(/)(etc|var)(/)(hidden|secret|shadow|ninja|passwd|tmp)(/)?', // Matches paths attempting to access sensitive directories
				'url_manipulation'    => '(s)?(ftp|inurl|php)(s)?(:(/|%2f|%u2215)(/|%2f|%u2215))', // Matches attempts to access or manipulate URLs
				'query_manipulation'  => '(/)(=|\\$&?|&?(pws|rk)=0|_mm|_vti_|cgi(\\.|-)?|(=|/|;|,)nt\\.)', // Matches various query string manipulations and CGI invocations
				'sensitive_files'     => '(\\.)(ds_store|htaccess|htpasswd|init?|mysql-select-db)(/)?', // Matches attempts to access sensitive server files
				'bin_commands'        => '(/)(bin)(/)(cc|chmod|chsh|cpp|echo|id|kill|mail|nasm|perl|ping|ps|python|tclsh)(/)?', // Matches commands that should not be accessible via URL
				'obfuscated_js'       => '(/)?j(\\s+)?a(\\s+)?v(\\s+)?a(\\s+)?s(\\s+)?c(\\s+)?r(\\s+)?i(\\s+)?p(\\s+)?t(\\s+)?(%3a|:)', // Matches obfuscated JavaScript
				'server_dirs'         => '(/)(filemanager|htdocs|httpdocs|https?|mailman|mailto|msoffice|undefined|usage|var|vhosts|webmaster|www)(/)', // Matches common server directories or services
				'injection_attempts'  => '(\\(null\\)|\\{\\$itemURL\\}|cast\\(0x|echo(.*)kae|etc/passwd|eval\\(|null(.*)null|open_basedir|self/environ|\\+union\\+all\\+select)', // Matches various injection or exploit attempts
				'config_files'        => '(/)(db-?|j-?|my(sql)?-?|setup-?|web-?|wp-?)?(admin-?)?(setup-?)?(conf\\b|conf(ig)?)(uration)?(\\.?bak|\\.inc)?(\\.inc|\\.old|\\.php|\\.txt)', // Matches configuration or setup files
				'exploit_paths'       => '(/)((.*)crlf-?injection|(.*)xss-?protection|__(inc|jsc)|administrator|author-panel|cgi-bin|database|downloader|(db|mysql)-?admin)(/)', // Matches common admin or exploit paths
				'malicious_php'       => '(/)(haders|head|helpear|incahe|includes?|indo(sec)?|infos?|install|ioptimizes?|jmail|js|king|kiss|kodox|kro|legion|libsoft)(\\.php)', // Matches potentially malicious or sensitive PHP scripts
				'info_leak_paths'     => '(/)(awstats|document_root|dologin\\.action|error.log|extension/ext|htaccess\\.|lib/php|listinfo|phpunit/php|remoteview|server/php|www\\.root\\.)', // Matches server management or info leak paths
				'code_execution'      => '(base64_(en|de)code|benchmark|curl_exec|e?chr|eval\(|function|fwrite|(f|p)open|html|leak|passthru|p?fsockopen|phpinfo)(.*)(\\(|%28)(.*)(\\)|%29)', // Matches code execution or info leak functions
				'process_management'  => '(posix_(kill|mkfifo|setpgid|setsid|setuid)|(child|proc)_(close|get_status|nice|open|terminate)|(shell_)?exec|system)(.*)(\\(|%28)(.*)(\\)|%29)', // Matches commands or functions that manage processes or execute commands
				'remote_admin_tools'  => '(/)((c99|php|web)?shell|crossdomain|fileditor|locus7|nstview|php(get|remoteview|writer)|r57|remview|sshphp|storm7|webadmin)(.*)(\\.|%2e|\\(|%28)', // Matches shells or remote admin tools
				'wp_paths'            => '/((wp-)((201\\d|202\\d|[0-9]{2})|ad|admin(fx|rss|setup)|booking|confirm|crons|data|file|mail|one|plugins?|readindex|reset|setups?|story))(\\.php)', // Matches WordPress-specific paths
				'various_php'         => '(/)(^$|-|\\!|\\w|\\.(.*)|100|123|([^iI])?ndex|index\\.php/index|7yn|90sec|aill|ajs\\.delivery|al277|alexuse?|ali|allwrite)(\\.php)', // Matches various PHP scripts or obfuscated paths
				'site_management'     => '(/)(analyser|apache|apikey|apismtp|authenticat(e|ing)|autoload_classmap|backup(_index)?|bakup|bkht|black|bogel|bookmark|bypass|cachee?)(\\.php)', // Matches scripts related to site management or exploits
				'cleanup_scripts'     => '(/)(clean|cm(d|s)|con|connector\\.minimal|contexmini|contral|curl(test)?|data(base)?|db|db-cache|db-safe-mode|defau11|defau1t|dompdf|dst)(\\.php)', // Matches database, cleanup, or testing scripts
				'exploit_scripts'     => '(/)(elements|emails?|error.log|ecscache|edit-form|eval-stdin|export|evil|fbrrchive|filemga|filenetworks?|f0x|gank(\\.php)?|gass|gel|guide)(\\.php)', // Matches various scripts that could be used for exploits or management
				'custom_scripts'      => '(/)(logo_img|lufix|mage|marg|mass|mide|moon|mssqli|mybak|myshe|mysql|mytag_js?|nasgor|newfile|news|nf_?tracking|nginx|ngoi|ohayo|old-?index)(\\.php)', // Matches scripts that could indicate custom or exploit functionality
				'diagnostic_scripts'  => '(/)(olux|owl|pekok|petx|php-?info|phpping|popup-pomo|priv|r3x|radio|rahma|randominit|readindex|readmy|reads|repair-?bak|robot(s\\.txt)?|root)(\\.php)', // Matches diagnostic, exploit, or specific functionality scripts
				'system_control'      => '(/)(router|savepng|semayan|shell|shootme|sky|socket(c|i|iasrgasf)ontrol|sql(bak|_?dump)?|sym403|sys|system_log|tmp-?(uploads)?)(\\.php)', // Matches testing, system control, or potential exploit scripts
				'admin_tools'         => '(/)(traffic-advice|u2p|udd|ukauka|up__uzegp|up14|upa?|upxx?|vega|vip|vu(ln)?(\\w)?|webroot|weki|wikindex|wp_logns?|wp_wrong_datlib)(\\.php)', // Matches various specific or general scripts and admin tools
				'cms_scripts'         => '(/)((wp-?)?install(ation)?|wp(3|4|5|6)|wpfootes|wpzip|ws0|wsdl|wso(\\w)?|www|(uploads|wp-admin)?xleet(-shell)?|xmlsrpc|xup|xxu|zibi|zipy)(\\.php)', // Matches WordPress and other CMS related scripts or tools
				'malware_signatures'  => '(bkv74|cachedsimilar|core-stab|crgrvnkb|ctivrc|deadcode|deathshop|e7xue|eqxafaj90zir|exploits|ffmkpcal|filellli7|(fox|sid)wso|gel4y|goog1es|gvqqpinc)', // removed dkiz as it is too common
				'common_exploits'     => '(@md5|00\\.temp00|0byte|0d4y|0xor|wso1337|1h6j5|40dd1d|4price|70bex?|a57bze893|abbrevsprl|abruzi|adminer|aqbmkwwx|archivarix|beez5|bgvzc29)', // removed 'backdoor' as it is too common '0day', '3XP' also
				'exploit_keywords'    => '(handler_to_code|hax(0|o)r|hmei7|hnap1|home_url=|ibqyiove|icxbsx|indoxploi|jahat|jijle3|kcrew|laobiao|lock360|mod_(aratic|ariimag))', // removed longdog and keywordspy as they are too common and marijuan
				'exploit_tools'       => '(mobiquo|muiebl|nessus|osbxamip|priv8|qcmpecgy|r3vn330|racrew|raiz0|reportserver|r00t|respectmus|rom2823|roseleif|sh3ll|site((.){0,2})copier|sqlpatch|sux0r)', // removed phpunit
				'exploit_signatures'  => '(sym403|telerik|uddatasql|utchiha|visualfrontend|w0rm|wangdafa|wpyii2|wsoyanzo|x5cv|xattack|xbaner|xertive|xiaolei|xltavrat|xorz|xsamxad|xsvip|xxxs?s?|zabbix|zebda)',
			)
		);

		$query_string_array = apply_filters(
			'query_string_items',
			array(
				'start_hyphen'           => '^(-|%2d)[^=]+$', // Matches strings that start with a hyphen or its encoding, without an equals sign following.
				'long_alnum'             => '[a-z0-9]{4000,}', // Matches any alphanumeric string longer than 4000 characters.
				'url_obfuscate'          => '(/|%2f)(:|%3a)(/|%2f)', // Matches sequences attempting to simulate or obfuscate forward slashes and colons, commonly used in URLs.
				'sensitive_files'        => 'etc/(hosts|motd|shadow)', // Matches attempts to access sensitive system files.
				'sql_injection_order'    => 'order(\s|%20)by(\s|%20)1--', // Matches SQL injection attempts that use ORDER BY clauses.
				'asterisk_injection'     => '(/|%2f)(\*|%2a)(\*|%2a)(/|%2f)', // Matches sequences of asterisks surrounded by slashes, potentially indicating comment injection or bypass attempts.
				'special_chars'          => '(`|<|>|\\^|\\||0x00|%00|%0d%0a)', // Matches attempts to use special characters or encoded null bytes and line terminators.
				'file_tools'             => '(f?ckfinder|f?ckeditor|fullclick)', // Matches common file upload and management tools that might be targeted for exploitation.
				'header_manipulation'    => '((.*)header:|(.*)set-cookie:(.*)=)', // Matches manipulation attempts of HTTP headers.
				'local_ip'               => '(localhost|127(\\.|%2e)0(\\.|%2e)0(\\.|%2e)1)', // Matches local IP addresses, potentially indicating SSRF attacks.
				'cmd_injection'          => '(cmd|command)(=|%3d)(chdir|mkdir)(.*)(x20)', // Matches command injection attempts.
				'php_globals'            => '(^|&)(globals|mosconfig[a-z_]{1,22}|request)(=|\\[)', // Matches attempts to manipulate PHP globals or Joomla configuration globals.
				'wp_config_access'       => '(/|%2f)((wp-)?config)((\\.|%2e)inc)?((\\.|%2e)php)', // Matches attempts to access WordPress or other configuration files.
				'thumb_exploit'          => '(thumbs?(_editor|open)?|tim(thumbs?)?)((\\.|%2e)php)', // Matches attempts to exploit thumb generator scripts.
				'dir_path_manipulation'  => '(absolute_|base|root_)(dir|path)(=|%3d)(ftp|https?)', // Matches attempts to manipulate PHP directory or path settings.
				'url_injection'          => '(s)?(ftp|inurl|php)(s)?(:(/|%2f|%u2215)(/|%2f|%u2215))', // Matches attempts to inject FTP or PHP URLs.
				'wp_permalink_injection' => '(\\.|20)(get|the)(_|%5f)(permalink|posts_page_url)(\\(|%28)', // Matches complex injection attempts, potentially targeting WordPress permalink structure.
				'critical_file_access'   => '((boot|win)((\\.|%2e)ini)|etc(/|%2f)passwd|self(/|%2f)environ)', // Matches attempts to access critical system files or environment paths.
				'path_traversal'         => '(((/|%2f){3,})|((\\.|%2e){3,})|((\\.|%2e){2,})(/|%2f|%u2215))', // Matches path traversal attempts with excessive slashes or dots.
				'code_exec_injection'    => '(benchmark|char|exec|fopen|function|html)(.*)(\\(|%28)(.*)(\\)|%29)', // Matches code execution or injection attempts.
				'php_object_injection'   => '(php)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', // Matches PHP object injection attempts.
				'eval_injection'         => '(e|%65|%45)(v|%76|%56)(a|%61|%31)(l|%6c|%4c)(.*)(\\(|%28)(.*)(\\)|%29)', // Matches eval function attempts with various encodings.
				'various_injections'     => '(/|%2f)(=|%3d|$&|_mm|cgi(\\.|\\-)|inurl(:|%3a)(/|%2f)|(mod|path)(=|%3d)(\\.|%2e))', // Matches various injection or manipulation attempts.
				'html_embed'             => '(<|%3c)(.*)(e|%65|%45)(m|%6d|%4d)(b|%62|%42)(e|%65|%45)(d|%64|%44)(.*)(>|%3e)', // Matches embedded HTML or scripts
				'iframe_injection'       => '(<|%3c)(.*)(i|%69|%49)(f|%66|%46)(r|%72|%52)(a|%61|%41)(m|%6d|%4d)(e|%65|%45)(.*)(>|%3e)', // Matches iframe injections
				'object_injection'       => '(<|%3c)(.*)(o|%4f|%6f)(b|%62|%42)(j|%4a|%6a)(e|%65|%45)(c|%63|%43)(t|%74|%54)(.*)(>|%3e)', // Matches object tag injections
				'script_injection'       => '(<|%3c)(.*)(s|%73|%53)(c|%63|%43)(r|%72|%52)(i|%69|%49)(p|%70|%50)(t|%74|%54)(.*)(>|%3e)', // Matches script tag injections
				'space_delete'           => '(\\+|%2b|%20)(d|%64|%44)(e|%65|%45)(l|%6c|%4c)(e|%65|%45)(t|%74|%54)(e|%65|%45)(\\+|%2b|%20)', // Matches space or encoded space followed by 'delete'
				'space_insert'           => '(\\+|%2b|%20)(i|%69|%49)(n|%6e|%4e)(s|%73|%53)(e|%65|%45)(r|%72|%52)(t|%74|%54)(\\+|%2b|%20)', // Matches space or encoded space followed by 'insert'
				'space_select'           => '(\\+|%2b|%20)(s|%73|%53)(e|%65|%45)(l|%6c|%4c)(e|%65|%45)(c|%63|%43)(t|%74|%54)(\\+|%2b|%20)', // Matches space or encoded space followed by 'select'
				'space_update'           => '(\\+|%2b|%20)(u|%75|%55)(p|%70|%50)(d|%64|%44)(a|%61|%41)(t|%74|%54)(e|%65|%45)(\\+|%2b|%20)', // Matches space or encoded space followed by 'update'
				'sql_null_byte'          => '(\\\\x00|(\"|%22|\'|%27)?0(\"|%22|\'|%27)?(=|%3d)(\"|%22|\'|%27)?0|cast(\\(|%28)0x|or%201(=|%3d)1)', // Matches null byte, zero equals zero, or SQL 'or 1=1' pattern
				'php_globals_access'     => '(^|&)(g|%67|%47)(l|%6c|%4c)(o|%6f|%4f)(b|%62|%42)(a|%61|%41)(l|%6c|%4c)(s|%73|%53)(=|\\[|%[0-9A-Z]{0,2})', // Matches attempts to access or manipulate PHP globals
				'request_array_access'   => '(_|%5f)(r|%72|%52)(e|%65|%45)(q|%71|%51)(u|%75|%55)(e|%65|%45)(s|%73|%53)(t|%74|%54)(=|\\[|%[0-9A-Z]{2,})', // Matches attempts to access or manipulate the request array
				'js_protocol_injection'  => '(j|%6a|%4a)(a|%61|%41)(v|%76|%56)(a|%61|%31)(s|%73|%53)(c|%63|%43)(r|%72|%52)(i|%69|%49)(p|%70|%50)(t|%74|%54)(:|%3a)(.*)(;|%3b|\\)|%29)', // Matches javascript protocol injection
				'base64_encode_attempt'  => '(b|%62|%42)(a|%61|%41)(s|%73|%53)(e|%65|%45)(6|%36)(4|%34)(_|%5f)(d|%64|%44)(e|%65|%45|n|%6e|%4e)(c|%63|%43)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(.*)(\\()(.*)(\\))', // Matches base64 encode function attempts

			)
		);

		$user_agent_array = apply_filters(
			'user_agent_items',
			array(
				'ua_long_alnum'       => '[a-z0-9]{2000,}', // Matches user agents longer than 2000 alphanumeric characters.
				'ua_encoded_entities' => '(&lt;|%0a|%0d|%27|%3c|%3e|%00|0x00|\\\x22)', // Matches specific encoded entities and characters often used in injection attacks.
				'ua_scraping_tools'   => '(curl|libwww-perl|pycurl|scan)', // Matches a list of common scraping and automation user agents. - note removed ahrefs and archiver
				'ua_malicious_tools'  => '(oppo\\sa33|(c99|php|web)shell|site((.){0,2})copier)', // Matches known malicious or automated tools user agents.
				'ua_command_exec'     => '(base64_decode|bin/bash|disconnect|eval|unserializ)', // Matches patterns indicative of command execution or deserialization attempts.
				'ua_bots_1'           => '(acapbot|acoonbot|alexibot|asterias|attackbot|awario|backdor|becomebot|binlar|blackwidow|blekkobot|blex|blowfish|bullseye|bunnys|butterfly|careerbot|casper)',
				'ua_bots_2'           => '(checkpriv|cheesebot|cherrypick|chinaclaw|choppy|clshttp|cmsworld|copernic|copyrightcheck|cosmos|crescent|datacha|\\bdemon\\b|diavol|discobot|dittospyder)',
				'ua_bots_3'           => '(dotbot|dotnetdotcom|dumbot|econtext|emailcollector|emailsiphon|emailwolf|eolasbot|eventures|extract|eyenetie|feedfinder|flaming|flashget|flicky|foobot|fuck)',
				'ua_bots_4'           => '(g00g1e|getright|gigabot|go-ahead-got|gozilla|grabnet|grafula|harvest|heritrix|httracks?|icarus6j|jetbot|jetcar|jikespider|kmccrew|leechftp|libweb|liebaofast)',
				'ua_bots_5'           => '(linkscan|linkwalker|loader|lwp-download|majestic|masscan|miner|mechanize|mj12bot|morfeus|moveoverbot|netmechanic|netspider|nicerspro|nikto|nominet|nutch)',
				'ua_bots_6'           => '(octopus|pagegrabber|petalbot|planetwork|postrank|proximic|purebot|queryn|queryseeker|radian6|radiation|realdownload|remoteview|rogerbot|scan|scooter|seekerspid)',
				'ua_bots_7'           => '(semalt|siclab|sindice|sistrix|sitebot|siteexplorer|sitesnagger|skygrid|smartdownload|snoopy|sosospider|spankbot|spbot|sqlmap|stackrambler|stripper|sucker|surftbot)',
				'ua_bots_8'           => '(sux0r|suzukacz|suzuran|takeout|telesoft|true_robots|turingos|turnit|vampire|vikspider|voideye|webleacher|webreaper|webstripper|webvac|webviewer|webwhacker)',
				'ua_bots_9'           => '(winhttp|wwwoffle|woxbot|xaldon|xxxyy|yamanalab|yioopbot|youda|zeus|zmeu|zune|zyborg)', // Matches various known bots, crawlers, and potentially harmful automated agents.
			)
		);

		$referrer_array = apply_filters(
			'referrer_items',
			array(
				'ref_100dollars'      => '100dollars',
				'ref_unlink'          => '@unlink',
				'ref_assert'          => 'assert\(',
				'ref_best_seo'        => 'best-seo',
				'ref_blue_pill'       => 'blue\s?pill',
				'ref_cocaine'         => 'cocaine',
				'ref_ejaculat'        => 'ejaculat',
				'ref_erectile'        => 'erectile',
				'ref_erections'       => 'erections',
				'ref_hoodia'          => 'hoodia',
				'ref_huronriveracres' => 'huronriveracres',
				'ref_impotence'       => 'impotence',
				'ref_levitra'         => 'levitra',
				'ref_libido'          => 'libido',
				'ref_lipitor'         => 'lipitor',
				'ref_mopub'           => 'mopub\.com',
				'ref_order_by'        => 'order(\s|%20)by(\s|%20)1--',
				'ref_phentermin'      => 'phentermin',
				'ref_pornhelm'        => 'pornhelm',
				'ref_print_r'         => 'print_r\(',
				'ref_prozac'          => 'pro[sz]ac',
				'ref_sandyauer'       => 'sandyauer',
				'ref_semalt'          => 'semalt\.com',
				'ref_social_buttons'  => 'social-buttions',
				'ref_todaperfeita'    => 'todaperfeita',
				'ref_tramadol'        => 'tramadol',
				'ref_troyhamby'       => 'troyhamby',
				'ref_ultram'          => 'ultram',
				'ref_unicauca'        => 'unicauca',
				'ref_valium'          => 'valium',
				'ref_viagra'          => 'viagra',
				'ref_vicodin'         => 'vicodin',
				'ref_x00'             => 'x00',
				'ref_xanax'           => 'xanax',
				'ref_xbshell'         => 'xbshell',
				'ref_ypxaieo'         => 'ypxaieo',
			)
		);

		$blocked_hosts_array = apply_filters(
			'blocked_hosts_items',
			array(
				'blocked_163data'           => '163data',
				'blocked_colocrossing'      => 'colocrossing',
				'blocked_crimea'            => 'crimea',
				'blocked_g00g1e'            => 'g00g1e',
				'blocked_justhost'          => 'justhost',
				'blocked_kanagawa'          => 'kanagawa',
				'blocked_loopia'            => 'loopia',
				'blocked_masterhost'        => 'masterhost',
				'blocked_onlinehome'        => 'onlinehome',
				'blocked_poneytel'          => 'poneytel',
				'blocked_sprintdatacenter'  => 'sprintdatacenter',
				'blocked_reverse_softlayer' => 'reverse.softlayer',
				'blocked_safenet'           => 'safenet',
				'blocked_ttnet'             => 'ttnet',
				'blocked_woodpecker'        => 'woodpecker',
				'blocked_wowrack'           => 'wowrack',
			)
		);

		$request_uri_string  = false;
		$query_string_string = false;
		$user_agent_string   = false;
		$referrer_string     = false;

		if ( isset( $_SERVER['REQUEST_URI'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) {
			$request_uri_string = $_SERVER['REQUEST_URI'];
		}
		if ( isset( $_SERVER['QUERY_STRING'] ) && ! empty( $_SERVER['QUERY_STRING'] ) ) {
			$query_string_string = $_SERVER['QUERY_STRING'];
		}
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
			$user_agent_string = $_SERVER['HTTP_USER_AGENT'];
		}
		if ( isset( $_SERVER['HTTP_REFERER'] ) && ! empty( $_SERVER['HTTP_REFERER'] ) ) {
			$referrer_string = $_SERVER['HTTP_REFERER'];
		}

		$has_remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) && ! empty( $_SERVER['REMOTE_ADDR'] );
		if ( ! $request_uri_string && ! $query_string_string && ! $user_agent_string && ! $referrer_string && ! $has_remote_addr ) {
			return false;
		}

		$response = array();

		if ( $request_uri_string ) {
			foreach ( $request_uri_array as $key => $pattern ) {
				// Direct use of pattern in preg_match, without preg_quote
				if ( preg_match( '#' . $pattern . '#i', $request_uri_string, $req_matches ) ) {
					$response = array(
						'request_uri'  => esc_html( $req_matches[0] ),
						'matched_rule' => esc_html( $key ),
						'message'      => 'A match was found in the request URI.',
					);
					break;
				}
			}
		}

		if ( empty( $response ) && $query_string_string ) {
			foreach ( $query_string_array as $key => $item ) {
				// Directly use the pattern without preg_quote() for regex matching
				if ( preg_match( '#' . $item . '#i', $query_string_string, $query_matches ) ) {
					$response = array(
						'query_string'        => esc_html( $query_matches[0] ),
						'query_string_string' => esc_html( $query_string_string ),
						'matched_rule'        => esc_html( $key ),
						'message'             => 'A match was found in the query string.',
					);
					break;
				}
			}
		}

		if ( empty( $response ) && $user_agent_string ) {
			foreach ( $user_agent_array as $key => $item ) {
				// Using '#' as delimiter to avoid conflicts with common characters in user agents
				if ( preg_match( '#' . $item . '#i', $user_agent_string, $ua_matches ) ) {
					$response = array(
						'http_user_agent'   => esc_html( $ua_matches[0] ),
						'user_agent_string' => esc_html( $user_agent_string ),
						'matched_rule'      => esc_html( $key ),
						'message'           => 'A match was found in the user agent.',
					);
					break;
				}
			}
		}

		if ( empty( $response ) && $referrer_string ) {
			foreach ( $referrer_array as $key => $item ) {
				if ( preg_match( '/' . preg_quote( $item, '/' ) . '/i', $referrer_string, $rf_matches ) ) {
					$response = array(
						'referrer'        => esc_html( $rf_matches[0] ),
						'referrer_string' => esc_html( $referrer_string ),
						'matched_rule'    => esc_html( $key ),
						'message'         => 'A match was found in the referrer.',
					);
					break;
				}
			}
		}

		if ( empty( $response ) && $has_remote_addr ) {
			$visitor_host = self::get_visitor_hostname( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) );
			foreach ( $blocked_hosts_array as $key => $item ) {
				if ( preg_match( '#\b' . preg_quote( $item, '#' ) . '\b#i', $visitor_host, $matches ) ) {
					$response = array(
						'blocked_host' => esc_html( $matches[0] ),
						'visitor_host' => esc_html( $visitor_host ),
						'matched_rule' => esc_html( $key ),
						'message'      => 'A match was found in the blocked hosts.',
					);
					break;
				}
			}
		}

		if ( ! empty( $response ) ) {
			return $response;
		}
		return false;
	}

	/**
	 * Check if current request is a temporary login link from a supported plugin.
	 *
	 * Detects temporary login links from plugins like "Temporary Login Without Password",
	 * "One Time Login", "Magic Login", and "Login Links". Only bypasses checks if the
	 * corresponding plugin is active to prevent abuse.
	 *
	 * @author  Lars Koudal
	 * @since   5.270
	 * @return  bool True if temporary login link detected and plugin is active, false otherwise.
	 */
	public static function is_temporary_login_link() {
		// Allow filter to override detection entirely
		$request_uri  = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
		$query_string = isset( $_SERVER['QUERY_STRING'] ) ? $_SERVER['QUERY_STRING'] : '';

		$is_temporary_login = apply_filters( 'securityninja_is_temporary_login_link', null, $request_uri, $query_string );

		// If filter explicitly returns true/false, use that
		if ( $is_temporary_login === true ) {
			// Log detection for audit purposes
			$current_user_ip = self::get_user_ip();
			wf_sn_el_modules::log_event(
				'security_ninja',
				'temporary_login_link_detected',
				__( 'Temporary login link detected (filter override)', 'security-ninja' ),
				array(
					'ip'          => $current_user_ip,
					'request_uri' => $request_uri,
				)
			);
			return true;
		}

		if ( $is_temporary_login === false ) {
			return false;
		}

		// Default detection: check known temporary login parameters
		$default_params = array(
			array(
				'param'       => 'wtlwp_token',
				'plugin_file' => 'temporary-login-without-password/temporary-login-without-password.php',
			),
			array(
				'param'       => 'one_time_login_token',
				'plugin_file' => 'one-time-login/one-time-login.php',
			),
			array(
				'param'       => 'magic-login',
				'plugin_file' => 'magic-login/plugin.php',
				'requires'    => array( 'user_id', 'token' ),
			),
			array(
				'param'       => 'll',
				'plugin_file' => 'login-links/login-links.php',
				'prefix'      => true,
			),
		);

		$params = apply_filters( 'securityninja_temporary_login_params', $default_params );

		if ( ! is_array( $params ) || empty( $params ) ) {
			return false;
		}

		// Ensure plugin.php is loaded for is_plugin_active()
		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		foreach ( $params as $param_config ) {
			if ( ! is_array( $param_config ) || empty( $param_config['param'] ) || empty( $param_config['plugin_file'] ) ) {
				continue;
			}

			$param_name  = $param_config['param'];
			$plugin_file = $param_config['plugin_file'];
			$requires    = isset( $param_config['requires'] ) && is_array( $param_config['requires'] ) ? $param_config['requires'] : array();
			$is_prefix   = isset( $param_config['prefix'] ) && $param_config['prefix'] === true;

			// Check if plugin is active before checking parameters
			if ( ! is_plugin_active( $plugin_file ) ) {
				continue;
			}

			// Check for prefix pattern (e.g., Login Links uses 'll' + token)
			if ( $is_prefix ) {
				foreach ( $_GET as $key => $value ) {
					if ( strpos( $key, $param_name ) === 0 && strlen( $key ) > strlen( $param_name ) ) {
						// Found parameter starting with prefix
						$current_user_ip = self::get_user_ip();
						wf_sn_el_modules::log_event(
							'security_ninja',
							'temporary_login_link_detected',
							sprintf( __( 'Temporary login link detected: %1$s (plugin: %2$s)', 'security-ninja' ), esc_html( $key ), esc_html( $plugin_file ) ),
							array(
								'ip'     => $current_user_ip,
								'param'  => $key,
								'plugin' => $plugin_file,
							)
						);
						return true;
					}
				}
				continue;
			}

			// Check for simple parameter
			if ( isset( $_GET[ $param_name ] ) ) {
				// If requires additional parameters, check they exist
				if ( ! empty( $requires ) ) {
					$all_required_present = true;
					foreach ( $requires as $required_param ) {
						if ( ! isset( $_GET[ $required_param ] ) ) {
							$all_required_present = false;
							break;
						}
					}
					if ( ! $all_required_present ) {
						continue;
					}
				}

				// Found matching parameter and plugin is active
				$current_user_ip = self::get_user_ip();
				wf_sn_el_modules::log_event(
					'security_ninja',
					'temporary_login_link_detected',
					sprintf( __( 'Temporary login link detected: %1$s (plugin: %2$s)', 'security-ninja' ), esc_html( $param_name ), esc_html( $plugin_file ) ),
					array(
						'ip'     => $current_user_ip,
						'param'  => $param_name,
						'plugin' => $plugin_file,
					)
				);
				return true;
			}
		}

		return false;
	}

	/**
	 * Get the name of the temporary login plugin used in current request, if any.
	 *
	 * Returns the plugin name if a temporary login link is detected, false otherwise.
	 * Used for logging purposes to identify which plugin was used for login.
	 * Checks both current query parameters and HTTP referrer as fallback.
	 *
	 * @author  Lars Koudal
	 * @since   5.270
	 * @return  string|false Plugin name if temporary login detected, false otherwise.
	 */
	public static function get_temporary_login_plugin_name() {
		$request_uri  = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
		$query_string = isset( $_SERVER['QUERY_STRING'] ) ? $_SERVER['QUERY_STRING'] : '';
		$http_referer = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : '';

		// Check filter override first
		$is_temporary_login = apply_filters( 'securityninja_is_temporary_login_link', null, $request_uri, $query_string );
		if ( $is_temporary_login === true ) {
			return __( 'Temporary Login (filter override)', 'security-ninja' );
		}

		if ( $is_temporary_login === false ) {
			return false;
		}

		// Map plugin files to readable names
		$plugin_names = array(
			'temporary-login-without-password/temporary-login-without-password.php' => 'Temporary Login Without Password',
			'one-time-login/one-time-login.php' => 'One Time Login',
			'magic-login/plugin.php'            => 'Magic Login',
			'login-links/login-links.php'       => 'Login Links',
		);

		$default_params = array(
			array(
				'param'       => 'wtlwp_token',
				'plugin_file' => 'temporary-login-without-password/temporary-login-without-password.php',
			),
			array(
				'param'       => 'one_time_login_token',
				'plugin_file' => 'one-time-login/one-time-login.php',
			),
			array(
				'param'       => 'magic-login',
				'plugin_file' => 'magic-login/plugin.php',
				'requires'    => array( 'user_id', 'token' ),
			),
			array(
				'param'       => 'll',
				'plugin_file' => 'login-links/login-links.php',
				'prefix'      => true,
			),
		);

		$params = apply_filters( 'securityninja_temporary_login_params', $default_params );

		if ( ! is_array( $params ) || empty( $params ) ) {
			return false;
		}

		// Ensure plugin.php is loaded for is_plugin_active()
		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		// Check current query parameters first
		foreach ( $params as $param_config ) {
			if ( ! is_array( $param_config ) || empty( $param_config['param'] ) || empty( $param_config['plugin_file'] ) ) {
				continue;
			}

			$param_name  = $param_config['param'];
			$plugin_file = $param_config['plugin_file'];
			$requires    = isset( $param_config['requires'] ) && is_array( $param_config['requires'] ) ? $param_config['requires'] : array();
			$is_prefix   = isset( $param_config['prefix'] ) && $param_config['prefix'] === true;

			// Check if plugin is active
			if ( ! is_plugin_active( $plugin_file ) ) {
				continue;
			}

			// Check for prefix pattern
			if ( $is_prefix ) {
				foreach ( $_GET as $key => $value ) {
					if ( strpos( $key, $param_name ) === 0 && strlen( $key ) > strlen( $param_name ) ) {
						return isset( $plugin_names[ $plugin_file ] ) ? $plugin_names[ $plugin_file ] : $plugin_file;
					}
				}
				// Also check referrer as fallback
				if ( ! empty( $http_referer ) && strpos( $http_referer, $param_name ) !== false ) {
					return isset( $plugin_names[ $plugin_file ] ) ? $plugin_names[ $plugin_file ] : $plugin_file;
				}
				continue;
			}

			// Check for simple parameter
			if ( isset( $_GET[ $param_name ] ) ) {
				// If requires additional parameters, check they exist
				if ( ! empty( $requires ) ) {
					$all_required_present = true;
					foreach ( $requires as $required_param ) {
						if ( ! isset( $_GET[ $required_param ] ) ) {
							$all_required_present = false;
							break;
						}
					}
					if ( ! $all_required_present ) {
						continue;
					}
				}

				return isset( $plugin_names[ $plugin_file ] ) ? $plugin_names[ $plugin_file ] : $plugin_file;
			}

			// Fallback: check referrer URL for parameter
			if ( ! empty( $http_referer ) ) {
				$referer_has_param = false;
				if ( $is_prefix ) {
					// For prefix patterns, check if referrer contains the prefix
					if ( strpos( $http_referer, $param_name ) !== false ) {
						$referer_has_param = true;
					}
				} elseif ( strpos( $http_referer, $param_name . '=' ) !== false || strpos( $http_referer, $param_name . '&' ) !== false ) {
					// For simple parameters, check if referrer contains param= or param&
					$referer_has_param = true;
				}

				if ( $referer_has_param ) {
					// If requires additional parameters, check referrer for them too
					if ( ! empty( $requires ) ) {
						$all_required_present = true;
						foreach ( $requires as $required_param ) {
							if ( strpos( $http_referer, $required_param . '=' ) === false && strpos( $http_referer, $required_param . '&' ) === false ) {
								$all_required_present = false;
								break;
							}
						}
						if ( ! $all_required_present ) {
							continue;
						}
					}

					return isset( $plugin_names[ $plugin_file ] ) ? $plugin_names[ $plugin_file ] : $plugin_file;
				}
			}
		}

		return false;
	}

	/**
	 * Terminate current request - Checks if option is set to redirect to an URL first
	 *
	 * @author  Lars Koudal
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Saturday, December 10th, 2022.
	 * @version v1.0.2  Wednesday, December 20th, 2023.
	 * @version v1.0.3  Monday, May 12th, 2025.
	 * @access  public static
	 * @param   mixed   $current_user_ip    Default: null
	 * @param   string  $reason             Default: ''
	 * @param   mixed   $time               Default: null
	 * @param   boolean $register_block     Should the IP block be registered in the database. Default: false
	 * @return  void
	 */
	public static function kill_request( $current_user_ip = null, $reason = '', $time = 1 * DAY_IN_SECONDS, $register_block = false ) {

		// @todo - update the database with new columns -
		/*
				$table_name = $wpdb->prefix . 'wf_sn_cf_bl_ips';
				$sql = "CREATE TABLE {$table_name} (tid datetime NOT NULL DEFAULT NOW(),ip varchar(46) NOT NULL, reason varchar(255) NOT NULL, PRIMARY KEY  (ip),KEY tid (tid)) {$charset}";
		*/
		// if ($register_block) {
		//  $wpdb->insert(
		//      $wpdp->prefix.'wf_sn_cf_bl_ips',

		//  )
		// }

		// Set the constant to prevent caching
		if ( ! defined( 'DONOTCACHEPAGE' ) ) {
			define( 'DONOTCACHEPAGE', true );
		}

		// Add headers to prevent caching on Cloudflare and other proxies
		header( 'Cache-Control: no-cache, no-store, must-revalidate' );
		header( 'Pragma: no-cache' );
		header( 'Expires: 0' );

		// Add Cloudflare specific header to prevent caching
		header( 'Cache-Tag: dontcache' ); // Cloudflare specific header to control caching
		header( 'CF-Cache-Status: DYNAMIC' ); // Forces dynamic content, not cacheable

		// Checks if we need to redirect the killed request.
		$redirect_url = esc_url_raw( self::$options['redirect_url'] );
		if ( ! empty( $redirect_url ) && wp_http_validate_url( $redirect_url ) ) {
			$redirect_scheme = wp_parse_url( $redirect_url, PHP_URL_SCHEME );
			$redirect_scheme = is_string( $redirect_scheme ) ? strtolower( $redirect_scheme ) : '';

			if ( in_array( $redirect_scheme, array( 'http', 'https' ), true ) ) {
				// Redirect target is admin-configured and validated above.
				wp_redirect( $redirect_url, 301 ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
				exit;
			}
		}
		$message = '<p>' . esc_html( self::$options['message'] ) . '</p>';
		// Add IP info to message
		if ( is_null( $current_user_ip ) ) {
			$current_user_ip = self::get_user_ip();
		}
		$message .= '<p><small>IP: ' . esc_html( $current_user_ip ) . '</small></p>';

		// Removes a couple of filters that uses a check "is_embed()" which is too soon to be available
		// and that creates a PHP warning.
		remove_filter( 'wp_robots', 'wp_robots_noindex_search' );
		remove_filter( 'wp_robots', 'wp_robots_noindex_embeds' );

		wp_die(
			$message,
			'Blocked',
			array(
				'response' => 403,
			)
		);
	}





	/**
	 * Updates global blocked visits count
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   mixed   $ip IP that was blocked - NOT IN USE YET
	 * @return  void
	 */
	public static function update_blocked_count( $ip ) {
		// @todo - store block count per IP
		$blocked_count = get_option( 'wf_sn_cf_blocked_count' );
		if ( $blocked_count ) {
			++$blocked_count;
		} else {
			$blocked_count = 1;
		}
		update_option( 'wf_sn_cf_blocked_count', $blocked_count, false );

		// Track blocked visits "today" in site timezone, without storing history.
		$today         = current_time( 'Y-m-d' );
		$option_name   = 'wf_sn_cf_blocked_today';
		$blocked_today = get_option( $option_name, false );

		if ( false === $blocked_today || ! is_array( $blocked_today ) ) {
			$blocked_today = array(
				'date'  => $today,
				'count' => 1,
			);
			add_option( $option_name, $blocked_today, '', false );
			return;
		}

		$stored_date  = isset( $blocked_today['date'] ) ? (string) $blocked_today['date'] : '';
		$stored_count = isset( $blocked_today['count'] ) ? (int) $blocked_today['count'] : 0;

		if ( $stored_date === $today ) {
			$blocked_today['count'] = $stored_count + 1;
		} else {
			$blocked_today['date']  = $today;
			$blocked_today['count'] = 1;
		}

		update_option( $option_name, $blocked_today, false );
	}

	/**
	 * Get blocked visits for the current day (site timezone).
	 *
	 * @return int
	 */
	public static function get_blocked_today_count() {
		$today         = current_time( 'Y-m-d' );
		$blocked_today = get_option( 'wf_sn_cf_blocked_today', array() );

		if ( ! is_array( $blocked_today ) ) {
			return 0;
		}

		$stored_date = isset( $blocked_today['date'] ) ? (string) $blocked_today['date'] : '';
		if ( $stored_date !== $today ) {
			return 0;
		}

		return isset( $blocked_today['count'] ) ? (int) $blocked_today['count'] : 0;
	}

	/**
	 * Logs visitor event.
	 * Accepted parameters:
	 * ip - detected automatically if not parsed
	 * country - detected automatically if not parsed
	 * user_agent - can be empty
	 * banned - can be empty, default 0
	 * ban_reason - can be empty
	 * description - can be empty
	 * raw_data - can be empty - accepts array
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, July 22nd, 2021.
	 * @version v1.0.1  Saturday, November 19th, 2022.
	 * @version v1.0.2  Monday, January 16th, 2023.
	 * @access  public static
	 * @param   mixed   $params Default: array()
	 * @return  mixed
	 */
	public static function log_visitor__premium_only( $params = array() ) {
		// Early return if tracking is disabled
		if ( empty( self::$options['trackvisits'] ) ) {
			return false;
		}

		// Check if the visitor log class exists before using it
		if ( ! class_exists( __NAMESPACE__ . '\\Wf_Sn_Visitor_log' ) ) {
			return false;
		}

		$filterable_urls = Wf_Sn_Visitor_log::return_filterable_urls();
		$current_url     = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';

		// Check if the current URL matches any filterable URL
		foreach ( $filterable_urls as $filterable_url ) {
			if ( strpos( $current_url, $filterable_url ) !== false ) {
				return false;
			}
		}

		global $wpdb;
		$table_name = $wpdb->prefix . 'wf_sn_cf_vl';

		// Prepare default values with current request data
		$default_values = array(
			'ip'          => '',
			'raw_data'    => array(),
			'ban_reason'  => '',
			'description' => '',
			'banned'      => 0,
			'country'     => '',
			'user_agent'  => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '',
		);

		// Merge with provided params, using array_replace for better performance
		$params = array_replace( $default_values, $params );

		// Prepare insert data
		$args = array(
			'timestamp'   => current_time( 'mysql' ),
			'ip'          => $params['ip'],
			'country'     => $params['country'],
			'user_agent'  => $params['user_agent'],
			'banned'      => (int) $params['banned'],
			'ban_reason'  => $params['ban_reason'],
			'description' => maybe_serialize( $params['description'] ),
			'URL'         => isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( $_SERVER['REQUEST_URI'] ) : '',
			'raw_data'    => ! empty( $params['raw_data'] ) ? wp_json_encode( $params['raw_data'] ) : '',
		);

		// Insert the log entry
		return $wpdb->insert(
			$table_name,
			$args,
			array( '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s' )
		) ? $wpdb->insert_id : false;
	}


	/**
	 * Update local list of blocked IPs.
	 * First delete expired > 24 hours.
	 * Then download and bulk add entries
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Sunday, May 30th, 2021.
	 * @version v1.0.1  Wednesday, June 9th, 2021.
	 * @access  public static
	 * @param bool $force Default false.
	 * @return bool True on success, false on failure.
	 */
	public static function action_update_blocked_ips__premium_only( $force = false ) {

			$listips = self::get_network_listips__premium_only();

		if ( ! $listips ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'update_blocked_ips', 'Error getting blocked IPs from server' );
			return false;
		}

			global $wpdb;
			// Cleaning up
			$table_name = $wpdb->prefix . 'wf_sn_cf_bl_ips';

			$delquery = "DELETE FROM `{$table_name}` WHERE HOUR(TIMEDIFF(NOW(), tid))>24;";
			$delres   = $wpdb->query( $delquery );
		if ( $delres ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'update_blocked_ips', sprintf( esc_html__( 'Removed %1$s IPs from the Blocklist - older than 24 hours.', 'security-ninja' ), intval( $delres ) ), '' );
		} else {
			wf_sn_el_modules::log_event( 'security_ninja', 'update_blocked_ips', 'No old IPs needs to be removed.' );
		}

		$blockedips = json_decode( $listips, true );

		if ( $blockedips && is_array( $blockedips ) && isset( $blockedips['ips'] ) && is_array( $blockedips['ips'] ) ) {

			global $wpdb;
			$current_count = 0;
			$limit         = 15;
			$longquery     = '';
			$totalcount    = 0;
			$timenow       = current_time( 'mysql' );
			foreach ( $blockedips['ips'] as $ip ) {
				if ( 0 === $current_count ) {
					$longquery .= ' INSERT IGNORE INTO `' . $table_name . "` (`ip`) VALUES ('" . esc_sql( $ip ) . "')";
				} else {
					$longquery .= ",('" . esc_sql( $ip ) . "')";
				}
				++$current_count;

				if ( $current_count > $limit ) {
					$longquery .= ';'; // add ending semicolon before executing
					$wpdb->query( $longquery );
					$longquery     = '';
					$current_count = 0;
				}
				++$totalcount;
			}

			// Leftovers?
			if ( $current_count > 0 ) {
				$longquery .= ';'; // add ending semicolon before executing
				$wpdb->query( $longquery );
				$longquery     = '';
				$current_count = 0;
			}
		}
	}

	/**
	 * Prune events log table
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   boolean $force  Default: false
	 * @return  boolean
	 */
	public static function prune_visitor_log( $force = false ) {
		global $wpdb;

		$trackvisits_howlong = absint( self::$options['trackvisits_howlong'] );

		if ( ! $trackvisits_howlong ) {
			$trackvisits_howlong = 2; // in days
		}

		$table_name = $wpdb->prefix . 'wf_sn_cf_vl';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) {
			$wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'wf_sn_cf_vl WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)', $trackvisits_howlong ) );

			$max_records     = 5000; // Sane limit for visitor log entries
			$remaining_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name}" );

			if ( $remaining_count > $max_records ) {
				// Find the timestamp of the record that would be the $max_records-th newest
				$cutoff_data = $wpdb->get_row(
					$wpdb->prepare(
						"SELECT timestamp, id FROM {$table_name} 
					ORDER BY timestamp DESC, id DESC 
					LIMIT 1 OFFSET %d",
						$max_records - 1
					)
				);

				if ( $cutoff_data ) {
					// For records with the same timestamp, delete those with lower IDs
					$wpdb->query(
						$wpdb->prepare(
							"DELETE FROM {$table_name} 
						WHERE timestamp < %s 
						OR (timestamp = %s AND id < %d)",
							$cutoff_data->timestamp,
							$cutoff_data->timestamp,
							$cutoff_data->id
						)
					);
				}

				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event( 'security_ninja', 'pruned_visitor_log', sprintf( esc_html__( 'Pruned firewall visitors log - %1$s days, then reduced to %2$s entries (was %3$s).', 'security-ninja' ), $trackvisits_howlong, $max_records, $remaining_count ), '' );
			} else {
				\WPSecurityNinja\Plugin\wf_sn_el_modules::log_event( 'security_ninja', 'pruned_visitor_log', sprintf( esc_html__( 'Pruned firewall visitors log - %1$s days.', 'security-ninja' ), $trackvisits_howlong ), '' );
			}
		}

		return true;
	}

	/**
	 * Delete all firewall visitor log entries.
	 *
	 * @return bool
	 */
	public static function clear_visitor_log() {
		global $wpdb;

		$table_name = $wpdb->prefix . 'wf_sn_cf_vl';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
			return false;
		}

		$wpdb->query( 'TRUNCATE TABLE ' . $table_name );

		if ( class_exists( __NAMESPACE__ . '\wf_sn_el_modules' ) ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'cleared_visitor_log', esc_html__( 'Manually cleared firewall visitor log.', 'security-ninja' ), '' );
		}

		return true;
	}



	/**
	 * clean-up when deactivated
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, January 14th, 2021.
	 * @version v1.0.1  Monday, February 21st, 2022.
	 * @version v1.0.2  Saturday, November 19th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function deactivate() {

		//$centraloptions = Wf_Sn::get_options();
		// $centraloptions = $options = Wf_sn_cf::$options;
		if ( ! isset( self::$options['remove_settings_deactivate'] ) ) {
			return;
		}
		if ( self::$options['remove_settings_deactivate'] ) {
			global $wpdb;

			$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'wf_sn_cf_vl' );
			$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'wf_sn_cf_bl_ips' );

			delete_option( 'wf_sn_cf_bl_ips' );
			delete_option( 'wf_sn_cf_vl' );
			delete_option( WF_SN_CF_VALIDATED_CRAWLERS );
			delete_option( WF_SN_CF_AI_CRAWLER_RANGES );
			delete_option( 'wf_sn_cf_blocked_count' );
			delete_option( WF_SN_CF_OPTIONS_KEY );
			delete_option( 'wf_sn_cf_ips' );
			self::clear_cloud_ips_cache();

			delete_option( 'wf_sn_banned_ips' ); // list of locally banned IPs
		}
	}


	/**
	 * Schedule cron jobs
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Thursday, January 14th, 2021.
	 * @access  public static
	 * @return  void
	 */
	public static function schedule_cron_jobs() {

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			// Update GEOIP database - once a month
			if ( ! wp_next_scheduled( 'secnin_update_geoip' ) ) {
				wp_schedule_event( time() + 30, 'weekly', 'secnin_update_geoip' );
			}

			// Update cloud IPs
			if ( ! wp_next_scheduled( 'secnin_update_cloud_firewall' ) ) {
				wp_schedule_event( time() + 15, 'daily', 'secnin_update_cloud_firewall' );
			}

			// Prune visitor log entries according to retention setting
			if ( ! wp_next_scheduled( 'secnin_prune_visitor_log' ) ) {
				wp_schedule_event( time() + 45, 'daily', 'secnin_prune_visitor_log' );
			}

			// Remove expired local firewall IP bans
			if ( ! wp_next_scheduled( 'secnin_prune_banned' ) ) {
				wp_schedule_event( time() + 60, 'daily', 'secnin_prune_banned' );
			}
		}
	}



	/**
	 * Wrapper method to update GEOIP database via cron.
	 * Loads the SN_Geolocation class only when needed.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @access  public static
	 * @return  void
	 */
	public static function update_geoip_database__premium_only() {
			// Load the class file if not already loaded
		if ( ! class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
			require_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geolocation.php';
		}

			// Call the actual method
		if ( class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
			SN_Geolocation::update_database();
		}
	}


	/**
	 * Check if IP is from banned country
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   boolean $ip User IP - optionals
	 * @return  boolean
	 */
	public static function is_banned_country__premium_only( $ip = false ) {

		if ( $ip ) {
			$current_user_ip = $ip;
		} else {
			$current_user_ip = self::get_user_ip();
		}

		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		$blocked_countries = self::get_blocked_countries();

		if ( ! class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
			$geo_file = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geolocation.php';
			if ( file_exists( $geo_file ) ) {
				require_once $geo_file;
			}
		}
		if ( ! class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
			return false;
		}
		$geolocate_ip = SN_Geolocation::geolocate_ip( $current_user_ip, true );

		if ( isset( $geolocate_ip['country'] ) && '' !== (string) $geolocate_ip['country'] ) {
			if ( in_array( $geolocate_ip['country'], $blocked_countries, true ) ) {
				$country_code = $geolocate_ip['country'];
				if ( self::try_satellite_soften_block__premium_only(
					$current_user_ip,
					'country',
					'Country is blocked ' . $country_code,
					array( 'country_code' => $country_code )
				) ) {
					return false;
				}
				return $country_code;
			}
		}
		return false;
	}



	/**
	 * Enqueues JS and CSS needed for Firewall tab
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function enqueue_scripts() {
		if ( ! Wf_Sn::is_plugin_page() ) {
			return;
		}

		wp_enqueue_style( 'select2', WF_SN_PLUGIN_URL . 'modules/cloud-firewall/select2/css/select2.css', array(), filemtime( WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/select2/css/select2.css' ) );
		wp_enqueue_script( 'select2', WF_SN_PLUGIN_URL . 'modules/cloud-firewall/select2/js/select2.js', array( 'jquery' ), filemtime( WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/select2/js/select2.js' ) );

		wp_enqueue_style( 'dashicons' );
		wp_enqueue_style( 'sn-cf-css', WF_SN_PLUGIN_URL . 'modules/cloud-firewall/css/wf-sn-cf.css', array( 'dashicons' ), filemtime( WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/css/wf-sn-cf.css' ) );

		wp_register_script( 'sn-cf-js', WF_SN_PLUGIN_URL . 'modules/cloud-firewall/js/wf-sn-cf.js', array( 'select2', 'sn-dialog' ), filemtime( WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/js/wf-sn-cf.js' ) );

		$js_vars = array(
			'nonce'         => wp_create_nonce( 'wf_sn_cf' ),
			'sn_plugin_url' => WF_SN_PLUGIN_URL,
			'site_host'     => wp_parse_url( home_url(), PHP_URL_HOST ),
			'strings'       => array(
				'copy_blacklisted'    => __( 'Copy blacklisted IPs', 'security-ninja' ),
				'copy_whitelisted'    => __( 'Copy whitelisted IPs', 'security-ninja' ),
				'copied'              => __( 'Copied to clipboard.', 'security-ninja' ),
				'copy_failed'         => __( 'Could not copy to clipboard.', 'security-ninja' ),
				'confirm_remove_ip'   => __( 'Remove your current IP from the whitelist? You may lock yourself out.', 'security-ninja' ),
				'no_entries'          => __( 'No IP entries match your filters.', 'security-ninja' ),
				'remove'              => __( 'Remove', 'security-ninja' ),
				'copy'                => __( 'Copy', 'security-ninja' ),
				'prev'                => __( 'Prev', 'security-ninja' ),
				'next'                => __( 'Next', 'security-ninja' ),
				'copy_ip'             => __( 'Copy IP', 'security-ninja' ),
				'cancel'              => __( 'Cancel', 'security-ninja' ),
				'list_blacklist'      => __( 'blacklist', 'security-ninja' ),
				'list_whitelist'      => __( 'whitelist', 'security-ninja' ),
				'remove_confirm'      => __( 'Remove %1$s from the %2$s?', 'security-ninja' ),
				'source_label'        => __( 'Source:', 'security-ninja' ),
				'expires_label'       => __( 'Expires:', 'security-ninja' ),
				'last_visit_label'    => __( 'Last visit:', 'security-ninja' ),
				'note_label'          => __( 'Note:', 'security-ninja' ),
				'status_whitelisted'  => __( 'Whitelisted', 'security-ninja' ),
				'status_blacklisted'  => __( 'Blacklisted', 'security-ninja' ),
				'add_rule'            => __( 'Add IP rule', 'security-ninja' ),
				'edit_rule'           => __( 'Edit IP rule', 'security-ninja' ),
				'edit'                => __( 'Edit', 'security-ninja' ),
				'save'                => __( 'Save', 'security-ninja' ),
				'add_success'         => __( 'IP rule added.', 'security-ninja' ),
				'edit_success'        => __( 'IP rule updated.', 'security-ninja' ),
				'list_type_blacklist' => __( 'Blacklist', 'security-ninja' ),
				'list_type_whitelist' => __( 'Whitelist', 'security-ninja' ),
			),
		);

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Cf_Ip_Management' ) ) {
				$entries               = Wf_Sn_Cf_Ip_Management::get_ip_management_entries();
				$js_vars['ip_entries'] = Wf_Sn_Cf_Ip_Management::entries_for_js( $entries );
			}
		}

		wp_localize_script( 'sn-cf-js', 'wf_sn_cf', $js_vars );

		wp_enqueue_script( 'sn-cf-js' );
	}


	/**
	 * Return firewall options
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  mixed
	 */
	public static function get_options() {

		if ( ! is_null( self::$options ) ) {
			return self::$options;
		}

		$options = get_option( 'wf_sn_cf', array() );

		$defaults = array(
			'active'                        => 0,
			'globalbannetwork'              => 1,
			'global'                        => 0,
			'filterqueries'                 => 1,
			'trackvisits'                   => 1,
			'trackvisits_howlong'           => 7,
			'usecloud'                      => 1,
			'protect_login_form'            => 1,
			'hide_login_errors'             => 1,
			'failed_login_email_warning'    => 0,
			'blocked_countries'             => array(),
			'countryblock_loginonly'        => 0,
			'blacklist'                     => array(),
			'whitelist'                     => array( self::get_user_ip() ),
			'blacklist_notes'               => array(),
			'whitelist_notes'               => array(),
			'whitelist_managewp'            => 1,
			'whitelist_wprocket'            => 0,
			'whitelist_uptimia'             => 0,
			'whitelist_uptimerobot'         => 0,
			'max_login_attempts'            => 5,
			'max_login_attempts_time'       => 5,
			'bruteforce_ban_time'           => 120,
			'login_msg'                     => 'Warning: Multiple failed login attempts will get you locked out temporarily.',
			'login_error_msg'               => 'Something went wrong',
			'message'                       => 'You are not allowed to visit this website.',
			'redirect_url'                  => '',
			'blockadminlogin'               => 0,
			'change_login_url'              => 0,
			'new_login_url'                 => 'my-login',
			'unblock_url'                   => '',
			'2fa_enabled'                   => 0,
			'2fa_enabled_timestamp'         => '',
			'2fa_required_roles'            => array(),
			'2fa_methods'                   => array( 'app' ),
			'2fa_grace_period'              => 14,
			'2fa_backup_codes_enabled'      => 1,
			'2fa_intro'                     => 'Secure your account with two-factor authentication.',
			'2fa_enter_code'                => 'Enter the code from your 2FA app to continue logging in.',
			'404guard_enabled'              => 1,
			'404guard_threshold'            => 10,
			'404guard_window'               => 300,
			'404guard_block_time'           => 600,
			// WooCommerce protection options
			'woo_rate_limiting_enabled'     => 0,
			'woo_checkout_rate_limit'       => 3,
			'woo_checkout_window'           => 300,
			'woo_add_to_cart_limit'         => 10,
			'woo_add_to_cart_window'        => 60,
			'woo_order_rate_limit'          => 2,
			'woo_order_window'              => 600,
			'woo_coupon_protection_enabled' => 0,
			'woo_coupon_failed_attempts'    => 3,
			'woo_coupon_window'             => 180,
			'woo_coupon_ban_time'           => 900,
			'satellite_soft_enabled'        => 1,
			'satellite_soft_asns'           => array( '14593' ),
		);

		$return = array_merge( $defaults, $options );

		// Backwards compatibility: normalize all boolean values to integers (0 or 1)
		$boolean_keys = array(
			'active',
			'globalbannetwork',
			'global',
			'filterqueries',
			'trackvisits',
			'usecloud',
			'protect_login_form',
			'hide_login_errors',
			'failed_login_email_warning',
			'countryblock_loginonly',
			'satellite_soft_enabled',
			'whitelist_managewp',
			'whitelist_wprocket',
			'whitelist_uptimia',
			'whitelist_uptimerobot',
			'2fa_enabled',
			'2fa_backup_codes_enabled',
			'404guard_enabled',
			'woo_rate_limiting_enabled',
			'woo_coupon_protection_enabled',
			'change_login_url',
			'blockadminlogin',
		);

		foreach ( $boolean_keys as $key ) {
			if ( isset( $return[ $key ] ) ) {
				$return[ $key ] = \WPSecurityNinja\Plugin\Utils::normalize_flag( $return[ $key ] );
			}
		}

		$two_fa_on                    = ! empty( $return['2fa_enabled'] );
		$return['2fa_required_roles'] = self::normalize_2fa_required_roles( $return['2fa_required_roles'] ?? array(), $two_fa_on );
		$return['2fa_methods']        = self::normalize_2fa_methods( $return['2fa_methods'] ?? array() );

		return $return;
	}

	/**
	 * Keep only role slugs that exist in WordPress.
	 *
	 * @param mixed $raw Roles from option (array, string, or other).
	 * @return string[] Unique valid role slugs.
	 */
	public static function sanitize_2fa_required_role_slugs( $raw ) {
		$wp_roles = function_exists( 'wp_roles' ) ? wp_roles() : null;
		$valid    = ( is_object( $wp_roles ) && isset( $wp_roles->roles ) ) ? array_keys( $wp_roles->roles ) : array();

		if ( is_string( $raw ) && '' !== $raw ) {
			$raw = array( $raw );
		} elseif ( ! is_array( $raw ) ) {
			$raw = array();
		}

		$out = array();
		foreach ( $raw as $slug ) {
			$slug = sanitize_text_field( (string) $slug );
			if ( '' === $slug ) {
				continue;
			}
			if ( empty( $valid ) ) {
				// wp_roles() unavailable (very early); accept nothing so callers can fall back safely.
				continue;
			}
			if ( in_array( $slug, $valid, true ) ) {
				$out[] = $slug;
			}
		}

		return array_values( array_unique( $out ) );
	}

	/**
	 * Normalize 2FA required roles: valid slugs only (may be empty for opt-in-only mode).
	 *
	 * @param mixed $raw             Stored or submitted roles.
	 * @param bool  $two_fa_enabled  Whether 2FA is enabled (unused; kept for callers).
	 * @return string[]
	 */
	public static function normalize_2fa_required_roles( $raw, $two_fa_enabled ) {
		unset( $two_fa_enabled );
		return self::sanitize_2fa_required_role_slugs( $raw );
	}

	/**
	 * Sanitize allowed 2FA methods from submitted values (may be empty).
	 *
	 * @param mixed $raw Stored or submitted methods.
	 * @return string[]
	 */
	public static function sanitize_2fa_methods_list( $raw ) {
		$allowed = array( 'app', 'email' );

		if ( is_string( $raw ) && '' !== $raw ) {
			$raw = array( $raw );
		} elseif ( ! is_array( $raw ) ) {
			$raw = array();
		}

		$out = array();
		foreach ( $raw as $method ) {
			$method = sanitize_text_field( (string) $method );
			if ( in_array( $method, $allowed, true ) ) {
				$out[] = $method;
			}
		}

		return array_values( array_unique( $out ) );
	}

	/**
	 * Normalize allowed 2FA methods (app and/or email); defaults to app when empty on read.
	 *
	 * @param mixed $raw Stored or submitted methods.
	 * @return string[]
	 */
	public static function normalize_2fa_methods( $raw ) {
		$out = self::sanitize_2fa_methods_list( $raw );
		if ( empty( $out ) ) {
			return array( 'app' );
		}

		return $out;
	}

	/**
	 * Clear 2FA user meta when 2FA is first enabled, preserving voluntary opt-in enrollments.
	 *
	 * @return void
	 */
	private static function clear_2fa_user_meta_on_first_enable() {
		$users = get_users();
		foreach ( $users as $user ) {
			if ( '1' === get_user_meta( $user->ID, 'secnin_2fa_optin', true ) ) {
				continue;
			}
			delete_user_meta( $user->ID, 'secnin_2fa_secret' );
			delete_user_meta( $user->ID, 'secnin_2fa_setup_complete' );
			delete_user_meta( $user->ID, 'secnin_2fa_code_validated' );
			delete_user_meta( $user->ID, 'secnin_2fa_session_validated' );
			delete_user_meta( $user->ID, 'secnin_2fa_method' );
		}
	}


	/**
	 * Enables the firewall - via AJAX
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Monday, February 6th, 2023.
	 * @access  public static
	 * @return  void
	 */
	public static function ajax_enable_firewall() {
		check_ajax_referer( 'wf_sn_cf' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		self::$options['active'] = 1;
		update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );
		if ( class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
			\WPSecurityNinja\Plugin\SN_Geolocation::update_database(); // updates the geoip when turning on firewall + via cron afterwards.
		}

		// Add notice about whitelist management
		add_settings_error(
			'wf_sn_cf',
			'firewall_activated',
			__( 'Firewall activated. For security best practices, remember to manually review and manage your IP whitelist.', 'security-ninja' ),
			'info'
		);

		wp_send_json_success(
			array(
				'message' => __( 'Firewall activated successfully.', 'security-ninja' ),
				'notices' => get_settings_errors( 'wf_sn_cf' ),
			)
		);
	}




	/**
	 * Tests if an IP is banned, via AJAX
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  string
	 */
	public static function ajax_test_ip() {
		check_ajax_referer( 'wf_sn_cf' );

		self::require_premium_ip_management_ajax();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		if ( ! isset( $_POST['ip'] ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Missing IP.', 'security-ninja' ),
				)
			);
		}

		$ip = sanitize_text_field( wp_unslash( $_POST['ip'] ) );

		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			wp_send_json_success( __( 'Please enter a valid IP address to test.', 'security-ninja' ) );
		}

		if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Cf_Ip_Management' ) ) {
			$check = Wf_Sn_Cf_Ip_Management::get_block_reasons_for_ip( $ip );

			if ( ! empty( $check['whitelisted'] ) && empty( $check['blocked'] ) ) {
				wp_send_json_success(
					sprintf(
						/* translators: %s: IP address */
						__( '%s is whitelisted and will not be blocked.', 'security-ninja' ),
						esc_html( $ip )
					)
				);
			}

			if ( ! empty( $check['blocked'] ) && ! empty( $check['reasons'] ) ) {
				wp_send_json_success(
					sprintf(
						/* translators: 1: IP address, 2: comma-separated block reasons */
						__( '%1$s is blocked. Reasons: %2$s', 'security-ninja' ),
						esc_html( $ip ),
						esc_html( implode( '; ', $check['reasons'] ) )
					)
				);
			}

			wp_send_json_success(
				sprintf(
					/* translators: %s: IP address */
					__( '%s is NOT blocked.', 'security-ninja' ),
					esc_html( $ip )
				)
			);
		}

		if ( $reason = self::is_banned_ip( $ip ) ) {
			wp_send_json_success(
				sprintf(
					/* translators: 1: IP - 2: The reason, leave as is. */
					__( '%1$s is banned. %2$s', 'security-ninja' ),
					esc_html( $ip ),
					esc_html( $reason )
				)
			);
		}

		wp_send_json_success(
			sprintf(
				/* translators: %s: IP address */
				__( '%s is NOT banned.', 'security-ninja' ),
				esc_html( $ip )
			)
		);
	}




	/**
	 * Return domain from full parsed URL
	 *
	 * https://stackoverflow.com/a/18560043/452515
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   mixed   $url
	 * @return  mixed
	 */
	public static function url_to_domain( $url ) {
		return implode( array_slice( explode( '/', preg_replace( '/https?:\/\/(www\.)?/', '', $url ) ), 0, 1 ) );
	}

	/**
	 * Reject IP Management AJAX when premium code is unavailable.
	 *
	 * @return void
	 */
	private static function require_premium_ip_management_ajax() {
		$denied = true;
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			$denied = false;
		}
		if ( $denied ) {
			wp_send_json_error(
				array(
					'message' => __( 'IP Management is not available.', 'security-ninja' ),
				)
			);
		}
	}

	/**
	 * Add manual blacklist/whitelist rule via AJAX.
	 *
	 * @return void
	 */
	public static function ajax_add_ip_rule() {
		check_ajax_referer( 'wf_sn_cf' );

		self::require_premium_ip_management_ajax();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed.', 'security-ninja' ) ) );
		}

		if ( ! isset( $_POST['ip'], $_POST['list_type'] ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing required fields.', 'security-ninja' ) ) );
		}

		$ip        = sanitize_text_field( wp_unslash( $_POST['ip'] ) );
		$list_type = sanitize_key( wp_unslash( $_POST['list_type'] ) );

		if ( ! class_exists( __NAMESPACE__ . '\Wf_Sn_Cf_Ip_Management' ) ) {
			wp_send_json_error( array( 'message' => __( 'IP Management is not available.', 'security-ninja' ) ) );
		}

		$note   = isset( $_POST['note'] ) ? Wf_Sn_Cf_Ip_Management::sanitize_note( wp_unslash( $_POST['note'] ) ) : '';
		$result = Wf_Sn_Cf_Ip_Management::add_manual_rule( $ip, $list_type, $note );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'message' => __( 'IP rule added.', 'security-ninja' ),
				'data'    => $result,
			)
		);
	}

	/**
	 * Edit manual blacklist/whitelist rule via AJAX.
	 *
	 * @return void
	 */
	public static function ajax_edit_ip_rule() {
		check_ajax_referer( 'wf_sn_cf' );

		self::require_premium_ip_management_ajax();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed.', 'security-ninja' ) ) );
		}

		if ( ! isset( $_POST['old_ip'], $_POST['ip'], $_POST['list_type'] ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing required fields.', 'security-ninja' ) ) );
		}

		$old_ip    = sanitize_text_field( wp_unslash( $_POST['old_ip'] ) );
		$ip        = sanitize_text_field( wp_unslash( $_POST['ip'] ) );
		$list_type = sanitize_key( wp_unslash( $_POST['list_type'] ) );

		if ( ! class_exists( __NAMESPACE__ . '\Wf_Sn_Cf_Ip_Management' ) ) {
			wp_send_json_error( array( 'message' => __( 'IP Management is not available.', 'security-ninja' ) ) );
		}

		$note   = isset( $_POST['note'] ) ? Wf_Sn_Cf_Ip_Management::sanitize_note( wp_unslash( $_POST['note'] ) ) : '';
		$result = Wf_Sn_Cf_Ip_Management::edit_manual_rule( $old_ip, $ip, $list_type, $note );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'message' => __( 'IP rule updated.', 'security-ninja' ),
				'data'    => $result,
			)
		);
	}

	/**
	 * Remove manual blacklist/whitelist rule via AJAX.
	 *
	 * @return void
	 */
	public static function ajax_remove_ip_rule() {
		check_ajax_referer( 'wf_sn_cf' );

		self::require_premium_ip_management_ajax();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed.', 'security-ninja' ) ) );
		}

		if ( ! isset( $_POST['ip'], $_POST['list_type'] ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing required fields.', 'security-ninja' ) ) );
		}

		$ip        = sanitize_text_field( wp_unslash( $_POST['ip'] ) );
		$list_type = sanitize_key( wp_unslash( $_POST['list_type'] ) );

		if ( ! class_exists( __NAMESPACE__ . '\Wf_Sn_Cf_Ip_Management' ) ) {
			wp_send_json_error( array( 'message' => __( 'IP Management is not available.', 'security-ninja' ) ) );
		}

		$result = Wf_Sn_Cf_Ip_Management::remove_manual_rule( $ip, $list_type );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'message' => __( 'IP rule removed.', 'security-ninja' ),
				'data'    => $result,
			)
		);
	}

	/**
	 * Bulk-add manual blacklist/whitelist rules via AJAX.
	 *
	 * @return void
	 */
	public static function ajax_bulk_add_ip_rules() {
		check_ajax_referer( 'wf_sn_cf' );

		self::require_premium_ip_management_ajax();

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Failed.', 'security-ninja' ) ) );
		}

		if ( ! isset( $_POST['ips'], $_POST['list_type'] ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing required fields.', 'security-ninja' ) ) );
		}

		$ips       = wp_unslash( $_POST['ips'] );
		$list_type = sanitize_key( wp_unslash( $_POST['list_type'] ) );

		if ( ! class_exists( __NAMESPACE__ . '\Wf_Sn_Cf_Ip_Management' ) ) {
			wp_send_json_error( array( 'message' => __( 'IP Management is not available.', 'security-ninja' ) ) );
		}

		$note   = isset( $_POST['note'] ) ? Wf_Sn_Cf_Ip_Management::sanitize_note( wp_unslash( $_POST['note'] ) ) : '';
		$result = Wf_Sn_Cf_Ip_Management::add_manual_rules_bulk( $ips, $list_type, $note );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		$summary = isset( $result['summary'] ) ? $result['summary'] : array();
		unset( $result['summary'] );

		$added   = isset( $summary['added'] ) ? (int) $summary['added'] : 0;
		$invalid = isset( $summary['invalid'] ) ? (int) $summary['invalid'] : 0;
		$skipped = isset( $summary['skipped_duplicate'] ) ? (int) $summary['skipped_duplicate'] : 0;

		$message = sprintf(
			/* translators: %d: number of IP rules added */
			_n( 'Added %d IP rule.', 'Added %d IP rules.', $added, 'security-ninja' ),
			$added
		);

		if ( $invalid > 0 ) {
			$message .= ' ' . sprintf(
				/* translators: %d: number of invalid lines skipped */
				_n( '%d invalid line skipped.', '%d invalid lines skipped.', $invalid, 'security-ninja' ),
				$invalid
			);
		}

		if ( $skipped > 0 ) {
			$message .= ' ' . sprintf(
				/* translators: %d: number of duplicate IPs skipped */
				_n( '%d duplicate skipped.', '%d duplicates skipped.', $skipped, 'security-ninja' ),
				$skipped
			);
		}

		wp_send_json_success(
			array(
				'message' => trim( $message ),
				'data'    => $result,
			)
		);
	}


	/**
	 * get_banned_ips.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Wednesday, June 5th, 2024.
	 * @access  public static
	 * @return  mixed
	 */
	public static function get_banned_ips() {
		if ( ! is_null( self::$banned_ips ) ) {
			return self::$banned_ips;
		}

		// get the option wf_sn_banned_ips
		$wf_sn_banned_ips = get_option( 'wf_sn_banned_ips' );

		self::$banned_ips = is_array( $wf_sn_banned_ips ) ? $wf_sn_banned_ips : array();
		return self::$banned_ips;
	}



	/**
	 * Function to send email with unblock link via AJAX
	 *
	 * Moved email sending to seperate function March 2022 - send_secret_access_unblock_url()
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Wednesday, March 16th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function ajax_send_unblock_email() {
		check_ajax_referer( 'wf_sn_cf' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		if ( ! isset( $_POST['email'] ) ) {
			$error = new \WP_Error( '001', 'No email?' );
			wp_send_json_error( $error );
		}

		$sanitized_email = sanitize_email( wp_unslash( $_POST['email'] ) );

		if ( false === is_email( $sanitized_email ) ) {
			$error = new \WP_Error( '002', 'Not a valid email!' );
			wp_send_json_error( $error );
		}

		if ( ! ( array_key_exists( 'unblock_url', self::$options ) ) ) {
			self::$options['unblock_url'] = md5( time() );
			update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );
		}

		$sendresult = self::send_secret_access_unblock_url( $sanitized_email );

		if ( $sendresult ) {
			wp_send_json_success(
				array(
					'message' => __( 'Email sent.', 'security-ninja' ),
				)
			);
		} else {
			wp_send_json_error(
				array(
					'message' => __( 'Email could not be sent.', 'security-ninja' ),
				)
			);
		}

		die();
	}



	/**
	 * send_secret_access_unblock_url.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Wednesday, March 16th, 2022.
	 * @access  public static
	 * @param   mixed   $email
	 * @return  boolean
	 */
	public static function send_secret_access_unblock_url( $email ) {
		if ( ! $email ) {
			return false;
		}

		$sanitized_email = sanitize_email( $email );

		if ( false === is_email( $sanitized_email ) ) {
			$error = new \WP_Error( '002', 'Not a valid email!' );
			wp_send_json_error( $error );
		}
		$subject = __( 'Security Ninja Firewall secret access link', 'security-ninja' );

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Wl' ) ) {
				if ( \WPSecurityNinja\Plugin\Wf_Sn_Wl::is_active() ) {
					$pluginname = \WPSecurityNinja\Plugin\Wf_Sn_Wl::get_new_name();
					$subject    = $pluginname . ' ' . __( 'Secret access link', 'security-ninja' );
				}
			}
		}

		$body  = '<p>' . esc_html__( 'Thank you for installing.', 'security-ninja' ) . '</p>';
		$body .= '<p>' . esc_html__( 'Please keep this email for your records.', 'security-ninja' ) . '</p>';
		$body .= '<p>' . esc_html__( 'In the unlikely situation that your IP gets banned, you will need the secret access link.', 'security-ninja' ) . '</p>';

		$body .= '<p><strong>' . sprintf(
			esc_html__( 'Your secret access link is %s', 'security-ninja' ),
			self::get_unblock_url()
		) . '</strong></p>';

		$body .= '<p>' . esc_html__( 'Copy-paste this URL to your browser to whitelist your IP, allowing you to log back in.', 'security-ninja' ) . '</p>';

		$body .= '<p>' . esc_html__( 'Please keep it safe and do not share it with others. Use it only if you get blocked by the firewall.', 'security-ninja' ) . '</p>';

		$show_docs_link = true;
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Wl' ) ) {
				if ( \WPSecurityNinja\Plugin\Wf_Sn_Wl::is_active() ) {
					$show_docs_link = false;
				}
			}
		}
		if ( $show_docs_link ) {
			$sal_email_link = Utils::generate_sn_web_link( 'secret_access_link', '/docs/firewall-protection/secret-access-link/', array( 'utm_medium' => 'email' ) );
			$body          .= '<p><a href="' . $sal_email_link . '" target="_blank" rel="noopener">Documentation for Secret Access Link</a></p>';
		}

		$headers = array( 'Content-Type: text/html; charset=UTF-8' );

		$emailintrotext = 'Save your secret access link for ' . self::url_to_domain( site_url() );

		$dashboardlink       = admin_url( '?page=wf-sn' );
		$dashboardlinkanchor = 'Settings';

		$body .= '<p><a href="' . $dashboardlink . '" target="_blank" rel="noopener">' . $dashboardlinkanchor . '</a></p>';

		$my_replacements = array(
			'%%emailintrotext%%'      => $emailintrotext,
			'%%websitedomain%%'       => site_url(),
			'%%dashboardlink%%'       => $dashboardlink,
			'%%dashboardlinkanchor%%' => $dashboardlinkanchor,
			'%%secninlogourl%%'       => WF_SN_PLUGIN_URL . 'images/security-ninja-logo.png',
			'%%emailtitle%%'          => $subject,
			'%%sentfromtext%%'        => 'This email was sent from ' . self::url_to_domain( site_url() ),
			'%%emailcontent%%'        => nl2br( $body ),
		);

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Wl' ) ) {
				if ( \WPSecurityNinja\Plugin\Wf_Sn_Wl::is_active() ) {
					$pluginname                           = \WPSecurityNinja\Plugin\Wf_Sn_Wl::get_new_name();
					$my_replacements['%%secninlogourl%%'] = esc_url( \WPSecurityNinja\Plugin\Wf_Sn_Wl::get_email_logo_url() );
					$my_replacements['%%sentfromtext%%']  = 'This email was sent by ' . esc_attr( $pluginname ) . ' from ' . esc_url( self::url_to_domain( site_url() ) );
				}
			}
		}

		$template_path = WF_SN_PLUGIN_DIR . 'includes/email-default.php';

		// Use secure file reading with validation
		$html = Wf_Sn_Security_Utils::secure_file_get_contents(
			$template_path,
			array(
				'check_readable'     => true,
				'check_size'         => true,
				'max_size'           => 1024 * 1024, // 1MB for templates
				'check_type'         => true,
				'allowed_extensions' => array( 'php' ),
				'check_mime'         => false, // Skip MIME check for performance
				'check_path'         => true,
				'allowed_dirs'       => array( WF_SN_PLUGIN_DIR ),
			)
		);

		if ( $html === false ) {
			// Handle error - template not found
			$html = '<p>Email template not found.</p>';
		}
		foreach ( $my_replacements as $needle => $replacement ) {
			$html = str_replace( $needle, $replacement, $html );
		}

		$sendresult = wp_mail( $sanitized_email, $subject, $html, $headers );

		wf_sn_el_modules::log_event( 'security_ninja', 'install_wizard', 'Sent unblock URL to email ' . $sanitized_email );

		return $sendresult;
	}


	/**
	 * Send failed login warning email to administrator
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, January 14th, 2025.
	 * @access  public static
	 * @param   string $username   Username that was attempted
	 * @param   string $ip         IP address of the attempt
	 * @param   string $user_agent User agent string
	 * @return  bool True on success, false on failure
	 */
	public static function send_failed_login_warning_email( $username, $ip, $user_agent ) {
		// Check if setting is enabled
		if ( empty( self::$options['failed_login_email_warning'] ) ) {
			return false;
		}

		// Validate username
		if ( empty( $username ) ) {
			return false;
		}

		// Check rate limiting - prevent multiple emails for same username within 15 minutes
		$rate_limit_key  = 'wf_sn_failed_login_email_' . md5( $username );
		$last_email_time = get_transient( $rate_limit_key );

		if ( $last_email_time && ( time() - $last_email_time ) < 900 ) {
			// Email was sent recently, skip
			return false;
		}

		// Get user by username
		$user = get_user_by( 'login', $username );

		// Check if user exists and is administrator
		if ( ! $user || ! in_array( 'administrator', (array) $user->roles, true ) ) {
			return false;
		}

		// Get user email
		$user_email = $user->user_email;

		if ( ! is_email( $user_email ) ) {
			return false;
		}

		// Build email subject
		$subject = __( 'Was that you trying to log in?', 'security-ninja' );

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Wl' ) ) {
				if ( \WPSecurityNinja\Plugin\Wf_Sn_Wl::is_active() ) {
					$pluginname = \WPSecurityNinja\Plugin\Wf_Sn_Wl::get_new_name();
					$subject    = $pluginname . ' - ' . __( 'Was that you trying to log in?', 'security-ninja' );
				}
			}
		}

		// Build email body
		$body = '<p>' . sprintf(
			/* translators: 1: Username */
			__( 'Someone just tried to log in to your account with username: %s', 'security-ninja' ),
			esc_html( $username )
		) . '</p>';

		$body .= '<p><strong>' . __( 'Details:', 'security-ninja' ) . '</strong></p>';
		$body .= '<ul>';
		$body .= '<li>' . sprintf(
			/* translators: 1: IP address */
			__( 'IP Address: %s', 'security-ninja' ),
			esc_html( $ip )
		) . '</li>';

		if ( ! empty( $user_agent ) ) {
			$body .= '<li>' . sprintf(
				/* translators: 1: User agent */
				__( 'User Agent: %s', 'security-ninja' ),
				esc_html( $user_agent )
			) . '</li>';
		}

		$body .= '<li>' . sprintf(
			/* translators: 1: Timestamp */
			__( 'Time: %s', 'security-ninja' ),
			date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), current_time( 'timestamp' ) )
		) . '</li>';
		$body .= '</ul>';

		$body .= '<p>' . __( 'If this was you, you can ignore this email. If this was not you, we recommend:', 'security-ninja' ) . '</p>';
		$body .= '<ul>';
		$body .= '<li>' . __( 'Change your password immediately', 'security-ninja' ) . '</li>';
		$body .= '<li>' . __( 'Review your account security settings', 'security-ninja' ) . '</li>';
		$body .= '<li>' . __( 'Enable two-factor authentication if available', 'security-ninja' ) . '</li>';
		$body .= '</ul>';

		$dashboardlink       = admin_url( '?page=wf-sn' );
		$dashboardlinkanchor = __( 'Security Settings', 'security-ninja' );

		$body .= '<p><a href="' . esc_url( $dashboardlink ) . '" target="_blank" rel="noopener">' . esc_html( $dashboardlinkanchor ) . '</a></p>';

		$headers = array( 'Content-Type: text/html; charset=UTF-8' );

		$emailintrotext = sprintf(
			/* translators: 1: Site domain */
			__( 'Failed login attempt for %s', 'security-ninja' ),
			self::url_to_domain( site_url() )
		);

		$my_replacements = array(
			'%%emailintrotext%%'      => $emailintrotext,
			'%%websitedomain%%'       => site_url(),
			'%%dashboardlink%%'       => $dashboardlink,
			'%%dashboardlinkanchor%%' => $dashboardlinkanchor,
			'%%secninlogourl%%'       => WF_SN_PLUGIN_URL . 'images/security-ninja-logo.png',
			'%%emailtitle%%'          => $subject,
			'%%sentfromtext%%'        => 'This email was sent from ' . self::url_to_domain( site_url() ),
			'%%emailcontent%%'        => nl2br( $body ),
		);

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( class_exists( __NAMESPACE__ . '\Wf_Sn_Wl' ) ) {
				if ( \WPSecurityNinja\Plugin\Wf_Sn_Wl::is_active() ) {
					$pluginname                           = \WPSecurityNinja\Plugin\Wf_Sn_Wl::get_new_name();
					$my_replacements['%%secninlogourl%%'] = esc_url( \WPSecurityNinja\Plugin\Wf_Sn_Wl::get_email_logo_url() );
					$my_replacements['%%sentfromtext%%']  = 'This email was sent by ' . esc_attr( $pluginname ) . ' from ' . esc_url( self::url_to_domain( site_url() ) );
				}
			}
		}

		$template_path = WF_SN_PLUGIN_DIR . 'includes/email-default.php';

		// Use secure file reading with validation
		$html = Wf_Sn_Security_Utils::secure_file_get_contents(
			$template_path,
			array(
				'check_readable'     => true,
				'check_size'         => true,
				'max_size'           => 1024 * 1024, // 1MB for templates
				'check_type'         => true,
				'allowed_extensions' => array( 'php' ),
				'check_mime'         => false, // Skip MIME check for performance
				'check_path'         => true,
				'allowed_dirs'       => array( WF_SN_PLUGIN_DIR ),
			)
		);

		if ( $html === false ) {
			// Handle error - template not found
			$html = '<p>Email template not found.</p>';
		}

		foreach ( $my_replacements as $needle => $replacement ) {
			$html = str_replace( $needle, $replacement, $html );
		}

		$sendresult = wp_mail( $user_email, $subject, $html, $headers );

		// Log the email sending event
		if ( $sendresult ) {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'failed_login_email_sent',
				sprintf(
					/* translators: 1: Username, 2: Email address */
					__( 'Failed login warning email sent to %1$s (%2$s)', 'security-ninja' ),
					esc_html( $username ),
					esc_html( $user_email )
				),
				array(
					'username'   => $username,
					'email'      => $user_email,
					'ip'         => $ip,
					'user_agent' => $user_agent,
				)
			);

			// Set rate limiting transient
			set_transient( $rate_limit_key, time(), 900 );
		} else {
			wf_sn_el_modules::log_event(
				'security_ninja',
				'failed_login_email_failed',
				sprintf(
					/* translators: 1: Username, 2: Email address */
					__( 'Failed to send failed login warning email to %1$s (%2$s)', 'security-ninja' ),
					esc_html( $username ),
					esc_html( $user_email )
				),
				array(
					'username' => $username,
					'email'    => $user_email,
					'ip'       => $ip,
				)
			);
		}

		return $sendresult;
	}


	/**
	 * Checking if visitor is even allowed to see the login form.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function form_init_check() {

		if ( 1 !== self::is_active() ) {
			return;
		}

		self::check_visitor();
		$current_user_ip = self::get_user_ip();
		// Check if country blocking is enabled and restricted to login only
		if ( self::is_active() && isset( self::$options['countryblock_loginonly'] ) && self::$options['countryblock_loginonly'] ) {
			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				$blocked_country_login = self::is_banned_country__premium_only( $current_user_ip );
				if ( $blocked_country_login ) {
					wf_sn_el_modules::log_event( 'security_ninja', 'blocked_ip_country_ban_login', $blocked_country_login . ' is blocked from login.', '' );
					self::update_blocked_count( $current_user_ip );
					self::kill_request( $current_user_ip, 'blocked_ip_country_ban_login', 1 * HOUR_IN_SECONDS, false );
				}
			}
		}

		if ( $reason = self::is_banned_ip( $current_user_ip ) ) {
			self::update_blocked_count( $current_user_ip );
			wf_sn_el_modules::log_event( 'security_ninja', 'login_form_blocked_ip', esc_attr( $current_user_ip ) . ' blocked from accessing the login page. ' . esc_attr( $reason ) );
			wp_clear_auth_cookie();
			self::kill_request( $current_user_ip, 'login_form_blocked_ip', 1 * HOUR_IN_SECONDS, false );
			return false;
		}
	}





	/**
	 * login_filter.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   mixed   $user
	 * @param   mixed   $username
	 * @param   mixed   $password
	 * @return  mixed
	 */
	public static function login_filter( $user, $username, $password ) {

		if ( 1 !== self::is_active() ) {
			return $user;
		}

		$protect_login_form = self::$options['protect_login_form'];
		if ( ! $protect_login_form ) {
			return $user;
		}

		$blockadminlogin = self::$options['blockadminlogin'];
		$current_user_ip = self::get_user_ip();
		if ( $blockadminlogin && 'admin' === strtolower( $username ) ) {

			wf_sn_el_modules::log_event( 'security_ninja', 'blockadminlogin', $current_user_ip . ' Attempt to log in as "admin" blocked.', '' );
			self::update_blocked_count( $current_user_ip );

			self::kill_request( $current_user_ip, 'attempted_admin_login', 1 * HOUR_IN_SECONDS, true );
		}

		if ( $reason = self::is_banned_ip( $current_user_ip ) ) {

			// Gets IP and country array with 'ip' and 'country'
			self::update_blocked_count( $current_user_ip );
			wf_sn_el_modules::log_event( 'security_ninja', 'login_form_blocked_ip', $current_user_ip . ' blocked from logging in. ' . $reason );
			// Kills the request or redirects based on settings
			wp_clear_auth_cookie();

			self::kill_request();
		}

		return $user;
	}




	/**
	 * Update the locally cached banned IP list.
	 *
	 * @param array<string, int> $new_list IP => expiry timestamp.
	 * @return bool False when $new_list is not an array.
	 */
	public static function update_banned_ips( $new_list ) {

		// Check if $new_list is an array
		if ( ! is_array( $new_list ) ) {
			return false;
		}

		update_option( 'wf_sn_banned_ips', $new_list, false );
		self::$banned_ips = $new_list;
	}

	/**
	 * Prune banned ips
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function prune_banned() {
		$update = false;

		$banned_ips = self::get_banned_ips();
		if ( $banned_ips ) {
			foreach ( $banned_ips as $ip => $time ) {
				if ( $time < current_time( 'timestamp' ) ) {
					unset( $banned_ips[ $ip ] );
					$update = true;
				}
			}
		}

		if ( $update ) {
			self::update_banned_ips( $banned_ips );
			if ( class_exists( __NAMESPACE__ . '\wf_sn_el_modules' ) ) {
				wf_sn_el_modules::log_event( 'security_ninja', 'pruned_banned_ips', 'Pruned Firewall local banned IPs.', '' );
			}
		}
	}





	/**
	 * Handle failed login - basic logging for free, advanced protection for premium
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Tuesday, January 14th, 2025 - Consolidated free and premium logic
	 * @access  public static
	 * @param   mixed   $username
	 * @return  void
	 */
	public static function failed_login( $username ) {
		if ( ! is_string( $username ) ) {
			$username = '';
		}
		$current_user_ip = self::get_user_ip();
		$ua_string       = '';
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			$ua_string = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
		}

		// Basic logging for all versions (free and premium)
		$description = sprintf(
			/* translators: 1: IP address, 2: Username */
			__( '%1$s failed login attempt. Username: %2$s.', 'security-ninja' ),
			esc_html( $current_user_ip ),
			esc_html( $username )
		);
		$event_data = array(
			'ip'         => $current_user_ip,
			'username'   => sanitize_key( $username ),
			'user_agent' => $ua_string,
		);
		wf_sn_el_modules::log_event( 'security_ninja', 'wp_login_failed', $description, $event_data );

		// Premium features: brute force protection, visitor logging, etc.
		// Only enforce bans / kills when the firewall is active.
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( 1 !== self::is_active() ) {
				return;
			}

			global $wpdb;

			// To prevent double logging
			$logged = false;

			$my_banned_ips = self::get_banned_ips();

			$date           = date( 'Y-m-d H:i:m', current_time( 'timestamp' ) );
			$query          = $wpdb->prepare(
				'SELECT COUNT(id) FROM ' . $wpdb->prefix . 'wf_sn_el WHERE ip = %s AND action = %s AND timestamp >= DATE_SUB(%s, INTERVAL %s MINUTE)',
				$current_user_ip,
				'wp_login_failed',
				$date,
				self::$options['max_login_attempts_time']
			);
			$login_attempts = intval( $wpdb->get_var( $query ) );

			if ( $login_attempts >= intval( self::$options['max_login_attempts'] ) && ! isset( $my_banned_ips[ $current_user_ip ] ) ) {

				$my_banned_ips[ $current_user_ip ] = current_time( 'timestamp' ) + self::$options['bruteforce_ban_time'] * 60;

				self::update_banned_ips( $my_banned_ips );

				// Translators: 1: User IP address, 2: Number of login attempts, 3: Time in minutes
				$block_details = sprintf(
					__( '%1$s blocked due to multiple failed login attempts. %2$d in %3$d min.', 'security-ninja' ),
					esc_html( $current_user_ip ),
					intval( $login_attempts ),
					intval( self::$options['max_login_attempts_time'] )
				);
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					self::log_visitor__premium_only(
						array(
							'ip'          => $current_user_ip,
							'user_agent'  => $ua_string,
							'banned'      => 1,
							'username'    => sanitize_key( $username ),
							'description' => $block_details,
						)
					);
				}
				// Logging to event module (if enabled)
				wf_sn_el_modules::log_event( 'security_ninja', 'firewall_ip_banned', $current_user_ip . ' blocked due to multiple failed login attempts. ' . $login_attempts, '' );

				wp_clear_auth_cookie();
				update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );
				$logged = true; // We have logged this event

				$submitargs = array(
					'ban_type'   => 'failed_login',
					'ban_reason' => 'multiple_failed_logins',
				);
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					self::network_reportip__premium_only( $current_user_ip, $submitargs );
				}

				self::kill_request( $current_user_ip );
			} else {
				// Increase count of failed logins for IP
				++$login_attempts;
			}

			update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );

			$ban_reason = self::is_banned_ip();

			if ( $ban_reason && ! $logged ) {
				wf_sn_el_modules::log_event( 'security_ninja', 'login_denied_banned_IP', $current_user_ip . ' blocked from logging in.', '' );
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					self::log_visitor__premium_only(
						array(
							'ip'          => esc_attr( $current_user_ip ),
							'user_agent'  => esc_attr( $ua_string ),
							'banned'      => 1,
							'ban_type'    => 'login_denied_banned_ip',
							'ban_reason'  => 'login_denied_banned_ip',
							'description' => esc_attr( $current_user_ip ) . ' blocked from logging in. ' . esc_attr( $ban_reason ),
							'username'    => esc_attr( $username ),
						)
					);
				}
				$logged = true; // We have logged this event
				wp_clear_auth_cookie();
				self::kill_request();
			}

			if ( ! $logged ) {
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					self::log_visitor__premium_only(
						array(
							'ip'          => $current_user_ip,
							'user_agent'  => $ua_string,
							'banned'      => 0,
							'description' => 'Failed login attempt!- ' . $login_attempts,
							'action'      => 'wp_login_failed',
							'username'    => sanitize_key( $username ),
						)
					);
				}
				// Update event data with premium details
				$event_data['previous_attempts']   = $login_attempts;
				$event_data['time_window_minutes'] = intval( self::$options['max_login_attempts_time'] );
				$event_data['max_attempts']        = intval( self::$options['max_login_attempts'] );

				// Send failed login warning email if enabled
				if ( ! empty( $username ) ) {
					self::send_failed_login_warning_email( $username, $current_user_ip, $ua_string );
				}
			}
		}
	}


	/**
	 * Check lost password attempt and log it
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.1.0  Tuesday, January 7th, 2025.
	 * @access  public static
	 * @param   \WP_Error $errors
	 * @return  void
	 */
	public static function check_lost_password_attempt( $errors ) {
		// Get the user login from the form
		$user_login = isset( $_POST['user_login'] ) ? sanitize_text_field( wp_unslash( $_POST['user_login'] ) ) : '';

		if ( empty( $user_login ) ) {
			return;
		}

		// Check if user exists
		$user_data = get_user_by( 'login', $user_login );
		if ( ! $user_data ) {
			$user_data = get_user_by( 'email', $user_login );
		}

		// If user doesn't exist, log it as a failed attempt
		if ( ! $user_data ) {
			// Get the current count before logging this attempt
			$current_user_ip = self::get_user_ip();
			global $wpdb;
			$date              = date( 'Y-m-d H:i:m', current_time( 'timestamp' ) );
			$query             = $wpdb->prepare(
				'SELECT COUNT(id) FROM ' . $wpdb->prefix . 'wf_sn_el WHERE ip = %s AND action = %s AND timestamp >= DATE_SUB(%s, INTERVAL %s MINUTE)',
				$current_user_ip,
				'wp_lost_password_failed',
				$date,
				self::$options['max_login_attempts_time']
			);
			$previous_attempts = intval( $wpdb->get_var( $query ) );

			// Log the failed attempt (this will increment the count)
			self::failed_lost_password( $user_login );

			// Create structured data for the event with the correct count
			$event_data = array(
				'ip'                  => $current_user_ip,
				'username_email'      => $user_login,
				'previous_attempts'   => $previous_attempts + 1, // Add 1 for the attempt we just logged
				'time_window_minutes' => intval( self::$options['max_login_attempts_time'] ),
				'max_attempts'        => intval( self::$options['max_login_attempts'] ),
				'user_agent'          => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '',
			);

			// Simple description for the event log
			$description = sprintf(
				/* translators: 1: IP address, 2: Attempted username/email */
				__( '%1$s lost password error. Username/email: %2$s.', 'security-ninja' ),
				esc_html( $current_user_ip ),
				esc_html( $user_login )
			);

			wf_sn_el_modules::log_event( 'security_ninja', 'lost_password_error', $description, $event_data );
		}
	}


	/**
	 * log failed lost password attempt - uses same settings as login protection
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, January 7th, 2025.
	 * @access  public static
	 * @param   mixed   $user_login
	 * @return  void
	 */
	public static function failed_lost_password( $user_login ) {
		global $wpdb;

		// To prevent double logging
		$logged = false;

		$my_banned_ips = self::get_banned_ips();

		$current_user_ip = self::get_user_ip();
		$ua_string       = '';
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
			$ua_string = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
		}

		// Count lost password attempts for this IP in the specified time window
		// Uses the same settings as login protection
		$date                   = date( 'Y-m-d H:i:m', current_time( 'timestamp' ) );
		$query                  = $wpdb->prepare(
			'SELECT COUNT(id) FROM ' . $wpdb->prefix . 'wf_sn_el WHERE ip = %s AND action = %s AND timestamp >= DATE_SUB(%s, INTERVAL %s MINUTE)',
			$current_user_ip,
			'wp_lost_password_failed',
			$date,
			self::$options['max_login_attempts_time']
		);
		$lost_password_attempts = intval( $wpdb->get_var( $query ) );

		if ( $lost_password_attempts >= intval( self::$options['max_login_attempts'] ) && ! isset( $my_banned_ips[ $current_user_ip ] ) ) {

			$my_banned_ips[ $current_user_ip ] = current_time( 'timestamp' ) + self::$options['bruteforce_ban_time'] * 60;

			self::update_banned_ips( $my_banned_ips );

			// Translators: 1: User IP address, 2: Number of lost password attempts, 3: Time in minutes
			$block_details = sprintf(
				__( '%1$s blocked due to multiple lost password attempts. %2$d in %3$d min.', 'security-ninja' ),
				esc_html( $current_user_ip ),
				intval( $lost_password_attempts ),
				intval( self::$options['max_login_attempts_time'] )
			);
			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				self::log_visitor__premium_only(
					array(
						'ip'          => $current_user_ip,
						'user_agent'  => $ua_string,
						'banned'      => 1,
						'username'    => sanitize_key( $user_login ),
						'description' => $block_details,
					)
				);
			}
			// Logging to event module (if enabled)
			wf_sn_el_modules::log_event( 'security_ninja', 'firewall_ip_banned_lost_password', $current_user_ip . ' blocked due to multiple lost password attempts. ' . $lost_password_attempts, '' );

			wp_clear_auth_cookie();
			update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );
			$logged = true; // We have logged this event
			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				$submitargs = array(
					'ban_type'   => 'failed_lost_password',
					'ban_reason' => 'multiple_lost_password_attempts',
				);
				self::network_reportip__premium_only( $current_user_ip, $submitargs );
			}

			self::kill_request( $current_user_ip );
		} else {
			// Increase count of failed lost password attempts for IP
			++$lost_password_attempts;
		}

		update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );

		$ban_reason = self::is_banned_ip();

		if ( $ban_reason && ! $logged ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'lost_password_denied_banned_IP', $current_user_ip . ' blocked from lost password form.', '' );
			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				self::log_visitor__premium_only(
					array(
						'ip'          => esc_attr( $current_user_ip ),
						'user_agent'  => esc_attr( $ua_string ),
						'banned'      => 1,
						'ban_type'    => 'lost_password_denied_banned_ip',
						'ban_reason'  => 'lost_password_denied_banned_ip',
						'description' => esc_attr( $current_user_ip ) . ' blocked from lost password form. ' . esc_attr( $ban_reason ),
						'username'    => esc_attr( $user_login ),
					)
				);
			}
			$logged = true; // We have logged this event
			wp_clear_auth_cookie();
			self::kill_request();
		}

		if ( ! $logged ) {
			if ( secnin_fs()->can_use_premium_code__premium_only() ) {
				self::log_visitor__premium_only(
					array(
						'ip'          => $current_user_ip,
						'user_agent'  => $ua_string,
						'banned'      => 0,
						'description' => 'Failed lost password attempt!- ' . $lost_password_attempts,
						'action'      => 'wp_lost_password_failed',
						'username'    => sanitize_key( $user_login ),
					)
				);
			}
			// Also log to event module with structured data
			$event_data = array(
				'ip'                  => $current_user_ip,
				'username_email'      => sanitize_key( $user_login ),
				'previous_attempts'   => $lost_password_attempts,
				'time_window_minutes' => intval( self::$options['max_login_attempts_time'] ),
				'max_attempts'        => intval( self::$options['max_login_attempts'] ),
				'user_agent'          => $ua_string,
			);

			$description = sprintf(
				/* translators: 1: IP address, 2: Username/email */
				__( '%1$s failed lost password attempt. Username/email: %2$s.', 'security-ninja' ),
				esc_html( $current_user_ip ),
				esc_html( $user_login )
			);

			wf_sn_el_modules::log_event( 'security_ninja', 'wp_lost_password_failed', $description, $event_data );
		}
	}


	/**
	 * Checks if an IP is in array
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   mixed   $needle
	 * @param   mixed   $haystack
	 * @return  void
	 */
	public static function IP_in_array( $needle, $haystack ) {
		$ip_arr = array();

		// Check if haystack is array and makes sure it is trimmed from apostrophes
		if ( is_array( $haystack ) ) {
			foreach ( $haystack as $key => $item ) {
				$ip_arr[] = trim( $item, "'" );
			}

			if ( in_array( $needle, $ip_arr ) ) {
				return true;
			}
		}

		if ( is_iterable( $haystack ) ) {
			foreach ( $haystack as $key => $item ) {
				if ( $item === $needle ) {
					return true;
				}
			}
		}

		return false;
	}




	/**
	 * Satellite ASN softening metadata for an IP (single resolution per request via static cache).
	 *
	 * @param string $ip Client IP.
	 * @return array{applies: bool, asn: int, as_label: string}
	 */
	public static function get_satellite_soften_info( $ip ) {
		$empty = array(
			'applies'  => false,
			'asn'      => 0,
			'as_label' => '',
		);

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( ! is_string( $ip ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
				return $empty;
			}

			if ( isset( self::$satellite_soften_info_cache[ $ip ] ) ) {
				return self::$satellite_soften_info_cache[ $ip ];
			}

			if ( is_null( self::$options ) ) {
				self::$options = self::get_options();
			}

			if ( empty( self::$options['satellite_soft_enabled'] ) ) {
				self::$satellite_soften_info_cache[ $ip ] = $empty;
				return $empty;
			}

			$allowed_asns = self::$options['satellite_soft_asns'] ?? array( '14593' );
			if ( ! is_array( $allowed_asns ) || empty( $allowed_asns ) ) {
				self::$satellite_soften_info_cache[ $ip ] = $empty;
				return $empty;
			}

			$asn_file = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-ip-asn.php';
			if ( ! file_exists( $asn_file ) ) {
				self::$satellite_soften_info_cache[ $ip ] = $empty;
				return $empty;
			}

			require_once $asn_file;

			if ( ! class_exists( __NAMESPACE__ . '\\SN_Ip_Asn' ) ) {
				self::$satellite_soften_info_cache[ $ip ] = $empty;
				return $empty;
			}

			$data = \WPSecurityNinja\Plugin\SN_Ip_Asn::get_asn_for_ip( $ip );
			if ( null === $data || empty( $data['asn'] ) ) {
				self::$satellite_soften_info_cache[ $ip ] = $empty;
				return $empty;
			}

			$asn_str     = (string) (int) $data['asn'];
			$as_label    = isset( $data['label'] ) && is_string( $data['label'] ) ? $data['label'] : '';
			$match_found = false;
			foreach ( $allowed_asns as $a ) {
				if ( (string) (int) $a === $asn_str ) {
					$match_found = true;
					break;
				}
			}

			$result = array(
				'applies'  => $match_found,
				'asn'      => (int) $data['asn'],
				'as_label' => $as_label,
			);

			self::$satellite_soften_info_cache[ $ip ] = $result;
			return $result;
		}

		return $empty;
	}

	/**
	 * Whether satellite/residential ASN softening applies for reputation/geo-style blocks (not manual blacklist / WAF).
	 *
	 * @param string $ip Client IP.
	 * @return bool
	 */
	public static function should_soften_satellite_network_for_ip( $ip ) {
		$info = self::get_satellite_soften_info( $ip );
		return ! empty( $info['applies'] );
	}

	/**
	 * If satellite ASN softening applies, log once per IP+rule per request and allow the block to be skipped.
	 *
	 * @param string               $ip               Client IP.
	 * @param string               $softened_rule    'cloud' or 'country'.
	 * @param string               $would_block_text Original ban message that was not applied.
	 * @param array<string, mixed> $extra_raw        Extra keys merged into raw_data.
	 * @return bool True if the block was softened (caller should not block).
	 */
	private static function try_satellite_soften_block__premium_only( $ip, $softened_rule, $would_block_text, array $extra_raw = array() ) {
		$info = self::get_satellite_soften_info( $ip );
		if ( empty( $info['applies'] ) ) {
			return false;
		}

		$log_key = md5( $ip . '|' . $softened_rule );
		if ( ! isset( self::$satellite_soften_logged[ $log_key ] ) ) {
			self::$satellite_soften_logged[ $log_key ] = true;
			self::log_satellite_soften_event__premium_only( $ip, $softened_rule, $would_block_text, $info, $extra_raw );
		}

		return true;
	}

	/**
	 * Cloud reputation block reason for an IP (no ASN softening).
	 *
	 * @param string $ip Client IP.
	 * @return string Empty string if not blocked by cloud lists.
	 */
	private static function compute_cloud_block_reason( $ip ) {
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}
		if ( '1' !== (string) ( self::$options['usecloud'] ?? '' ) ) {
			return '';
		}

		// Cloud reputation applies to public routable IPs only.
		if ( Wf_sn_cf_Utils::is_non_public_ip( $ip ) ) {
			return '';
		}

		$ips = self::get_cloud_ips_data();
		if ( ! is_array( $ips ) ) {
			return '';
		}

		$cloud_ips = isset( $ips['ips'] ) && is_array( $ips['ips'] ) ? $ips['ips'] : array();
		if ( self::IP_in_array( $ip, $cloud_ips ) ) {
			return 'IP in cloud blacklist.';
		}

		if ( isset( $ips['subnets'] ) && is_array( $ips['subnets'] ) ) {
			$nework_array = explode( '.', $ip, 2 );
			if ( isset( $nework_array[0] ) && array_key_exists( $nework_array[0], $ips['subnets'] ) ) {
				foreach ( $ips['subnets'][ $nework_array[0] ] as $subnet ) {
					$subnet = trim( $subnet, "'" );
					if ( Wf_sn_cf_Utils::ipCIDRMatch( $ip, $subnet ) ) {
						return 'IP in cloud blacklist range.';
					}
				}
			}
		}

		return '';
	}

	/**
	 * Cloud reputation block reason with satellite ASN softening applied when configured.
	 *
	 * @param string $ip Client IP.
	 * @return string Empty string if not blocked (or softened).
	 */
	public static function get_cloud_block_reason_for_ip__premium_only( $ip ) {
		$reason = self::compute_cloud_block_reason( $ip );
		if ( '' === $reason ) {
			return '';
		}
		if ( self::try_satellite_soften_block__premium_only( $ip, 'cloud', $reason ) ) {
			return '';
		}
		return $reason;
	}

	/**
	 * Log to Events when a country or cloud block is softened for a matching satellite ASN.
	 *
	 * @param string               $ip              Client IP.
	 * @param string               $softened_rule   'cloud' or 'country'.
	 * @param string               $would_block_text Original ban message that was not applied.
	 * @param array                $info            Result from get_satellite_soften_info() with applies true.
	 * @param array<string, mixed> $extra_raw       Extra keys merged into raw_data (e.g. country_code).
	 */
	private static function log_satellite_soften_event__premium_only( $ip, $softened_rule, $would_block_text, array $info, array $extra_raw = array() ) {
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( empty( $info['applies'] ) ) {
				return;
			}
			if ( ! class_exists( __NAMESPACE__ . '\\wf_sn_el_modules' ) ) {
				return;
			}

			$raw_data = array_merge(
				array(
					'softened_rule' => $softened_rule,
					'would_block'   => $would_block_text,
					'asn'           => isset( $info['asn'] ) ? (int) $info['asn'] : 0,
					'as_label'      => isset( $info['as_label'] ) ? sanitize_text_field( $info['as_label'] ) : '',
				),
				$extra_raw
			);

			$desc = sprintf(
				/* translators: 1: ASN number, 2: rule type (cloud or country) */
				__( 'Satellite ASN %1$d: softened %2$s reputation/geo block for this IP.', 'security-ninja' ),
				(int) $info['asn'],
				'country' === $softened_rule ? __( 'country', 'security-ninja' ) : __( 'cloud', 'security-ninja' )
			);

			wf_sn_el_modules::log_event(
				'security_ninja',
				'satellite_asn_block_softened',
				$desc,
				$raw_data,
				null,
				$ip
			);
		}
	}

	/**
	 * Checks a specific IP is banned or not
	 *
	 * @author  Lars Koudal
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Wednesday, June 9th, 2021.
	 * @version v1.0.2  Sunday, June 13th, 2021.
	 * @version v1.0.3  Monday, November 8th, 2021.
	 * @version v1.0.4  Wednesday, February 9th, 2022.
	 * @access  public static
	 * @param   string|false $ip Client IP (required).
	 * @return  string|false Ban reason string or false if not banned.
	 */
	public static function is_banned_ip( $ip = false ) {

		if ( ! $ip ) {
			return false;
		}

		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		// Checks if IP is set or try to get it - always use $current_user_ip from here
		if ( $ip ) {
			$current_user_ip = $ip;
		} else {
			$current_user_ip = self::get_user_ip();
		}

		$server_ip = self::get_server_ip();

		// If server IP same as referring IP, continue
		if ( $server_ip === $current_user_ip ) {
			return false;
		}

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			// Checks if IP is from a whitelisted external service - note, the function checks if settings are enabled before allowing
			if ( self::is_whitelisted_service( $current_user_ip ) ) {
				return false;
			}
		}

		// Checks if the IP is in local whitelist (supports both exact IPs and CIDR ranges)
		$local_whitelist = self::$options['whitelist'];

		// Ensure $local_whitelist is an array
		if ( ! is_array( $local_whitelist ) ) {
			$local_whitelist = array();
		}

		// Use proper CIDR-aware whitelist checking
		if ( Wf_sn_cf_Utils::is_whitelisted( $current_user_ip, $local_whitelist ) ) {
			return false;
		}

		// Kill-switch: no ban checks when firewall is disabled.
		if ( 1 !== self::is_active() ) {
			return false;
		}

		$blacklist = self::$options['blacklist'];
		if ( is_array( $blacklist ) ) {
			foreach ( $blacklist as $bl ) {
				if ( trim( $bl ) === $ip ) {
					return 'IP is in local blacklist.';
				}
				if ( Wf_sn_cf_Utils::ipCIDRMatch( $ip, $bl ) ) {
					return 'IP is in local blacklist mask - ' . $bl;
				}
			}
		}

		$banned_ips = self::get_banned_ips();
		if ( array_key_exists( $current_user_ip, $banned_ips ) ) {
			$expiry = $banned_ips[ $current_user_ip ];
			if ( $expiry > current_time( 'timestamp' ) ) {
				return 'Local blacklist.';
			}
			$updated = $banned_ips;
			unset( $updated[ $current_user_ip ] );
			self::update_banned_ips( $updated );
			self::$banned_ips = $updated;
		}

		$cloud_block_reason = '';
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			$cloud_block_reason = self::get_cloud_block_reason_for_ip__premium_only( $current_user_ip );
		}
		$use_compute_cloud = true;
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			$use_compute_cloud = false;
		}
		if ( $use_compute_cloud ) {
			$cloud_block_reason = self::compute_cloud_block_reason( $current_user_ip );
		}
		if ( '' !== $cloud_block_reason ) {
			return $cloud_block_reason;
		}

		// checks for visitor ban
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( $blocked_country = self::is_banned_country__premium_only( $current_user_ip ) ) {
				return 'Country is blocked ' . $blocked_country;
			}
		}

		// Allow known social/link-preview crawlers through Block IP Network (e.g. Facebook, LinkedIn).
		if ( Wf_sn_cf_Utils::is_social_crawler_ua() ) {
			return false;
		}

		// Checks if included in SecNin Global Block network
		global $wpdb;
		$table_name = $wpdb->prefix . 'wf_sn_cf_bl_ips';
		$answer     = $wpdb->get_var(
			$wpdb->prepare( "SELECT tid FROM `{$table_name}` WHERE ip = %s", $ip )
		);
		if ( $answer ) {
			return 'SecNin Global Block network.';
		}
		return false;
	}


	/**
	 * Update cloud firewall blocked IPs and update server IP to whitelist
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function update_cloud_ips__premium_only() {

		if ( secnin_fs()->can_use_premium_code__premium_only() ) {

			$server_ip = self::get_server_ip();
			// $options          = self::$options;

			$local_whitelist = ( isset( self::$options ) && is_array( self::$options ) && isset( self::$options['whitelist'] ) && is_array( self::$options['whitelist'] ) ) ? self::$options['whitelist'] : array();

			if ( ! Wf_sn_cf_Utils::is_whitelisted( $server_ip, $local_whitelist ) ) {
				self::$options['whitelist'][] = trim( $server_ip );
				update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );
				wf_sn_el_modules::log_event( 'security_ninja', 'unblocked_ip', 'Added server IP to whitelist ' . $server_ip );
			}

			$firehol = 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset';

			$response = wp_remote_get( $firehol );

			if ( ! is_wp_error( $response ) ) {
				$body             = wp_remote_retrieve_body( $response );
				$sn_firewall_data = array(
					'ips'     => array(),
					'subnets' => array(),
				);

				$lines = explode( PHP_EOL, $body );

				foreach ( $lines as $line_num => $line ) {

					if ( strpos( $line, '#' ) !== false ) {
						// Skip comments
						continue;
					} elseif ( strpos( $line, '/' ) !== false ) {

						$nework_array = explode( '.', trim( $line ), 2 );

						if ( ! array_key_exists( $nework_array[0], $sn_firewall_data['subnets'] ) ) {

							$sn_firewall_data['subnets'][ $nework_array[0] ] = array();
						}
						$sn_firewall_data['subnets'][ $nework_array[0] ][] = trim( $line );
					} else {
						$sn_firewall_data['ips'][] = trim( $line );
					}
				}
				$sn_firewall_data['timestamp'] = time();
				update_option( 'wf_sn_cf_ips', $sn_firewall_data, false );
				self::clear_cloud_ips_cache();
			} else {
				wf_sn_el_modules::log_event( 'security_ninja', 'geolocation_download', 'Unable to download GeoIP Database: ' . $response->get_error_message(), '' );
			}
		}
	}







	/**
	 * Register module settings.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function register_settings() {
		register_setting( WF_SN_CF_OPTIONS_KEY, 'wf_sn_cf', array( __NAMESPACE__ . '\\wf_sn_cf', 'sanitize_settings' ) );
	}


	/**
	 * Centralized way to get users IP - @todo - replace med opdateret version
	 *
	 * @author  Lars Koudal
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Tuesday, May 14th, 2024.
	 * @access  public static
	 * @return  boolean
	 * @todo    - replace med opdateret version
	 */
	public static function get_user_ip() {

		// Check if we have already cached the IP
		if ( self::$cached_ip !== null ) {
			return self::$cached_ip;
		}

		$headers = array(
			'HTTP_CF_CONNECTING_IP', // CloudFlare
			'HTTP_X_FORWARDED_FOR', // May contain a comma+space separated list of IP addresses
			'HTTP_X_REAL_IP',
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED',
			'HTTP_X_CLUSTER_CLIENT_IP',
			'HTTP_X_COMING_FROM',
			'HTTP_PROXY_CONNECTION',
			'HTTP_FORWARDED_FOR',
			'HTTP_FORWARDED',
			'HTTP_COMING_FROM',
			'HTTP_VIA',
			'REMOTE_ADDR',
		);

		foreach ( $headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				foreach ( explode( ',', $_SERVER[ $header ] ) as $ip ) {
					$ip = trim( $ip );
					// Check if IP is valid, including private/reserved ranges for local/dev environments
					if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
						// Cache the result
						self::$cached_ip = $ip;
						return $ip;
					}
				}
			}
		}

		// If no valid IP is found, cache and return false
		self::$cached_ip = false;
		return false;
	}






	/**
	 * Function runs if WooCommerce installed and you add the account shortcode [woocommerce_my_account] somewhere on the website.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function process_woocommerce_login_form_start() {
		$protect_login_form = self::$options['protect_login_form'];
		if ( ! $protect_login_form ) {
			return;
		}

		$show_message = apply_filters( 'secnin_show_woocommerce_login_message', true );

		if ( ! $show_message ) {
			return;
		}

		echo '<p class="message">' . esc_html( self::$options['login_msg'] ) . '</p>';
	}



	/**
	 * Process login errors and optionally hide detailed error messages.
	 *
	 * @since   v0.0.1
	 * @version v1.3.0
	 *
	 * @param string|\WP_Error $error The error message or WP_Error object.
	 * @return string The processed error message.
	 */
	public static function process_login_errors( $error ): string {
		$current_user_ip = self::get_user_ip();
		$error_message   = $error instanceof \WP_Error ? $error->get_error_message() : $error;

		// Get attempted username from POST data
		$attempted_username = isset( $_POST['log'] ) ? sanitize_text_field( wp_unslash( $_POST['log'] ) ) : '';

		// Create structured data for the event
		$event_data = array(
			'ip'            => $current_user_ip,
			'username'      => $attempted_username,
			'error_message' => wp_strip_all_tags( $error_message ),
			'user_agent'    => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '',
		);

		// Simple description for the event log
		$description = sprintf(
			/* translators: 1: IP address, 2: Error message */
			__( '%1$s login error. Message: %2$s.', 'security-ninja' ),
			esc_html( $current_user_ip ),
			wp_strip_all_tags( $error_message )
		);

		wf_sn_el_modules::log_event( 'security_ninja', 'login_error', $description, $event_data );

		// If error hiding is enabled, return generic message
		if ( self::$options['hide_login_errors'] ) {
			$login_error_msg = ! empty( self::$options['login_error_msg'] ) ? self::$options['login_error_msg'] : __( 'Something went wrong', 'security-ninja' );

			return sprintf(
				'<strong>%s</strong>: %s',
				esc_html__( 'Error', 'security-ninja' ),
				wp_kses(
					$login_error_msg,
					array(
						'p'  => array(),
						'br' => array(),
					)
				)
			);
		}

		return $error;
	}



	/**
	 * Adds warning message above login form
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.1  Tuesday, March 5th, 2024.
	 * @access  public static
	 * @param   string $msg The existing message content.
	 * @return  string Modified message content.
	 */
	public static function login_message( $msg ) {
		if ( empty( self::$options['protect_login_form'] ) || empty( self::$options['login_msg'] ) ) {
			return $msg;
		}

		$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
		if ( ! in_array( $action, array( 'register', 'lostpassword' ), true ) ) {
			$custom_msg = '<p class="message">' . esc_html( self::$options['login_msg'] ) . '</p>';
			$msg        = $custom_msg . $msg;
		}

		return $msg;
	}



	/**
	 * isValidCIDR.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, August 27th, 2024.
	 * @access  private static
	 * @param   mixed   $cidr
	 * @return  boolean
	 */
	private static function isValidCIDR( $cidr ) {
		$parts = explode( '/', $cidr );
		if ( count( $parts ) !== 2 ) {
			return false;
		}

		$subnet = $parts[0];
		$mask   = (int) $parts[1];

		if ( filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			return $mask >= 0 && $mask <= 32;
		} elseif ( filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
			return $mask >= 0 && $mask <= 128;
		}

		return false;
	}


	/**
	 * sanitize settings on save
	 *
	 * @author  Lars Koudal
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @version v1.0.1  Friday, May 31st, 2024.
	 * @access  public static
	 * @param   mixed   $values
	 * @return  mixed
	 */
	public static function sanitize_settings( $values ) {
		// Handle null or non-array values (WordPress may call this with null)
		if ( ! is_array( $values ) ) {
			// Return existing options normalized
			$old_options = get_option( 'wf_sn_cf', array() );
			if ( ! is_array( $old_options ) ) {
				$old_options = array();
			}
			// Normalize existing boolean values
			$boolean_keys = array(
				'active',
				'globalbannetwork',
				'global',
				'filterqueries',
				'trackvisits',
				'usecloud',
				'protect_login_form',
				'hide_login_errors',
				'failed_login_email_warning',
				'countryblock_loginonly',
				'whitelist_managewp',
				'whitelist_uptimerobot',
				'whitelist_wprocket',
				'whitelist_uptimia',
				'2fa_enabled',
				'2fa_backup_codes_enabled',
				'404guard_enabled',
				'woo_rate_limiting_enabled',
				'woo_coupon_protection_enabled',
				'change_login_url',
				'blockadminlogin',
			);
			foreach ( $boolean_keys as $key ) {
				if ( isset( $old_options[ $key ] ) ) {
					$old_options[ $key ] = \WPSecurityNinja\Plugin\Utils::normalize_flag( $old_options[ $key ] );
				}
			}
			return $old_options;
		}

		// Get existing options first (like Events Logger does)
		$old_options = get_option( 'wf_sn_cf', array() );
		if ( ! is_array( $old_options ) ) {
			$old_options = array();
		}

		// Start with existing options, then merge with form values
		$new_options = $old_options;

		$defaults = array(
			'active'                        => 0,
			'globalbannetwork'              => 1,
			'global'                        => 1,
			'filterqueries'                 => 1,
			'trackvisits'                   => 1,
			'trackvisits_howlong'           => 7,
			'usecloud'                      => 1,
			'protect_login_form'            => 1,
			'hide_login_errors'             => 1,
			'failed_login_email_warning'    => 0,
			'blocked_countries'             => array(),
			'countryblock_loginonly'        => 0,
			'blacklist'                     => array(),
			'whitelist'                     => array( self::get_user_ip() ),
			'blacklist_notes'               => array(),
			'whitelist_notes'               => array(),
			'whitelist_managewp'            => 1,
			'whitelist_wprocket'            => 0,
			'whitelist_uptimia'             => 0,
			'whitelist_uptimerobot'         => 0,
			'max_login_attempts'            => 5,
			'max_login_attempts_time'       => 5,
			'bruteforce_ban_time'           => 120,
			'login_msg'                     => __( 'Warning: Multiple failed login attempts will get you locked out temporarily.', 'security-ninja' ),
			'login_error_msg'               => __( 'Something went wrong', 'security-ninja' ),
			'message'                       => __( 'You are not allowed to visit this website.', 'security-ninja' ),
			'redirect_url'                  => '',
			'blockadminlogin'               => 0,
			'change_login_url'              => 0,
			'new_login_url'                 => 'my-login',
			'unblock_url'                   => '',
			'2fa_required_roles'            => array(),
			'2fa_methods'                   => array(),
			'2fa_enabled'                   => 0,
			'2fa_enabled_timestamp'         => '',
			'2fa_backup_codes_enabled'      => 0,
			'2fa_grace_period'              => 14,
			'2fa_intro'                     => '',
			'2fa_enter_code'                => '',
			'404guard_enabled'              => 1,
			'404guard_threshold'            => 10,
			'404guard_window'               => 300,
			'404guard_block_time'           => 600,
			'woo_rate_limiting_enabled'     => 0,
			'woo_checkout_rate_limit'       => 3,
			'woo_checkout_window'           => 300,
			'woo_add_to_cart_limit'         => 10,
			'woo_add_to_cart_window'        => 60,
			'woo_order_rate_limit'          => 2,
			'woo_order_window'              => 600,
			'woo_coupon_protection_enabled' => 0,
			'woo_coupon_failed_attempts'    => 5,
			'woo_coupon_window'             => 300,
			'woo_coupon_ban_time'           => 900,
			'satellite_soft_enabled'        => 1,
			'satellite_soft_asns'           => array( '14593' ),
		);

		$current_options = self::get_options();
		$old_2fa_status  = $current_options['2fa_enabled'];

		// List of all boolean/flag settings that should be normalized to 0/1
		$boolean_keys = array(
			'active',
			'globalbannetwork',
			'global',
			'filterqueries',
			'trackvisits',
			'usecloud',
			'protect_login_form',
			'hide_login_errors',
			'failed_login_email_warning',
			'countryblock_loginonly',
			'satellite_soft_enabled',
			'whitelist_managewp',
			'whitelist_uptimerobot',
			'whitelist_wprocket',
			'whitelist_uptimia',
			'whitelist_uptimerobot',
			'2fa_enabled',
			'2fa_backup_codes_enabled',
			'404guard_enabled',
			'woo_rate_limiting_enabled',
			'woo_coupon_protection_enabled',
			'change_login_url',
			'blockadminlogin',
		);

		// Handle boolean keys
		// Fields that are always in the form (free features): filterqueries
		// If these are missing from form submission, they're unchecked = 0
		// Premium-only fields might not be in form for free users, so preserve existing
		$always_in_form_keys = array( 'filterqueries' ); // Free features / always-in-form toggles (failed_login_email_warning moved to Login tab)

		foreach ( $boolean_keys as $key ) {
			if ( isset( $values[ $key ] ) ) {
				// Value is in form submission - normalize it
				$new_options[ $key ] = \WPSecurityNinja\Plugin\Utils::normalize_flag( $values[ $key ] );
			} elseif ( 'active' === $key ) {
				// 'active' is a hidden field - preserve existing value if not in form
				$new_options[ $key ] = isset( $old_options[ $key ] ) ? \WPSecurityNinja\Plugin\Utils::normalize_flag( $old_options[ $key ] ) : ( isset( $defaults[ $key ] ) ? $defaults[ $key ] : 0 );
			} elseif ( in_array( $key, $always_in_form_keys, true ) ) {
				// Field is always in form - missing = unchecked = 0
				$new_options[ $key ] = 0;
			} else {
				// Premium-only field or field that might not be in form - preserve existing
				$new_options[ $key ] = isset( $old_options[ $key ] ) ? \WPSecurityNinja\Plugin\Utils::normalize_flag( $old_options[ $key ] ) : ( isset( $defaults[ $key ] ) ? $defaults[ $key ] : 0 );
			}
		}

		// Process all other form values (non-boolean)
		foreach ( $values as $key => $value ) {
			// Skip boolean keys as they're already handled above
			if ( in_array( $key, $boolean_keys, true ) ) {
				continue;
			}

			if ( array_key_exists( $key, $defaults ) ) {
				switch ( $key ) {
					case '2fa_required_roles':
					case '2fa_methods':
					case 'blocked_countries':
						if ( is_array( $value ) ) {
							$new_options[ $key ] = array_map( 'sanitize_text_field', $value );
						} else {
							$new_options[ $key ] = sanitize_text_field( $value );
						}
						break;
					case 'blacklist':
					case 'whitelist':
						if ( ! is_array( $value ) && is_string( $value ) ) {
							// Split the string into an array by line breaks
							$ips = explode( "\n", $value );
							// Trim whitespace, sanitize each IP address or CIDR, and ensure uniqueness
							$new_options[ $key ] = array_unique(
								array_filter(
									array_map(
										function ( $ip ) {
											$sanitized_ip = sanitize_text_field( trim( $ip ) );
											if (
											filter_var( $sanitized_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ||
											filter_var( $sanitized_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 )
											) {
												return $sanitized_ip;
											}
											if ( strpos( $sanitized_ip, '/' ) !== false && self::isValidCIDR( $sanitized_ip ) ) {
												return $sanitized_ip;
											}

											// Return null if invalid to filter it out
											return null;
										},
										$ips
									),
									function ( $ip ) {
										return ! is_null( $ip );
									}
								)
							);
						} elseif ( is_array( $value ) ) {
							// If it's already an array, sanitize and validate each entry
							$new_options[ $key ] = array_unique(
								array_filter(
									array_map(
										function ( $ip ) {
											$sanitized_ip = sanitize_text_field( trim( $ip ) );

											// Validate IP or CIDR using your existing function
											if (
											filter_var( $sanitized_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ||
											filter_var( $sanitized_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ||
											( strpos( $sanitized_ip, '/' ) !== false && self::isValidCIDR( $sanitized_ip ) )
											) {
												return $sanitized_ip;
											}
											return null;
										},
										$value
									),
									function ( $ip ) {
										return ! is_null( $ip );
									}
								)
							);
						} else {
							// If the value is neither a string nor an array, set an empty array
							$new_options[ $key ] = array();
						}
						break;
					case '2fa_grace_period':
					case 'max_login_attempts':
					case 'max_login_attempts_time':
					case 'bruteforce_ban_time':
					case 'trackvisits_howlong': // number of days to track visits
						$new_options[ $key ] = intval( $value );
						break;
					case '404guard_threshold':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 5, maximum 50
						if ( $new_options[ $key ] < 5 ) {
							$new_options[ $key ] = 5;
						} elseif ( $new_options[ $key ] > 50 ) {
							$new_options[ $key ] = 50;
						}
						break;
					case '404guard_window':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 60 seconds (1 minute), maximum 3600 seconds (1 hour)
						if ( $new_options[ $key ] < 60 ) {
							$new_options[ $key ] = 60;
						} elseif ( $new_options[ $key ] > 3600 ) {
							$new_options[ $key ] = 3600;
						}
						break;
					case '404guard_block_time':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 300 seconds (5 minutes), maximum 86400 seconds (24 hours)
						if ( $new_options[ $key ] < 300 ) {
							$new_options[ $key ] = 300;
						} elseif ( $new_options[ $key ] > 86400 ) {
							$new_options[ $key ] = 86400;
						}
						break;
					// WooCommerce protection options
					case 'woo_rate_limiting_enabled':
					case 'woo_coupon_protection_enabled':
						// These are boolean keys, already handled above - skip
						break;
					case 'woo_checkout_rate_limit':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 2, maximum 10
						if ( $new_options[ $key ] < 2 ) {
							$new_options[ $key ] = 2;
						} elseif ( $new_options[ $key ] > 10 ) {
							$new_options[ $key ] = 10;
						}
						break;
					case 'woo_checkout_window':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 180 seconds (3 minutes), maximum 1800 seconds (30 minutes)
						if ( $new_options[ $key ] < 180 ) {
							$new_options[ $key ] = 180;
						} elseif ( $new_options[ $key ] > 1800 ) {
							$new_options[ $key ] = 1800;
						}
						break;
					case 'woo_add_to_cart_limit':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 5, maximum 20
						if ( $new_options[ $key ] < 5 ) {
							$new_options[ $key ] = 5;
						} elseif ( $new_options[ $key ] > 20 ) {
							$new_options[ $key ] = 20;
						}
						break;
					case 'woo_add_to_cart_window':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 30 seconds, maximum 300 seconds (5 minutes)
						if ( $new_options[ $key ] < 30 ) {
							$new_options[ $key ] = 30;
						} elseif ( $new_options[ $key ] > 300 ) {
							$new_options[ $key ] = 300;
						}
						break;
					case 'woo_order_rate_limit':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 1, maximum 5
						if ( $new_options[ $key ] < 1 ) {
							$new_options[ $key ] = 1;
						} elseif ( $new_options[ $key ] > 5 ) {
							$new_options[ $key ] = 5;
						}
						break;
					case 'woo_order_window':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 300 seconds (5 minutes), maximum 3600 seconds (1 hour)
						if ( $new_options[ $key ] < 300 ) {
							$new_options[ $key ] = 300;
						} elseif ( $new_options[ $key ] > 3600 ) {
							$new_options[ $key ] = 3600;
						}
						break;
					// Coupon protection options
					case 'woo_coupon_failed_attempts':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 3, maximum 15
						if ( $new_options[ $key ] < 3 ) {
							$new_options[ $key ] = 3;
						} elseif ( $new_options[ $key ] > 15 ) {
							$new_options[ $key ] = 15;
						}
						break;
					case 'woo_coupon_window':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 180 seconds (3 minutes), maximum 1800 seconds (30 minutes)
						if ( $new_options[ $key ] < 180 ) {
							$new_options[ $key ] = 180;
						} elseif ( $new_options[ $key ] > 1800 ) {
							$new_options[ $key ] = 1800;
						}
						break;
					case 'woo_coupon_ban_time':
						$new_options[ $key ] = intval( $value );
						// Sanity check: minimum 300 seconds (5 minutes), maximum 3600 seconds (1 hour)
						if ( $new_options[ $key ] < 300 ) {
							$new_options[ $key ] = 300;
						} elseif ( $new_options[ $key ] > 3600 ) {
							$new_options[ $key ] = 3600;
						}
						break;
					case '2fa_intro':
					case '2fa_enter_code':
					case '2fa_enabled_timestamp':
					case 'login_msg':
					case 'login_error_msg':
					case 'message':
					case 'new_login_url':
						$new_options[ $key ] = sanitize_text_field( $value );
						break;
					case 'redirect_url':
						$new_options[ $key ] = esc_url_raw( $value );
						break;
					case 'satellite_soft_asns':
						$raw   = is_string( $value ) ? $value : '';
						$parts = preg_split( '/[\s,]+/', $raw, -1, PREG_SPLIT_NO_EMPTY );
						$out   = array();
						foreach ( $parts as $p ) {
							$p = trim( $p );
							if ( preg_match( '/^\d+$/', $p ) ) {
								$out[] = $p;
							}
						}
						if ( empty( $out ) ) {
							$out = array( '14593' );
						}
						$new_options['satellite_soft_asns'] = array_unique( $out );
						break;
					default:
						$new_options[ $key ] = sanitize_text_field( $value );
						break;
				}
			}
			// Keys not in defaults are ignored (not added to new_options).
		}

		// Premium GEO list: an empty HTML multi-select submits no blocked_countries keys. Hidden blocked_countries_present (settings.php) marks intentional saves from that screen.
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( ! empty( $values['blocked_countries_present'] ) ) {
				if ( empty( $values['blocked_countries'] ) || ! is_array( $values['blocked_countries'] ) ) {
					$new_options['blocked_countries'] = array();
				}
			}
		}

		// // Check for user IP whitelisting if the firewall is active
		// $user_ip = self::get_user_ip();
		// if (isset($new_options['active']) && $new_options['active'] && !in_array($user_ip, $new_options['whitelist'], true)) {
		//  $new_options['whitelist'][] = $user_ip;
		// }

		// Handle 404 Guard status changes
		$old_404guard_status = (int) ( $current_options['404guard_enabled'] ?? 0 );
		$new_404guard_status = (int) ( $new_options['404guard_enabled'] ?? 0 );

		// If 404 Guard was just enabled, we need to load it dynamically (premium only)
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			if ( $new_404guard_status === 1 && $old_404guard_status === 0 ) {
				if ( ! class_exists( __NAMESPACE__ . '\SN_404_Guard' ) ) {
					require_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-404-guard.php';
				}
				SN_404_Guard::init();
			}
		}

		$current_twofa_status = (int) ( $current_options['2fa_enabled'] ?? 0 );
		// If the 2fa_enabled is set to 1 and it used to be 0, set the timestamp '2fa_enabled_timestamp' to the current time
		$old_2fa_status = $current_twofa_status;

		// Check if 2FA was just enabled
		if ( isset( $new_options['2fa_enabled'] ) && (int) $new_options['2fa_enabled'] === 1 && $old_2fa_status === 0 ) {
			$new_options['2fa_enabled_timestamp'] = current_time( 'timestamp' );
			self::clear_2fa_user_meta_on_first_enable();
		}

		// Ensure a non-empty login URL if the change login URL feature is active
		if ( class_exists( __NAMESPACE__ . '\\SecNin_Rename_WP_Login' ) && isset( $new_options['change_login_url'] ) && $new_options['change_login_url'] && ( '' === ( $new_options['new_login_url'] ?? '' ) ) ) {
			$new_options['new_login_url'] = \WPSecurityNinja\Plugin\SecNin_Rename_WP_Login::$default_login_url;
		}

		// Merge sanitized values with defaults to ensure all settings are complete and valid
		// Use $new_options which already has existing values preserved for missing keys
		$merged = array_merge( $defaults, $new_options );

		$two_fa_on                    = ! empty( $merged['2fa_enabled'] );
		$merged['2fa_required_roles'] = self::normalize_2fa_required_roles( $merged['2fa_required_roles'] ?? array(), $two_fa_on );
		$merged['2fa_methods']        = self::normalize_2fa_methods( $merged['2fa_methods'] ?? array() );

		return $merged;
	}









	/**
	 * add new tab
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @param   mixed   $tabs
	 * @return  mixed
	 */
	public static function sn_tabs( $tabs ) {
		$core_tab   = array(
			'id'       => 'sn_cf',
			'class'    => '',
			'label'    => 'Firewall',
			'callback' => array( __NAMESPACE__ . '\\wf_sn_cf', 'do_page' ),
		);
		$done       = 0;
		$tabs_count = count( $tabs );

		for ( $i = 0; $i < $tabs_count; $i++ ) {
			if ( 'sn_cf' === $tabs[ $i ]['id'] ) {
				$tabs[ $i ] = $core_tab;
				$done       = 1;
				break;
			}
		}

		if ( ! $done ) {
			$tabs[] = $core_tab;
		}

		$login_tab_class = 'profeature';
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			$login_tab_class = '';
		}
		$login_tab = array(
			'id'       => 'sn_login',
			'class'    => $login_tab_class,
			'label'    => esc_html__( 'Login', 'security-ninja' ),
			'callback' => array( __NAMESPACE__ . '\\wf_sn_cf', 'do_login_page' ),
		);

		$inserted = false;
		foreach ( $tabs as $idx => $tab ) {
			if ( isset( $tab['id'] ) && $tab['id'] === 'sn_cf' ) {
				array_splice( $tabs, $idx + 1, 0, array( $login_tab ) );
				$inserted = true;
				break;
			}
		}
		if ( ! $inserted ) {
			$tabs[] = $login_tab;
		}

		return $tabs;
	}


	/**
	 * Renders the Login top-level tab (login protection settings or upgrade notice).
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, March 10th, 2025.
	 * @access  public static
	 * @return  void
	 */
	public static function do_login_page() {
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		if ( get_transient( 'secnin_2fa_optin_only_notice' ) ) {
			delete_transient( 'secnin_2fa_optin_only_notice' );
			echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'Two-factor authentication is enabled but no roles are required. Users can opt in from their profile; nobody will be forced to set up 2FA.', 'security-ninja' ) . '</p></div>';
		}
		if ( get_transient( 'secnin_2fa_methods_empty_notice' ) ) {
			delete_transient( 'secnin_2fa_methods_empty_notice' );
			echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'At least one 2FA method must be selected. Your previous method settings were kept.', 'security-ninja' ) . '</p></div>';
		}
		?>
		<div class="sncard settings-card">
			<h2><span class="dashicons dashicons-lock"></span> <?php esc_html_e( 'Login', 'security-ninja' ); ?></h2>
			<p><?php esc_html_e( 'Protect your login page from brute force attacks, enable 2FA, and customize your login URL.', 'security-ninja' ); ?></p>
		<?php
		if ( secnin_fs()->can_use_premium_code__premium_only() ) {
			?>
			<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
				<?php wp_nonce_field( 'sn_save_login_settings', 'sn_login_nonce' ); ?>
				<input type="hidden" name="action" value="sn_save_login_settings">
				<?php
				$login_protection_file = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/tabs/login-protection.php';
				if ( file_exists( $login_protection_file ) ) {
					require_once $login_protection_file;
					wf_sn_cf_render_login_protection_content( self::$options );
				}
				?>
				<p class="submit">
					<input type="submit" value="<?php echo esc_attr__( 'Save Changes', 'security-ninja' ); ?>" class="input-button button-primary" name="Submit" />
				</p>
			</form>
			<?php
		} else {
			?>
			<div class="sncard infobox">
				<div class="inner">
					<h3>Upgrade to Pro for Login Protection</h3>
					<p>Upgrade to Security Ninja Pro to unlock powerful login protection features:</p>
					<ul style="list-style: disc; margin-left: 20px; margin-top: 10px;">
						<li>Advanced login form protection with brute force detection</li>
						<li>Auto-ban rules for failed login attempts</li>
						<li>Block "admin" username login attempts</li>
						<li>Hide login errors to prevent username enumeration</li>
						<li>Email warnings for failed login attempts</li>
						<li>Change login URL to hide wp-login.php</li>
						<li>Two-Factor Authentication (2FA) for enhanced security</li>
					</ul>
					<p style="margin-top: 15px;">
						<a href="<?php echo esc_url( \WPSecurityNinja\Plugin\Utils::generate_sn_web_link( 'upgrade_tab_login', '/upgrade/' ) ); ?>" class="button button-primary button-small" target="_blank" rel="noopener">Upgrade to Pro</a>
					</p>
				</div>
			</div>
			<?php
		}
		echo '</div>';
	}


	/**
	 * Handles saving of login protection settings (admin_post handler).
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, March 10th, 2025.
	 * @access  public static
	 * @return  void
	 */
	public static function save_login_settings() {
		if ( ! isset( $_POST['sn_login_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['sn_login_nonce'] ) ), 'sn_save_login_settings' ) ) {
			wp_die( 'Security check failed.' );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( 'You do not have sufficient permissions.' );
		}

		$values   = isset( $_POST['wf_sn_cf'] ) && is_array( $_POST['wf_sn_cf'] ) ? wp_unslash( $_POST['wf_sn_cf'] ) : array();
		$existing = get_option( 'wf_sn_cf', array() );
		if ( ! is_array( $existing ) ) {
			$existing = array();
		}

		$boolean_keys = array(
			'protect_login_form',
			'hide_login_errors',
			'failed_login_email_warning',
			'change_login_url',
			'blockadminlogin',
			'2fa_enabled',
			'2fa_backup_codes_enabled',
		);
		foreach ( $boolean_keys as $key ) {
			$existing[ $key ] = isset( $values[ $key ] ) ? \WPSecurityNinja\Plugin\Utils::normalize_flag( $values[ $key ] ) : 0;
		}

		$int_keys = array(
			'max_login_attempts'      => array( 1, 100 ),
			'max_login_attempts_time' => array( 1, 60 ),
			'bruteforce_ban_time'     => array( 2, 5256000 ),
			'2fa_grace_period'        => array( 0, 90 ),
		);
		foreach ( $int_keys as $key => $bounds ) {
			if ( isset( $values[ $key ] ) ) {
				$v                = intval( $values[ $key ] );
				$existing[ $key ] = max( $bounds[0], min( $bounds[1], $v ) );
			}
		}

		$text_keys = array( 'login_msg', 'login_error_msg', 'new_login_url', '2fa_intro', '2fa_enter_code' );
		foreach ( $text_keys as $key ) {
			if ( array_key_exists( $key, $values ) ) {
				$existing[ $key ] = sanitize_text_field( $values[ $key ] );
			}
		}

		if ( ! empty( $values['2fa_required_roles_present'] ) ) {
			if ( isset( $values['2fa_required_roles'] ) && is_array( $values['2fa_required_roles'] ) ) {
				$existing['2fa_required_roles'] = array_map( 'sanitize_text_field', $values['2fa_required_roles'] );
			} else {
				$existing['2fa_required_roles'] = array();
			}
		}
		if ( ! empty( $values['2fa_methods_present'] ) ) {
			$methods = self::sanitize_2fa_methods_list( $values['2fa_methods'] ?? array() );
			if ( empty( $methods ) ) {
				set_transient( 'secnin_2fa_methods_empty_notice', 1, MINUTE_IN_SECONDS );
			} else {
				$existing['2fa_methods'] = $methods;
			}
		} elseif ( isset( $values['2fa_methods'] ) ) {
			$existing['2fa_methods'] = self::normalize_2fa_methods( $values['2fa_methods'] );
		}

		$current_options = self::get_options();
		$old_2fa         = (int) ( isset( $current_options['2fa_enabled'] ) ? $current_options['2fa_enabled'] : 0 );
		if ( isset( $existing['2fa_enabled'] ) && (int) $existing['2fa_enabled'] === 1 && $old_2fa === 0 ) {
			$existing['2fa_enabled_timestamp'] = (string) current_time( 'timestamp' );
			self::clear_2fa_user_meta_on_first_enable();
		}

		if ( ! empty( $existing['change_login_url'] ) && ( '' === ( $existing['new_login_url'] ?? '' ) ) ) {
			$existing['new_login_url'] = class_exists( __NAMESPACE__ . '\\SecNin_Rename_WP_Login' ) ? \WPSecurityNinja\Plugin\SecNin_Rename_WP_Login::$default_login_url : 'my-login';
		}
		if ( isset( $existing['new_login_url'] ) && $existing['new_login_url'] !== '' ) {
			$existing['new_login_url'] = preg_replace( '/[^a-zA-Z0-9_-]/', '', $existing['new_login_url'] );
			if ( $existing['new_login_url'] === '' ) {
				$existing['new_login_url'] = 'my-login';
			}
		}

		if ( isset( $current_options['unblock_url'] ) && $current_options['unblock_url'] !== '' ) {
			$existing['unblock_url'] = $current_options['unblock_url'];
		}

		$two_fa_on                      = ! empty( $existing['2fa_enabled'] );
		$existing['2fa_required_roles'] = self::normalize_2fa_required_roles( $existing['2fa_required_roles'] ?? array(), $two_fa_on );
		$existing['2fa_methods']        = self::normalize_2fa_methods( $existing['2fa_methods'] ?? array() );

		if ( $two_fa_on && empty( $existing['2fa_required_roles'] ) ) {
			set_transient( 'secnin_2fa_optin_only_notice', 1, MINUTE_IN_SECONDS );
		}

		update_option( 'wf_sn_cf', $existing, false );
		do_action( 'security_ninja_firewall_settings_saved' );
		self::$options = null;

		wp_safe_redirect( admin_url( 'admin.php?page=wf-sn&settings-updated=1#sn_login' ) );
		exit;
	}


	/**
	 * add custom message to overlay
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function overlay_content() {
		echo '<div id="sn-cloud-firewall" style="display: none; text-align:center;">';
		echo '<h2 style="font-weight: bold;">' . __( 'Important! Please READ!', 'security-ninja' ) . '</h2>';
		echo '<p>' . __( 'In the unlikely situation that your IP gets banned, you will not be able to login or access the site. In that case you need the secret access link.', 'security-ninja' ) . '</p>';

		echo '<p>' . __( 'It whitelists your IP and enables access. Please store the link in a safe place or use the form below to get it sent to your email address.', 'security-ninja' ) . '</p>';

		echo '<p><code>' . self::get_unblock_url() . '</code></p>';
		echo '<div id="sn-firewall-status">' . __( 'Enabling firewall, please wait', 'security-ninja' ) . '</p><p class="spinner is-active"></div>';
		echo '<p>' . __( 'Enter your email below to receive the secret access link in case you get locked out', 'security-ninja' ) . '</p>';
		echo '<input style="width: 250px;" type="text" id="sn-ublock-email" name="sn-ublock-email" value="' . get_option( 'admin_email' ) . '" placeholder="john@example.com"><br />
																									<p id="sn-unblock-message"></p>';

		?>
		<input type="button" value="<?php esc_html_e( 'Send secret access link', 'security-ninja' ); ?>" id="sn-send-unlock-code" class="input-button button button-secondary" />
		<?php
		echo '<p><br><input type="button" value="Close (3)" id="sn-close-firewall" class="input-button button-primary" /></p>';

		echo '</div>';
	} // overlay_content

	/**
	 * Checks if the firewall module is active
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  int 1 when the firewall is active, 0 otherwise.
	 */
	public static function is_active() {
		if ( is_null( self::$options ) ) {
			self::get_options();
		}
		return (int) ( 1 === (int) self::$options['active'] );
	}

	/**
	 * Check if 404 Guard is available and enabled
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Friday, January 1st, 2021.
	 * @return  boolean
	 */
	public static function is_404guard_available() {
		if ( ! class_exists( __NAMESPACE__ . '\SN_404_Guard' ) ) {
			return false;
		}
		return SN_404_Guard::is_loaded_and_enabled();
	}







	/**
	 * Returns list of blocked country codes for use with GEOIP.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  mixed
	 */
	public static function get_blocked_countries() {
		$blocked_countries = self::$options['blocked_countries'];
		if ( ! $blocked_countries ) {
			return array();
		}
		if ( is_array( $blocked_countries ) ) {
			$bclist = array();
			foreach ( $blocked_countries as $key => $ba ) {
				$bclist[] = $ba;
			}
			return $bclist;
		}
		return array();
	}



	/**
	 * Adds an IP address to the whitelist if it's not already present
	 *
	 * @author  Lars Koudal
	 * @since   v1.0.0
	 * @version v1.0.0  Monday, January 13th, 2025
	 * @access  public static
	 * @param   string $ip The IP address to add
	 * @return  boolean True if IP was added, false if already exists or invalid
	 */
	public static function add_ip_to_whitelist( $ip ) {
		if ( ! $ip || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return false;
		}

		// Load options if not already loaded
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		// Ensure whitelist is an array
		if ( ! is_array( self::$options['whitelist'] ) ) {
			self::$options['whitelist'] = array();
		}

		// Check if IP is already in whitelist (exact match or covered by a CIDR entry).
		if ( Wf_sn_cf_Utils::is_whitelisted( $ip, self::$options['whitelist'] ) ) {
			return false; // Already whitelisted
		}

		// Add IP to whitelist
		self::$options['whitelist'][] = $ip;
		update_option( WF_SN_CF_OPTIONS_KEY, self::$options, false );

		// Log the event
		if ( class_exists( __NAMESPACE__ . '\\wf_sn_el_modules' ) ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'unblocked_ip', sprintf( __( 'IP %s automatically added to whitelist.', 'security-ninja' ), $ip ), '' );
		}

		return true;
	}



	/**
	 * get_unblock_url.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  mixed
	 */
	public static function get_unblock_url() {
		$my_options  = self::get_options();
		$unblock_url = '';
		if ( isset( $my_options['unblock_url'] ) && ! empty( $my_options['unblock_url'] ) ) {
			$unblock_url = $my_options['unblock_url'];
		}

		// check if already set
		if ( ! $unblock_url ) {
			$my_options['unblock_url'] = md5( time() );
			update_option( WF_SN_CF_OPTIONS_KEY, $my_options, false );
		}

		$outurl = add_query_arg(
			array(
				'snf' => $my_options['unblock_url'],
			),
			get_site_url()
		);

		return $outurl;
	}





	/**
	 * Return bad IPs from the central API
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, February 11th, 2021.
	 * @access  private static
	 * @return  boolean
	 */
	private static function get_network_listips__premium_only() {
		// Check if the feature is enabled
		if ( ! self::$options['globalbannetwork'] ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'get_network_listips', 'Global network feature is disabled' );
			return false;
		}

		$license_id            = secnin_fs()->_get_license()->id;
		$install_id            = secnin_fs()->get_site()->id;
		$site_private_key      = secnin_fs()->get_site()->secret_key;
		$nonce                 = date( 'Y-m-d' );
		$pk_hash               = hash( 'sha512', $site_private_key . '|' . $nonce );
		$authentication_string = base64_encode( $pk_hash . '|' . $nonce );

		$url = self::$central_api_url . 'listips/';

		$remote_addr = isset( $_SERVER['REMOTE_ADDR'] )
			? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) )
			: '';

		$response = wp_remote_get( // Cannot use wp_safe_remote_get because we need to set the header
			$url,
			array(
				'headers'   => array(
					'Authorization' => $authentication_string,
				),
				'body'      => array(
					'install_id' => $install_id,
					'license_id' => $license_id,
				),
				'blocking'  => true,
				'timeout'   => 15,
				'sslverify' => ! in_array( $remote_addr, array( '127.0.0.1', '::1' ), true ), // Only disable SSL verify on localhost
			)
		);

		if ( is_wp_error( $response ) ) {
			$error_message = $response->get_error_message();
			wf_sn_el_modules::log_event( 'security_ninja', 'update_blocked_ips', 'Error getting IPs from network: "' . esc_html( $error_message ) . '"' );
			return false;
		} else {
			$body    = wp_remote_retrieve_body( $response );
			$decoded = json_decode( $body );
			$newips  = 0;
			if ( is_object( $decoded ) && isset( $decoded->ips ) ) {
				$newips = count( $decoded->ips );
				wf_sn_el_modules::log_event( 'security_ninja', 'update_blocked_ips', sprintf( esc_html__( 'Added/updated %1$s IPs from the blocklist.', 'security-ninja' ), $newips ), '' );
			}

			return $body;
		}

		return false;
	}





	/**
	 * network_reportip.
	 *
	 * @author  Lars Koudal
	 * @since   v5.114
	 * @version v1.0.0  Thursday, January 28th, 2021.
	 * @version v1.0.1  Friday, March 3rd, 2023.
	 * @access  private static
	 * @param   mixed   $ip
	 * @param   mixed   $args   Default: array()
	 * @return  boolean
	 */
	private static function network_reportip__premium_only( $ip, $args = array() ) {

		if ( ! self::$options['globalbannetwork'] ) {
			return false;
		}
		if ( ! $ip ) {
			return false;
		}
		if ( '::1' === $ip ) {
			return false;
		}
		if ( '127.0.0.1' === $ip ) {
			return false;
		}
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return false;
		}

		$ban_type   = '';
		$ban_reason = '';

		if ( isset( $args['ban_reason'] ) ) {
			$ban_reason = $args['ban_reason'];
		}
		if ( isset( $args['ban_type'] ) ) {
			$ban_type = $args['ban_type'];
		}

		$license_id            = secnin_fs()->_get_license()->id;
		$install_id            = secnin_fs()->get_site()->id;
		$site_private_key      = secnin_fs()->get_site()->secret_key;
		$nonce                 = current_time( 'Y-m-d', true );
		$pk_hash               = hash( 'sha512', $site_private_key . '|' . $nonce );
		$authentication_string = base64_encode( $pk_hash . '|' . $nonce );

		$url = self::$central_api_url . 'reportip/';

		$response = wp_remote_post(
			$url,
			array(
				'headers'  => array(
					'Authorization' => $authentication_string,
				),
				'body'     => array(
					'ban_reason' => esc_attr( $ban_reason ),
					'ban_ip'     => $ip,
					'install_id' => $install_id,
					'license_id' => $license_id,
					'ban_type'   => esc_attr( $ban_type ),
					'ver'        => esc_attr( Wf_Sn::$version ),
				),
				'blocking' => false,
				'timeout'  => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			$error_message = $response->get_error_message();
			wf_sn_el_modules::log_event( 'security_ninja', 'ip_network_submit_error', 'Error submitting IP to network ' . $error_message );
			return false;
		} else {
			$body = wp_remote_retrieve_body( $response );
			wf_sn_el_modules::log_event( 'security_ninja', 'ip_network_submit_success', 'Blocked IP submitted to network.' );
		}
		return true;
	}



	/**
	 * display results
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, December 21st, 2020.
	 * @access  public static
	 * @return  void
	 */
	public static function do_page() {
		global $wpdb;

		$ips = get_option( 'wf_sn_cf_ips' );

		if ( $ips && ! array_key_exists( 'total', $ips ) ) {
			$total_ips = 0;
			if ( isset( $ips['subnets'] ) ) {
				foreach ( $ips['subnets'] as $prefix => $subnet ) {
					foreach ( $subnet as $sub ) {
						$mask       = explode( '/', str_replace( '\'', '', $sub ) );
						$total_ips += pow( 2, 32 - $mask[1] ) - 2;
					}
				}
			}

			$total_ips = isset( $total_ips ) ? intval( $total_ips ) : 0;

			$banned_ips       = self::get_banned_ips();
			$banned_ips_count = is_array( $banned_ips ) ? count( $banned_ips ) : 0;

			$ips['total'] = $total_ips + count( $ips['ips'] ) + $banned_ips_count;
			// update_option('wf_sn_cf_ips', $ips, false);
		}

		?>
		<div class="sncard settings-card">
			<h2><span class="dashicons dashicons-shield-alt"></span> <?php esc_html_e( 'Firewall', 'security-ninja' ); ?></h2>
			<p>Protect your website from malicious traffic and attacks</p>

			<div id="snfwtop">
				<div class="col left">
					<?php
					$blocked_count = get_option( 'wf_sn_cf_blocked_count' );
					if ( $blocked_count ) {
						?>
						<div class="snfw-blocked-count">Total Blocked Visits <div class="val"><?php echo number_format_i18n( $blocked_count ); ?></div>
						</div>
						<?php
					}
					if ( 1 === self::is_active() ) {
						echo '<input type="button" value="' . __( 'Disable Firewall', 'security-ninja' ) . '" id="sn-disable-firewall" class="button snbutton" />';
					} else {
						echo '<input type="button" value="' . __( 'Enable Firewall', 'security-ninja' ) . '" id="sn-enable-firewall-overlay" class="button button-primary button-hero"/>';
					}
					?>

				</div>
				<div class="col right">
				<?php
				if ( 1 === self::is_active() ) {

					echo '<h3>' . esc_html__( 'Secret Access URL', 'security-ninja' ) . '</h3>';
					?>
						<input type="text" id="sn-unblock-url" value="<?php echo esc_url( self::get_unblock_url() ); ?>" disabled>
						<?php
																																														echo '<p class="description">' . esc_html__( 'Do not share this URL! Use it only to access your website if your IP gets banned.', 'security-ninja' ) . '</p>';
				} else {
					echo '<h3>' . esc_html__( 'Firewall is not active', 'security-ninja' ) . '</h3>';
					echo '<p>' . esc_html__( 'Activating the firewall enables protection against outside threats trying to access your website.', 'security-ninja' ) . '</p>';
				}
				?>
				</div>
			</div>


			<?php
			global $secnin_fs;

			if ( 1 === self::is_active() ) {
				echo '</div>';
				echo '<div class="sncard settings-card">';
				echo '<form action="options.php" id="sn-firewall-settings-form" method="post">';
				settings_fields( 'wf_sn_cf' );
				?>
		<div class="wf-sn-tab-content">
			<div class="nav-tab-wrapper" id="wf-sn-cf-subtabs">
				<a href="#sn_cf_settings" class="nav-tab nav-tab-active">Settings</a>
				<?php
				$can_use_premium = false;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$can_use_premium = true;
				}
				$pro_class = $can_use_premium ? '' : ' profeature';
				?>
				<a href="#sn_cf_visitors" class="nav-tab<?php echo esc_attr( $pro_class ); ?>">Visitor Logging</a>
				<a href="#sn_cf_ip" class="nav-tab<?php echo esc_attr( $pro_class ); ?>">IP Management</a>
				<a href="#sn_cf_404guard" class="nav-tab<?php echo esc_attr( $pro_class ); ?>">404 Guard</a>
				<a href="#sn_cf_woocommerce" class="nav-tab<?php echo esc_attr( $pro_class ); ?>">WooCommerce</a>
			</div>

			<div id="sn_cf_settings" class="wf-sn-subtab">
				<?php
				// Include settings content from separate file
				$settings_file = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/tabs/settings.php';
				if ( file_exists( $settings_file ) ) {
					require_once $settings_file;
					wf_sn_cf_render_settings_content( self::$options, $ips );
				}
				?>
			</div>

			<div id="sn_cf_visitors" class="wf-sn-subtab">
				<?php
				$sn_cf_show_visitor_upgrade = true;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$sn_cf_show_visitor_upgrade = false;
					$visitor_logging_file       = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/tabs/visitor-logging.php';
					if ( file_exists( $visitor_logging_file ) ) {
						require_once $visitor_logging_file;
						wf_sn_cf_render_visitor_logging_content( self::$options );
					}
				}
				if ( $sn_cf_show_visitor_upgrade ) {
					?>
					<table class="form-table">
						<tbody>
							<tr>
								<td colspan="2">
									<div class="sncard infobox">
										<div class="inner">
											<h3>Upgrade to Pro for Visitor Logging</h3>
											<p>The free version provides basic firewall protection. Upgrade to Security Ninja Pro to unlock visitor logging features:</p>
											<ul style="list-style: disc; margin-left: 20px; margin-top: 10px;">
												<li>Track all visitors and page requests</li>
												<li>Visitor logging with geolocation data</li>
												<li>Detailed visitor analytics and insights</li>
												<li>Customizable log retention periods</li>
											</ul>
											<p style="margin-top: 15px;">
												<a href="<?php echo esc_url( \WPSecurityNinja\Plugin\Utils::generate_sn_web_link( 'upgrade_tab_visitor_logging', '/upgrade/' ) ); ?>" class="button button-primary button-small" target="_blank" rel="noopener">Upgrade to Pro</a>
											</p>
										</div>
									</div>
								</td>
							</tr>
						</tbody>
					</table>
					<?php
				}
				?>
			</div>

			<div id="sn_cf_ip" class="wf-sn-subtab">
				<?php
				$sn_cf_show_ip_upgrade = true;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$sn_cf_show_ip_upgrade = false;
					$ip_management_file    = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/tabs/ip-management.php';
					if ( file_exists( $ip_management_file ) ) {
						require_once $ip_management_file;
						wf_sn_cf_render_ip_management_content( self::$options );
					}
				}
				if ( $sn_cf_show_ip_upgrade ) {
					?>
					<table class="form-table">
						<tbody>
							<tr>
								<td colspan="2">
									<div class="sncard infobox">
										<div class="inner">
											<h3>Upgrade to Pro for IP Management</h3>
											<p>The free version provides basic firewall protection. Upgrade to Security Ninja Pro to unlock advanced IP management features:</p>
											<ul style="list-style: disc; margin-left: 20px; margin-top: 10px;">
												<li>Manual IP blacklist and whitelist management</li>
												<li>Automatic whitelisting for WP Rocket, Uptimia, UptimeRobot, and ManageWP</li>
												<li>View and manage locally banned IPs</li>
												<li>Test IP addresses to check ban status</li>
												<li>Clear banned IP lists with one click</li>
											</ul>
											<p style="margin-top: 15px;">
												<a href="<?php echo esc_url( \WPSecurityNinja\Plugin\Utils::generate_sn_web_link( 'upgrade_tab_ip_management', '/upgrade/' ) ); ?>" class="button button-primary button-small" target="_blank" rel="noopener">Upgrade to Pro</a>
											</p>
										</div>
									</div>
								</td>
							</tr>
						</tbody>
					</table>
					<?php
				}
				?>
			</div>

			<div id="sn_cf_404guard" class="wf-sn-subtab">
				<?php
				$sn_cf_show_404guard_upgrade = true;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$sn_cf_show_404guard_upgrade = false;
					$fofguard                    = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/tabs/404guard.php';
					if ( file_exists( $fofguard ) ) {
						require_once $fofguard;
						wf_sn_cf_render_404guard_content( self::$options, $ips );
					}
				}
				if ( $sn_cf_show_404guard_upgrade ) {
					?>
					<table class="form-table">
						<tbody>
							<tr>
								<td colspan="2">
									<div class="sncard infobox">
										<div class="inner">
											<h3>Upgrade to Pro for 404 Guard</h3>
											<p>The free version provides basic firewall protection. Upgrade to Security Ninja Pro to unlock 404 Guard features:</p>
											<ul style="list-style: disc; margin-left: 20px; margin-top: 10px;">
												<li>Block IPs that generate excessive 404 errors</li>
												<li>Prevent bots from scanning your site for vulnerabilities</li>
												<li>Configurable threshold and time window settings</li>
												<li>Automatic whitelisting of search engines and crawlers</li>
												<li>Temporary blocks that automatically expire</li>
												<li>Comprehensive logging of all blocked IPs</li>
											</ul>
											<p style="margin-top: 15px;">
												<a href="<?php echo esc_url( \WPSecurityNinja\Plugin\Utils::generate_sn_web_link( 'upgrade_tab_404guard', '/upgrade/' ) ); ?>" class="button button-primary button-small" target="_blank" rel="noopener">Upgrade to Pro</a>
											</p>
										</div>
									</div>
								</td>
							</tr>
						</tbody>
					</table>
					<?php
				}
				?>
			</div>

			<div id="sn_cf_woocommerce" class="wf-sn-subtab">
				<?php
				$sn_cf_show_woocommerce_upgrade = true;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$sn_cf_show_woocommerce_upgrade = false;
					$woocommerce_file               = WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/tabs/woocommerce.php';
					if ( file_exists( $woocommerce_file ) ) {
						require_once $woocommerce_file;
						wf_sn_cf_render_woocommerce_content( self::$options, $ips );
					}
				}
				if ( $sn_cf_show_woocommerce_upgrade ) {
					?>
					<table class="form-table">
						<tbody>
							<tr>
								<td colspan="2">
									<div class="sncard infobox">
										<div class="inner">
											<h3>Upgrade to Pro for WooCommerce Protection</h3>
											<p>Upgrade to Security Ninja Pro to unlock WooCommerce protection features:</p>
											<ul style="list-style: disc; margin-left: 20px; margin-top: 10px;">
												<li>Rate limiting for checkout, add to cart, and order placement</li>
												<li>Coupon code brute force protection with temporary bans</li>
												<li>Automatic blocking of suspicious IPs</li>
												<li>Comprehensive logging of all security events</li>
												<li>Safe for legitimate customers with conservative default settings</li>
												<li>Configurable thresholds for different store sizes</li>
											</ul>
											<p style="margin-top: 15px;">
												<a href="<?php echo esc_url( \WPSecurityNinja\Plugin\Utils::generate_sn_web_link( 'upgrade_tab_woocommerce', '/upgrade/' ) ); ?>" class="button button-primary button-small" target="_blank" rel="noopener">Upgrade to Pro</a>
											</p>
										</div>
									</div>
								</td>
							</tr>
						</tbody>
					</table>
					<?php
				}
				?>
			</div>
		</div>


		<p class="submit"><br><input type="submit" value="<?php echo esc_html__( 'Save Changes', 'security-ninja' ); ?>" class="input-button button-primary" name="Submit" /></p>
		</form>
		</div>
				<?php

			} else {
				$sn_cf_show_inactive_upgrade = true;
				if ( secnin_fs()->can_use_premium_code__premium_only() ) {
					$sn_cf_show_inactive_upgrade = false;
				}
				if ( $sn_cf_show_inactive_upgrade ) {
					?>
				<div class="sncard infobox">
					<div class="inner">
						<h3>Upgrade to Pro for Advanced Firewall Features</h3>
						<p>The free version provides basic firewall protection with 8G rules. Upgrade to Security Ninja Pro to unlock powerful advanced features:</p>
						<ul style="list-style: disc; margin-left: 20px; margin-top: 10px;">
							<li>Control how banned IPs are handled - block completely or only from login</li>
							<li>Cloud Firewall with 600+ million known bad IPs, automatically updated</li>
							<li>Participate in global IP threat network and share threat intelligence</li>
							<li>Block entire countries from accessing your website</li>
							<li>Customize messages shown to blocked visitors</li>
							<li>Redirect blocked visitors to any URL using 301 redirects</li>
						</ul>
						<p style="margin-top: 15px;">
							<a href="<?php echo esc_url( \WPSecurityNinja\Plugin\Utils::generate_sn_web_link( 'upgrade_tab_firewall', '/upgrade/' ) ); ?>" class="button button-primary button-small" target="_blank" rel="noopener">Upgrade to Pro</a>
						</p>
					</div>
				</div>
					<?php
				}
				?>
		</div>
				<?php
			}

			// Only show modal if firewall is not active
			if ( 1 !== self::is_active() ) {
				?>
	<div id="sn-firewall-modal" class="sn-modal">
		<div class="sn-modal-content">
			<h2><?php esc_html_e( 'Enable Firewall', 'security-ninja' ); ?></h2>
			<p><?php esc_html_e( 'To ensure you can regain access to your website if you get blocked, please enter your email address. You will be sent a secret access URL to help you regain access.', 'security-ninja' ); ?></p>

			<div class="sn-input-container">
				<input
					type="email"
					id="sn-firewall-email"
					autocomplete="off"
					placeholder="<?php esc_attr_e( 'Enter your email', 'security-ninja' ); ?>"
					value="<?php echo esc_attr( wp_get_current_user()->user_email ); ?>"
					required
					data-1p-ignore
					data-lpignore="true">
				<div class="sn-buttons">
					<a href="#" id="sn-modal-continue" class="button button-large button-primary">
						<?php esc_html_e( 'Send email', 'security-ninja' ); ?>
					</a>
					<a href="#" id="sn-modal-skip" class="button button-large button-secondary">
						<?php esc_html_e( 'Activate, but no email', 'security-ninja' ); ?>
					</a>
				</div>
			</div>


			<div id="sn-unblock-message"></div>
			<div id="sn-firewall-status"></div>
		</div>
	</div>
				<?php
			}
			?>


		<?php
	}

	/**
	 * Whether a ban should only apply on login-related URLs (frontend allowed).
	 *
	 * Country bans: login-only when countryblock_loginonly is ON.
	 * IP/cloud bans: login-only when global is OFF.
	 *
	 * @author  Lars Koudal
	 * @since   5.289
	 * @param   string $ban_reason Ban reason from is_banned_ip().
	 * @return  bool True when non-login requests should be allowed.
	 */
	private static function should_limit_ban_to_login_only__premium_only( $ban_reason ) {
		if ( is_null( self::$options ) ) {
			self::$options = self::get_options();
		}

		$is_country_ban = ( false !== strpos( $ban_reason, 'Country is blocked' ) );

		if ( $is_country_ban ) {
			return ! empty( self::$options['countryblock_loginonly'] );
		}

		return empty( self::$options['global'] );
	}

	/**
	 * Check if the current request is login-related
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Monday, January 13th, 2025.
	 * @access  private static
	 * @param   string  $request_uri The request URI to check
	 * @return  boolean True if login-related, false otherwise
	 */
	private static function is_login_related_request__premium_only( $request_uri = '' ) {
		if ( empty( $request_uri ) ) {
			$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
		}

		$login_paths = array(
			'/wp-login.php',
			'/wp-admin/',
			'/login',
			'/admin',
			'/my-account', // WooCommerce login/account pages
		);

		// Check for custom login URL if the rename login feature is active
		if ( class_exists( __NAMESPACE__ . '\\SecNin_Rename_WP_Login' ) ) {
			$custom_login_slug = \WPSecurityNinja\Plugin\SecNin_Rename_WP_Login::new_login_slug();
			if ( $custom_login_slug ) {
				$login_paths[] = '/' . $custom_login_slug;
			}
		}

		foreach ( $login_paths as $path ) {
			if ( strpos( $request_uri, $path ) !== false ) {
				return true;
			}
		}

		return false;
	}
}

add_action( 'plugins_loaded', array( __NAMESPACE__ . '\wf_sn_cf', 'init' ) );
register_deactivation_hook( WF_SN_BASE_FILE, array( __NAMESPACE__ . '\wf_sn_cf', 'deactivate' ) );
