PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.15.2
Independent Analytics – WordPress Analytics Plugin v2.15.2
2.15.4 2.15.3 2.15.2 2.15.1 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 2 weeks ago Reader.php 2 weeks ago
Reader.php
302 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace IAWPSCOPED\MaxMind\Db;
5
6 use IAWPSCOPED\MaxMind\Db\Reader\Decoder;
7 use IAWPSCOPED\MaxMind\Db\Reader\InvalidDatabaseException;
8 use IAWPSCOPED\MaxMind\Db\Reader\Metadata;
9 use IAWPSCOPED\MaxMind\Db\Reader\Util;
10 /**
11 * Instances of this class provide a reader for the MaxMind DB format. IP
12 * addresses can be looked up using the get method.
13 * @internal
14 */
15 class Reader
16 {
17 /**
18 * @var int
19 */
20 private static $DATA_SECTION_SEPARATOR_SIZE = 16;
21 /**
22 * @var string
23 */
24 private static $METADATA_START_MARKER = "\xab\xcd\xefMaxMind.com";
25 /**
26 * @var int<0, max>
27 */
28 private static $METADATA_START_MARKER_LENGTH = 14;
29 /**
30 * @var int
31 */
32 private static $METADATA_MAX_SIZE = 131072;
33 // 128 * 1024 = 128KiB
34 /**
35 * @var Decoder
36 */
37 private $decoder;
38 /**
39 * @var resource
40 */
41 private $fileHandle;
42 /**
43 * @var int
44 */
45 private $fileSize;
46 /**
47 * @var int
48 */
49 private $ipV4Start;
50 /**
51 * @var Metadata
52 */
53 private $metadata;
54 /**
55 * Constructs a Reader for the MaxMind DB format. The file passed to it must
56 * be a valid MaxMind DB file such as a GeoIp2 database file.
57 *
58 * @param string $database the MaxMind DB file to use
59 *
60 * @throws \InvalidArgumentException for invalid database path or unknown arguments
61 * @throws InvalidDatabaseException
62 * if the database is invalid or there is an error reading
63 * from it
64 */
65 public function __construct(string $database)
66 {
67 if (\func_num_args() !== 1) {
68 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
69 }
70 if (\is_dir($database)) {
71 // This matches the error that the C extension throws.
72 throw new InvalidDatabaseException("Error opening database file ({$database}). Is this a valid MaxMind DB file?");
73 }
74 $fileHandle = @\fopen($database, 'rb');
75 if ($fileHandle === \false) {
76 throw new \InvalidArgumentException("The file \"{$database}\" does not exist or is not readable.");
77 }
78 $this->fileHandle = $fileHandle;
79 $fstat = \fstat($fileHandle);
80 if ($fstat === \false) {
81 throw new \UnexpectedValueException("Error determining the size of \"{$database}\".");
82 }
83 $this->fileSize = $fstat['size'];
84 $start = $this->findMetadataStart($database);
85 $metadataDecoder = new Decoder($this->fileHandle, $start);
86 [$metadataArray] = $metadataDecoder->decode($start);
87 $this->metadata = new Metadata($metadataArray);
88 $this->decoder = new Decoder($this->fileHandle, $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE);
89 $this->ipV4Start = $this->ipV4StartNode();
90 }
91 /**
92 * Retrieves the record for the IP address.
93 *
94 * @param string $ipAddress the IP address to look up
95 *
96 * @throws \BadMethodCallException if this method is called on a closed database
97 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
98 * @throws InvalidDatabaseException
99 * if the database is invalid or there is an error reading
100 * from it
101 *
102 * @return mixed the record for the IP address
103 */
104 public function get(string $ipAddress)
105 {
106 if (\func_num_args() !== 1) {
107 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
108 }
109 [$record] = $this->getWithPrefixLen($ipAddress);
110 return $record;
111 }
112 /**
113 * Retrieves the record for the IP address and its associated network prefix length.
114 *
115 * @param string $ipAddress the IP address to look up
116 *
117 * @throws \BadMethodCallException if this method is called on a closed database
118 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
119 * @throws InvalidDatabaseException
120 * if the database is invalid or there is an error reading
121 * from it
122 *
123 * @return array{0:mixed, 1:int} an array where the first element is the record and the
124 * second the network prefix length for the record
125 */
126 public function getWithPrefixLen(string $ipAddress) : array
127 {
128 if (\func_num_args() !== 1) {
129 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
130 }
131 if (!\is_resource($this->fileHandle)) {
132 throw new \BadMethodCallException('Attempt to read from a closed MaxMind DB.');
133 }
134 [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
135 if ($pointer === 0) {
136 return [null, $prefixLen];
137 }
138 return [$this->resolveDataPointer($pointer), $prefixLen];
139 }
140 /**
141 * @return array{0:int, 1:int}
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 if ($rawAddress === \false) {
151 throw new InvalidDatabaseException('Could not unpack the unsigned char of the packed in_addr representation.');
152 }
153 $bitCount = \count($rawAddress) * 8;
154 // The first node of the tree is always node 0, at the beginning of the
155 // value
156 $node = 0;
157 $metadata = $this->metadata;
158 // Check if we are looking up an IPv4 address in an IPv6 tree. If this
159 // is the case, we can skip over the first 96 nodes.
160 if ($metadata->ipVersion === 6) {
161 if ($bitCount === 32) {
162 $node = $this->ipV4Start;
163 }
164 } elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
165 throw new \InvalidArgumentException("Error looking up {$ipAddress}. You attempted to look up an" . ' IPv6 address in an IPv4-only database.');
166 }
167 $nodeCount = $metadata->nodeCount;
168 for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
169 $tempBit = 0xff & $rawAddress[($i >> 3) + 1];
170 $bit = 1 & $tempBit >> 7 - $i % 8;
171 $node = $this->readNode($node, $bit);
172 }
173 if ($node === $nodeCount) {
174 // Record is empty
175 return [0, $i];
176 }
177 if ($node > $nodeCount) {
178 // Record is a data pointer
179 return [$node, $i];
180 }
181 throw new InvalidDatabaseException('Invalid or corrupt database. Maximum search depth reached without finding a leaf node');
182 }
183 private function ipV4StartNode() : int
184 {
185 // If we have an IPv4 database, the start node is the first node
186 if ($this->metadata->ipVersion === 4) {
187 return 0;
188 }
189 $node = 0;
190 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
191 $node = $this->readNode($node, 0);
192 }
193 return $node;
194 }
195 private function readNode(int $nodeNumber, int $index) : int
196 {
197 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
198 switch ($this->metadata->recordSize) {
199 case 24:
200 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
201 $rc = \unpack('N', "\x00" . $bytes);
202 if ($rc === \false) {
203 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
204 }
205 [, $node] = $rc;
206 return $node;
207 case 28:
208 $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
209 if ($index === 0) {
210 $middle = (0xf0 & \ord($bytes[3])) >> 4;
211 } else {
212 $middle = 0xf & \ord($bytes[0]);
213 }
214 $rc = \unpack('N', \chr($middle) . \substr($bytes, $index, 3));
215 if ($rc === \false) {
216 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
217 }
218 [, $node] = $rc;
219 return $node;
220 case 32:
221 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
222 $rc = \unpack('N', $bytes);
223 if ($rc === \false) {
224 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
225 }
226 [, $node] = $rc;
227 return $node;
228 default:
229 throw new InvalidDatabaseException('Unknown record size: ' . $this->metadata->recordSize);
230 }
231 }
232 /**
233 * @return mixed
234 */
235 private function resolveDataPointer(int $pointer)
236 {
237 $resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize;
238 if ($resolved >= $this->fileSize) {
239 throw new InvalidDatabaseException("The MaxMind DB file's search tree is corrupt");
240 }
241 [$data] = $this->decoder->decode($resolved);
242 return $data;
243 }
244 /*
245 * This is an extremely naive but reasonably readable implementation. There
246 * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
247 * an issue, but I suspect it won't be.
248 */
249 private function findMetadataStart(string $filename) : int
250 {
251 $handle = $this->fileHandle;
252 $fileSize = $this->fileSize;
253 $marker = self::$METADATA_START_MARKER;
254 $markerLength = self::$METADATA_START_MARKER_LENGTH;
255 $minStart = $fileSize - \min(self::$METADATA_MAX_SIZE, $fileSize);
256 for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
257 if (\fseek($handle, $offset) !== 0) {
258 break;
259 }
260 $value = \fread($handle, $markerLength);
261 if ($value === $marker) {
262 return $offset + $markerLength;
263 }
264 }
265 throw new InvalidDatabaseException("Error opening database file ({$filename}). " . 'Is this a valid MaxMind DB file?');
266 }
267 /**
268 * @throws \InvalidArgumentException if arguments are passed to the method
269 * @throws \BadMethodCallException if the database has been closed
270 *
271 * @return Metadata object for the database
272 */
273 public function metadata() : Metadata
274 {
275 if (\func_num_args()) {
276 throw new \ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
277 }
278 // Not technically required, but this makes it consistent with
279 // C extension and it allows us to change our implementation later.
280 if (!\is_resource($this->fileHandle)) {
281 throw new \BadMethodCallException('Attempt to read from a closed MaxMind DB.');
282 }
283 return clone $this->metadata;
284 }
285 /**
286 * Closes the MaxMind DB and returns resources to the system.
287 *
288 * @throws \Exception
289 * if an I/O error occurs
290 */
291 public function close() : void
292 {
293 if (\func_num_args()) {
294 throw new \ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
295 }
296 if (!\is_resource($this->fileHandle)) {
297 throw new \BadMethodCallException('Attempt to close a closed MaxMind DB.');
298 }
299 \fclose($this->fileHandle);
300 }
301 }
302