Files
WP-Security-Pack/security-pack/includes/class-wpsp-geo-blocking.php
T
2026-02-01 21:12:06 +01:00

313 lines
7.4 KiB
PHP

<?php
/**
* Geo-blocking for Security Pack.
*
* @package Security_Pack
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Geo-blocking class using IP2Location Lite database.
*/
class WPSP_Geo_Blocking {
/**
* IP2Location database file path.
*
* @var string
*/
private $db_path = '';
/**
* IP2Location database object.
*
* @var object|null
*/
private $db = null;
/**
* Country lookup cache.
*
* @var array
*/
private $cache = array();
/**
* Constructor.
*/
public function __construct() {
$this->db_path = Security_Pack::get_setting( 'geo_database_path', '' );
if ( empty( $this->db_path ) ) {
// Default path in plugin directory.
$this->db_path = WPSP_PLUGIN_DIR . 'data/IP2LOCATION-LITE-DB1.BIN';
}
// Check geo-blocking immediately (constructor runs during init).
// This blocks access to the entire website for blocked countries.
if ( Security_Pack::get_setting( 'geo_blocking_enabled', false ) ) {
$this->check_geo_access();
}
}
/**
* Check if geo-blocking should apply.
*/
public function check_geo_access() {
$ip = WPSP_Helper::get_client_ip();
if ( ! $ip ) {
return;
}
// Check if IP is whitelisted.
$ip_control = wpsp()->get_component( 'ip_control' );
if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) {
return;
}
// Get country code.
$country_code = $this->get_country_code( $ip );
if ( ! $country_code ) {
return; // Can't determine country, allow access.
}
// Check if country is blocked.
$blocked_countries = Security_Pack::get_setting( 'geo_blocked_countries', array() );
if ( ! empty( $blocked_countries ) && in_array( $country_code, $blocked_countries, true ) ) {
WPSP_Activity_Log::log(
WPSP_Activity_Log::EVENT_GEO_BLOCKED,
$ip,
null,
sprintf(
/* translators: %s: Country code */
__( 'Blocked country: %s', 'security-pack' ),
$country_code
)
);
$this->block_access( $country_code );
}
}
/**
* Get country code for an IP address.
*
* @param string $ip IP address.
* @return string|null Two-letter country code or null.
*/
public function get_country_code( $ip ) {
// Check cache first.
if ( isset( $this->cache[ $ip ] ) ) {
return $this->cache[ $ip ];
}
// Check transient cache.
$cache_key = 'wpsp_geo_' . md5( $ip );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
$this->cache[ $ip ] = $cached;
return $cached;
}
$country_code = null;
// Try IP2Location database first.
$country_code = $this->lookup_ip2location( $ip );
// Fallback to PHP geoip extension.
if ( ! $country_code && function_exists( 'geoip_country_code_by_name' ) ) {
// phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.geoip_country_code_by_nameDeprecated
$country_code = @geoip_country_code_by_name( $ip );
}
// Cache the result.
if ( $country_code ) {
$country_code = strtoupper( $country_code );
$this->cache[ $ip ] = $country_code;
set_transient( $cache_key, $country_code, HOUR_IN_SECONDS );
}
return $country_code;
}
/**
* Lookup IP using IP2Location database.
*
* @param string $ip IP address.
* @return string|null
*/
private function lookup_ip2location( $ip ) {
if ( ! file_exists( $this->db_path ) ) {
return null;
}
// Load the database reader.
if ( null === $this->db ) {
require_once WPSP_PLUGIN_DIR . 'includes/class-wpsp-ip2location.php';
$this->db = new WPSP_IP2Location( $this->db_path );
}
if ( ! $this->db ) {
return null;
}
$record = $this->db->lookup( $ip );
if ( $record && ! empty( $record['country_code'] ) && '-' !== $record['country_code'] ) {
return $record['country_code'];
}
return null;
}
/**
* Block access for geo-blocked countries.
*
* @param string $country_code Country code.
*/
private function block_access( $country_code ) {
status_header( 403 );
nocache_headers();
$countries = WPSP_Helper::get_countries();
$country_name = isset( $countries[ $country_code ] ) ? $countries[ $country_code ] : $country_code;
$message = sprintf(
/* translators: %s: Country name */
__( 'Access from %s is not permitted.', 'security-pack' ),
$country_name
);
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
wp_send_json_error( array( 'message' => $message ), 403 );
}
wp_die(
esc_html( $message ),
esc_html__( 'Access Denied', 'security-pack' ),
array(
'response' => 403,
'back_link' => false,
)
);
}
/**
* Check if geo-database exists.
*
* @return bool
*/
public function database_exists() {
return file_exists( $this->db_path );
}
/**
* Get database info.
*
* @return array
*/
public function get_database_info() {
$info = array(
'exists' => false,
'path' => $this->db_path,
'size' => 0,
'modified' => null,
'type' => '',
);
if ( file_exists( $this->db_path ) ) {
$info['exists'] = true;
$info['size'] = filesize( $this->db_path );
$info['modified'] = filemtime( $this->db_path );
// Try to get database type.
if ( null === $this->db ) {
require_once WPSP_PLUGIN_DIR . 'includes/class-wpsp-ip2location.php';
$this->db = new WPSP_IP2Location( $this->db_path );
}
if ( $this->db ) {
$info['type'] = $this->db->get_database_type();
}
}
return $info;
}
/**
* Download IP2Location Lite database.
*
* @param string $download_token IP2Location download token (optional for LITE).
* @return bool|WP_Error
*/
public function download_database( $download_token = '' ) {
// IP2Location LITE DB1 (Country only) - free, no token required.
$download_url = 'https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.BIN.ZIP';
// If token provided, use the authenticated endpoint.
if ( ! empty( $download_token ) ) {
$download_url = 'https://www.ip2location.com/download/?token=' . urlencode( $download_token ) . '&file=DB1LITEBIN';
}
// Create data directory.
$data_dir = WPSP_PLUGIN_DIR . 'data';
if ( ! file_exists( $data_dir ) ) {
wp_mkdir_p( $data_dir );
}
// Download the file.
$tmp_file = download_url( $download_url, 300 );
if ( is_wp_error( $tmp_file ) ) {
return $tmp_file;
}
// Check if it's a ZIP file.
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mime = finfo_file( $finfo, $tmp_file );
finfo_close( $finfo );
$target_path = $data_dir . '/IP2LOCATION-LITE-DB1.BIN';
if ( 'application/zip' === $mime || 'application/x-zip-compressed' === $mime ) {
// Extract ZIP.
$zip = new ZipArchive();
if ( true === $zip->open( $tmp_file ) ) {
// Find the BIN file.
for ( $i = 0; $i < $zip->numFiles; $i++ ) {
$filename = $zip->getNameIndex( $i );
if ( preg_match( '/\.BIN$/i', $filename ) ) {
$content = $zip->getFromIndex( $i );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $target_path, $content );
break;
}
}
$zip->close();
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
unlink( $tmp_file );
} else {
// Direct BIN file.
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
rename( $tmp_file, $target_path );
}
if ( file_exists( $target_path ) ) {
// Update database path setting.
Security_Pack::update_setting( 'geo_database_path', $target_path );
return true;
}
return new WP_Error( 'download_failed', __( 'Failed to download or extract database.', 'security-pack' ) );
}
}