File: /home/crowdandsafety/public_html/wp-content/plugins/convertplug/framework/lib/geolite2/Reader.php
<?php
/**
* 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).
* (https://github.com/maxmind/MaxMind-DB-Reader-php/blob/master/LICENSE)
* Distributed under the terms of the Apache License 2.0.
*/
// phpcs:ignoreFile -- this is a third-party lib
namespace CPlus\MaxMind\Db;
use CPlus\MaxMind\Db\Reader\Decoder;
use CPlus\MaxMind\Db\Reader\InvalidDatabaseException;
use CPlus\MaxMind\Db\Reader\Metadata;
use CPlus\MaxMind\Db\Reader\Util;
/**
* Instances of this class provide a reader for the MaxMind DB format. IP
* addresses can be looked up using the <code>get</code> method.
*/
class Reader {
private static $DATA_SECTION_SEPARATOR_SIZE = 16;
private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
private static $METADATA_START_MARKER_LENGTH = 14;
private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB
private $decoder;
private $fileHandle;
private $fileSize;
private $ipV4Start;
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 CPlus\MaxMind\Db\Reader\InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*/
public function __construct( $database ) {
if ( func_num_args() !== 1 ) {
throw new \InvalidArgumentException(
'The constructor takes exactly one argument.'
);
}
if ( ! is_readable( $database ) ) {
throw new \InvalidArgumentException(
"The file \"$database\" does not exist or is not readable."
);
}
$this->fileHandle = @fopen( $database, 'rb' );
if ( $this->fileHandle === false ) {
throw new \InvalidArgumentException(
"Error opening \"$database\"."
);
}
$this->fileSize = @filesize( $database );
if ( $this->fileSize === false ) {
throw new \UnexpectedValueException(
"Error determining the size of \"$database\"."
);
}
$start = $this->findMetadataStart( $database );
$metadataDecoder = new Decoder( $this->fileHandle, $start );
list($metadataArray) = $metadataDecoder->decode( $start );
$this->metadata = new Metadata( $metadataArray );
$this->decoder = new Decoder(
$this->fileHandle,
$this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
);
}
/**
* Looks up the <code>address</code> in the MaxMind DB.
*
* @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 the record for the IP address
*/
public function get( $ipAddress ) {
if ( func_num_args() !== 1 ) {
throw new \InvalidArgumentException(
'Method takes exactly one argument.'
);
}
if ( ! is_resource( $this->fileHandle ) ) {
throw new \BadMethodCallException(
'Attempt to read from a closed MaxMind DB.'
);
}
if ( ! filter_var( $ipAddress, FILTER_VALIDATE_IP ) ) {
throw new \InvalidArgumentException(
"The value \"$ipAddress\" is not a valid IP address."
);
}
if ( $this->metadata->ipVersion === 4 && strrpos( $ipAddress, ':' ) ) {
throw new \InvalidArgumentException(
"Error looking up $ipAddress. You attempted to look up an"
. ' IPv6 address in an IPv4-only database.'
);
}
$pointer = $this->findAddressInTree( $ipAddress );
if ( $pointer === 0 ) {
return null;
}
return $this->resolveDataPointer( $pointer );
}
private function findAddressInTree( $ipAddress ) {
// XXX - could simplify. Done as a byte array to ease porting
$rawAddress = array_merge( unpack( 'C*', inet_pton( $ipAddress ) ) );
$bitCount = count( $rawAddress ) * 8;
// The first node of the tree is always node 0, at the beginning of the
// value
$node = $this->startNode( $bitCount );
for ( $i = 0; $i < $bitCount; $i++ ) {
if ( $node >= $this->metadata->nodeCount ) {
break;
}
$tempBit = 0xFF & $rawAddress[ $i >> 3 ];
$bit = 1 & ( $tempBit >> 7 - ( $i % 8 ) );
$node = $this->readNode( $node, $bit );
}
if ( $node === $this->metadata->nodeCount ) {
// Record is empty
return 0;
} elseif ( $node > $this->metadata->nodeCount ) {
// Record is a data pointer
return $node;
}
throw new InvalidDatabaseException( 'Something bad happened' );
}
private function startNode( $length ) {
// 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 ( $this->metadata->ipVersion === 6 && $length === 32 ) {
return $this->ipV4StartNode();
}
// The first node of the tree is always node 0, at the beginning of the
// value
return 0;
}
private function ipV4StartNode() {
// This is a defensive check. There is no reason to call this when you
// have an IPv4 tree.
if ( $this->metadata->ipVersion === 4 ) {
return 0;
}
if ( $this->ipV4Start ) {
return $this->ipV4Start;
}
$node = 0;
for ( $i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++ ) {
$node = $this->readNode( $node, 0 );
}
$this->ipV4Start = $node;
return $node;
}
private function readNode( $nodeNumber, $index ) {
$baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
// XXX - probably could condense this.
switch ( $this->metadata->recordSize ) {
case 24:
$bytes = Util::read( $this->fileHandle, $baseOffset + $index * 3, 3 );
list(, $node) = unpack( 'N', "\x00" . $bytes );
return $node;
case 28:
$middleByte = Util::read( $this->fileHandle, $baseOffset + 3, 1 );
list(, $middle) = unpack( 'C', $middleByte );
if ( $index === 0 ) {
$middle = ( 0xF0 & $middle ) >> 4;
} else {
$middle = 0x0F & $middle;
}
$bytes = Util::read( $this->fileHandle, $baseOffset + $index * 4, 3 );
list(, $node) = unpack( 'N', chr( $middle ) . $bytes );
return $node;
case 32:
$bytes = Util::read( $this->fileHandle, $baseOffset + $index * 4, 4 );
list(, $node) = unpack( 'N', $bytes );
return $node;
default:
throw new InvalidDatabaseException(
'Unknown record size: '
. $this->metadata->recordSize
);
}
}
private function resolveDataPointer( $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"
);
}
list($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( $filename ) {
$handle = $this->fileHandle;
$fstat = fstat( $handle );
$fileSize = $fstat['size'];
$marker = self::$METADATA_START_MARKER;
$markerLength = self::$METADATA_START_MARKER_LENGTH;
$metadataMaxLengthExcludingMarker
= min( self::$METADATA_MAX_SIZE, $fileSize ) - $markerLength;
for ( $i = 0; $i <= $metadataMaxLengthExcludingMarker; $i++ ) {
for ( $j = 0; $j < $markerLength; $j++ ) {
fseek( $handle, $fileSize - $i - $j - 1 );
$matchBit = fgetc( $handle );
if ( $matchBit !== $marker[ $markerLength - $j - 1 ] ) {
continue 2;
}
}
return $fileSize - $i;
}
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() {
if ( func_num_args() ) {
throw new \InvalidArgumentException(
'Method takes no arguments.'
);
}
// 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 $this->metadata;
}
/**
* Closes the MaxMind DB and returns resources to the system.
*
* @throws \Exception
* if an I/O error occurs
*/
public function close() {
if ( ! is_resource( $this->fileHandle ) ) {
throw new \BadMethodCallException(
'Attempt to close a closed MaxMind DB.'
);
}
fclose( $this->fileHandle );
}
}