Seditio Source
Root |
./othercms/b2evolution_7.2.3/plugins/geoip_plugin/maxmind/src/MaxMind/Db/Reader.php
<?php

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 <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 \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);
    }
}