PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 1.18
Independent Analytics – WordPress Analytics Plugin v1.18
2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / vendor / maxmind-db / reader / src / MaxMind / Db / Reader.php
independent-analytics / vendor / maxmind-db / reader / src / MaxMind / Db Last commit date
Reader 3 years ago Reader.php 3 years ago
Reader.php
288 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace IAWP\MaxMind\Db;
5
6 use ArgumentCountError;
7 use BadMethodCallException;
8 use Exception;
9 use InvalidArgumentException;
10 use IAWP\MaxMind\Db\Reader\Decoder;
11 use IAWP\MaxMind\Db\Reader\InvalidDatabaseException;
12 use IAWP\MaxMind\Db\Reader\Metadata;
13 use IAWP\MaxMind\Db\Reader\Util;
14 use UnexpectedValueException;
15 /**
16 * Instances of this class provide a reader for the MaxMind DB format. IP
17 * addresses can be looked up using the get method.
18 */
19 class Reader
20 {
21 /**
22 * @var int
23 */
24 private static $DATA_SECTION_SEPARATOR_SIZE = 16;
25 /**
26 * @var string
27 */
28 private static $METADATA_START_MARKER = "\xab\xcd\xefMaxMind.com";
29 /**
30 * @var int
31 */
32 private static $METADATA_START_MARKER_LENGTH = 14;
33 /**
34 * @var int
35 */
36 private static $METADATA_MAX_SIZE = 131072;
37 // 128 * 1024 = 128KiB
38 /**
39 * @var Decoder
40 */
41 private $decoder;
42 /**
43 * @var resource
44 */
45 private $fileHandle;
46 /**
47 * @var int
48 */
49 private $fileSize;
50 /**
51 * @var int
52 */
53 private $ipV4Start;
54 /**
55 * @var Metadata
56 */
57 private $metadata;
58 /**
59 * Constructs a Reader for the MaxMind DB format. The file passed to it must
60 * be a valid MaxMind DB file such as a GeoIp2 database file.
61 *
62 * @param string $database
63 * the MaxMind DB file to use
64 *
65 * @throws InvalidArgumentException for invalid database path or unknown arguments
66 * @throws InvalidDatabaseException
67 * if the database is invalid or there is an error reading
68 * from it
69 */
70 public function __construct(string $database)
71 {
72 if (\func_num_args() !== 1) {
73 throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
74 }
75 $fileHandle = @\fopen($database, 'rb');
76 if ($fileHandle === \false) {
77 throw new InvalidArgumentException("The file \"{$database}\" does not exist or is not readable.");
78 }
79 $this->fileHandle = $fileHandle;
80 $fileSize = @\filesize($database);
81 if ($fileSize === \false) {
82 throw new UnexpectedValueException("Error determining the size of \"{$database}\".");
83 }
84 $this->fileSize = $fileSize;
85 $start = $this->findMetadataStart($database);
86 $metadataDecoder = new Decoder($this->fileHandle, $start);
87 [$metadataArray] = $metadataDecoder->decode($start);
88 $this->metadata = new Metadata($metadataArray);
89 $this->decoder = new Decoder($this->fileHandle, $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE);
90 $this->ipV4Start = $this->ipV4StartNode();
91 }
92 /**
93 * Retrieves the record for the IP address.
94 *
95 * @param string $ipAddress
96 * the IP address to look up
97 *
98 * @throws BadMethodCallException if this method is called on a closed database
99 * @throws InvalidArgumentException if something other than a single IP address is passed to the method
100 * @throws InvalidDatabaseException
101 * if the database is invalid or there is an error reading
102 * from it
103 *
104 * @return mixed the record for the IP address
105 */
106 public function get(string $ipAddress)
107 {
108 if (\func_num_args() !== 1) {
109 throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
110 }
111 [$record] = $this->getWithPrefixLen($ipAddress);
112 return $record;
113 }
114 /**
115 * Retrieves the record for the IP address and its associated network prefix length.
116 *
117 * @param string $ipAddress
118 * the IP address to look up
119 *
120 * @throws BadMethodCallException if this method is called on a closed database
121 * @throws InvalidArgumentException if something other than a single IP address is passed to the method
122 * @throws InvalidDatabaseException
123 * if the database is invalid or there is an error reading
124 * from it
125 *
126 * @return array an array where the first element is the record and the
127 * second the network prefix length for the record
128 */
129 public function getWithPrefixLen(string $ipAddress) : array
130 {
131 if (\func_num_args() !== 1) {
132 throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
133 }
134 if (!\is_resource($this->fileHandle)) {
135 throw new BadMethodCallException('Attempt to read from a closed MaxMind DB.');
136 }
137 [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
138 if ($pointer === 0) {
139 return [null, $prefixLen];
140 }
141 return [$this->resolveDataPointer($pointer), $prefixLen];
142 }
143 private function findAddressInTree(string $ipAddress) : array
144 {
145 $packedAddr = @\inet_pton($ipAddress);
146 if ($packedAddr === \false) {
147 throw new InvalidArgumentException("The value \"{$ipAddress}\" is not a valid IP address.");
148 }
149 $rawAddress = \unpack('C*', $packedAddr);
150 $bitCount = \count($rawAddress) * 8;
151 // The first node of the tree is always node 0, at the beginning of the
152 // value
153 $node = 0;
154 $metadata = $this->metadata;
155 // Check if we are looking up an IPv4 address in an IPv6 tree. If this
156 // is the case, we can skip over the first 96 nodes.
157 if ($metadata->ipVersion === 6) {
158 if ($bitCount === 32) {
159 $node = $this->ipV4Start;
160 }
161 } elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
162 throw new InvalidArgumentException("Error looking up {$ipAddress}. You attempted to look up an" . ' IPv6 address in an IPv4-only database.');
163 }
164 $nodeCount = $metadata->nodeCount;
165 for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
166 $tempBit = 0xff & $rawAddress[($i >> 3) + 1];
167 $bit = 1 & $tempBit >> 7 - $i % 8;
168 $node = $this->readNode($node, $bit);
169 }
170 if ($node === $nodeCount) {
171 // Record is empty
172 return [0, $i];
173 }
174 if ($node > $nodeCount) {
175 // Record is a data pointer
176 return [$node, $i];
177 }
178 throw new InvalidDatabaseException('Invalid or corrupt database. Maximum search depth reached without finding a leaf node');
179 }
180 private function ipV4StartNode() : int
181 {
182 // If we have an IPv4 database, the start node is the first node
183 if ($this->metadata->ipVersion === 4) {
184 return 0;
185 }
186 $node = 0;
187 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
188 $node = $this->readNode($node, 0);
189 }
190 return $node;
191 }
192 private function readNode(int $nodeNumber, int $index) : int
193 {
194 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
195 switch ($this->metadata->recordSize) {
196 case 24:
197 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
198 [, $node] = \unpack('N', "\x00" . $bytes);
199 return $node;
200 case 28:
201 $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
202 if ($index === 0) {
203 $middle = (0xf0 & \ord($bytes[3])) >> 4;
204 } else {
205 $middle = 0xf & \ord($bytes[0]);
206 }
207 [, $node] = \unpack('N', \chr($middle) . \substr($bytes, $index, 3));
208 return $node;
209 case 32:
210 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
211 [, $node] = \unpack('N', $bytes);
212 return $node;
213 default:
214 throw new InvalidDatabaseException('Unknown record size: ' . $this->metadata->recordSize);
215 }
216 }
217 /**
218 * @return mixed
219 */
220 private function resolveDataPointer(int $pointer)
221 {
222 $resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize;
223 if ($resolved >= $this->fileSize) {
224 throw new InvalidDatabaseException("The MaxMind DB file's search tree is corrupt");
225 }
226 [$data] = $this->decoder->decode($resolved);
227 return $data;
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(string $filename) : int
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 $minStart = $fileSize - \min(self::$METADATA_MAX_SIZE, $fileSize);
242 for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
243 if (\fseek($handle, $offset) !== 0) {
244 break;
245 }
246 $value = \fread($handle, $markerLength);
247 if ($value === $marker) {
248 return $offset + $markerLength;
249 }
250 }
251 throw new InvalidDatabaseException("Error opening database file ({$filename}). " . 'Is this a valid MaxMind DB file?');
252 }
253 /**
254 * @throws InvalidArgumentException if arguments are passed to the method
255 * @throws BadMethodCallException if the database has been closed
256 *
257 * @return Metadata object for the database
258 */
259 public function metadata() : Metadata
260 {
261 if (\func_num_args()) {
262 throw new ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
263 }
264 // Not technically required, but this makes it consistent with
265 // C extension and it allows us to change our implementation later.
266 if (!\is_resource($this->fileHandle)) {
267 throw new BadMethodCallException('Attempt to read from a closed MaxMind DB.');
268 }
269 return clone $this->metadata;
270 }
271 /**
272 * Closes the MaxMind DB and returns resources to the system.
273 *
274 * @throws Exception
275 * if an I/O error occurs
276 */
277 public function close() : void
278 {
279 if (\func_num_args()) {
280 throw new ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
281 }
282 if (!\is_resource($this->fileHandle)) {
283 throw new BadMethodCallException('Attempt to close a closed MaxMind DB.');
284 }
285 \fclose($this->fileHandle);
286 }
287 }
288