handle = fopen( $db_path, 'rb' ); if ( ! $this->handle ) { return; } // Read database header. $this->db_type = $this->read_byte( 1 ); $this->db_column = $this->read_byte( 2 ); $this->db_year = $this->read_byte( 3 ); $this->db_month = $this->read_byte( 4 ); $this->db_day = $this->read_byte( 5 ); $this->ipv4_count = $this->read_word( 6 ); $this->ipv4_addr = $this->read_word( 10 ); $this->ipv6_count = $this->read_word( 14 ); $this->ipv6_addr = $this->read_word( 18 ); $this->ipv4_index_addr = $this->read_word( 22 ); $this->ipv6_index_addr = $this->read_word( 26 ); } /** * Destructor. */ public function __destruct() { if ( $this->handle ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose( $this->handle ); } } /** * Lookup an IP address. * * @param string $ip IP address. * @return array|null */ public function lookup( $ip ) { if ( ! $this->handle ) { return null; } // Determine IP version. $ip_version = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ? 6 : 4; if ( 4 === $ip_version ) { return $this->lookup_ipv4( $ip ); } return $this->lookup_ipv6( $ip ); } /** * Lookup IPv4 address. * * @param string $ip IPv4 address. * @return array|null */ private function lookup_ipv4( $ip ) { if ( $this->ipv4_count <= 0 ) { return null; } $ip_num = sprintf( '%u', ip2long( $ip ) ); // Use index for faster lookup. $index = ( $ip_num >> 16 ); $low = 0; $high = $this->ipv4_count; if ( $this->ipv4_index_addr > 0 ) { $low = $this->read_word( $this->ipv4_index_addr + ( $index * 8 ) ); $high = $this->read_word( $this->ipv4_index_addr + ( $index * 8 ) + 4 ); } // Binary search. while ( $low <= $high ) { $mid = (int) ( ( $low + $high ) / 2 ); $ip_from = $this->read_word( $this->ipv4_addr + $mid * $this->db_column * 4 ); $ip_to = $this->read_word( $this->ipv4_addr + ( $mid + 1 ) * $this->db_column * 4 ); if ( $ip_num >= $ip_from && $ip_num < $ip_to ) { return $this->read_record( $this->ipv4_addr + $mid * $this->db_column * 4, 4 ); } if ( $ip_num < $ip_from ) { $high = $mid - 1; } else { $low = $mid + 1; } } return null; } /** * Lookup IPv6 address. * * @param string $ip IPv6 address. * @return array|null */ private function lookup_ipv6( $ip ) { if ( $this->ipv6_count <= 0 ) { return null; } $ip_num = $this->ipv6_to_number( $ip ); $low = 0; $high = $this->ipv6_count; if ( $this->ipv6_index_addr > 0 ) { $index_value = bcdiv( $ip_num, bcpow( '2', '112' ) ); $index = bcmod( $index_value, '65536' ); $low = $this->read_word( $this->ipv6_index_addr + (int) $index * 8 ); $high = $this->read_word( $this->ipv6_index_addr + (int) $index * 8 + 4 ); } // Binary search. while ( $low <= $high ) { $mid = (int) ( ( $low + $high ) / 2 ); $ip_from = $this->read_ipv6( $this->ipv6_addr + $mid * ( $this->db_column * 4 + 12 ) ); $ip_to = $this->read_ipv6( $this->ipv6_addr + ( $mid + 1 ) * ( $this->db_column * 4 + 12 ) ); if ( bccomp( $ip_num, $ip_from ) >= 0 && bccomp( $ip_num, $ip_to ) < 0 ) { return $this->read_record( $this->ipv6_addr + $mid * ( $this->db_column * 4 + 12 ) + 12, 6 ); } if ( bccomp( $ip_num, $ip_from ) < 0 ) { $high = $mid - 1; } else { $low = $mid + 1; } } return null; } /** * Read record from database. * * @param int $row_addr Row address. * @param int $ip_version IP version. * @return array */ private function read_record( $row_addr, $ip_version ) { $record = array( 'country_code' => '-', 'country_name' => '-', ); // Country is always at column 0 for DB1-DB24. $country_pos = $row_addr + 4; if ( 6 === $ip_version ) { $country_pos = $row_addr; } $country_offset = $this->read_word( $country_pos ); if ( $country_offset > 0 ) { $record['country_code'] = $this->read_string( $country_offset ); $record['country_name'] = $this->read_string( $country_offset + 3 ); } return $record; } /** * Read a byte from database. * * @param int $pos Position. * @return int */ private function read_byte( $pos ) { fseek( $this->handle, $pos - 1, SEEK_SET ); $data = fread( $this->handle, 1 ); return unpack( 'C', $data )[1]; } /** * Read a 32-bit word from database. * * @param int $pos Position. * @return int */ private function read_word( $pos ) { fseek( $this->handle, $pos - 1, SEEK_SET ); $data = fread( $this->handle, 4 ); return unpack( 'V', $data )[1]; } /** * Read a string from database. * * @param int $pos Position. * @return string */ private function read_string( $pos ) { fseek( $this->handle, $pos, SEEK_SET ); $length = unpack( 'C', fread( $this->handle, 1 ) )[1]; return fread( $this->handle, $length ); } /** * Read IPv6 address from database. * * @param int $pos Position. * @return string */ private function read_ipv6( $pos ) { fseek( $this->handle, $pos - 1, SEEK_SET ); $data = fread( $this->handle, 16 ); $int = unpack( 'V4', $data ); $result = bcadd( bcadd( bcmul( $int[4], bcpow( '4294967296', '3' ) ), bcmul( $int[3], bcpow( '4294967296', '2' ) ) ), bcadd( bcmul( $int[2], '4294967296' ), $int[1] ) ); return $result; } /** * Convert IPv6 to number. * * @param string $ip IPv6 address. * @return string */ private function ipv6_to_number( $ip ) { $bin = inet_pton( $ip ); $hex = bin2hex( $bin ); $result = '0'; for ( $i = 0; $i < strlen( $hex ); $i++ ) { $result = bcadd( bcmul( $result, '16' ), hexdec( $hex[ $i ] ) ); } return $result; } /** * Get database type string. * * @return string */ public function get_database_type() { if ( ! $this->handle ) { return ''; } $types = array( 1 => 'DB1 (Country)', 2 => 'DB2 (Country + ISP)', 3 => 'DB3 (Country + Region + City)', 4 => 'DB4 (Country + Region + City + ISP)', 5 => 'DB5 (Country + Region + City + Lat/Long)', 11 => 'DB11 (Full)', 24 => 'DB24 (Full)', ); $date = sprintf( '%04d-%02d-%02d', 2000 + $this->db_year, $this->db_month, $this->db_day ); $type = isset( $types[ $this->db_type ] ) ? $types[ $this->db_type ] : 'DB' . $this->db_type; return $type . ' (' . $date . ')'; } }