GeoIP2 PHP API v2.4.4
  • Namespace
  • Class

Namespaces

  • GeoIp2
    • Database
    • Exception
    • Model
    • Record
    • WebService
  • MaxMind
    • Db
      • Reader
  • PHP

Classes

  • GeoIp2\Database\Reader
  • GeoIp2\Model\AnonymousIp
  • GeoIp2\Model\City
  • GeoIp2\Model\ConnectionType
  • GeoIp2\Model\Country
  • GeoIp2\Model\Domain
  • GeoIp2\Model\Enterprise
  • GeoIp2\Model\Insights
  • GeoIp2\Model\Isp
  • GeoIp2\Record\AbstractPlaceRecord
  • GeoIp2\Record\AbstractRecord
  • GeoIp2\Record\City
  • GeoIp2\Record\Continent
  • GeoIp2\Record\Country
  • GeoIp2\Record\Location
  • GeoIp2\Record\MaxMind
  • GeoIp2\Record\Postal
  • GeoIp2\Record\RepresentedCountry
  • GeoIp2\Record\Subdivision
  • GeoIp2\Record\Traits
  • GeoIp2\WebService\Client
  • MaxMind\Db\Reader
  • MaxMind\Db\Reader\Decoder
  • MaxMind\Db\Reader\Metadata
  • MaxMind\Db\Reader\Util

Interfaces

  • GeoIp2\ProviderInterface

Exceptions

  • BadFunctionCallException
  • BadMethodCallException
  • Exception
  • GeoIp2\Exception\AddressNotFoundException
  • GeoIp2\Exception\AuthenticationException
  • GeoIp2\Exception\GeoIp2Exception
  • GeoIp2\Exception\HttpException
  • GeoIp2\Exception\InvalidRequestException
  • GeoIp2\Exception\OutOfQueriesException
  • InvalidArgumentException
  • LogicException
  • MaxMind\Db\Reader\InvalidDatabaseException
  1 <?php
  2 
  3 namespace MaxMind\Db;
  4 
  5 use MaxMind\Db\Reader\Decoder;
  6 use MaxMind\Db\Reader\InvalidDatabaseException;
  7 use MaxMind\Db\Reader\Metadata;
  8 use MaxMind\Db\Reader\Util;
  9 
 10 /**
 11  * Instances of this class provide a reader for the MaxMind DB format. IP
 12  * addresses can be looked up using the <code>get</code> method.
 13  */
 14 class Reader
 15 {
 16     private static $DATA_SECTION_SEPARATOR_SIZE = 16;
 17     private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
 18     private static $METADATA_START_MARKER_LENGTH = 15;
 19 
 20     private $decoder;
 21     private $fileHandle;
 22     private $fileSize;
 23     private $ipV4Start;
 24     private $metadata;
 25 
 26     /**
 27      * Constructs a Reader for the MaxMind DB format. The file passed to it must
 28      * be a valid MaxMind DB file such as a GeoIp2 database file.
 29      *
 30      * @param string $database
 31      *            the MaxMind DB file to use.
 32      * @throws \InvalidArgumentException for invalid database path or unknown arguments
 33      * @throws \MaxMind\Db\Reader\InvalidDatabaseException
 34      *             if the database is invalid or there is an error reading
 35      *             from it.
 36      */
 37     public function __construct($database)
 38     {
 39         if (func_num_args() != 1) {
 40             throw new \InvalidArgumentException(
 41                 'The constructor takes exactly one argument.'
 42             );
 43         }
 44 
 45         if (!is_readable($database)) {
 46             throw new \InvalidArgumentException(
 47                 "The file \"$database\" does not exist or is not readable."
 48             );
 49         }
 50         $this->fileHandle = @fopen($database, 'rb');
 51         if ($this->fileHandle === false) {
 52             throw new \InvalidArgumentException(
 53                 "Error opening \"$database\"."
 54             );
 55         }
 56         $this->fileSize = @filesize($database);
 57         if ($this->fileSize === false) {
 58             throw new \UnexpectedValueException(
 59                 "Error determining the size of \"$database\"."
 60             );
 61         }
 62 
 63         $start = $this->findMetadataStart($database);
 64         $metadataDecoder = new Decoder($this->fileHandle, $start);
 65         list($metadataArray) = $metadataDecoder->decode($start);
 66         $this->metadata = new Metadata($metadataArray);
 67         $this->decoder = new Decoder(
 68             $this->fileHandle,
 69             $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
 70         );
 71     }
 72 
 73     /**
 74      * Looks up the <code>address</code> in the MaxMind DB.
 75      *
 76      * @param string $ipAddress
 77      *            the IP address to look up.
 78      * @return array the record for the IP address.
 79      * @throws \BadMethodCallException if this method is called on a closed database.
 80      * @throws \InvalidArgumentException if something other than a single IP address is passed to the method.
 81      * @throws InvalidDatabaseException
 82      *             if the database is invalid or there is an error reading
 83      *             from it.
 84      */
 85     public function get($ipAddress)
 86     {
 87         if (func_num_args() != 1) {
 88             throw new \InvalidArgumentException(
 89                 'Method takes exactly one argument.'
 90             );
 91         }
 92 
 93         if (!is_resource($this->fileHandle)) {
 94             throw new \BadMethodCallException(
 95                 'Attempt to read from a closed MaxMind DB.'
 96             );
 97         }
 98 
 99         if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
100             throw new \InvalidArgumentException(
101                 "The value \"$ipAddress\" is not a valid IP address."
102             );
103         }
104 
105         if ($this->metadata->ipVersion == 4 && strrpos($ipAddress, ':')) {
106             throw new \InvalidArgumentException(
107                 "Error looking up $ipAddress. You attempted to look up an"
108                 . " IPv6 address in an IPv4-only database."
109             );
110         }
111         $pointer = $this->findAddressInTree($ipAddress);
112         if ($pointer == 0) {
113             return null;
114         }
115         return $this->resolveDataPointer($pointer);
116     }
117 
118     private function findAddressInTree($ipAddress)
119     {
120         // XXX - could simplify. Done as a byte array to ease porting
121         $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
122 
123         $bitCount = count($rawAddress) * 8;
124 
125         // The first node of the tree is always node 0, at the beginning of the
126         // value
127         $node = $this->startNode($bitCount);
128 
129         for ($i = 0; $i < $bitCount; $i++) {
130             if ($node >= $this->metadata->nodeCount) {
131                 break;
132             }
133             $tempBit = 0xFF & $rawAddress[$i >> 3];
134             $bit = 1 & ($tempBit >> 7 - ($i % 8));
135 
136             $node = $this->readNode($node, $bit);
137         }
138         if ($node == $this->metadata->nodeCount) {
139             // Record is empty
140             return 0;
141         } elseif ($node > $this->metadata->nodeCount) {
142             // Record is a data pointer
143             return $node;
144         }
145         throw new InvalidDatabaseException("Something bad happened");
146     }
147 
148 
149     private function startNode($length)
150     {
151         // Check if we are looking up an IPv4 address in an IPv6 tree. If this
152         // is the case, we can skip over the first 96 nodes.
153         if ($this->metadata->ipVersion == 6 && $length == 32) {
154             return $this->ipV4StartNode();
155         }
156         // The first node of the tree is always node 0, at the beginning of the
157         // value
158         return 0;
159     }
160 
161     private function ipV4StartNode()
162     {
163         // This is a defensive check. There is no reason to call this when you
164         // have an IPv4 tree.
165         if ($this->metadata->ipVersion == 4) {
166             return 0;
167         }
168 
169         if ($this->ipV4Start != 0) {
170             return $this->ipV4Start;
171         }
172         $node = 0;
173 
174         for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) {
175             $node = $this->readNode($node, 0);
176         }
177         $this->ipV4Start = $node;
178         return $node;
179     }
180 
181     private function readNode($nodeNumber, $index)
182     {
183         $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
184 
185         // XXX - probably could condense this.
186         switch ($this->metadata->recordSize) {
187             case 24:
188                 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
189                 list(, $node) = unpack('N', "\x00" . $bytes);
190                 return $node;
191             case 28:
192                 $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
193                 list(, $middle) = unpack('C', $middleByte);
194                 if ($index == 0) {
195                     $middle = (0xF0 & $middle) >> 4;
196                 } else {
197                     $middle = 0x0F & $middle;
198                 }
199                 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
200                 list(, $node) = unpack('N', chr($middle) . $bytes);
201                 return $node;
202             case 32:
203                 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
204                 list(, $node) = unpack('N', $bytes);
205                 return $node;
206             default:
207                 throw new InvalidDatabaseException(
208                     'Unknown record size: '
209                     . $this->metadata->recordSize
210                 );
211         }
212     }
213 
214     private function resolveDataPointer($pointer)
215     {
216         $resolved = $pointer - $this->metadata->nodeCount
217             + $this->metadata->searchTreeSize;
218         if ($resolved > $this->fileSize) {
219             throw new InvalidDatabaseException(
220                 "The MaxMind DB file's search tree is corrupt"
221             );
222         }
223 
224         list($data) = $this->decoder->decode($resolved);
225         return $data;
226     }
227 
228     /*
229      * This is an extremely naive but reasonably readable implementation. There
230      * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
231      * an issue, but I suspect it won't be.
232      */
233     private function findMetadataStart($filename)
234     {
235         $handle = $this->fileHandle;
236         $fstat = fstat($handle);
237         $fileSize = $fstat['size'];
238         $marker = self::$METADATA_START_MARKER;
239         $markerLength = self::$METADATA_START_MARKER_LENGTH;
240 
241         for ($i = 0; $i < $fileSize - $markerLength; $i++) {
242             for ($j = 0; $j < $markerLength; $j++) {
243                 fseek($handle, $fileSize - $i - $j - 1);
244                 $matchBit = fgetc($handle);
245                 if ($matchBit != $marker[$markerLength - $j - 1]) {
246                     continue 2;
247                 }
248             }
249             return $fileSize - $i;
250         }
251         throw new InvalidDatabaseException(
252             "Error opening database file ($filename). " .
253             'Is this a valid MaxMind DB file?'
254         );
255     }
256 
257     /**
258      * @throws \InvalidArgumentException if arguments are passed to the method.
259      * @throws \BadMethodCallException if the database has been closed.
260      * @return Metadata object for the database.
261      */
262     public function metadata()
263     {
264         if (func_num_args()) {
265             throw new \InvalidArgumentException(
266                 'Method takes no arguments.'
267             );
268         }
269 
270         // Not technically required, but this makes it consistent with
271         // C extension and it allows us to change our implementation later.
272         if (!is_resource($this->fileHandle)) {
273             throw new \BadMethodCallException(
274                 'Attempt to read from a closed MaxMind DB.'
275             );
276         }
277 
278         return $this->metadata;
279     }
280 
281     /**
282      * Closes the MaxMind DB and returns resources to the system.
283      *
284      * @throws \Exception
285      *             if an I/O error occurs.
286      */
287     public function close()
288     {
289         if (!is_resource($this->fileHandle)) {
290             throw new \BadMethodCallException(
291                 'Attempt to close a closed MaxMind DB.'
292             );
293         }
294         fclose($this->fileHandle);
295     }
296 }
297 
GeoIP2 PHP API v2.4.4 API documentation generated by ApiGen