v1.5.0
This commit is contained in:
parent
467560d420
commit
054060374e
36 changed files with 4058 additions and 73 deletions
|
|
@ -1,5 +1,34 @@
|
|||
== Changelog ==
|
||||
|
||||
= 1.5.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
**Added**
|
||||
|
||||
* Full audit user table (WP_List_Table) with sortable columns, role/status filters, and CSV export.
|
||||
* GeoIP country restrictions via optional MaxMind GeoLite2 database: per-country allow list, deny list, always-challenge list.
|
||||
* Require recent 2FA verification before creating Application Passwords (REST endpoint, 15-min window, filterable).
|
||||
* WP-CLI `wp 2fa export` — CSV report via stdout.
|
||||
* `robotstxt_2fa_force_challenge` filter — override frequency skip; used by GeoIP always-challenge.
|
||||
* `maxmind-db/reader` added as optional production Composer dependency.
|
||||
|
||||
**Fixed**
|
||||
|
||||
* Email digest cron now reschedules correctly when frequency changes (weekly ↔ monthly).
|
||||
|
||||
**Compatibility**
|
||||
|
||||
* WordPress: 6.4 – 7.1
|
||||
* PHP: 8.0 – 8.5
|
||||
|
||||
**Tests**
|
||||
|
||||
* PHP Coding Standards: PHP_CodeSniffer 3.13.5 / WPCS 3.3.0
|
||||
* PHPStan: level 9 — 0 errors
|
||||
* PHPCompatibility: 8.0–8.5 — 0 issues
|
||||
* PHPUnit: 9.6.34 — 42 tests, 109 assertions
|
||||
|
||||
= 1.4.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
namespace Robotstxt\TwoFA\Admin;
|
||||
|
||||
use Robotstxt\TwoFA\Attempts_Log;
|
||||
use Robotstxt\TwoFA\User\Two_Factor_Config;
|
||||
use Robotstxt\TwoFA\User\User_Settings_Repository;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
|
|
@ -29,13 +31,39 @@ class Dashboard_Page {
|
|||
*/
|
||||
private Attempts_Log $attempts_log;
|
||||
|
||||
/**
|
||||
* Global two-factor configuration.
|
||||
*
|
||||
* @var Two_Factor_Config
|
||||
*/
|
||||
private Two_Factor_Config $config;
|
||||
|
||||
/**
|
||||
* User settings repository.
|
||||
*
|
||||
* @var User_Settings_Repository
|
||||
*/
|
||||
private User_Settings_Repository $repository;
|
||||
|
||||
/**
|
||||
* Users list table.
|
||||
*
|
||||
* @var Users_List_Table
|
||||
*/
|
||||
private Users_List_Table $users_table;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Attempts_Log $attempts_log Failed attempts log instance.
|
||||
* @param Attempts_Log $attempts_log Failed attempts log instance.
|
||||
* @param Two_Factor_Config $config Global two-factor configuration.
|
||||
* @param User_Settings_Repository $repository User settings repository.
|
||||
*/
|
||||
public function __construct( Attempts_Log $attempts_log ) {
|
||||
public function __construct( Attempts_Log $attempts_log, Two_Factor_Config $config, User_Settings_Repository $repository ) {
|
||||
$this->attempts_log = $attempts_log;
|
||||
$this->config = $config;
|
||||
$this->repository = $repository;
|
||||
$this->users_table = new Users_List_Table( $config, $repository );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,6 +77,7 @@ class Dashboard_Page {
|
|||
add_action( 'admin_menu', array( $this, 'register_menu' ) );
|
||||
add_filter( 'manage_users_columns', array( $this, 'add_users_column' ) );
|
||||
add_filter( 'manage_users_custom_column', array( $this, 'render_users_column' ), 10, 3 );
|
||||
add_action( 'admin_post_robotstxt_2fa_export_users', array( $this, 'handle_csv_export' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,6 +165,22 @@ class Dashboard_Page {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h2><?php esc_html_e( 'Users', 'robotstxt-2fa' ); ?></h2>
|
||||
|
||||
<form method="get">
|
||||
<input type="hidden" name="page" value="<?php echo esc_attr( self::PAGE_SLUG ); ?>" />
|
||||
<?php
|
||||
$this->users_table->prepare_items();
|
||||
$this->users_table->display();
|
||||
?>
|
||||
</form>
|
||||
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-top:1em;">
|
||||
<input type="hidden" name="action" value="robotstxt_2fa_export_users" />
|
||||
<?php wp_nonce_field( 'robotstxt_2fa_export_users' ); ?>
|
||||
<?php submit_button( __( 'Export CSV', 'robotstxt-2fa' ), 'secondary', 'submit', false ); ?>
|
||||
</form>
|
||||
|
||||
<h2><?php esc_html_e( 'Recent failed attempts', 'robotstxt-2fa' ); ?></h2>
|
||||
|
||||
<?php if ( empty( $recent_attempts ) ) : ?>
|
||||
|
|
@ -170,6 +215,100 @@ class Dashboard_Page {
|
|||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the CSV export admin-post action.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_csv_export(): void {
|
||||
check_admin_referer( 'robotstxt_2fa_export_users' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'robotstxt-2fa' ) );
|
||||
}
|
||||
|
||||
$users = get_users(
|
||||
array(
|
||||
'number' => -1,
|
||||
'fields' => 'all',
|
||||
)
|
||||
);
|
||||
$rows = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$settings = $this->repository->get_user_settings( $user->ID );
|
||||
$is_enabled = $settings['enabled'];
|
||||
|
||||
$required_methods = $this->config->get_required_methods_for_user( $user );
|
||||
$is_forced = ! empty( $required_methods );
|
||||
|
||||
if ( $is_enabled ) {
|
||||
$status = 'active';
|
||||
} elseif ( $is_forced ) {
|
||||
$status = 'required';
|
||||
} else {
|
||||
$status = 'optional';
|
||||
}
|
||||
|
||||
$last_verified_ts = 0;
|
||||
$raw_verifications = get_user_meta( $user->ID, 'robotstxt_2fa_last_verifications', true );
|
||||
|
||||
if ( is_array( $raw_verifications ) ) {
|
||||
foreach ( $raw_verifications as $method_data ) {
|
||||
if ( is_array( $method_data ) ) {
|
||||
foreach ( $method_data as $context_data ) {
|
||||
if ( is_array( $context_data ) && isset( $context_data['verified_at'] ) && is_int( $context_data['verified_at'] ) ) {
|
||||
$last_verified_ts = max( $last_verified_ts, $context_data['verified_at'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$last_verified_str = $last_verified_ts > 0 ? (string) wp_date( 'Y-m-d H:i', $last_verified_ts ) : '';
|
||||
|
||||
$unused_codes = 0;
|
||||
$raw_codes = get_user_meta( $user->ID, 'robotstxt_2fa_recovery_codes', true );
|
||||
|
||||
if ( is_array( $raw_codes ) && isset( $raw_codes['codes'] ) && is_array( $raw_codes['codes'] ) ) {
|
||||
foreach ( $raw_codes['codes'] as $code_entry ) {
|
||||
if ( is_array( $code_entry ) && empty( $code_entry['used_at'] ) ) {
|
||||
++$unused_codes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
'ID' => (string) $user->ID,
|
||||
'user_login' => $user->user_login,
|
||||
'user_email' => $user->user_email,
|
||||
'roles' => implode( '|', $user->roles ),
|
||||
'status' => $status,
|
||||
'methods' => implode( '|', $settings['methods'] ),
|
||||
'last_verified' => $last_verified_str,
|
||||
'unused_codes' => (string) $unused_codes,
|
||||
);
|
||||
}
|
||||
|
||||
header( 'Content-Type: text/csv' );
|
||||
header( 'Content-Disposition: attachment; filename="2fa-users-' . gmdate( 'Y-m-d' ) . '.csv"' );
|
||||
|
||||
$headers = array( 'ID', 'Login', 'Email', 'Roles', 'Status', 'Methods', 'Last Verified', 'Unused Codes' );
|
||||
echo implode( ',', array_map( 'addslashes', $headers ) ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV output to a file download.
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
echo implode( ',', array_map( 'addslashes', array_values( $row ) ) ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV output to a file download.
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a 2FA status column to the users list table.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -247,6 +247,14 @@ class Settings_Page {
|
|||
'robotstxt_2fa_access'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'robotstxt_2fa_app_passwords_require_2fa_creation',
|
||||
__( 'Require 2FA to create Application Passwords', 'robotstxt-2fa' ),
|
||||
array( $this, 'render_app_passwords_require_2fa_creation_field' ),
|
||||
self::PAGE_SLUG,
|
||||
'robotstxt_2fa_access'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'robotstxt_2fa_ip_allow',
|
||||
__( 'IP allow list', 'robotstxt-2fa' ),
|
||||
|
|
@ -301,6 +309,45 @@ class Settings_Page {
|
|||
self::PAGE_SLUG,
|
||||
'robotstxt_2fa_notifications'
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'robotstxt_2fa_geoip',
|
||||
__( 'GeoIP / Country restrictions', 'robotstxt-2fa' ),
|
||||
array( $this, 'render_geoip_section' ),
|
||||
self::PAGE_SLUG
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'robotstxt_2fa_geoip_database_path',
|
||||
__( 'Database path', 'robotstxt-2fa' ),
|
||||
array( $this, 'render_geoip_database_path_field' ),
|
||||
self::PAGE_SLUG,
|
||||
'robotstxt_2fa_geoip'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'robotstxt_2fa_geoip_country_allow',
|
||||
__( 'Country allow list (bypass 2FA)', 'robotstxt-2fa' ),
|
||||
array( $this, 'render_geoip_country_allow_field' ),
|
||||
self::PAGE_SLUG,
|
||||
'robotstxt_2fa_geoip'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'robotstxt_2fa_geoip_country_deny',
|
||||
__( 'Country deny list (block login)', 'robotstxt-2fa' ),
|
||||
array( $this, 'render_geoip_country_deny_field' ),
|
||||
self::PAGE_SLUG,
|
||||
'robotstxt_2fa_geoip'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'robotstxt_2fa_geoip_always_challenge',
|
||||
__( 'Always challenge countries', 'robotstxt-2fa' ),
|
||||
array( $this, 'render_geoip_always_challenge_field' ),
|
||||
self::PAGE_SLUG,
|
||||
'robotstxt_2fa_geoip'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -504,7 +551,7 @@ class Settings_Page {
|
|||
*
|
||||
* @param mixed $raw_settings Raw settings submitted by the user.
|
||||
*
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}}
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}, app_passwords_require_2fa_creation: bool, geoip_database_path: string, geoip_country_allow: string, geoip_country_deny: string, geoip_always_challenge: string}
|
||||
*/
|
||||
public function sanitize_settings( $raw_settings ): array {
|
||||
$capability = $this->get_required_capability();
|
||||
|
|
@ -616,13 +663,33 @@ class Settings_Page {
|
|||
'digest_frequency' => isset( $raw_notif['digest_frequency'] ) && 'monthly' === $raw_notif['digest_frequency'] ? 'monthly' : 'weekly',
|
||||
);
|
||||
|
||||
if ( array_key_exists( 'app_passwords_require_2fa_creation', $raw_settings ) ) {
|
||||
$sanitized['app_passwords_require_2fa_creation'] = '1' === ( is_scalar( $raw_settings['app_passwords_require_2fa_creation'] ) ? (string) $raw_settings['app_passwords_require_2fa_creation'] : '' );
|
||||
}
|
||||
|
||||
if ( isset( $raw_settings['geoip_database_path'] ) && is_string( $raw_settings['geoip_database_path'] ) ) {
|
||||
$sanitized['geoip_database_path'] = sanitize_text_field( $raw_settings['geoip_database_path'] );
|
||||
}
|
||||
|
||||
if ( isset( $raw_settings['geoip_country_allow'] ) && is_string( $raw_settings['geoip_country_allow'] ) ) {
|
||||
$sanitized['geoip_country_allow'] = strtoupper( sanitize_textarea_field( $raw_settings['geoip_country_allow'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $raw_settings['geoip_country_deny'] ) && is_string( $raw_settings['geoip_country_deny'] ) ) {
|
||||
$sanitized['geoip_country_deny'] = strtoupper( sanitize_textarea_field( $raw_settings['geoip_country_deny'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $raw_settings['geoip_always_challenge'] ) && is_string( $raw_settings['geoip_always_challenge'] ) ) {
|
||||
$sanitized['geoip_always_challenge'] = strtoupper( sanitize_textarea_field( $raw_settings['geoip_always_challenge'] ) );
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve plugin settings, migrating from legacy format when necessary.
|
||||
*
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}}
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}, app_passwords_require_2fa_creation: bool, geoip_database_path: string, geoip_country_allow: string, geoip_country_deny: string, geoip_always_challenge: string}
|
||||
*/
|
||||
public function get_settings(): array {
|
||||
$options = array();
|
||||
|
|
@ -647,19 +714,19 @@ class Settings_Page {
|
|||
$def_notif = $defaults['notifications'];
|
||||
|
||||
return array(
|
||||
'role_methods' => $this->resolve_role_methods( $options ),
|
||||
'frequency' => Frequency_Options::sanitize( isset( $options['frequency'] ) && is_string( $options['frequency'] ) ? $options['frequency'] : '' ),
|
||||
'force_frequency' => ! empty( $options['force_frequency'] ),
|
||||
'delete_on_uninstall' => ! empty( $options['delete_on_uninstall'] ),
|
||||
'max_attempts' => isset( $options['max_attempts'] ) && is_numeric( $options['max_attempts'] ) ? max( 0, absint( $options['max_attempts'] ) ) : $defaults['max_attempts'],
|
||||
'lockout_duration' => isset( $options['lockout_duration'] ) && is_numeric( $options['lockout_duration'] ) ? max( 1, absint( $options['lockout_duration'] ) ) : $defaults['lockout_duration'],
|
||||
'trust_device_days' => isset( $options['trust_device_days'] ) && is_numeric( $options['trust_device_days'] ) ? max( 0, absint( $options['trust_device_days'] ) ) : $defaults['trust_device_days'],
|
||||
'grace_period_days' => isset( $options['grace_period_days'] ) && is_numeric( $options['grace_period_days'] ) ? max( 0, absint( $options['grace_period_days'] ) ) : $defaults['grace_period_days'],
|
||||
'grace_period_action' => isset( $options['grace_period_action'] ) && in_array( $options['grace_period_action'], array( 'block', 'wizard' ), true ) ? $options['grace_period_action'] : $defaults['grace_period_action'],
|
||||
'app_passwords_exempt' => isset( $options['app_passwords_exempt'] ) ? (bool) $options['app_passwords_exempt'] : $defaults['app_passwords_exempt'],
|
||||
'ip_allow' => isset( $options['ip_allow'] ) && is_string( $options['ip_allow'] ) ? $options['ip_allow'] : $defaults['ip_allow'],
|
||||
'ip_deny' => isset( $options['ip_deny'] ) && is_string( $options['ip_deny'] ) ? $options['ip_deny'] : $defaults['ip_deny'],
|
||||
'notifications' => array(
|
||||
'role_methods' => $this->resolve_role_methods( $options ),
|
||||
'frequency' => Frequency_Options::sanitize( isset( $options['frequency'] ) && is_string( $options['frequency'] ) ? $options['frequency'] : '' ),
|
||||
'force_frequency' => ! empty( $options['force_frequency'] ),
|
||||
'delete_on_uninstall' => ! empty( $options['delete_on_uninstall'] ),
|
||||
'max_attempts' => isset( $options['max_attempts'] ) && is_numeric( $options['max_attempts'] ) ? max( 0, absint( $options['max_attempts'] ) ) : $defaults['max_attempts'],
|
||||
'lockout_duration' => isset( $options['lockout_duration'] ) && is_numeric( $options['lockout_duration'] ) ? max( 1, absint( $options['lockout_duration'] ) ) : $defaults['lockout_duration'],
|
||||
'trust_device_days' => isset( $options['trust_device_days'] ) && is_numeric( $options['trust_device_days'] ) ? max( 0, absint( $options['trust_device_days'] ) ) : $defaults['trust_device_days'],
|
||||
'grace_period_days' => isset( $options['grace_period_days'] ) && is_numeric( $options['grace_period_days'] ) ? max( 0, absint( $options['grace_period_days'] ) ) : $defaults['grace_period_days'],
|
||||
'grace_period_action' => isset( $options['grace_period_action'] ) && in_array( $options['grace_period_action'], array( 'block', 'wizard' ), true ) ? $options['grace_period_action'] : $defaults['grace_period_action'],
|
||||
'app_passwords_exempt' => isset( $options['app_passwords_exempt'] ) ? (bool) $options['app_passwords_exempt'] : $defaults['app_passwords_exempt'],
|
||||
'ip_allow' => isset( $options['ip_allow'] ) && is_string( $options['ip_allow'] ) ? $options['ip_allow'] : $defaults['ip_allow'],
|
||||
'ip_deny' => isset( $options['ip_deny'] ) && is_string( $options['ip_deny'] ) ? $options['ip_deny'] : $defaults['ip_deny'],
|
||||
'notifications' => array(
|
||||
'on_enable' => isset( $raw_notif['on_enable'] ) ? (bool) $raw_notif['on_enable'] : $def_notif['on_enable'],
|
||||
'on_new_ip' => isset( $raw_notif['on_new_ip'] ) ? (bool) $raw_notif['on_new_ip'] : $def_notif['on_new_ip'],
|
||||
'on_recovery_used' => isset( $raw_notif['on_recovery_used'] ) ? (bool) $raw_notif['on_recovery_used'] : $def_notif['on_recovery_used'],
|
||||
|
|
@ -667,29 +734,34 @@ class Settings_Page {
|
|||
'digest_enabled' => isset( $raw_notif['digest_enabled'] ) ? (bool) $raw_notif['digest_enabled'] : $def_notif['digest_enabled'],
|
||||
'digest_frequency' => isset( $raw_notif['digest_frequency'] ) && 'monthly' === $raw_notif['digest_frequency'] ? 'monthly' : $def_notif['digest_frequency'],
|
||||
),
|
||||
'app_passwords_require_2fa_creation' => isset( $options['app_passwords_require_2fa_creation'] ) ? (bool) $options['app_passwords_require_2fa_creation'] : $defaults['app_passwords_require_2fa_creation'],
|
||||
'geoip_database_path' => isset( $options['geoip_database_path'] ) && is_string( $options['geoip_database_path'] ) ? $options['geoip_database_path'] : $defaults['geoip_database_path'],
|
||||
'geoip_country_allow' => isset( $options['geoip_country_allow'] ) && is_string( $options['geoip_country_allow'] ) ? $options['geoip_country_allow'] : $defaults['geoip_country_allow'],
|
||||
'geoip_country_deny' => isset( $options['geoip_country_deny'] ) && is_string( $options['geoip_country_deny'] ) ? $options['geoip_country_deny'] : $defaults['geoip_country_deny'],
|
||||
'geoip_always_challenge' => isset( $options['geoip_always_challenge'] ) && is_string( $options['geoip_always_challenge'] ) ? $options['geoip_always_challenge'] : $defaults['geoip_always_challenge'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve default settings.
|
||||
*
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}}
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}, app_passwords_require_2fa_creation: bool, geoip_database_path: string, geoip_country_allow: string, geoip_country_deny: string, geoip_always_challenge: string}
|
||||
*/
|
||||
private function get_default_settings(): array {
|
||||
return array(
|
||||
'role_methods' => array(),
|
||||
'frequency' => Frequency_Options::FREQUENCY_SESSION,
|
||||
'force_frequency' => false,
|
||||
'delete_on_uninstall' => false,
|
||||
'max_attempts' => 3,
|
||||
'lockout_duration' => 15,
|
||||
'trust_device_days' => 30,
|
||||
'grace_period_days' => 7,
|
||||
'grace_period_action' => 'wizard',
|
||||
'app_passwords_exempt' => true,
|
||||
'ip_allow' => '',
|
||||
'ip_deny' => '',
|
||||
'notifications' => array(
|
||||
'role_methods' => array(),
|
||||
'frequency' => Frequency_Options::FREQUENCY_SESSION,
|
||||
'force_frequency' => false,
|
||||
'delete_on_uninstall' => false,
|
||||
'max_attempts' => 3,
|
||||
'lockout_duration' => 15,
|
||||
'trust_device_days' => 30,
|
||||
'grace_period_days' => 7,
|
||||
'grace_period_action' => 'wizard',
|
||||
'app_passwords_exempt' => true,
|
||||
'ip_allow' => '',
|
||||
'ip_deny' => '',
|
||||
'notifications' => array(
|
||||
'on_enable' => false,
|
||||
'on_new_ip' => false,
|
||||
'on_recovery_used' => false,
|
||||
|
|
@ -697,6 +769,11 @@ class Settings_Page {
|
|||
'digest_enabled' => false,
|
||||
'digest_frequency' => 'weekly',
|
||||
),
|
||||
'app_passwords_require_2fa_creation' => false,
|
||||
'geoip_database_path' => '',
|
||||
'geoip_country_allow' => '',
|
||||
'geoip_country_deny' => '',
|
||||
'geoip_always_challenge' => '',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1257,6 +1334,107 @@ class Settings_Page {
|
|||
echo '</fieldset>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Application Passwords require 2FA creation checkbox.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_app_passwords_require_2fa_creation_field(): void {
|
||||
$settings = $this->get_settings();
|
||||
$is_checked = $settings['app_passwords_require_2fa_creation'];
|
||||
|
||||
printf(
|
||||
'<input type="hidden" name="%1$s[app_passwords_require_2fa_creation]" value="0" />',
|
||||
esc_attr( self::OPTION_NAME )
|
||||
);
|
||||
|
||||
printf(
|
||||
'<label><input type="checkbox" name="%1$s[app_passwords_require_2fa_creation]" value="1" %2$s /> %3$s</label>',
|
||||
esc_attr( self::OPTION_NAME ),
|
||||
checked( $is_checked, true, false ),
|
||||
esc_html__( 'Require a recent 2FA verification before creating a new Application Password.', 'robotstxt-2fa' )
|
||||
);
|
||||
|
||||
echo '<p class="description">' . esc_html__( 'When enabled, users must have completed a 2FA challenge within the last 15 minutes before they can generate a new Application Password via the REST API.', 'robotstxt-2fa' ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the GeoIP section description.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_geoip_section(): void {
|
||||
echo '<p>' . esc_html__( 'Requires a MaxMind GeoLite2 Country database file (.mmdb). Leave empty to disable country-based rules. Download the free GeoLite2 Country database from maxmind.com.', 'robotstxt-2fa' ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the GeoIP database path field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_geoip_database_path_field(): void {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
printf(
|
||||
'<input type="text" name="%1$s[geoip_database_path]" value="%2$s" size="60" />',
|
||||
esc_attr( self::OPTION_NAME ),
|
||||
esc_attr( $settings['geoip_database_path'] )
|
||||
);
|
||||
|
||||
echo '<p class="description">' . esc_html__( 'Absolute path to the GeoLite2-Country.mmdb file. Leave empty to disable GeoIP features.', 'robotstxt-2fa' ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the GeoIP country allow list textarea.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_geoip_country_allow_field(): void {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
printf(
|
||||
'<textarea name="%1$s[geoip_country_allow]" rows="3" cols="40">%2$s</textarea>',
|
||||
esc_attr( self::OPTION_NAME ),
|
||||
esc_textarea( $settings['geoip_country_allow'] )
|
||||
);
|
||||
|
||||
echo '<p class="description">' . esc_html__( 'ISO country codes (one per line, e.g. ES, US) whose logins bypass the 2FA challenge.', 'robotstxt-2fa' ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the GeoIP country deny list textarea.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_geoip_country_deny_field(): void {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
printf(
|
||||
'<textarea name="%1$s[geoip_country_deny]" rows="3" cols="40">%2$s</textarea>',
|
||||
esc_attr( self::OPTION_NAME ),
|
||||
esc_textarea( $settings['geoip_country_deny'] )
|
||||
);
|
||||
|
||||
echo '<p class="description">' . esc_html__( 'ISO country codes blocked from logging in altogether.', 'robotstxt-2fa' ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the GeoIP always-challenge countries textarea.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_geoip_always_challenge_field(): void {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
printf(
|
||||
'<textarea name="%1$s[geoip_always_challenge]" rows="3" cols="40">%2$s</textarea>',
|
||||
esc_attr( self::OPTION_NAME ),
|
||||
esc_textarea( $settings['geoip_always_challenge'] )
|
||||
);
|
||||
|
||||
echo '<p class="description">' . esc_html__( 'ISO country codes that always trigger a 2FA challenge, regardless of the verification frequency setting.', 'robotstxt-2fa' ) . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Documentation page.
|
||||
*
|
||||
|
|
|
|||
357
includes/admin/class-users-list-table.php
Normal file
357
includes/admin/class-users-list-table.php
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
<?php
|
||||
/**
|
||||
* WP_List_Table subclass for the 2FA audit dashboard.
|
||||
*
|
||||
* @package Robotstxt_2FA
|
||||
*/
|
||||
|
||||
namespace Robotstxt\TwoFA\Admin;
|
||||
|
||||
use Robotstxt\TwoFA\User\Two_Factor_Config;
|
||||
use Robotstxt\TwoFA\User\User_Settings_Repository;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WP_List_Table' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays all users with their 2FA status in a sortable, filterable table.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*/
|
||||
class Users_List_Table extends \WP_List_Table {
|
||||
/**
|
||||
* Global two-factor configuration.
|
||||
*
|
||||
* @var Two_Factor_Config
|
||||
*/
|
||||
private Two_Factor_Config $config;
|
||||
|
||||
/**
|
||||
* User settings repository.
|
||||
*
|
||||
* @var User_Settings_Repository
|
||||
*/
|
||||
private User_Settings_Repository $repository;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Two_Factor_Config $config Global two-factor configuration.
|
||||
* @param User_Settings_Repository $repository User settings repository.
|
||||
*/
|
||||
public function __construct( Two_Factor_Config $config, User_Settings_Repository $repository ) {
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'user',
|
||||
'plural' => 'users',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
|
||||
$this->config = $config;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of columns.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_columns(): array {
|
||||
return array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'user' => __( 'User', 'robotstxt-2fa' ),
|
||||
'roles' => __( 'Role', 'robotstxt-2fa' ),
|
||||
'status' => __( '2FA Status', 'robotstxt-2fa' ),
|
||||
'methods' => __( 'Methods', 'robotstxt-2fa' ),
|
||||
'last_verified' => __( 'Last verified', 'robotstxt-2fa' ),
|
||||
'unused_codes' => __( 'Unused codes', 'robotstxt-2fa' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of sortable columns.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return array<string, array{string, bool}>
|
||||
*/
|
||||
public function get_sortable_columns(): array {
|
||||
return array(
|
||||
'user' => array( 'user_login', false ),
|
||||
'last_verified' => array( 'last_verified', true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the checkbox column.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param mixed $item Current row data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( mixed $item ): string {
|
||||
if ( ! is_array( $item ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$raw_id = $item['ID'] ?? 0;
|
||||
$item_id = is_numeric( $raw_id ) ? (int) $raw_id : 0;
|
||||
|
||||
return sprintf(
|
||||
'<input type="checkbox" name="users[]" value="%s" />',
|
||||
absint( $item_id )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the user column with login, email, and edit link.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param mixed $item Current row data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_user( mixed $item ): string {
|
||||
if ( ! is_array( $item ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$raw_id = $item['ID'] ?? 0;
|
||||
$item_id = is_numeric( $raw_id ) ? (int) $raw_id : 0;
|
||||
$edit_link = get_edit_user_link( $item_id );
|
||||
$raw_login = $item['user_login'] ?? '';
|
||||
$login = esc_html( is_string( $raw_login ) ? $raw_login : '' );
|
||||
$raw_email = $item['user_email'] ?? '';
|
||||
$email = esc_html( is_string( $raw_email ) ? $raw_email : '' );
|
||||
|
||||
$output = '<strong>';
|
||||
|
||||
if ( '' !== $edit_link ) {
|
||||
$output .= '<a href="' . esc_url( $edit_link ) . '">' . $login . '</a>';
|
||||
} else {
|
||||
$output .= $login;
|
||||
}
|
||||
|
||||
$output .= '</strong><br /><span class="description">' . $email . '</span>';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a default column value.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param mixed $item Current row data.
|
||||
* @param mixed $column_name Column identifier.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_default( mixed $item, mixed $column_name ): string {
|
||||
if ( ! is_array( $item ) || ! is_string( $column_name ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$raw_value = $item[ $column_name ] ?? '';
|
||||
|
||||
return esc_html( is_string( $raw_value ) ? $raw_value : '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the available bulk actions.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_bulk_actions(): array {
|
||||
return array(
|
||||
'export_csv' => __( 'Export CSV', 'robotstxt-2fa' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the items for display: query users, compute 2FA status, sort, paginate.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_items(): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET display params only; no state changes.
|
||||
$role = isset( $_REQUEST['role_filter'] ) && is_string( $_REQUEST['role_filter'] ) ? sanitize_key( $_REQUEST['role_filter'] ) : '';
|
||||
$search = isset( $_REQUEST['s'] ) && is_string( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
|
||||
$status_filter = isset( $_REQUEST['status_filter'] ) && is_string( $_REQUEST['status_filter'] ) ? sanitize_key( $_REQUEST['status_filter'] ) : '';
|
||||
$orderby = isset( $_REQUEST['orderby'] ) && is_string( $_REQUEST['orderby'] ) ? sanitize_key( $_REQUEST['orderby'] ) : 'user_login';
|
||||
$raw_order = isset( $_REQUEST['order'] ) && is_string( $_REQUEST['order'] ) ? strtolower( sanitize_key( $_REQUEST['order'] ) ) : 'asc';
|
||||
$order = in_array( $raw_order, array( 'asc', 'desc' ), true ) ? $raw_order : 'asc';
|
||||
$paged = isset( $_REQUEST['paged'] ) && is_numeric( $_REQUEST['paged'] ) ? max( 1, (int) $_REQUEST['paged'] ) : 1;
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$query_args = array(
|
||||
'number' => -1,
|
||||
'fields' => 'all',
|
||||
);
|
||||
|
||||
if ( '' !== $role ) {
|
||||
$query_args['role'] = $role;
|
||||
}
|
||||
|
||||
if ( '' !== $search ) {
|
||||
$query_args['search'] = '*' . $search . '*';
|
||||
}
|
||||
|
||||
$users = get_users( $query_args );
|
||||
$rows = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$settings = $this->repository->get_user_settings( $user->ID );
|
||||
$is_enabled = $settings['enabled'];
|
||||
|
||||
$required_methods = $this->config->get_required_methods_for_user( $user );
|
||||
$is_forced = ! empty( $required_methods );
|
||||
|
||||
if ( $is_enabled ) {
|
||||
$status = 'active';
|
||||
} elseif ( $is_forced ) {
|
||||
$status = 'required';
|
||||
} else {
|
||||
$status = 'optional';
|
||||
}
|
||||
|
||||
// Compute last verified timestamp across all methods/contexts.
|
||||
$last_verified_ts = 0;
|
||||
$raw_verifications = get_user_meta( $user->ID, 'robotstxt_2fa_last_verifications', true );
|
||||
|
||||
if ( is_array( $raw_verifications ) ) {
|
||||
foreach ( $raw_verifications as $method_data ) {
|
||||
if ( is_array( $method_data ) ) {
|
||||
foreach ( $method_data as $context_data ) {
|
||||
if ( is_array( $context_data ) && isset( $context_data['verified_at'] ) && is_int( $context_data['verified_at'] ) ) {
|
||||
$last_verified_ts = max( $last_verified_ts, $context_data['verified_at'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$last_verified_str = $last_verified_ts > 0 ? (string) wp_date( 'Y-m-d H:i', $last_verified_ts ) : '—';
|
||||
|
||||
// Count unused recovery codes.
|
||||
$unused_codes = 0;
|
||||
$raw_codes = get_user_meta( $user->ID, 'robotstxt_2fa_recovery_codes', true );
|
||||
|
||||
if ( is_array( $raw_codes ) && isset( $raw_codes['codes'] ) && is_array( $raw_codes['codes'] ) ) {
|
||||
foreach ( $raw_codes['codes'] as $code_entry ) {
|
||||
if ( is_array( $code_entry ) && empty( $code_entry['used_at'] ) ) {
|
||||
++$unused_codes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
'ID' => $user->ID,
|
||||
'user_login' => $user->user_login,
|
||||
'user_email' => $user->user_email,
|
||||
'roles' => implode( ', ', $user->roles ),
|
||||
'status' => $status,
|
||||
'methods' => implode( ', ', $settings['methods'] ),
|
||||
'last_verified' => $last_verified_str,
|
||||
'unused_codes' => (string) $unused_codes,
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by status.
|
||||
if ( '' !== $status_filter ) {
|
||||
$rows = array_values(
|
||||
array_filter(
|
||||
$rows,
|
||||
static function ( array $row ) use ( $status_filter ): bool {
|
||||
return $row['status'] === $status_filter;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort.
|
||||
usort(
|
||||
$rows,
|
||||
static function ( array $a, array $b ) use ( $orderby, $order ): int {
|
||||
$val_a = (string) ( $a[ $orderby ] ?? '' );
|
||||
$val_b = (string) ( $b[ $orderby ] ?? '' );
|
||||
$cmp = strcmp( $val_a, $val_b );
|
||||
|
||||
return 'desc' === $order ? -$cmp : $cmp;
|
||||
}
|
||||
);
|
||||
|
||||
$total_items = count( $rows );
|
||||
$per_page = 20;
|
||||
$offset = ( $paged - 1 ) * $per_page;
|
||||
|
||||
$this->items = array_slice( $rows, $offset, $per_page );
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'total_items' => $total_items,
|
||||
'per_page' => $per_page,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render extra table navigation filters.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param mixed $which Which tablenav ('top' or 'bottom').
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function extra_tablenav( mixed $which ): void {
|
||||
if ( 'top' !== $which ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET display params only; no state changes.
|
||||
$current_role = isset( $_REQUEST['role_filter'] ) && is_string( $_REQUEST['role_filter'] ) ? sanitize_key( $_REQUEST['role_filter'] ) : '';
|
||||
$current_status = isset( $_REQUEST['status_filter'] ) && is_string( $_REQUEST['status_filter'] ) ? sanitize_key( $_REQUEST['status_filter'] ) : '';
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$roles = get_editable_roles();
|
||||
?>
|
||||
<div class="alignleft actions">
|
||||
<select name="role_filter">
|
||||
<option value=""><?php esc_html_e( '— All roles —', 'robotstxt-2fa' ); ?></option>
|
||||
<?php foreach ( $roles as $role_key => $role_info ) : ?>
|
||||
<option value="<?php echo esc_attr( $role_key ); ?>" <?php selected( $current_role, $role_key ); ?>>
|
||||
<?php echo esc_html( translate_user_role( $role_info['name'] ) ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="status_filter">
|
||||
<option value=""><?php esc_html_e( '— All statuses —', 'robotstxt-2fa' ); ?></option>
|
||||
<option value="active" <?php selected( $current_status, 'active' ); ?>><?php esc_html_e( 'Active', 'robotstxt-2fa' ); ?></option>
|
||||
<option value="required" <?php selected( $current_status, 'required' ); ?>><?php esc_html_e( 'Required (not set up)', 'robotstxt-2fa' ); ?></option>
|
||||
<option value="optional" <?php selected( $current_status, 'optional' ); ?>><?php esc_html_e( 'Optional (disabled)', 'robotstxt-2fa' ); ?></option>
|
||||
</select>
|
||||
<?php submit_button( __( 'Filter', 'robotstxt-2fa' ), '', 'filter_action', false ); ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
|
@ -84,23 +84,28 @@ class Email_Notifications {
|
|||
/**
|
||||
* Schedule or unschedule the digest cron event based on settings.
|
||||
*
|
||||
* Also reschedules when the frequency setting has changed.
|
||||
*
|
||||
* @since 1.3.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_schedule_digest(): void {
|
||||
$notifications = $this->config->get_notifications();
|
||||
$digest_enabled = $notifications['digest_enabled'];
|
||||
$digest_frequency = $notifications['digest_frequency'];
|
||||
$next_scheduled = wp_next_scheduled( self::DIGEST_CRON_HOOK );
|
||||
$notifications = $this->config->get_notifications();
|
||||
$digest_enabled = $notifications['digest_enabled'];
|
||||
|
||||
if ( $digest_enabled ) {
|
||||
if ( false === $next_scheduled ) {
|
||||
$interval = 'monthly' === $digest_frequency ? 'robotstxt_2fa_monthly' : 'weekly';
|
||||
$interval = 'monthly' === $notifications['digest_frequency'] ? 'robotstxt_2fa_monthly' : 'weekly';
|
||||
$scheduled_event = wp_get_scheduled_event( self::DIGEST_CRON_HOOK );
|
||||
|
||||
if ( false === $scheduled_event ) {
|
||||
wp_schedule_event( time() + DAY_IN_SECONDS, $interval, self::DIGEST_CRON_HOOK );
|
||||
} elseif ( isset( $scheduled_event->schedule ) && is_string( $scheduled_event->schedule ) && $scheduled_event->schedule !== $interval ) {
|
||||
wp_clear_scheduled_hook( self::DIGEST_CRON_HOOK );
|
||||
wp_schedule_event( time() + DAY_IN_SECONDS, $interval, self::DIGEST_CRON_HOOK );
|
||||
}
|
||||
} elseif ( false !== $next_scheduled ) {
|
||||
wp_clear_scheduled_hook( self::DIGEST_CRON_HOOK );
|
||||
} elseif ( (bool) wp_next_scheduled( self::DIGEST_CRON_HOOK ) ) {
|
||||
wp_clear_scheduled_hook( self::DIGEST_CRON_HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
222
includes/class-geo-restrictions.php
Normal file
222
includes/class-geo-restrictions.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
/**
|
||||
* GeoIP-based country restrictions for the 2FA challenge.
|
||||
*
|
||||
* @package Robotstxt_2FA
|
||||
*/
|
||||
|
||||
namespace Robotstxt\TwoFA;
|
||||
|
||||
use Robotstxt\TwoFA\User\Two_Factor_Config;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies country-based allow/deny/force-challenge rules using MaxMind GeoIP.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*/
|
||||
class Geo_Restrictions {
|
||||
/**
|
||||
* Global two-factor configuration.
|
||||
*
|
||||
* @var Two_Factor_Config
|
||||
*/
|
||||
private Two_Factor_Config $config;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Two_Factor_Config $config Global two-factor configuration.
|
||||
*/
|
||||
public function __construct( Two_Factor_Config $config ) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
add_filter( 'robotstxt_2fa_skip_challenge', array( $this, 'maybe_skip_for_allowed_country' ), 6, 2 );
|
||||
add_filter( 'authenticate', array( $this, 'maybe_block_denied_country' ), 2, 3 );
|
||||
add_filter( 'robotstxt_2fa_force_challenge', array( $this, 'maybe_force_challenge_for_country' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the 2FA challenge for users logging in from an allowed country.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param bool $skip Whether the challenge is already being skipped.
|
||||
* @param \WP_User $user Authenticated user about to be challenged.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function maybe_skip_for_allowed_country( bool $skip, \WP_User $user ): bool {
|
||||
unset( $user );
|
||||
|
||||
if ( $skip ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$allow_list = $this->config->get_geoip_country_allow();
|
||||
|
||||
if ( '' === $allow_list ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$country = $this->get_country_for_ip();
|
||||
|
||||
if ( '' === $country ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->country_in_list( $country, $allow_list );
|
||||
}
|
||||
|
||||
/**
|
||||
* Block logins from denied countries before credentials are checked.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param mixed $user Previously authenticated user or null.
|
||||
* @param string $username Submitted username.
|
||||
* @param string $password Submitted password.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function maybe_block_denied_country( mixed $user, string $username, string $password ): mixed {
|
||||
unset( $username, $password );
|
||||
|
||||
$deny_list = $this->config->get_geoip_country_deny();
|
||||
|
||||
if ( '' === $deny_list ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$country = $this->get_country_for_ip();
|
||||
|
||||
if ( '' === $country ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
if ( $this->country_in_list( $country, $deny_list ) ) {
|
||||
return new \WP_Error(
|
||||
'robotstxt_2fa_country_blocked',
|
||||
__( 'Login is not allowed from your country.', 'robotstxt-2fa' )
|
||||
);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a fresh 2FA challenge for users logging in from a high-risk country.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param bool $force Whether the challenge is already being forced.
|
||||
* @param \WP_User $user User about to be challenged.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function maybe_force_challenge_for_country( bool $force, \WP_User $user ): bool {
|
||||
unset( $user );
|
||||
|
||||
if ( $force ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$always_challenge = $this->config->get_geoip_always_challenge();
|
||||
|
||||
if ( '' === $always_challenge ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$country = $this->get_country_for_ip();
|
||||
|
||||
if ( '' === $country ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->country_in_list( $country, $always_challenge );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ISO 3166-1 alpha-2 country code for the current request IP.
|
||||
*
|
||||
* Returns an empty string when GeoIP is unavailable or the lookup fails.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_country_for_ip(): string {
|
||||
$db_path = $this->config->get_geoip_database_path();
|
||||
|
||||
if ( '' === $db_path || ! file_exists( $db_path ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! class_exists( '\MaxMind\Db\Reader' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$ip = '';
|
||||
|
||||
if ( isset( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] ) ) {
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
|
||||
}
|
||||
|
||||
if ( '' === $ip ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
$reader = new \MaxMind\Db\Reader( $db_path );
|
||||
$record = $reader->get( $ip );
|
||||
$reader->close();
|
||||
|
||||
if ( ! is_array( $record ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$country = $record['country'] ?? null;
|
||||
|
||||
if ( ! is_array( $country ) || ! isset( $country['iso_code'] ) || ! is_string( $country['iso_code'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return strtoupper( $country['iso_code'] );
|
||||
} catch ( \Throwable $e ) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a country code appears in a newline/comma-separated list.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param string $country ISO 3166-1 alpha-2 country code (uppercase).
|
||||
* @param string $codelist Newline- or comma-separated country codes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function country_in_list( string $country, string $codelist ): bool {
|
||||
if ( '' === $country || '' === $codelist ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$split_result = preg_split( '/[\s,]+/', strtoupper( $codelist ) );
|
||||
$codes = array_filter( array_map( 'trim', is_array( $split_result ) ? $split_result : array() ) );
|
||||
|
||||
return in_array( $country, $codes, true );
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ use Robotstxt\TwoFA\User\Profile_Settings;
|
|||
use Robotstxt\TwoFA\User\Recovery_Codes;
|
||||
use Robotstxt\TwoFA\User\Trusted_Devices;
|
||||
use Robotstxt\TwoFA\User\Two_Factor_Config;
|
||||
use Robotstxt\TwoFA\User\User_Settings_Repository;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
|
|
@ -103,6 +104,13 @@ class Plugin {
|
|||
*/
|
||||
private IP_Restrictions $ip_restrictions;
|
||||
|
||||
/**
|
||||
* GeoIP country restrictions handler.
|
||||
*
|
||||
* @var Geo_Restrictions
|
||||
*/
|
||||
private Geo_Restrictions $geo_restrictions;
|
||||
|
||||
/**
|
||||
* Audit dashboard page.
|
||||
*
|
||||
|
|
@ -128,6 +136,7 @@ class Plugin {
|
|||
*/
|
||||
public function __construct() {
|
||||
$config = new Two_Factor_Config();
|
||||
$repository = new User_Settings_Repository();
|
||||
$this->attempts_log = new Attempts_Log();
|
||||
$this->login_manager = new Login_Form_Manager();
|
||||
$this->profile_settings = new Profile_Settings();
|
||||
|
|
@ -138,7 +147,8 @@ class Plugin {
|
|||
$this->grace_period = new Grace_Period();
|
||||
$this->email_notifications = new Email_Notifications( $config );
|
||||
$this->ip_restrictions = new IP_Restrictions( $config );
|
||||
$this->dashboard_page = new Dashboard_Page( $this->attempts_log );
|
||||
$this->geo_restrictions = new Geo_Restrictions( $config );
|
||||
$this->dashboard_page = new Dashboard_Page( $this->attempts_log, $config, $repository );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -151,6 +161,7 @@ class Plugin {
|
|||
$this->attempts_log->register_hooks();
|
||||
$this->email_notifications->register_hooks();
|
||||
$this->ip_restrictions->register_hooks();
|
||||
$this->geo_restrictions->register_hooks();
|
||||
$this->dashboard_page->register_hooks();
|
||||
$this->login_manager->register_hooks();
|
||||
$this->profile_settings->register_hooks();
|
||||
|
|
@ -160,11 +171,86 @@ class Plugin {
|
|||
$this->trusted_devices->register_hooks();
|
||||
$this->grace_period->register_hooks();
|
||||
|
||||
add_filter( 'rest_pre_dispatch', array( $this, 'maybe_block_app_password_creation' ), 10, 3 );
|
||||
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
add_action( 'cli_init', array( $this, 'register_cli_commands' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block Application Password creation when the user has not recently verified 2FA.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param mixed $result Current result (null = not yet dispatched).
|
||||
* @param \WP_REST_Server $server REST server instance.
|
||||
* @param \WP_REST_Request $request Current request.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function maybe_block_app_password_creation( mixed $result, \WP_REST_Server $server, \WP_REST_Request $request ): mixed {
|
||||
unset( $server );
|
||||
|
||||
if ( null !== $result ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( 'POST' !== $request->get_method() ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$config = new Two_Factor_Config();
|
||||
|
||||
if ( ! $config->is_app_passwords_require_2fa_creation() ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( ! preg_match( '#^/wp/v2/users/\d+/application-passwords$#', $request->get_route() ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
|
||||
if ( ! $user->exists() ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$verified_at = 0;
|
||||
$raw = get_user_meta( $user->ID, 'robotstxt_2fa_last_verifications', true );
|
||||
|
||||
if ( is_array( $raw ) ) {
|
||||
foreach ( $raw as $method_data ) {
|
||||
if ( is_array( $method_data ) ) {
|
||||
foreach ( $method_data as $context_data ) {
|
||||
if ( is_array( $context_data ) && isset( $context_data['verified_at'] ) && is_int( $context_data['verified_at'] ) ) {
|
||||
$verified_at = max( $verified_at, $context_data['verified_at'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the 2FA recency window (in seconds) required before creating an Application Password.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param int $window Seconds since last 2FA verification. Default 900 (15 minutes).
|
||||
*/
|
||||
$window = (int) apply_filters( 'robotstxt_2fa_app_password_verification_window', 900 );
|
||||
|
||||
if ( $verified_at > 0 && ( time() - $verified_at ) <= $window ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return new \WP_Error(
|
||||
'robotstxt_2fa_required',
|
||||
__( 'A recent two-factor authentication verification is required to create an Application Password. Please log in and complete 2FA, then try again.', 'robotstxt-2fa' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WP-CLI commands.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -310,6 +310,37 @@ class Cli_Command extends WP_CLI_Command {
|
|||
\WP_CLI\Utils\format_items( 'table', $rows, array( 'ID', 'Login', 'Email', '2FA', 'Frequency' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a full 2FA status report for all users.
|
||||
*
|
||||
* Equivalent to `wp 2fa list --format=csv` but outputs to stdout for easy
|
||||
* redirection to a file.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--role=<role>]
|
||||
* : Restrict export to a specific WordPress role.
|
||||
*
|
||||
* [--without-2fa]
|
||||
* : Include only users who do not have 2FA enabled.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp 2fa export > 2fa-audit.csv
|
||||
* wp 2fa export --role=editor > editors.csv
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string> $assoc_args Associative arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function export( array $args, array $assoc_args ): void {
|
||||
$assoc_args['format'] = 'csv';
|
||||
$this->list_users( $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce 2FA for a specific user or all users of a role.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1043,7 +1043,20 @@ class Login_Form_Manager {
|
|||
$frequency = $this->get_effective_frequency( $user, $user_settings );
|
||||
|
||||
if ( $this->has_recent_verification( $user, $methods, $frequency ) ) {
|
||||
return '';
|
||||
/**
|
||||
* Filter whether to force a fresh 2FA challenge regardless of the frequency setting.
|
||||
*
|
||||
* Return true to always challenge the user, overriding the frequency-based skip.
|
||||
* Used by GeoIP restrictions to mandate verification from new countries.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @param bool $force Whether to force the challenge. Default false.
|
||||
* @param \WP_User $user User about to be challenged.
|
||||
*/
|
||||
if ( ! (bool) apply_filters( 'robotstxt_2fa_force_challenge', false, $user ) ) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->select_preferred_method( $methods );
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Two_Factor_Config {
|
|||
/**
|
||||
* Retrieve global two-factor settings with multisite support.
|
||||
*
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}}
|
||||
* @return array{role_methods: array<string, array<int, string>>, frequency: string, force_frequency: bool, delete_on_uninstall: bool, max_attempts: int, lockout_duration: int, trust_device_days: int, grace_period_days: int, grace_period_action: string, app_passwords_exempt: bool, ip_allow: string, ip_deny: string, notifications: array{on_enable: bool, on_new_ip: bool, on_recovery_used: bool, on_recovery_used_admin: bool, digest_enabled: bool, digest_frequency: string}, app_passwords_require_2fa_creation: bool, geoip_database_path: string, geoip_country_allow: string, geoip_country_deny: string, geoip_always_challenge: string}
|
||||
*/
|
||||
public function get_settings(): array {
|
||||
$settings_page = new Settings_Page();
|
||||
|
|
@ -225,4 +225,59 @@ class Two_Factor_Config {
|
|||
public function get_notifications(): array {
|
||||
return $this->get_settings()['notifications'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a recent 2FA verification is required before creating an Application Password.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_app_passwords_require_2fa_creation(): bool {
|
||||
return $this->get_settings()['app_passwords_require_2fa_creation'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the GeoIP database file path.
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_geoip_database_path(): string {
|
||||
return $this->get_settings()['geoip_database_path'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the GeoIP country allow list string (newline-separated ISO codes).
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_geoip_country_allow(): string {
|
||||
return $this->get_settings()['geoip_country_allow'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the GeoIP country deny list string (newline-separated ISO codes).
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_geoip_country_deny(): string {
|
||||
return $this->get_settings()['geoip_country_deny'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the GeoIP always-challenge country list string (newline-separated ISO codes).
|
||||
*
|
||||
* @since 1.5.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_geoip_always_challenge(): string {
|
||||
return $this->get_settings()['geoip_always_challenge'];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
readme.txt
34
readme.txt
|
|
@ -4,7 +4,7 @@ Tags: security, two-factor authentication, login, otp
|
|||
Requires at least: 6.4
|
||||
Tested up to: 7.0
|
||||
Requires PHP: 8.0
|
||||
Stable tag: 1.4.0
|
||||
Stable tag: 1.5.0
|
||||
License: GPLv3 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
|
@ -59,6 +59,22 @@ Yes. Activate the plugin at the network level. Network administrators can set an
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 1.5.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
**Added**
|
||||
|
||||
* Full audit user table — WP_List_Table with sortable columns (user, last verified), role and status filters, and one-click CSV export.
|
||||
* GeoIP country restrictions — optional MaxMind GeoLite2 integration: per-country allow list (bypass 2FA), deny list (block login), always-challenge list (override frequency). Disabled when no database file is configured.
|
||||
* Require 2FA before creating Application Passwords — REST endpoint rejects requests unless 2FA was completed within the last 15 minutes (configurable via `robotstxt_2fa_app_password_verification_window` filter).
|
||||
* WP-CLI `wp 2fa export` — outputs a full CSV report.
|
||||
* New developer filter `robotstxt_2fa_force_challenge` — override frequency-based skip and always require a fresh challenge.
|
||||
|
||||
**Fixed**
|
||||
|
||||
* Email digest cron now correctly reschedules when the frequency is changed between weekly and monthly.
|
||||
|
||||
= 1.4.0 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
|
@ -93,22 +109,6 @@ _Release date: 2026-06-05_
|
|||
* `wp 2fa bypass <user_id> [--days=<n>]` — grant a temporary bypass for a locked-out user.
|
||||
* WP-CLI commands are only loaded when the `WP_CLI` constant is defined and truthy.
|
||||
|
||||
= 1.2.1 =
|
||||
|
||||
_Release date: 2026-06-05_
|
||||
|
||||
**Fixed**
|
||||
|
||||
* Recovery code confirmation without checking the method checkbox now correctly activates the method and enables 2FA.
|
||||
* Fatal error on admin profile pages in multisite — `maybe_handle_frontend_save()` now returns early when `is_admin()` is true.
|
||||
* OTP and recovery confirmation text inputs were disabled by JS when the Enable toggle was off, preventing values from reaching the server.
|
||||
* "Regenerate codes" now removes recovery codes from active methods and requires re-confirmation before reactivating.
|
||||
* Entering a valid OTP code or recovery confirmation code activates the method even without checking the Enable checkbox.
|
||||
|
||||
**Security**
|
||||
|
||||
* `RECOVERY_REGENERATE_FIELD` POST value now validated with strict `=== '1'` check.
|
||||
|
||||
= Previous versions =
|
||||
|
||||
For the full changelog see the [changelog.txt](https://git.robotstxt.es/ROBOTSTXT/robotstxt-2fa/raw/branch/main/changelog.txt) file.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: 2FA (by ROBOTSTXT)
|
||||
* Plugin URI: https://www.robotstxt.es/plugins/robotstxt-2fa/
|
||||
* Description: Adds two-factor authentication to the WordPress login flow.
|
||||
* Version: 1.4.0
|
||||
* Version: 1.5.0
|
||||
* Author: ROBOTSTXT
|
||||
* Author URI: https://www.robotstxt.es/
|
||||
* Text Domain: robotstxt-2fa
|
||||
|
|
@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
if ( ! defined( 'ROBOTSTXT_2FA_VERSION' ) ) {
|
||||
define( 'ROBOTSTXT_2FA_VERSION', '1.4.0' );
|
||||
define( 'ROBOTSTXT_2FA_VERSION', '1.5.0' );
|
||||
}
|
||||
|
||||
if ( ! defined( 'ROBOTSTXT_2FA_FILE' ) ) {
|
||||
|
|
|
|||
18
update.json
18
update.json
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "2FA (by ROBOTSTXT)",
|
||||
"slug": "robotstxt-2fa",
|
||||
"version": "1.4.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-2fa/releases/download/1.4.0/robotstxt-2fa-1.4.0.zip",
|
||||
"version": "1.5.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-2fa/releases/download/1.5.0/robotstxt-2fa-1.5.0.zip",
|
||||
"requires": "6.4",
|
||||
"requires_php": "8.0",
|
||||
"tested": "7.1",
|
||||
|
|
@ -11,17 +11,11 @@
|
|||
"author_profile": "https://www.robotstxt.es/",
|
||||
"homepage": "https://www.robotstxt.es/plugins/robotstxt-2fa/",
|
||||
"description": "Adds per-role two-factor authentication to the WordPress login flow. Supports email codes, authenticator apps (TOTP), and recovery codes.",
|
||||
"changelog": "<h3>1.4.0 - 2026-06-05</h3><ul><li><strong>Added:</strong> Audit Dashboard with summary cards, failed attempts log, and 2FA status column in Users list</li><li><strong>Added:</strong> Email notifications — admin-enabled 2FA, new location login, recovery code used, weekly/monthly digest</li><li><strong>Added:</strong> Application Passwords exemption — REST API clients skip the 2FA challenge by default</li><li><strong>Added:</strong> IP whitelist (bypass 2FA) and IP blacklist (block login) with CIDR support</li></ul>",
|
||||
"changelog": "<h3>1.5.0 - 2026-06-05</h3><ul><li><strong>Added:</strong> Full audit dashboard with WP_List_Table (sortable, filterable by role/status, CSV export)</li><li><strong>Added:</strong> GeoIP country allow/deny/always-challenge rules via MaxMind GeoLite2 (optional)</li><li><strong>Added:</strong> Require recent 2FA verification before creating Application Passwords</li><li><strong>Added:</strong> WP-CLI export subcommand</li><li><strong>Fixed:</strong> Email digest cron now correctly reschedules when frequency setting changes</li></ul>",
|
||||
"sections": {
|
||||
"description": "Adds per-role two-factor authentication to the WordPress login flow. Supports email codes, authenticator apps (TOTP), and recovery codes.",
|
||||
"changelog": "<h3>1.4.0 - 2026-06-05</h3><ul><li><strong>Added:</strong> Audit Dashboard with summary cards, failed attempts log, and 2FA status column in Users list</li><li><strong>Added:</strong> Email notifications — admin-enabled 2FA, new location login, recovery code used, weekly/monthly digest</li><li><strong>Added:</strong> Application Passwords exemption — REST API clients skip the 2FA challenge by default</li><li><strong>Added:</strong> IP whitelist (bypass 2FA) and IP blacklist (block login) with CIDR support</li></ul>"
|
||||
"changelog": "<h3>1.5.0 - 2026-06-05</h3><ul><li><strong>Added:</strong> Full audit dashboard with WP_List_Table (sortable, filterable by role/status, CSV export)</li><li><strong>Added:</strong> GeoIP country allow/deny/always-challenge rules via MaxMind GeoLite2 (optional)</li><li><strong>Added:</strong> Require recent 2FA verification before creating Application Passwords</li><li><strong>Added:</strong> WP-CLI export subcommand</li><li><strong>Fixed:</strong> Email digest cron now correctly reschedules when frequency setting changes</li></ul>"
|
||||
},
|
||||
"banners": {
|
||||
"low": "",
|
||||
"high": ""
|
||||
},
|
||||
"icons": {
|
||||
"1x": "",
|
||||
"2x": ""
|
||||
}
|
||||
"banners": { "low": "", "high": "" },
|
||||
"icons": { "1x": "", "2x": "" }
|
||||
}
|
||||
|
|
|
|||
5
vendor/composer/autoload_classmap.php
vendored
5
vendor/composer/autoload_classmap.php
vendored
|
|
@ -79,4 +79,9 @@ return array(
|
|||
'DASPRiD\\Enum\\Exception\\SerializeNotSupportedException' => $vendorDir . '/dasprid/enum/src/Exception/SerializeNotSupportedException.php',
|
||||
'DASPRiD\\Enum\\Exception\\UnserializeNotSupportedException' => $vendorDir . '/dasprid/enum/src/Exception/UnserializeNotSupportedException.php',
|
||||
'DASPRiD\\Enum\\NullValue' => $vendorDir . '/dasprid/enum/src/NullValue.php',
|
||||
'MaxMind\\Db\\Reader' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader.php',
|
||||
'MaxMind\\Db\\Reader\\Decoder' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php',
|
||||
'MaxMind\\Db\\Reader\\InvalidDatabaseException' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php',
|
||||
'MaxMind\\Db\\Reader\\Metadata' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php',
|
||||
'MaxMind\\Db\\Reader\\Util' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php',
|
||||
);
|
||||
|
|
|
|||
1
vendor/composer/autoload_psr4.php
vendored
1
vendor/composer/autoload_psr4.php
vendored
|
|
@ -6,6 +6,7 @@ $vendorDir = dirname(__DIR__);
|
|||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'),
|
||||
'DASPRiD\\Enum\\' => array($vendorDir . '/dasprid/enum/src'),
|
||||
'BaconQrCode\\' => array($vendorDir . '/bacon/bacon-qr-code/src'),
|
||||
);
|
||||
|
|
|
|||
13
vendor/composer/autoload_static.php
vendored
13
vendor/composer/autoload_static.php
vendored
|
|
@ -7,6 +7,10 @@ namespace Composer\Autoload;
|
|||
class ComposerStaticInit3c8f2c10a8655d88b9115205b533c7e2
|
||||
{
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'M' =>
|
||||
array (
|
||||
'MaxMind\\Db\\' => 11,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'DASPRiD\\Enum\\' => 13,
|
||||
|
|
@ -18,6 +22,10 @@ class ComposerStaticInit3c8f2c10a8655d88b9115205b533c7e2
|
|||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'MaxMind\\Db\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db',
|
||||
),
|
||||
'DASPRiD\\Enum\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/dasprid/enum/src',
|
||||
|
|
@ -102,6 +110,11 @@ class ComposerStaticInit3c8f2c10a8655d88b9115205b533c7e2
|
|||
'DASPRiD\\Enum\\Exception\\SerializeNotSupportedException' => __DIR__ . '/..' . '/dasprid/enum/src/Exception/SerializeNotSupportedException.php',
|
||||
'DASPRiD\\Enum\\Exception\\UnserializeNotSupportedException' => __DIR__ . '/..' . '/dasprid/enum/src/Exception/UnserializeNotSupportedException.php',
|
||||
'DASPRiD\\Enum\\NullValue' => __DIR__ . '/..' . '/dasprid/enum/src/NullValue.php',
|
||||
'MaxMind\\Db\\Reader' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader.php',
|
||||
'MaxMind\\Db\\Reader\\Decoder' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php',
|
||||
'MaxMind\\Db\\Reader\\InvalidDatabaseException' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php',
|
||||
'MaxMind\\Db\\Reader\\Metadata' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php',
|
||||
'MaxMind\\Db\\Reader\\Util' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
|
|
|
|||
67
vendor/composer/installed.json
vendored
67
vendor/composer/installed.json
vendored
|
|
@ -110,6 +110,73 @@
|
|||
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
|
||||
},
|
||||
"install-path": "../dasprid/enum"
|
||||
},
|
||||
{
|
||||
"name": "maxmind-db/reader",
|
||||
"version": "v1.13.1",
|
||||
"version_normalized": "1.13.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
|
||||
"reference": "2194f58d0f024ce923e685cdf92af3daf9951908"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/2194f58d0f024ce923e685cdf92af3daf9951908",
|
||||
"reference": "2194f58d0f024ce923e685cdf92af3daf9951908",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-maxminddb": "<1.11.1 || >=2.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.*",
|
||||
"phpstan/phpstan": "*",
|
||||
"phpunit/phpunit": ">=8.0.0,<10.0.0",
|
||||
"squizlabs/php_codesniffer": "4.*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
|
||||
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
|
||||
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups",
|
||||
"maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
|
||||
},
|
||||
"time": "2025-11-21T22:24:26+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MaxMind\\Db\\": "src/MaxMind/Db"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gregory J. Oschwald",
|
||||
"email": "goschwald@maxmind.com",
|
||||
"homepage": "https://www.maxmind.com/"
|
||||
}
|
||||
],
|
||||
"description": "MaxMind DB Reader API",
|
||||
"homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
|
||||
"keywords": [
|
||||
"database",
|
||||
"geoip",
|
||||
"geoip2",
|
||||
"geolocation",
|
||||
"maxmind"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
|
||||
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.13.1"
|
||||
},
|
||||
"install-path": "../maxmind-db/reader"
|
||||
}
|
||||
],
|
||||
"dev": false,
|
||||
|
|
|
|||
9
vendor/composer/installed.php
vendored
9
vendor/composer/installed.php
vendored
|
|
@ -28,6 +28,15 @@
|
|||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'maxmind-db/reader' => array(
|
||||
'pretty_version' => 'v1.13.1',
|
||||
'version' => '1.13.1.0',
|
||||
'reference' => '2194f58d0f024ce923e685cdf92af3daf9951908',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../maxmind-db/reader',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'robotstxt/robotstxt-2fa' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
|
|
|
|||
261
vendor/maxmind-db/reader/CHANGELOG.md
vendored
Normal file
261
vendor/maxmind-db/reader/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
1.13.1 (2025-11-21)
|
||||
-------------------
|
||||
|
||||
* First PIE release. No other changes.
|
||||
|
||||
1.13.0 (2025-11-20)
|
||||
-------------------
|
||||
|
||||
* A redundant `filesize()` call in the reader's constructor was removed.
|
||||
Pull request by Pavel Djundik. GitHub #189.
|
||||
|
||||
1.12.1 (2025-05-05)
|
||||
-------------------
|
||||
|
||||
* The C extension now checks that the database metadata lookup was
|
||||
successful.
|
||||
|
||||
1.12.0 (2024-11-14)
|
||||
-------------------
|
||||
|
||||
* Improve the error handling when the user tries to open a directory
|
||||
with the pure PHP reader.
|
||||
* Improve the typehints on arrays in the PHPDocs.
|
||||
|
||||
1.11.1 (2023-12-01)
|
||||
-------------------
|
||||
|
||||
* Resolve warnings when compiling the C extension.
|
||||
* Fix various type issues detected by PHPStan level. Pull request by
|
||||
LauraTaylorUK. GitHub #160.
|
||||
|
||||
1.11.0 (2021-10-18)
|
||||
-------------------
|
||||
|
||||
* Replace runtime define of a constant to facilitate opcache preloading.
|
||||
Reported by vedadkajtaz. GitHub #134.
|
||||
* Resolve minor issue found by the Clang static analyzer in the C
|
||||
extension.
|
||||
|
||||
1.10.1 (2021-04-14)
|
||||
-------------------
|
||||
|
||||
* Fix a `TypeError` exception in the pure PHP reader when using large
|
||||
databases on 32-bit PHP builds with the `bcmath` extension. Reported
|
||||
by dodo1708. GitHub #124.
|
||||
|
||||
1.10.0 (2021-02-09)
|
||||
-------------------
|
||||
|
||||
* When using the pure PHP reader, unsigned integers up to PHP_MAX_INT
|
||||
will now be integers in PHP rather than strings. Previously integers
|
||||
greater than 2^24 on 32-bit platforms and 2^56 on 64-bit platforms
|
||||
would be strings due to the use of `gmp` or `bcmath` to decode them.
|
||||
Reported by Alejandro Celaya. GitHub #119.
|
||||
|
||||
1.9.0 (2021-01-07)
|
||||
------------------
|
||||
|
||||
* The `maxminddb` extension is now buildable on Windows. Pull request
|
||||
by Jan Ehrhardt. GitHub #115.
|
||||
|
||||
1.8.0 (2020-10-01)
|
||||
------------------
|
||||
|
||||
* Fixes for PHP 8.0. Pull Request by Remi Collet. GitHub #108.
|
||||
|
||||
1.7.0 (2020-08-07)
|
||||
------------------
|
||||
|
||||
* IMPORTANT: PHP 7.2 or greater is now required.
|
||||
* The extension no longer depends on the pure PHP classes in
|
||||
`maxmind-db/reader`. You can use it independently.
|
||||
* Type hints have been added to both the pure PHP implementation
|
||||
and the extension.
|
||||
* The `metadata` method on the reader now returns a new copy of the
|
||||
metadata object rather than the actual object used by the reader.
|
||||
* Work around PHP `is_readable()` bug. Reported by Ben Roberts. GitHub
|
||||
#92.
|
||||
* This is the first release of the extension as a PECL package.
|
||||
GitHub #34.
|
||||
|
||||
1.6.0 (2019-12-19)
|
||||
------------------
|
||||
|
||||
* 1.5.0 and 1.5.1 contained a possible memory corruptions when using
|
||||
`getWithPrefixLen`. This has been fixed. Reported by proton-ab.
|
||||
GitHub #96.
|
||||
* The `composer.json` file now conflicts with all versions of the
|
||||
`maxminddb` C extension less than the Composer version. This is to
|
||||
reduce the chance of having an older, conflicting version of the
|
||||
extension installed. You will need to upgrade the extension before
|
||||
running `composer update`. Pull request by Benoît Burnichon. GitHub
|
||||
#97.
|
||||
|
||||
1.5.1 (2019-12-12)
|
||||
------------------
|
||||
|
||||
* Minor performance improvements.
|
||||
* Make tests pass with older versions of libmaxminddb. PR by Remi
|
||||
Collet. GitHub #90.
|
||||
* Test enhancements. PR by Chun-Sheng, Li. GitHub #91.
|
||||
|
||||
1.5.0 (2019-09-30)
|
||||
------------------
|
||||
|
||||
* PHP 5.6 or greater is now required.
|
||||
* The C extension now supports PHP 8. Pull request by John Boehr.
|
||||
GitHub #87.
|
||||
* A new method, `getWithPrefixLen`, was added to the `Reader` class.
|
||||
This method returns an array containing the record and the prefix
|
||||
length for that record. GitHub #89.
|
||||
|
||||
1.4.1 (2019-01-04)
|
||||
------------------
|
||||
|
||||
* The `maxminddb` extension now returns a string when a `uint32`
|
||||
value is greater than `LONG_MAX`. Previously, the value would
|
||||
overflow. This generally only affects 32-bit machines. Reported
|
||||
by Remi Collet. GitHub #79.
|
||||
* For `uint64` values, the `maxminddb` extension now returns an
|
||||
integer rather than a string when the value is less than or equal
|
||||
to `LONG_MAX`. This more closely matches the behavior of the pure
|
||||
PHP reader.
|
||||
|
||||
1.4.0 (2018-11-20)
|
||||
------------------
|
||||
|
||||
* The `maxminddb` extension now has the arginfo when using reflection.
|
||||
PR by Remi Collet. GitHub #75.
|
||||
* The `maxminddb` extension now provides `MINFO()` function that
|
||||
displays the extension version and the libmaxminddb version. PR by
|
||||
Remi Collet. GitHub #74.
|
||||
* The `maxminddb` `configure` script now uses `pkg-config` when
|
||||
available to get libmaxmindb build info. PR by Remi Collet.
|
||||
GitHub #73.
|
||||
* The pure PHP reader now correctly decodes integers on 32-bit platforms.
|
||||
Previously, large integers would overflow. Reported by Remi Collet.
|
||||
GitHub #77.
|
||||
* There are small performance improvements for the pure PHP reader.
|
||||
|
||||
1.3.0 (2018-02-21)
|
||||
------------------
|
||||
|
||||
* IMPORTANT: The `maxminddb` extension now obeys `open_basedir`. If
|
||||
`open_basedir` is set, you _must_ store the database within the
|
||||
specified directory. Placing the file outside of this directory
|
||||
will result in an exception. Please test your integration before
|
||||
upgrading the extension. This does not affect the pure PHP
|
||||
implementation, which has always had this restriction. Reported
|
||||
by Benoît Burnichon. GitHub #61.
|
||||
* A custom `autoload.php` file is provided for installations without
|
||||
Composer. GitHub #56.
|
||||
|
||||
1.2.0 (2017-10-27)
|
||||
------------------
|
||||
|
||||
* PHP 5.4 or greater is now required.
|
||||
* The `Reader` class for the `maxminddb` extension is no longer final.
|
||||
This was change to match the behavior of the pure PHP class.
|
||||
Reported and fixed by venyii. GitHub #52 & #54.
|
||||
|
||||
1.1.3 (2017-01-19)
|
||||
------------------
|
||||
|
||||
* Fix incorrect version in `ext/php_maxminddb.h`. GitHub #48.
|
||||
|
||||
1.1.2 (2016-11-22)
|
||||
------------------
|
||||
|
||||
* Searching for database metadata only occurs within the last 128KB
|
||||
(128 * 1024 bytes) of the file, speeding detection of corrupt
|
||||
datafiles. Reported by Eric Teubert. GitHub #42.
|
||||
* Suggest relevant extensions when installing with Composer. GitHub #37.
|
||||
|
||||
1.1.1 (2016-09-15)
|
||||
------------------
|
||||
|
||||
* Development files were added to the `.gitattributes` as `export-ignore` so
|
||||
that they are not part of the Composer release. Pull request by Michele
|
||||
Locati. GitHub #39.
|
||||
|
||||
1.1.0 (2016-01-04)
|
||||
------------------
|
||||
|
||||
* The MaxMind DB extension now supports PHP 7. Pull request by John Boehr.
|
||||
GitHub #27.
|
||||
|
||||
1.0.3 (2015-03-13)
|
||||
------------------
|
||||
|
||||
* All uses of `strlen` were removed. This should prevent issues in situations
|
||||
where the function is overloaded or otherwise broken.
|
||||
|
||||
1.0.2 (2015-01-19)
|
||||
------------------
|
||||
|
||||
* Previously the MaxMind DB extension would cause a segfault if the Reader
|
||||
object's destructor was called without first having called the constructor.
|
||||
(Reported by Matthias Saou & Juan Peri. GitHub #20.)
|
||||
|
||||
1.0.1 (2015-01-12)
|
||||
------------------
|
||||
|
||||
* In the last several releases, the version number in the extension was
|
||||
incorrect. This release is being done to correct it. No other code changes
|
||||
are included.
|
||||
|
||||
1.0.0 (2014-09-22)
|
||||
------------------
|
||||
|
||||
* First production release.
|
||||
* In the pure PHP reader, a string length test after `fread()` was replaced
|
||||
with the difference between the start pointer and the end pointer. This
|
||||
provided a 15% speed increase.
|
||||
|
||||
0.3.3 (2014-09-15)
|
||||
------------------
|
||||
|
||||
* Clarified behavior of 128-bit type in documentation.
|
||||
* Updated phpunit and fixed some test breakage from the newer version.
|
||||
|
||||
0.3.2 (2014-09-10)
|
||||
------------------
|
||||
|
||||
* Fixed invalid reference to global class RuntimeException from namespaced
|
||||
code. Fixed by Steven Don. GitHub issue #15.
|
||||
* Additional documentation of `Metadata` class as well as misc. documentation
|
||||
cleanup.
|
||||
|
||||
0.3.1 (2014-05-01)
|
||||
------------------
|
||||
|
||||
* The API now works when `mbstring.func_overload` is set.
|
||||
* BCMath is no longer required. If the decoder encounters a big integer,
|
||||
it will try to use GMP and then BCMath. If both of those fail, it will
|
||||
throw an exception. No databases released by MaxMind currently use big
|
||||
integers.
|
||||
* The API now officially supports HHVM when using the pure PHP reader.
|
||||
|
||||
0.3.0 (2014-02-19)
|
||||
------------------
|
||||
|
||||
* This API is now licensed under the Apache License, Version 2.0.
|
||||
* The code for the C extension was cleaned up, fixing several potential
|
||||
issues.
|
||||
|
||||
0.2.0 (2013-10-21)
|
||||
------------------
|
||||
|
||||
* Added optional C extension for using libmaxminddb in place of the pure PHP
|
||||
reader.
|
||||
* Significantly improved error handling in pure PHP reader.
|
||||
* Improved performance for IPv4 lookups in an IPv6 database.
|
||||
|
||||
0.1.0 (2013-07-16)
|
||||
------------------
|
||||
|
||||
* Initial release
|
||||
202
vendor/maxmind-db/reader/LICENSE
vendored
Normal file
202
vendor/maxmind-db/reader/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
214
vendor/maxmind-db/reader/README.md
vendored
Normal file
214
vendor/maxmind-db/reader/README.md
vendored
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
# MaxMind DB Reader PHP API #
|
||||
|
||||
## Description ##
|
||||
|
||||
This is the PHP API for reading MaxMind DB files. MaxMind DB is a binary file
|
||||
format that stores data indexed by IP address subnets (IPv4 or IPv6).
|
||||
|
||||
## Installation ##
|
||||
|
||||
### C Extension (Recommended for Performance) ###
|
||||
|
||||
For significantly faster IP lookups, we recommend installing the C extension via
|
||||
[PIE](https://github.com/php/pie):
|
||||
|
||||
```bash
|
||||
pie install maxmind-db/reader-ext
|
||||
```
|
||||
|
||||
The C extension requires the [libmaxminddb](https://github.com/maxmind/libmaxminddb)
|
||||
C library. See the [installation instructions](https://github.com/maxmind/MaxMind-DB-Reader-php-ext#prerequisites)
|
||||
for your platform.
|
||||
|
||||
### Pure PHP (No Compilation Required) ###
|
||||
|
||||
If you prefer not to compile a C extension or need maximum portability, you can
|
||||
install the pure PHP implementation with [Composer](https://getcomposer.org/).
|
||||
|
||||
### Download Composer ###
|
||||
|
||||
To download Composer, run in the root directory of your project:
|
||||
|
||||
```bash
|
||||
curl -sS https://getcomposer.org/installer | php
|
||||
```
|
||||
|
||||
You should now have the file `composer.phar` in your project directory.
|
||||
|
||||
### Install Dependencies ###
|
||||
|
||||
Run in your project root:
|
||||
|
||||
```
|
||||
php composer.phar require maxmind-db/reader:^1.13.1
|
||||
```
|
||||
|
||||
You should now have the files `composer.json` and `composer.lock` as well as
|
||||
the directory `vendor` in your project directory. If you use a version control
|
||||
system, `composer.json` should be added to it.
|
||||
|
||||
### Require Autoloader ###
|
||||
|
||||
After installing the dependencies, you need to require the Composer autoloader
|
||||
from your code:
|
||||
|
||||
```php
|
||||
require 'vendor/autoload.php';
|
||||
```
|
||||
|
||||
## Installation (Standalone) ##
|
||||
|
||||
If you don't want to use Composer for some reason, a custom
|
||||
`autoload.php` is provided for you in the project root. To use the
|
||||
library, simply include that file,
|
||||
|
||||
```php
|
||||
require('/path/to/MaxMind-DB-Reader-php/autoload.php');
|
||||
```
|
||||
|
||||
and then instantiate the reader class normally:
|
||||
|
||||
```php
|
||||
use MaxMind\Db\Reader;
|
||||
$reader = new Reader('example.mmdb');
|
||||
```
|
||||
|
||||
## Installation (RPM)
|
||||
|
||||
RPMs are available in the [official Fedora repository](https://apps.fedoraproject.org/packages/php-maxminddb).
|
||||
|
||||
To install on Fedora, run:
|
||||
|
||||
```bash
|
||||
dnf install php-maxminddb
|
||||
```
|
||||
|
||||
To install on CentOS or RHEL 7, first [enable the EPEL repository](https://fedoraproject.org/wiki/EPEL)
|
||||
and then run:
|
||||
|
||||
```bash
|
||||
yum install php-maxminddb
|
||||
```
|
||||
|
||||
Please note that these packages are *not* maintained by MaxMind.
|
||||
|
||||
## Usage ##
|
||||
|
||||
## Example ##
|
||||
|
||||
```php
|
||||
<?php
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
use MaxMind\Db\Reader;
|
||||
|
||||
$ipAddress = '24.24.24.24';
|
||||
$databaseFile = 'GeoIP2-City.mmdb';
|
||||
|
||||
$reader = new Reader($databaseFile);
|
||||
|
||||
// get returns just the record for the IP address
|
||||
print_r($reader->get($ipAddress));
|
||||
|
||||
// getWithPrefixLen returns an array containing the record and the
|
||||
// associated prefix length for that record.
|
||||
print_r($reader->getWithPrefixLen($ipAddress));
|
||||
|
||||
$reader->close();
|
||||
```
|
||||
|
||||
## Optional PHP C Extension ##
|
||||
|
||||
MaxMind provides an optional C extension that is a drop-in replacement for
|
||||
`MaxMind\Db\Reader`. In order to use this extension, you must install the
|
||||
Reader API as described above and install the extension as described below. If
|
||||
you are using an autoloader, no changes to your code should be necessary.
|
||||
|
||||
### Installing Extension via PIE (Recommended) ###
|
||||
|
||||
We recommend installing the extension via [PIE](https://github.com/php/pie):
|
||||
|
||||
```bash
|
||||
pie install maxmind-db/reader-ext
|
||||
```
|
||||
|
||||
See the [extension repository](https://github.com/maxmind/MaxMind-DB-Reader-php-ext#prerequisites)
|
||||
for prerequisites including libmaxminddb installation instructions.
|
||||
|
||||
### Installing Extension via PECL (Legacy) ###
|
||||
|
||||
First install [libmaxminddb](https://github.com/maxmind/libmaxminddb) as
|
||||
described in its [README.md
|
||||
file](https://github.com/maxmind/libmaxminddb/blob/main/README.md#installing-from-a-tarball).
|
||||
After successfully installing libmaxmindb, you may install the extension
|
||||
from [PECL](https://pecl.php.net/package/maxminddb):
|
||||
|
||||
```
|
||||
pecl install maxminddb
|
||||
```
|
||||
|
||||
### Installing Extension from Source ###
|
||||
|
||||
Alternatively, you may install it from the source. To do so, run the following
|
||||
commands from the top-level directory of this distribution:
|
||||
|
||||
```
|
||||
cd ext
|
||||
phpize
|
||||
./configure
|
||||
make
|
||||
make test
|
||||
sudo make install
|
||||
```
|
||||
|
||||
You then must load your extension. The recommended method is to add the
|
||||
following to your `php.ini` file:
|
||||
|
||||
```
|
||||
extension=maxminddb.so
|
||||
```
|
||||
|
||||
Note: You may need to install the PHP development package on your OS such as
|
||||
php5-dev for Debian-based systems or php-devel for RedHat/Fedora-based ones.
|
||||
|
||||
## 128-bit Integer Support ##
|
||||
|
||||
The MaxMind DB format includes 128-bit unsigned integer as a type. Although
|
||||
no MaxMind-distributed database currently makes use of this type, both the
|
||||
pure PHP reader and the C extension support this type. The pure PHP reader
|
||||
requires gmp or bcmath to read databases with 128-bit unsigned integers.
|
||||
|
||||
The integer is currently returned as a hexadecimal string (prefixed with "0x")
|
||||
by the C extension and a decimal string (no prefix) by the pure PHP reader.
|
||||
Any change to make the reader implementations always return either a
|
||||
hexadecimal or decimal representation of the integer will NOT be considered a
|
||||
breaking change.
|
||||
|
||||
## Support ##
|
||||
|
||||
Please report all issues with this code using the [GitHub issue tracker](https://github.com/maxmind/MaxMind-DB-Reader-php/issues).
|
||||
|
||||
If you are having an issue with a MaxMind service that is not specific to the
|
||||
client API, please see [our support page](https://www.maxmind.com/en/support).
|
||||
|
||||
## Requirements ##
|
||||
|
||||
This library requires PHP 7.2 or greater.
|
||||
|
||||
The GMP or BCMath extension may be required to read some databases
|
||||
using the pure PHP API.
|
||||
|
||||
## Contributing ##
|
||||
|
||||
Patches and pull requests are encouraged. All code should follow the PSR-1 and
|
||||
PSR-2 style guidelines. Please include unit tests whenever possible.
|
||||
|
||||
## Versioning ##
|
||||
|
||||
The MaxMind DB Reader PHP API uses [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## Copyright and License ##
|
||||
|
||||
This software is Copyright (c) 2014-2025 by MaxMind, Inc.
|
||||
|
||||
This is free software, licensed under the Apache License, Version 2.0.
|
||||
47
vendor/maxmind-db/reader/autoload.php
vendored
Normal file
47
vendor/maxmind-db/reader/autoload.php
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PSR-4 autoloader implementation for the MaxMind\DB namespace.
|
||||
* First we define the 'mmdb_autoload' function, and then we register
|
||||
* it with 'spl_autoload_register' so that PHP knows to use it.
|
||||
*
|
||||
* @param mixed $class
|
||||
*/
|
||||
|
||||
/**
|
||||
* Automatically include the file that defines <code>class</code>.
|
||||
*
|
||||
* @param string $class
|
||||
* the name of the class to load
|
||||
*/
|
||||
function mmdb_autoload($class): void
|
||||
{
|
||||
/*
|
||||
* A project-specific mapping between the namespaces and where
|
||||
* they're located. By convention, we include the trailing
|
||||
* slashes. The one-element array here simply makes things easy
|
||||
* to extend in the future if (for example) the test classes
|
||||
* begin to use one another.
|
||||
*/
|
||||
$namespace_map = ['MaxMind\Db\\' => __DIR__ . '/src/MaxMind/Db/'];
|
||||
|
||||
foreach ($namespace_map as $prefix => $dir) {
|
||||
// First swap out the namespace prefix with a directory...
|
||||
$path = str_replace($prefix, $dir, $class);
|
||||
|
||||
// replace the namespace separator with a directory separator...
|
||||
$path = str_replace('\\', '/', $path);
|
||||
|
||||
// and finally, add the PHP file extension to the result.
|
||||
$path .= '.php';
|
||||
|
||||
// $path should now contain the path to a PHP file defining $class
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spl_autoload_register('mmdb_autoload');
|
||||
43
vendor/maxmind-db/reader/composer.json
vendored
Normal file
43
vendor/maxmind-db/reader/composer.json
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "maxmind-db/reader",
|
||||
"description": "MaxMind DB Reader API",
|
||||
"keywords": ["database", "geoip", "geoip2", "geolocation", "maxmind"],
|
||||
"homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
|
||||
"type": "library",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gregory J. Oschwald",
|
||||
"email": "goschwald@maxmind.com",
|
||||
"homepage": "https://www.maxmind.com/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
|
||||
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
|
||||
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups",
|
||||
"maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-maxminddb": "<1.11.1 || >=2.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.*",
|
||||
"phpunit/phpunit": ">=8.0.0,<10.0.0",
|
||||
"squizlabs/php_codesniffer": "4.*",
|
||||
"phpstan/phpstan": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MaxMind\\Db\\": "src/MaxMind/Db"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"MaxMind\\Db\\Test\\Reader\\": "tests/MaxMind/Db/Test/Reader"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
vendor/maxmind-db/reader/ext/config.m4
vendored
Normal file
40
vendor/maxmind-db/reader/ext/config.m4
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
PHP_ARG_WITH(maxminddb,
|
||||
[Whether to enable the MaxMind DB Reader extension],
|
||||
[ --with-maxminddb Enable MaxMind DB Reader extension support])
|
||||
|
||||
PHP_ARG_ENABLE(maxminddb-debug, for MaxMind DB debug support,
|
||||
[ --enable-maxminddb-debug Enable MaxMind DB debug support], no, no)
|
||||
|
||||
if test $PHP_MAXMINDDB != "no"; then
|
||||
|
||||
AC_PATH_PROG(PKG_CONFIG, pkg-config, no)
|
||||
|
||||
AC_MSG_CHECKING(for libmaxminddb)
|
||||
if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libmaxminddb; then
|
||||
dnl retrieve build options from pkg-config
|
||||
if $PKG_CONFIG libmaxminddb --atleast-version 1.0.0; then
|
||||
LIBMAXMINDDB_INC=`$PKG_CONFIG libmaxminddb --cflags`
|
||||
LIBMAXMINDDB_LIB=`$PKG_CONFIG libmaxminddb --libs`
|
||||
LIBMAXMINDDB_VER=`$PKG_CONFIG libmaxminddb --modversion`
|
||||
AC_MSG_RESULT(found version $LIBMAXMINDDB_VER)
|
||||
else
|
||||
AC_MSG_ERROR(system libmaxminddb must be upgraded to version >= 1.0.0)
|
||||
fi
|
||||
PHP_EVAL_LIBLINE($LIBMAXMINDDB_LIB, MAXMINDDB_SHARED_LIBADD)
|
||||
PHP_EVAL_INCLINE($LIBMAXMINDDB_INC)
|
||||
else
|
||||
AC_MSG_RESULT(pkg-config information missing)
|
||||
AC_MSG_WARN(will use libmaxmxinddb from compiler default path)
|
||||
|
||||
PHP_CHECK_LIBRARY(maxminddb, MMDB_open)
|
||||
PHP_ADD_LIBRARY(maxminddb, 1, MAXMINDDB_SHARED_LIBADD)
|
||||
fi
|
||||
|
||||
if test $PHP_MAXMINDDB_DEBUG != "no"; then
|
||||
CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror"
|
||||
fi
|
||||
|
||||
PHP_SUBST(MAXMINDDB_SHARED_LIBADD)
|
||||
|
||||
PHP_NEW_EXTENSION(maxminddb, maxminddb.c, $ext_shared)
|
||||
fi
|
||||
10
vendor/maxmind-db/reader/ext/config.w32
vendored
Normal file
10
vendor/maxmind-db/reader/ext/config.w32
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
ARG_WITH("maxminddb", "Enable MaxMind DB Reader extension support", "no");
|
||||
|
||||
if (PHP_MAXMINDDB == "yes") {
|
||||
if (CHECK_HEADER_ADD_INCLUDE("maxminddb.h", "CFLAGS_MAXMINDDB", PHP_MAXMINDDB + ";" + PHP_PHP_BUILD + "\\include\\maxminddb") &&
|
||||
CHECK_LIB("libmaxminddb.lib", "maxminddb", PHP_MAXMINDDB)) {
|
||||
EXTENSION("maxminddb", "maxminddb.c");
|
||||
} else {
|
||||
WARNING('Could not find maxminddb.h or libmaxminddb.lib; skipping');
|
||||
}
|
||||
}
|
||||
819
vendor/maxmind-db/reader/ext/maxminddb.c
vendored
Normal file
819
vendor/maxmind-db/reader/ext/maxminddb.c
vendored
Normal file
|
|
@ -0,0 +1,819 @@
|
|||
/* MaxMind, Inc., licenses this file to you under the Apache License, Version
|
||||
* 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#include "php_maxminddb.h"
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <php.h>
|
||||
#include <zend.h>
|
||||
|
||||
#include "Zend/zend_exceptions.h"
|
||||
#include "Zend/zend_types.h"
|
||||
#include "ext/spl/spl_exceptions.h"
|
||||
#include "ext/standard/info.h"
|
||||
#include <maxminddb.h>
|
||||
|
||||
#ifdef ZTS
|
||||
#include <TSRM.h>
|
||||
#endif
|
||||
|
||||
#define __STDC_FORMAT_MACROS
|
||||
#include <inttypes.h>
|
||||
|
||||
#define PHP_MAXMINDDB_NS ZEND_NS_NAME("MaxMind", "Db")
|
||||
#define PHP_MAXMINDDB_READER_NS ZEND_NS_NAME(PHP_MAXMINDDB_NS, "Reader")
|
||||
#define PHP_MAXMINDDB_METADATA_NS \
|
||||
ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "Metadata")
|
||||
#define PHP_MAXMINDDB_READER_EX_NS \
|
||||
ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "InvalidDatabaseException")
|
||||
|
||||
#define Z_MAXMINDDB_P(zv) php_maxminddb_fetch_object(Z_OBJ_P(zv))
|
||||
typedef size_t strsize_t;
|
||||
typedef zend_object free_obj_t;
|
||||
|
||||
/* For PHP 8 compatibility */
|
||||
#if PHP_VERSION_ID < 80000
|
||||
|
||||
#define PROP_OBJ(zv) (zv)
|
||||
|
||||
#else
|
||||
|
||||
#define PROP_OBJ(zv) Z_OBJ_P(zv)
|
||||
|
||||
#define TSRMLS_C
|
||||
#define TSRMLS_CC
|
||||
#define TSRMLS_DC
|
||||
|
||||
/* End PHP 8 compatibility */
|
||||
#endif
|
||||
|
||||
#ifndef ZEND_ACC_CTOR
|
||||
#define ZEND_ACC_CTOR 0
|
||||
#endif
|
||||
|
||||
/* IS_MIXED was added in 2020 */
|
||||
#ifndef IS_MIXED
|
||||
#define IS_MIXED IS_UNDEF
|
||||
#endif
|
||||
|
||||
/* ZEND_THIS was added in 7.4 */
|
||||
#ifndef ZEND_THIS
|
||||
#define ZEND_THIS (&EX(This))
|
||||
#endif
|
||||
|
||||
typedef struct _maxminddb_obj {
|
||||
MMDB_s *mmdb;
|
||||
zend_object std;
|
||||
} maxminddb_obj;
|
||||
|
||||
PHP_FUNCTION(maxminddb);
|
||||
|
||||
static int
|
||||
get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len);
|
||||
static const MMDB_entry_data_list_s *
|
||||
handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC);
|
||||
static const MMDB_entry_data_list_s *
|
||||
handle_array(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC);
|
||||
static const MMDB_entry_data_list_s *
|
||||
handle_map(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC);
|
||||
static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC);
|
||||
static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC);
|
||||
static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC);
|
||||
|
||||
#define CHECK_ALLOCATED(val) \
|
||||
if (!val) { \
|
||||
zend_error(E_ERROR, "Out of memory"); \
|
||||
return; \
|
||||
}
|
||||
|
||||
static zend_object_handlers maxminddb_obj_handlers;
|
||||
static zend_class_entry *maxminddb_ce, *maxminddb_exception_ce, *metadata_ce;
|
||||
|
||||
static inline maxminddb_obj *
|
||||
php_maxminddb_fetch_object(zend_object *obj TSRMLS_DC) {
|
||||
return (maxminddb_obj *)((char *)(obj)-XtOffsetOf(maxminddb_obj, std));
|
||||
}
|
||||
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_construct, 0, 0, 1)
|
||||
ZEND_ARG_TYPE_INFO(0, db_file, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
PHP_METHOD(MaxMind_Db_Reader, __construct) {
|
||||
char *db_file = NULL;
|
||||
strsize_t name_len;
|
||||
zval *_this_zval = NULL;
|
||||
|
||||
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
|
||||
getThis(),
|
||||
"Os",
|
||||
&_this_zval,
|
||||
maxminddb_ce,
|
||||
&db_file,
|
||||
&name_len) == FAILURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (0 != php_check_open_basedir(db_file TSRMLS_CC) ||
|
||||
0 != access(db_file, R_OK)) {
|
||||
zend_throw_exception_ex(
|
||||
spl_ce_InvalidArgumentException,
|
||||
0 TSRMLS_CC,
|
||||
"The file \"%s\" does not exist or is not readable.",
|
||||
db_file);
|
||||
return;
|
||||
}
|
||||
|
||||
MMDB_s *mmdb = (MMDB_s *)ecalloc(1, sizeof(MMDB_s));
|
||||
int const status = MMDB_open(db_file, MMDB_MODE_MMAP, mmdb);
|
||||
|
||||
if (MMDB_SUCCESS != status) {
|
||||
zend_throw_exception_ex(
|
||||
maxminddb_exception_ce,
|
||||
0 TSRMLS_CC,
|
||||
"Error opening database file (%s). Is this a valid "
|
||||
"MaxMind DB file?",
|
||||
db_file);
|
||||
efree(mmdb);
|
||||
return;
|
||||
}
|
||||
|
||||
maxminddb_obj *mmdb_obj = Z_MAXMINDDB_P(ZEND_THIS);
|
||||
mmdb_obj->mmdb = mmdb;
|
||||
}
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(
|
||||
arginfo_maxminddbreader_get, 0, 1, IS_MIXED, 1)
|
||||
ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
PHP_METHOD(MaxMind_Db_Reader, get) {
|
||||
int prefix_len = 0;
|
||||
get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, return_value, &prefix_len);
|
||||
}
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(
|
||||
arginfo_maxminddbreader_getWithPrefixLen, 0, 1, IS_ARRAY, 1)
|
||||
ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
PHP_METHOD(MaxMind_Db_Reader, getWithPrefixLen) {
|
||||
zval record, z_prefix_len;
|
||||
|
||||
int prefix_len = 0;
|
||||
if (get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, &record, &prefix_len) ==
|
||||
FAILURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
array_init(return_value);
|
||||
add_next_index_zval(return_value, &record);
|
||||
|
||||
ZVAL_LONG(&z_prefix_len, prefix_len);
|
||||
add_next_index_zval(return_value, &z_prefix_len);
|
||||
}
|
||||
|
||||
static int
|
||||
get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len) {
|
||||
char *ip_address = NULL;
|
||||
strsize_t name_len;
|
||||
zval *this_zval = NULL;
|
||||
|
||||
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
|
||||
getThis(),
|
||||
"Os",
|
||||
&this_zval,
|
||||
maxminddb_ce,
|
||||
&ip_address,
|
||||
&name_len) == FAILURE) {
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
const maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(ZEND_THIS);
|
||||
|
||||
MMDB_s *mmdb = mmdb_obj->mmdb;
|
||||
|
||||
if (NULL == mmdb) {
|
||||
zend_throw_exception_ex(spl_ce_BadMethodCallException,
|
||||
0 TSRMLS_CC,
|
||||
"Attempt to read from a closed MaxMind DB.");
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
struct addrinfo hints = {
|
||||
.ai_family = AF_UNSPEC,
|
||||
.ai_flags = AI_NUMERICHOST,
|
||||
/* We set ai_socktype so that we only get one result back */
|
||||
.ai_socktype = SOCK_STREAM};
|
||||
|
||||
struct addrinfo *addresses = NULL;
|
||||
int gai_status = getaddrinfo(ip_address, NULL, &hints, &addresses);
|
||||
if (gai_status) {
|
||||
zend_throw_exception_ex(spl_ce_InvalidArgumentException,
|
||||
0 TSRMLS_CC,
|
||||
"The value \"%s\" is not a valid IP address.",
|
||||
ip_address);
|
||||
return FAILURE;
|
||||
}
|
||||
if (!addresses || !addresses->ai_addr) {
|
||||
zend_throw_exception_ex(
|
||||
spl_ce_InvalidArgumentException,
|
||||
0 TSRMLS_CC,
|
||||
"getaddrinfo was successful but failed to set the addrinfo");
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
int sa_family = addresses->ai_addr->sa_family;
|
||||
|
||||
int mmdb_error = MMDB_SUCCESS;
|
||||
MMDB_lookup_result_s result =
|
||||
MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, &mmdb_error);
|
||||
|
||||
freeaddrinfo(addresses);
|
||||
|
||||
if (MMDB_SUCCESS != mmdb_error) {
|
||||
zend_class_entry *ex;
|
||||
if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
|
||||
ex = spl_ce_InvalidArgumentException;
|
||||
} else {
|
||||
ex = maxminddb_exception_ce;
|
||||
}
|
||||
zend_throw_exception_ex(ex,
|
||||
0 TSRMLS_CC,
|
||||
"Error looking up %s. %s",
|
||||
ip_address,
|
||||
MMDB_strerror(mmdb_error));
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
*prefix_len = result.netmask;
|
||||
|
||||
if (sa_family == AF_INET && mmdb->metadata.ip_version == 6) {
|
||||
/* We return the prefix length given the IPv4 address. If there is
|
||||
no IPv4 subtree, we return a prefix length of 0. */
|
||||
*prefix_len = *prefix_len >= 96 ? *prefix_len - 96 : 0;
|
||||
}
|
||||
|
||||
if (!result.found_entry) {
|
||||
ZVAL_NULL(record);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
MMDB_entry_data_list_s *entry_data_list = NULL;
|
||||
int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
|
||||
|
||||
if (MMDB_SUCCESS != status) {
|
||||
zend_throw_exception_ex(maxminddb_exception_ce,
|
||||
0 TSRMLS_CC,
|
||||
"Error while looking up data for %s. %s",
|
||||
ip_address,
|
||||
MMDB_strerror(status));
|
||||
MMDB_free_entry_data_list(entry_data_list);
|
||||
return FAILURE;
|
||||
} else if (NULL == entry_data_list) {
|
||||
zend_throw_exception_ex(
|
||||
maxminddb_exception_ce,
|
||||
0 TSRMLS_CC,
|
||||
"Error while looking up data for %s. Your database may "
|
||||
"be corrupt or you have found a bug in libmaxminddb.",
|
||||
ip_address);
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
const MMDB_entry_data_list_s *rv =
|
||||
handle_entry_data_list(entry_data_list, record TSRMLS_CC);
|
||||
if (rv == NULL) {
|
||||
/* We should have already thrown the exception in handle_entry_data_list
|
||||
*/
|
||||
return FAILURE;
|
||||
}
|
||||
MMDB_free_entry_data_list(entry_data_list);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_void, 0, 0, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
PHP_METHOD(MaxMind_Db_Reader, metadata) {
|
||||
zval *this_zval = NULL;
|
||||
|
||||
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
|
||||
getThis(),
|
||||
"O",
|
||||
&this_zval,
|
||||
maxminddb_ce) == FAILURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxminddb_obj *const mmdb_obj =
|
||||
(maxminddb_obj *)Z_MAXMINDDB_P(this_zval);
|
||||
|
||||
if (NULL == mmdb_obj->mmdb) {
|
||||
zend_throw_exception_ex(spl_ce_BadMethodCallException,
|
||||
0 TSRMLS_CC,
|
||||
"Attempt to read from a closed MaxMind DB.");
|
||||
return;
|
||||
}
|
||||
|
||||
object_init_ex(return_value, metadata_ce);
|
||||
|
||||
MMDB_entry_data_list_s *entry_data_list;
|
||||
int status =
|
||||
MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
|
||||
if (status != MMDB_SUCCESS) {
|
||||
zend_throw_exception_ex(maxminddb_exception_ce,
|
||||
0 TSRMLS_CC,
|
||||
"Error while decoding metadata. %s",
|
||||
MMDB_strerror(status));
|
||||
return;
|
||||
}
|
||||
|
||||
zval metadata_array;
|
||||
const MMDB_entry_data_list_s *rv =
|
||||
handle_entry_data_list(entry_data_list, &metadata_array TSRMLS_CC);
|
||||
if (rv == NULL) {
|
||||
return;
|
||||
}
|
||||
MMDB_free_entry_data_list(entry_data_list);
|
||||
zend_call_method_with_1_params(PROP_OBJ(return_value),
|
||||
metadata_ce,
|
||||
&metadata_ce->constructor,
|
||||
ZEND_CONSTRUCTOR_FUNC_NAME,
|
||||
NULL,
|
||||
&metadata_array);
|
||||
zval_ptr_dtor(&metadata_array);
|
||||
}
|
||||
|
||||
PHP_METHOD(MaxMind_Db_Reader, close) {
|
||||
zval *this_zval = NULL;
|
||||
|
||||
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
|
||||
getThis(),
|
||||
"O",
|
||||
&this_zval,
|
||||
maxminddb_ce) == FAILURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(this_zval);
|
||||
|
||||
if (NULL == mmdb_obj->mmdb) {
|
||||
zend_throw_exception_ex(spl_ce_BadMethodCallException,
|
||||
0 TSRMLS_CC,
|
||||
"Attempt to close a closed MaxMind DB.");
|
||||
return;
|
||||
}
|
||||
MMDB_close(mmdb_obj->mmdb);
|
||||
efree(mmdb_obj->mmdb);
|
||||
mmdb_obj->mmdb = NULL;
|
||||
}
|
||||
|
||||
static const MMDB_entry_data_list_s *
|
||||
handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC) {
|
||||
switch (entry_data_list->entry_data.type) {
|
||||
case MMDB_DATA_TYPE_MAP:
|
||||
return handle_map(entry_data_list, z_value TSRMLS_CC);
|
||||
case MMDB_DATA_TYPE_ARRAY:
|
||||
return handle_array(entry_data_list, z_value TSRMLS_CC);
|
||||
case MMDB_DATA_TYPE_UTF8_STRING:
|
||||
ZVAL_STRINGL(z_value,
|
||||
entry_data_list->entry_data.utf8_string,
|
||||
entry_data_list->entry_data.data_size);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_BYTES:
|
||||
ZVAL_STRINGL(z_value,
|
||||
(char const *)entry_data_list->entry_data.bytes,
|
||||
entry_data_list->entry_data.data_size);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_DOUBLE:
|
||||
ZVAL_DOUBLE(z_value, entry_data_list->entry_data.double_value);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_FLOAT:
|
||||
ZVAL_DOUBLE(z_value, entry_data_list->entry_data.float_value);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_UINT16:
|
||||
ZVAL_LONG(z_value, entry_data_list->entry_data.uint16);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_UINT32:
|
||||
handle_uint32(entry_data_list, z_value TSRMLS_CC);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_BOOLEAN:
|
||||
ZVAL_BOOL(z_value, entry_data_list->entry_data.boolean);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_UINT64:
|
||||
handle_uint64(entry_data_list, z_value TSRMLS_CC);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_UINT128:
|
||||
handle_uint128(entry_data_list, z_value TSRMLS_CC);
|
||||
break;
|
||||
case MMDB_DATA_TYPE_INT32:
|
||||
ZVAL_LONG(z_value, entry_data_list->entry_data.int32);
|
||||
break;
|
||||
default:
|
||||
zend_throw_exception_ex(maxminddb_exception_ce,
|
||||
0 TSRMLS_CC,
|
||||
"Invalid data type arguments: %d",
|
||||
entry_data_list->entry_data.type);
|
||||
return NULL;
|
||||
}
|
||||
return entry_data_list;
|
||||
}
|
||||
|
||||
static const MMDB_entry_data_list_s *
|
||||
handle_map(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC) {
|
||||
array_init(z_value);
|
||||
const uint32_t map_size = entry_data_list->entry_data.data_size;
|
||||
|
||||
uint32_t i;
|
||||
for (i = 0; i < map_size && entry_data_list; i++) {
|
||||
entry_data_list = entry_data_list->next;
|
||||
|
||||
char *key = estrndup(entry_data_list->entry_data.utf8_string,
|
||||
entry_data_list->entry_data.data_size);
|
||||
if (NULL == key) {
|
||||
zend_throw_exception_ex(maxminddb_exception_ce,
|
||||
0 TSRMLS_CC,
|
||||
"Invalid data type arguments");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
entry_data_list = entry_data_list->next;
|
||||
zval new_value;
|
||||
entry_data_list =
|
||||
handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC);
|
||||
if (entry_data_list != NULL) {
|
||||
add_assoc_zval(z_value, key, &new_value);
|
||||
}
|
||||
efree(key);
|
||||
}
|
||||
return entry_data_list;
|
||||
}
|
||||
|
||||
static const MMDB_entry_data_list_s *
|
||||
handle_array(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC) {
|
||||
const uint32_t size = entry_data_list->entry_data.data_size;
|
||||
|
||||
array_init(z_value);
|
||||
|
||||
uint32_t i;
|
||||
for (i = 0; i < size && entry_data_list; i++) {
|
||||
entry_data_list = entry_data_list->next;
|
||||
zval new_value;
|
||||
entry_data_list =
|
||||
handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC);
|
||||
if (entry_data_list != NULL) {
|
||||
add_next_index_zval(z_value, &new_value);
|
||||
}
|
||||
}
|
||||
return entry_data_list;
|
||||
}
|
||||
|
||||
static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC) {
|
||||
uint64_t high = 0;
|
||||
uint64_t low = 0;
|
||||
#if MMDB_UINT128_IS_BYTE_ARRAY
|
||||
int i;
|
||||
for (i = 0; i < 8; i++) {
|
||||
high = (high << 8) | entry_data_list->entry_data.uint128[i];
|
||||
}
|
||||
|
||||
for (i = 8; i < 16; i++) {
|
||||
low = (low << 8) | entry_data_list->entry_data.uint128[i];
|
||||
}
|
||||
#else
|
||||
high = entry_data_list->entry_data.uint128 >> 64;
|
||||
low = (uint64_t)entry_data_list->entry_data.uint128;
|
||||
#endif
|
||||
|
||||
char *num_str;
|
||||
spprintf(&num_str, 0, "0x%016" PRIX64 "%016" PRIX64, high, low);
|
||||
CHECK_ALLOCATED(num_str);
|
||||
|
||||
ZVAL_STRING(z_value, num_str);
|
||||
efree(num_str);
|
||||
}
|
||||
|
||||
static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC) {
|
||||
uint32_t val = entry_data_list->entry_data.uint32;
|
||||
|
||||
#if LONG_MAX >= UINT32_MAX
|
||||
ZVAL_LONG(z_value, val);
|
||||
return;
|
||||
#else
|
||||
if (val <= LONG_MAX) {
|
||||
ZVAL_LONG(z_value, val);
|
||||
return;
|
||||
}
|
||||
|
||||
char *int_str;
|
||||
spprintf(&int_str, 0, "%" PRIu32, val);
|
||||
CHECK_ALLOCATED(int_str);
|
||||
|
||||
ZVAL_STRING(z_value, int_str);
|
||||
efree(int_str);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list,
|
||||
zval *z_value TSRMLS_DC) {
|
||||
uint64_t val = entry_data_list->entry_data.uint64;
|
||||
|
||||
#if LONG_MAX >= UINT64_MAX
|
||||
ZVAL_LONG(z_value, val);
|
||||
return;
|
||||
#else
|
||||
if (val <= LONG_MAX) {
|
||||
ZVAL_LONG(z_value, val);
|
||||
return;
|
||||
}
|
||||
|
||||
char *int_str;
|
||||
spprintf(&int_str, 0, "%" PRIu64, val);
|
||||
CHECK_ALLOCATED(int_str);
|
||||
|
||||
ZVAL_STRING(z_value, int_str);
|
||||
efree(int_str);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void maxminddb_free_storage(free_obj_t *object TSRMLS_DC) {
|
||||
maxminddb_obj *obj =
|
||||
php_maxminddb_fetch_object((zend_object *)object TSRMLS_CC);
|
||||
if (obj->mmdb != NULL) {
|
||||
MMDB_close(obj->mmdb);
|
||||
efree(obj->mmdb);
|
||||
}
|
||||
|
||||
zend_object_std_dtor(&obj->std TSRMLS_CC);
|
||||
}
|
||||
|
||||
static zend_object *maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) {
|
||||
maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj));
|
||||
zend_object_std_init(&obj->std, type TSRMLS_CC);
|
||||
object_properties_init(&(obj->std), type);
|
||||
|
||||
obj->std.handlers = &maxminddb_obj_handlers;
|
||||
|
||||
return &obj->std;
|
||||
}
|
||||
|
||||
/* clang-format off */
|
||||
static zend_function_entry maxminddb_methods[] = {
|
||||
PHP_ME(MaxMind_Db_Reader, __construct, arginfo_maxminddbreader_construct,
|
||||
ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
|
||||
PHP_ME(MaxMind_Db_Reader, close, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC)
|
||||
PHP_ME(MaxMind_Db_Reader, get, arginfo_maxminddbreader_get, ZEND_ACC_PUBLIC)
|
||||
PHP_ME(MaxMind_Db_Reader, getWithPrefixLen, arginfo_maxminddbreader_getWithPrefixLen, ZEND_ACC_PUBLIC)
|
||||
PHP_ME(MaxMind_Db_Reader, metadata, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC)
|
||||
{ NULL, NULL, NULL }
|
||||
};
|
||||
/* clang-format on */
|
||||
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_metadata_construct, 0, 0, 1)
|
||||
ZEND_ARG_TYPE_INFO(0, metadata, IS_ARRAY, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
PHP_METHOD(MaxMind_Db_Reader_Metadata, __construct) {
|
||||
zval *object = NULL;
|
||||
zval *metadata_array = NULL;
|
||||
zend_long node_count = 0;
|
||||
zend_long record_size = 0;
|
||||
|
||||
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
|
||||
getThis(),
|
||||
"Oa",
|
||||
&object,
|
||||
metadata_ce,
|
||||
&metadata_array) == FAILURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
zval *tmp = NULL;
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"binary_format_major_version",
|
||||
sizeof("binary_format_major_version") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"binaryFormatMajorVersion",
|
||||
sizeof("binaryFormatMajorVersion") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"binary_format_minor_version",
|
||||
sizeof("binary_format_minor_version") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"binaryFormatMinorVersion",
|
||||
sizeof("binaryFormatMinorVersion") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"build_epoch",
|
||||
sizeof("build_epoch") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"buildEpoch",
|
||||
sizeof("buildEpoch") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"database_type",
|
||||
sizeof("database_type") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"databaseType",
|
||||
sizeof("databaseType") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"description",
|
||||
sizeof("description") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"description",
|
||||
sizeof("description") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"ip_version",
|
||||
sizeof("ip_version") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"ipVersion",
|
||||
sizeof("ipVersion") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(
|
||||
HASH_OF(metadata_array), "languages", sizeof("languages") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"languages",
|
||||
sizeof("languages") - 1,
|
||||
tmp);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"record_size",
|
||||
sizeof("record_size") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"recordSize",
|
||||
sizeof("recordSize") - 1,
|
||||
tmp);
|
||||
if (Z_TYPE_P(tmp) == IS_LONG) {
|
||||
record_size = Z_LVAL_P(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
if (record_size != 0) {
|
||||
zend_update_property_long(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"nodeByteSize",
|
||||
sizeof("nodeByteSize") - 1,
|
||||
record_size / 4);
|
||||
}
|
||||
|
||||
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
|
||||
"node_count",
|
||||
sizeof("node_count") - 1))) {
|
||||
zend_update_property(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"nodeCount",
|
||||
sizeof("nodeCount") - 1,
|
||||
tmp);
|
||||
if (Z_TYPE_P(tmp) == IS_LONG) {
|
||||
node_count = Z_LVAL_P(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
if (record_size != 0) {
|
||||
zend_update_property_long(metadata_ce,
|
||||
PROP_OBJ(object),
|
||||
"searchTreeSize",
|
||||
sizeof("searchTreeSize") - 1,
|
||||
record_size * node_count / 4);
|
||||
}
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
static zend_function_entry metadata_methods[] = {
|
||||
PHP_ME(MaxMind_Db_Reader_Metadata, __construct, arginfo_metadata_construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
|
||||
{NULL, NULL, NULL}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
PHP_MINIT_FUNCTION(maxminddb) {
|
||||
zend_class_entry ce;
|
||||
|
||||
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_EX_NS, NULL);
|
||||
maxminddb_exception_ce =
|
||||
zend_register_internal_class_ex(&ce, zend_ce_exception);
|
||||
|
||||
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_NS, maxminddb_methods);
|
||||
maxminddb_ce = zend_register_internal_class(&ce TSRMLS_CC);
|
||||
maxminddb_ce->create_object = maxminddb_create_handler;
|
||||
|
||||
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_METADATA_NS, metadata_methods);
|
||||
metadata_ce = zend_register_internal_class(&ce TSRMLS_CC);
|
||||
zend_declare_property_null(metadata_ce,
|
||||
"binaryFormatMajorVersion",
|
||||
sizeof("binaryFormatMajorVersion") - 1,
|
||||
ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(metadata_ce,
|
||||
"binaryFormatMinorVersion",
|
||||
sizeof("binaryFormatMinorVersion") - 1,
|
||||
ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(
|
||||
metadata_ce, "buildEpoch", sizeof("buildEpoch") - 1, ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(metadata_ce,
|
||||
"databaseType",
|
||||
sizeof("databaseType") - 1,
|
||||
ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(
|
||||
metadata_ce, "description", sizeof("description") - 1, ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(
|
||||
metadata_ce, "ipVersion", sizeof("ipVersion") - 1, ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(
|
||||
metadata_ce, "languages", sizeof("languages") - 1, ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(metadata_ce,
|
||||
"nodeByteSize",
|
||||
sizeof("nodeByteSize") - 1,
|
||||
ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(
|
||||
metadata_ce, "nodeCount", sizeof("nodeCount") - 1, ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(
|
||||
metadata_ce, "recordSize", sizeof("recordSize") - 1, ZEND_ACC_PUBLIC);
|
||||
zend_declare_property_null(metadata_ce,
|
||||
"searchTreeSize",
|
||||
sizeof("searchTreeSize") - 1,
|
||||
ZEND_ACC_PUBLIC);
|
||||
|
||||
memcpy(&maxminddb_obj_handlers,
|
||||
zend_get_std_object_handlers(),
|
||||
sizeof(zend_object_handlers));
|
||||
maxminddb_obj_handlers.clone_obj = NULL;
|
||||
maxminddb_obj_handlers.offset = XtOffsetOf(maxminddb_obj, std);
|
||||
maxminddb_obj_handlers.free_obj = maxminddb_free_storage;
|
||||
zend_declare_class_constant_string(maxminddb_ce,
|
||||
"MMDB_LIB_VERSION",
|
||||
sizeof("MMDB_LIB_VERSION") - 1,
|
||||
MMDB_lib_version() TSRMLS_CC);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
static PHP_MINFO_FUNCTION(maxminddb) {
|
||||
php_info_print_table_start();
|
||||
|
||||
php_info_print_table_row(2, "MaxMind DB Reader", "enabled");
|
||||
php_info_print_table_row(
|
||||
2, "maxminddb extension version", PHP_MAXMINDDB_VERSION);
|
||||
php_info_print_table_row(
|
||||
2, "libmaxminddb library version", MMDB_lib_version());
|
||||
|
||||
php_info_print_table_end();
|
||||
}
|
||||
|
||||
zend_module_entry maxminddb_module_entry = {STANDARD_MODULE_HEADER,
|
||||
PHP_MAXMINDDB_EXTNAME,
|
||||
NULL,
|
||||
PHP_MINIT(maxminddb),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
PHP_MINFO(maxminddb),
|
||||
PHP_MAXMINDDB_VERSION,
|
||||
STANDARD_MODULE_PROPERTIES};
|
||||
|
||||
#ifdef COMPILE_DL_MAXMINDDB
|
||||
ZEND_GET_MODULE(maxminddb)
|
||||
#endif
|
||||
24
vendor/maxmind-db/reader/ext/php_maxminddb.h
vendored
Normal file
24
vendor/maxmind-db/reader/ext/php_maxminddb.h
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* MaxMind, Inc., licenses this file to you under the Apache License, Version
|
||||
* 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#include <zend_interfaces.h>
|
||||
|
||||
#ifndef PHP_MAXMINDDB_H
|
||||
#define PHP_MAXMINDDB_H 1
|
||||
#define PHP_MAXMINDDB_VERSION "1.13.1"
|
||||
#define PHP_MAXMINDDB_EXTNAME "maxminddb"
|
||||
|
||||
extern zend_module_entry maxminddb_module_entry;
|
||||
#define phpext_maxminddb_ptr &maxminddb_module_entry
|
||||
|
||||
#endif
|
||||
12
vendor/maxmind-db/reader/ext/tests/001-load.phpt
vendored
Normal file
12
vendor/maxmind-db/reader/ext/tests/001-load.phpt
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
--TEST--
|
||||
Check for maxminddb presence
|
||||
--SKIPIF--
|
||||
<?php if (!extension_loaded('maxminddb')) {
|
||||
echo 'skip';
|
||||
} ?>
|
||||
--FILE--
|
||||
<?php
|
||||
echo 'maxminddb extension is available';
|
||||
?>
|
||||
--EXPECT--
|
||||
maxminddb extension is available
|
||||
13
vendor/maxmind-db/reader/ext/tests/002-final.phpt
vendored
Normal file
13
vendor/maxmind-db/reader/ext/tests/002-final.phpt
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
--TEST--
|
||||
Check that Reader class is not final
|
||||
--SKIPIF--
|
||||
<?php if (!extension_loaded('maxminddb')) {
|
||||
echo 'skip';
|
||||
} ?>
|
||||
--FILE--
|
||||
<?php
|
||||
$reflectionClass = new \ReflectionClass('MaxMind\Db\Reader');
|
||||
var_dump($reflectionClass->isFinal());
|
||||
?>
|
||||
--EXPECT--
|
||||
bool(false)
|
||||
12
vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt
vendored
Normal file
12
vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
--TEST--
|
||||
openbase_dir is followed
|
||||
--INI--
|
||||
open_basedir=/--dne--
|
||||
--FILE--
|
||||
<?php
|
||||
use MaxMind\Db\Reader;
|
||||
|
||||
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb');
|
||||
?>
|
||||
--EXPECTREGEX--
|
||||
.*open_basedir restriction in effect.*
|
||||
61
vendor/maxmind-db/reader/package.xml
vendored
Normal file
61
vendor/maxmind-db/reader/package.xml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0"?>
|
||||
<package version="2.0" xmlns="http://pear.php.net/dtd/package-2.0"
|
||||
xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
|
||||
|
||||
<name>maxminddb</name>
|
||||
<channel>pecl.php.net</channel>
|
||||
<summary>Reader for the MaxMind DB file format</summary>
|
||||
<description>This is the PHP extension for reading MaxMind DB files. MaxMind DB is a binary file format that stores data indexed by IP address subnets (IPv4 or IPv6).</description>
|
||||
<lead>
|
||||
<name>Greg Oschwald</name>
|
||||
<user>oschwald</user>
|
||||
<email>goschwald@maxmind.com</email>
|
||||
<active>yes</active>
|
||||
</lead>
|
||||
<date>2025-11-21</date>
|
||||
<version>
|
||||
<release>1.13.1</release>
|
||||
<api>1.13.1</api>
|
||||
</version>
|
||||
<stability>
|
||||
<release>stable</release>
|
||||
<api>stable</api>
|
||||
</stability>
|
||||
<license uri="https://github.com/maxmind/MaxMind-DB-Reader-php/blob/main/LICENSE">Apache License 2.0</license>
|
||||
<notes>* First PIE release. No other changes.</notes>
|
||||
<contents>
|
||||
<dir name="/">
|
||||
<file role="doc" name="LICENSE"/>
|
||||
<file role="doc" name="CHANGELOG.md"/>
|
||||
<file role="doc" name="README.md"/>
|
||||
|
||||
<dir name="ext">
|
||||
<file role="src" name="config.m4"/>
|
||||
<file role="src" name="config.w32"/>
|
||||
|
||||
<file role="src" name="maxminddb.c"/>
|
||||
<file role="src" name="php_maxminddb.h"/>
|
||||
|
||||
<dir name="tests">
|
||||
<file role="test" name="001-load.phpt"/>
|
||||
<file role="test" name="002-final.phpt"/>
|
||||
<file role="test" name="003-open-basedir.phpt"/>
|
||||
</dir>
|
||||
</dir>
|
||||
</dir>
|
||||
</contents>
|
||||
<dependencies>
|
||||
<required>
|
||||
<php>
|
||||
<min>7.2.0</min>
|
||||
</php>
|
||||
<pearinstaller>
|
||||
<min>1.10.0</min>
|
||||
</pearinstaller>
|
||||
</required>
|
||||
</dependencies>
|
||||
<providesextension>maxminddb</providesextension>
|
||||
<extsrcrelease />
|
||||
</package>
|
||||
404
vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php
vendored
Normal file
404
vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php
vendored
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db;
|
||||
|
||||
use MaxMind\Db\Reader\Decoder;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use MaxMind\Db\Reader\Metadata;
|
||||
use MaxMind\Db\Reader\Util;
|
||||
|
||||
/**
|
||||
* Instances of this class provide a reader for the MaxMind DB format. IP
|
||||
* addresses can be looked up using the get method.
|
||||
*/
|
||||
class Reader
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private static $DATA_SECTION_SEPARATOR_SIZE = 16;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
|
||||
|
||||
/**
|
||||
* @var int<0, max>
|
||||
*/
|
||||
private static $METADATA_START_MARKER_LENGTH = 14;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB
|
||||
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $fileHandle;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $fileSize;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $ipV4Start;
|
||||
|
||||
/**
|
||||
* @var Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* Constructs a Reader for the MaxMind DB format. The file passed to it must
|
||||
* be a valid MaxMind DB file such as a GeoIp2 database file.
|
||||
*
|
||||
* @param string $database the MaxMind DB file to use
|
||||
*
|
||||
* @throws \InvalidArgumentException for invalid database path or unknown arguments
|
||||
* @throws InvalidDatabaseException
|
||||
* if the database is invalid or there is an error reading
|
||||
* from it
|
||||
*/
|
||||
public function __construct(string $database)
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
if (is_dir($database)) {
|
||||
// This matches the error that the C extension throws.
|
||||
throw new InvalidDatabaseException(
|
||||
"Error opening database file ($database). Is this a valid MaxMind DB file?"
|
||||
);
|
||||
}
|
||||
|
||||
$fileHandle = @fopen($database, 'rb');
|
||||
if ($fileHandle === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
"The file \"$database\" does not exist or is not readable."
|
||||
);
|
||||
}
|
||||
$this->fileHandle = $fileHandle;
|
||||
|
||||
$fstat = fstat($fileHandle);
|
||||
if ($fstat === false) {
|
||||
throw new \UnexpectedValueException(
|
||||
"Error determining the size of \"$database\"."
|
||||
);
|
||||
}
|
||||
$this->fileSize = $fstat['size'];
|
||||
|
||||
$start = $this->findMetadataStart($database);
|
||||
$metadataDecoder = new Decoder($this->fileHandle, $start);
|
||||
[$metadataArray] = $metadataDecoder->decode($start);
|
||||
$this->metadata = new Metadata($metadataArray);
|
||||
$this->decoder = new Decoder(
|
||||
$this->fileHandle,
|
||||
$this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
|
||||
);
|
||||
$this->ipV4Start = $this->ipV4StartNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the record for the IP address.
|
||||
*
|
||||
* @param string $ipAddress the IP address to look up
|
||||
*
|
||||
* @throws \BadMethodCallException if this method is called on a closed database
|
||||
* @throws \InvalidArgumentException if something other than a single IP address is passed to the method
|
||||
* @throws InvalidDatabaseException
|
||||
* if the database is invalid or there is an error reading
|
||||
* from it
|
||||
*
|
||||
* @return mixed the record for the IP address
|
||||
*/
|
||||
public function get(string $ipAddress)
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
[$record] = $this->getWithPrefixLen($ipAddress);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the record for the IP address and its associated network prefix length.
|
||||
*
|
||||
* @param string $ipAddress the IP address to look up
|
||||
*
|
||||
* @throws \BadMethodCallException if this method is called on a closed database
|
||||
* @throws \InvalidArgumentException if something other than a single IP address is passed to the method
|
||||
* @throws InvalidDatabaseException
|
||||
* if the database is invalid or there is an error reading
|
||||
* from it
|
||||
*
|
||||
* @return array{0:mixed, 1:int} an array where the first element is the record and the
|
||||
* second the network prefix length for the record
|
||||
*/
|
||||
public function getWithPrefixLen(string $ipAddress): array
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_resource($this->fileHandle)) {
|
||||
throw new \BadMethodCallException(
|
||||
'Attempt to read from a closed MaxMind DB.'
|
||||
);
|
||||
}
|
||||
|
||||
[$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
|
||||
if ($pointer === 0) {
|
||||
return [null, $prefixLen];
|
||||
}
|
||||
|
||||
return [$this->resolveDataPointer($pointer), $prefixLen];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int, 1:int}
|
||||
*/
|
||||
private function findAddressInTree(string $ipAddress): array
|
||||
{
|
||||
$packedAddr = @inet_pton($ipAddress);
|
||||
if ($packedAddr === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
"The value \"$ipAddress\" is not a valid IP address."
|
||||
);
|
||||
}
|
||||
|
||||
$rawAddress = unpack('C*', $packedAddr);
|
||||
if ($rawAddress === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned char of the packed in_addr representation.'
|
||||
);
|
||||
}
|
||||
|
||||
$bitCount = \count($rawAddress) * 8;
|
||||
|
||||
// The first node of the tree is always node 0, at the beginning of the
|
||||
// value
|
||||
$node = 0;
|
||||
|
||||
$metadata = $this->metadata;
|
||||
|
||||
// Check if we are looking up an IPv4 address in an IPv6 tree. If this
|
||||
// is the case, we can skip over the first 96 nodes.
|
||||
if ($metadata->ipVersion === 6) {
|
||||
if ($bitCount === 32) {
|
||||
$node = $this->ipV4Start;
|
||||
}
|
||||
} elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Error looking up $ipAddress. You attempted to look up an"
|
||||
. ' IPv6 address in an IPv4-only database.'
|
||||
);
|
||||
}
|
||||
|
||||
$nodeCount = $metadata->nodeCount;
|
||||
|
||||
for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
|
||||
$tempBit = 0xFF & $rawAddress[($i >> 3) + 1];
|
||||
$bit = 1 & ($tempBit >> 7 - ($i % 8));
|
||||
|
||||
$node = $this->readNode($node, $bit);
|
||||
}
|
||||
if ($node === $nodeCount) {
|
||||
// Record is empty
|
||||
return [0, $i];
|
||||
}
|
||||
if ($node > $nodeCount) {
|
||||
// Record is a data pointer
|
||||
return [$node, $i];
|
||||
}
|
||||
|
||||
throw new InvalidDatabaseException(
|
||||
'Invalid or corrupt database. Maximum search depth reached without finding a leaf node'
|
||||
);
|
||||
}
|
||||
|
||||
private function ipV4StartNode(): int
|
||||
{
|
||||
// If we have an IPv4 database, the start node is the first node
|
||||
if ($this->metadata->ipVersion === 4) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$node = 0;
|
||||
|
||||
for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
|
||||
$node = $this->readNode($node, 0);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function readNode(int $nodeNumber, int $index): int
|
||||
{
|
||||
$baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
|
||||
|
||||
switch ($this->metadata->recordSize) {
|
||||
case 24:
|
||||
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
|
||||
$rc = unpack('N', "\x00" . $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned long of the node.'
|
||||
);
|
||||
}
|
||||
[, $node] = $rc;
|
||||
|
||||
return $node;
|
||||
|
||||
case 28:
|
||||
$bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
|
||||
if ($index === 0) {
|
||||
$middle = (0xF0 & \ord($bytes[3])) >> 4;
|
||||
} else {
|
||||
$middle = 0x0F & \ord($bytes[0]);
|
||||
}
|
||||
$rc = unpack('N', \chr($middle) . substr($bytes, $index, 3));
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned long of the node.'
|
||||
);
|
||||
}
|
||||
[, $node] = $rc;
|
||||
|
||||
return $node;
|
||||
|
||||
case 32:
|
||||
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
|
||||
$rc = unpack('N', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned long of the node.'
|
||||
);
|
||||
}
|
||||
[, $node] = $rc;
|
||||
|
||||
return $node;
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
'Unknown record size: '
|
||||
. $this->metadata->recordSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
private function resolveDataPointer(int $pointer)
|
||||
{
|
||||
$resolved = $pointer - $this->metadata->nodeCount
|
||||
+ $this->metadata->searchTreeSize;
|
||||
if ($resolved >= $this->fileSize) {
|
||||
throw new InvalidDatabaseException(
|
||||
"The MaxMind DB file's search tree is corrupt"
|
||||
);
|
||||
}
|
||||
|
||||
[$data] = $this->decoder->decode($resolved);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is an extremely naive but reasonably readable implementation. There
|
||||
* are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
|
||||
* an issue, but I suspect it won't be.
|
||||
*/
|
||||
private function findMetadataStart(string $filename): int
|
||||
{
|
||||
$handle = $this->fileHandle;
|
||||
$fileSize = $this->fileSize;
|
||||
$marker = self::$METADATA_START_MARKER;
|
||||
$markerLength = self::$METADATA_START_MARKER_LENGTH;
|
||||
|
||||
$minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize);
|
||||
|
||||
for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
|
||||
if (fseek($handle, $offset) !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$value = fread($handle, $markerLength);
|
||||
if ($value === $marker) {
|
||||
return $offset + $markerLength;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidDatabaseException(
|
||||
"Error opening database file ($filename). "
|
||||
. 'Is this a valid MaxMind DB file?'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException if arguments are passed to the method
|
||||
* @throws \BadMethodCallException if the database has been closed
|
||||
*
|
||||
* @return Metadata object for the database
|
||||
*/
|
||||
public function metadata(): Metadata
|
||||
{
|
||||
if (\func_num_args()) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
// Not technically required, but this makes it consistent with
|
||||
// C extension and it allows us to change our implementation later.
|
||||
if (!\is_resource($this->fileHandle)) {
|
||||
throw new \BadMethodCallException(
|
||||
'Attempt to read from a closed MaxMind DB.'
|
||||
);
|
||||
}
|
||||
|
||||
return clone $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the MaxMind DB and returns resources to the system.
|
||||
*
|
||||
* @throws \Exception
|
||||
* if an I/O error occurs
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (\func_num_args()) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_resource($this->fileHandle)) {
|
||||
throw new \BadMethodCallException(
|
||||
'Attempt to close a closed MaxMind DB.'
|
||||
);
|
||||
}
|
||||
fclose($this->fileHandle);
|
||||
}
|
||||
}
|
||||
452
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php
vendored
Normal file
452
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php
vendored
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
// @codingStandardsIgnoreLine
|
||||
|
||||
class Decoder
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $fileStream;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $pointerBase;
|
||||
|
||||
/**
|
||||
* This is only used for unit testing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $pointerTestHack;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $switchByteOrder;
|
||||
|
||||
private const _EXTENDED = 0;
|
||||
private const _POINTER = 1;
|
||||
private const _UTF8_STRING = 2;
|
||||
private const _DOUBLE = 3;
|
||||
private const _BYTES = 4;
|
||||
private const _UINT16 = 5;
|
||||
private const _UINT32 = 6;
|
||||
private const _MAP = 7;
|
||||
private const _INT32 = 8;
|
||||
private const _UINT64 = 9;
|
||||
private const _UINT128 = 10;
|
||||
private const _ARRAY = 11;
|
||||
// 12 is the container type
|
||||
// 13 is the end marker type
|
||||
private const _BOOLEAN = 14;
|
||||
private const _FLOAT = 15;
|
||||
|
||||
/**
|
||||
* @param resource $fileStream
|
||||
*/
|
||||
public function __construct(
|
||||
$fileStream,
|
||||
int $pointerBase = 0,
|
||||
bool $pointerTestHack = false
|
||||
) {
|
||||
$this->fileStream = $fileStream;
|
||||
$this->pointerBase = $pointerBase;
|
||||
|
||||
$this->pointerTestHack = $pointerTestHack;
|
||||
|
||||
$this->switchByteOrder = $this->isPlatformLittleEndian();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function decode(int $offset): array
|
||||
{
|
||||
$ctrlByte = \ord(Util::read($this->fileStream, $offset, 1));
|
||||
++$offset;
|
||||
|
||||
$type = $ctrlByte >> 5;
|
||||
|
||||
// Pointers are a special case, we don't read the next $size bytes, we
|
||||
// use the size to determine the length of the pointer and then follow
|
||||
// it.
|
||||
if ($type === self::_POINTER) {
|
||||
[$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);
|
||||
|
||||
// for unit testing
|
||||
if ($this->pointerTestHack) {
|
||||
return [$pointer];
|
||||
}
|
||||
|
||||
[$result] = $this->decode($pointer);
|
||||
|
||||
return [$result, $offset];
|
||||
}
|
||||
|
||||
if ($type === self::_EXTENDED) {
|
||||
$nextByte = \ord(Util::read($this->fileStream, $offset, 1));
|
||||
|
||||
$type = $nextByte + 7;
|
||||
|
||||
if ($type < 8) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Something went horribly wrong in the decoder. An extended type '
|
||||
. 'resolved to a type number < 8 ('
|
||||
. $type
|
||||
. ')'
|
||||
);
|
||||
}
|
||||
|
||||
++$offset;
|
||||
}
|
||||
|
||||
[$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);
|
||||
|
||||
return $this->decodeByType($type, $offset, $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<0, max> $size
|
||||
*
|
||||
* @return array{0:mixed, 1:int}
|
||||
*/
|
||||
private function decodeByType(int $type, int $offset, int $size): array
|
||||
{
|
||||
switch ($type) {
|
||||
case self::_MAP:
|
||||
return $this->decodeMap($size, $offset);
|
||||
|
||||
case self::_ARRAY:
|
||||
return $this->decodeArray($size, $offset);
|
||||
|
||||
case self::_BOOLEAN:
|
||||
return [$this->decodeBoolean($size), $offset];
|
||||
}
|
||||
|
||||
$newOffset = $offset + $size;
|
||||
$bytes = Util::read($this->fileStream, $offset, $size);
|
||||
|
||||
switch ($type) {
|
||||
case self::_BYTES:
|
||||
case self::_UTF8_STRING:
|
||||
return [$bytes, $newOffset];
|
||||
|
||||
case self::_DOUBLE:
|
||||
$this->verifySize(8, $size);
|
||||
|
||||
return [$this->decodeDouble($bytes), $newOffset];
|
||||
|
||||
case self::_FLOAT:
|
||||
$this->verifySize(4, $size);
|
||||
|
||||
return [$this->decodeFloat($bytes), $newOffset];
|
||||
|
||||
case self::_INT32:
|
||||
return [$this->decodeInt32($bytes, $size), $newOffset];
|
||||
|
||||
case self::_UINT16:
|
||||
case self::_UINT32:
|
||||
case self::_UINT64:
|
||||
case self::_UINT128:
|
||||
return [$this->decodeUint($bytes, $size), $newOffset];
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
'Unknown or unexpected type: ' . $type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function verifySize(int $expected, int $actual): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new InvalidDatabaseException(
|
||||
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:array<mixed>, 1:int}
|
||||
*/
|
||||
private function decodeArray(int $size, int $offset): array
|
||||
{
|
||||
$array = [];
|
||||
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
[$value, $offset] = $this->decode($offset);
|
||||
$array[] = $value;
|
||||
}
|
||||
|
||||
return [$array, $offset];
|
||||
}
|
||||
|
||||
private function decodeBoolean(int $size): bool
|
||||
{
|
||||
return $size !== 0;
|
||||
}
|
||||
|
||||
private function decodeDouble(string $bytes): float
|
||||
{
|
||||
// This assumes IEEE 754 doubles, but most (all?) modern platforms
|
||||
// use them.
|
||||
$rc = unpack('E', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack a double value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $double] = $rc;
|
||||
|
||||
return $double;
|
||||
}
|
||||
|
||||
private function decodeFloat(string $bytes): float
|
||||
{
|
||||
// This assumes IEEE 754 floats, but most (all?) modern platforms
|
||||
// use them.
|
||||
$rc = unpack('G', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack a float value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $float] = $rc;
|
||||
|
||||
return $float;
|
||||
}
|
||||
|
||||
private function decodeInt32(string $bytes, int $size): int
|
||||
{
|
||||
switch ($size) {
|
||||
case 0:
|
||||
return 0;
|
||||
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
$bytes = str_pad($bytes, 4, "\x00", \STR_PAD_LEFT);
|
||||
|
||||
break;
|
||||
|
||||
case 4:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
|
||||
);
|
||||
}
|
||||
|
||||
$rc = unpack('l', $this->maybeSwitchByteOrder($bytes));
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack a 32bit integer value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $int] = $rc;
|
||||
|
||||
return $int;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:array<string, mixed>, 1:int}
|
||||
*/
|
||||
private function decodeMap(int $size, int $offset): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
[$key, $offset] = $this->decode($offset);
|
||||
[$value, $offset] = $this->decode($offset);
|
||||
$map[$key] = $value;
|
||||
}
|
||||
|
||||
return [$map, $offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int, 1:int}
|
||||
*/
|
||||
private function decodePointer(int $ctrlByte, int $offset): array
|
||||
{
|
||||
$pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
|
||||
|
||||
$buffer = Util::read($this->fileStream, $offset, $pointerSize);
|
||||
$offset += $pointerSize;
|
||||
|
||||
switch ($pointerSize) {
|
||||
case 1:
|
||||
$packed = \chr($ctrlByte & 0x7) . $buffer;
|
||||
$rc = unpack('n', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned short value from the given bytes (pointerSize is 1).'
|
||||
);
|
||||
}
|
||||
[, $pointer] = $rc;
|
||||
$pointer += $this->pointerBase;
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
|
||||
$rc = unpack('N', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned long value from the given bytes (pointerSize is 2).'
|
||||
);
|
||||
}
|
||||
[, $pointer] = $rc;
|
||||
$pointer += $this->pointerBase + 2048;
|
||||
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$packed = \chr($ctrlByte & 0x7) . $buffer;
|
||||
|
||||
// It is safe to use 'N' here, even on 32 bit machines as the
|
||||
// first bit is 0.
|
||||
$rc = unpack('N', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned long value from the given bytes (pointerSize is 3).'
|
||||
);
|
||||
}
|
||||
[, $pointer] = $rc;
|
||||
$pointer += $this->pointerBase + 526336;
|
||||
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// We cannot use unpack here as we might overflow on 32 bit
|
||||
// machines
|
||||
$pointerOffset = $this->decodeUint($buffer, $pointerSize);
|
||||
|
||||
$pointerBase = $this->pointerBase;
|
||||
|
||||
if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) {
|
||||
$pointer = $pointerOffset + $pointerBase;
|
||||
} else {
|
||||
throw new \RuntimeException(
|
||||
'The database offset is too large to be represented on your platform.'
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
'Unexpected pointer size ' . $pointerSize
|
||||
);
|
||||
}
|
||||
|
||||
return [$pointer, $offset];
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function decodeUint(string $bytes, int $byteLength)
|
||||
{
|
||||
if ($byteLength === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// PHP integers are signed. PHP_INT_SIZE - 1 is the number of
|
||||
// complete bytes that can be converted to an integer. However,
|
||||
// we can convert another byte if the leading bit is zero.
|
||||
$useRealInts = $byteLength <= \PHP_INT_SIZE - 1
|
||||
|| ($byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0);
|
||||
|
||||
if ($useRealInts) {
|
||||
$integer = 0;
|
||||
for ($i = 0; $i < $byteLength; ++$i) {
|
||||
$part = \ord($bytes[$i]);
|
||||
$integer = ($integer << 8) + $part;
|
||||
}
|
||||
|
||||
return $integer;
|
||||
}
|
||||
|
||||
// We only use gmp or bcmath if the final value is too big
|
||||
$integerAsString = '0';
|
||||
for ($i = 0; $i < $byteLength; ++$i) {
|
||||
$part = \ord($bytes[$i]);
|
||||
|
||||
if (\extension_loaded('gmp')) {
|
||||
$integerAsString = gmp_strval(gmp_add(gmp_mul($integerAsString, '256'), $part));
|
||||
} elseif (\extension_loaded('bcmath')) {
|
||||
$integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part);
|
||||
} else {
|
||||
throw new \RuntimeException(
|
||||
'The gmp or bcmath extension must be installed to read this database.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $integerAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int, 1:int}
|
||||
*/
|
||||
private function sizeFromCtrlByte(int $ctrlByte, int $offset): array
|
||||
{
|
||||
$size = $ctrlByte & 0x1F;
|
||||
|
||||
if ($size < 29) {
|
||||
return [$size, $offset];
|
||||
}
|
||||
|
||||
$bytesToRead = $size - 28;
|
||||
$bytes = Util::read($this->fileStream, $offset, $bytesToRead);
|
||||
|
||||
if ($size === 29) {
|
||||
$size = 29 + \ord($bytes);
|
||||
} elseif ($size === 30) {
|
||||
$rc = unpack('n', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned short value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $adjust] = $rc;
|
||||
$size = 285 + $adjust;
|
||||
} else {
|
||||
$rc = unpack('N', "\x00" . $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned long value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $adjust] = $rc;
|
||||
$size = $adjust + 65821;
|
||||
}
|
||||
|
||||
return [$size, $offset + $bytesToRead];
|
||||
}
|
||||
|
||||
private function maybeSwitchByteOrder(string $bytes): string
|
||||
{
|
||||
return $this->switchByteOrder ? strrev($bytes) : $bytes;
|
||||
}
|
||||
|
||||
private function isPlatformLittleEndian(): bool
|
||||
{
|
||||
$testint = 0x00FF;
|
||||
$packed = pack('S', $testint);
|
||||
$rc = unpack('v', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned short value from the given bytes.'
|
||||
);
|
||||
}
|
||||
|
||||
return $testint === current($rc);
|
||||
}
|
||||
}
|
||||
11
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php
vendored
Normal file
11
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
/**
|
||||
* This class should be thrown when unexpected data is found in the database.
|
||||
*/
|
||||
// phpcs:disable
|
||||
class InvalidDatabaseException extends \Exception {}
|
||||
123
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php
vendored
Normal file
123
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
/**
|
||||
* This class provides the metadata for the MaxMind DB file.
|
||||
*/
|
||||
class Metadata
|
||||
{
|
||||
/**
|
||||
* This is an unsigned 16-bit integer indicating the major version number
|
||||
* for the database's binary format.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $binaryFormatMajorVersion;
|
||||
|
||||
/**
|
||||
* This is an unsigned 16-bit integer indicating the minor version number
|
||||
* for the database's binary format.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $binaryFormatMinorVersion;
|
||||
|
||||
/**
|
||||
* This is an unsigned 64-bit integer that contains the database build
|
||||
* timestamp as a Unix epoch value.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $buildEpoch;
|
||||
|
||||
/**
|
||||
* This is a string that indicates the structure of each data record
|
||||
* associated with an IP address. The actual definition of these
|
||||
* structures is left up to the database creator.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $databaseType;
|
||||
|
||||
/**
|
||||
* This key will always point to a map (associative array). The keys of
|
||||
* that map will be language codes, and the values will be a description
|
||||
* in that language as a UTF-8 string. May be undefined for some
|
||||
* databases.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* This is an unsigned 16-bit integer which is always 4 or 6. It indicates
|
||||
* whether the database contains IPv4 or IPv6 address data.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $ipVersion;
|
||||
|
||||
/**
|
||||
* An array of strings, each of which is a language code. A given record
|
||||
* may contain data items that have been localized to some or all of
|
||||
* these languages. This may be undefined.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
public $languages;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $nodeByteSize;
|
||||
|
||||
/**
|
||||
* This is an unsigned 32-bit integer indicating the number of nodes in
|
||||
* the search tree.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $nodeCount;
|
||||
|
||||
/**
|
||||
* This is an unsigned 16-bit integer. It indicates the number of bits in a
|
||||
* record in the search tree. Note that each node consists of two records.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $recordSize;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $searchTreeSize;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $metadata
|
||||
*/
|
||||
public function __construct(array $metadata)
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
$this->binaryFormatMajorVersion
|
||||
= $metadata['binary_format_major_version'];
|
||||
$this->binaryFormatMinorVersion
|
||||
= $metadata['binary_format_minor_version'];
|
||||
$this->buildEpoch = $metadata['build_epoch'];
|
||||
$this->databaseType = $metadata['database_type'];
|
||||
$this->languages = $metadata['languages'];
|
||||
$this->description = $metadata['description'];
|
||||
$this->ipVersion = $metadata['ip_version'];
|
||||
$this->nodeCount = $metadata['node_count'];
|
||||
$this->recordSize = $metadata['record_size'];
|
||||
$this->nodeByteSize = $this->recordSize / 4;
|
||||
$this->searchTreeSize = $this->nodeCount * $this->nodeByteSize;
|
||||
}
|
||||
}
|
||||
33
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php
vendored
Normal file
33
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* @param resource $stream
|
||||
* @param int<0, max> $numberOfBytes
|
||||
*/
|
||||
public static function read($stream, int $offset, int $numberOfBytes): string
|
||||
{
|
||||
if ($numberOfBytes === 0) {
|
||||
return '';
|
||||
}
|
||||
if (fseek($stream, $offset) === 0) {
|
||||
$value = fread($stream, $numberOfBytes);
|
||||
|
||||
// We check that the number of bytes read is equal to the number
|
||||
// asked for. We use ftell as getting the length of $value is
|
||||
// much slower.
|
||||
if ($value !== false && ftell($stream) - $offset === $numberOfBytes) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidDatabaseException(
|
||||
'The MaxMind DB file contains bad data'
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue