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