/wp-content/plugins/wp-statistics/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php
PHP | 296 lines | 215 code | 29 blank | 52 comment | 40 complexity | dd648dd18ea4df0c8ff6eabee28ddf57 MD5 | raw file
- <?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 $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.
- * @return array the record for the IP address.
- * @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.
- */
- 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 != 0) {
- 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;
- for ($i = 0; $i < $fileSize - $markerLength + 1; $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);
- }
- }