PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.3.1
Independent Analytics – WordPress Analytics Plugin v2.3.1
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 years ago Reader.php 2 years 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
59 * the MaxMind DB file to use
60 *
61 * @throws \InvalidArgumentException for invalid database path or unknown arguments
62 * @throws InvalidDatabaseException
63 * if the database is invalid or there is an error reading
64 * from it
65 */
66 public function __construct(string $database)
67 {
68 if (\func_num_args() !== 1) {
69 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
70 }
71 $fileHandle = @\fopen($database, 'rb');
72 if ($fileHandle === \false) {
73 throw new \InvalidArgumentException("The file \"{$database}\" does not exist or is not readable.");
74 }
75 $this->fileHandle = $fileHandle;
76 $fileSize = @\filesize($database);
77 if ($fileSize === \false) {
78 throw new \UnexpectedValueException("Error determining the size of \"{$database}\".");
79 }
80 $this->fileSize = $fileSize;
81 $start = $this->findMetadataStart($database);
82 $metadataDecoder = new Decoder($this->fileHandle, $start);
83 [$metadataArray] = $metadataDecoder->decode($start);
84 $this->metadata = new Metadata($metadataArray);
85 $this->decoder = new Decoder($this->fileHandle, $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE);
86 $this->ipV4Start = $this->ipV4StartNode();
87 }
88 /**
89 * Retrieves the record for the IP address.
90 *
91 * @param string $ipAddress
92 * the IP address to look up
93 *
94 * @throws \BadMethodCallException if this method is called on a closed database
95 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
96 * @throws InvalidDatabaseException
97 * if the database is invalid or there is an error reading
98 * from it
99 *
100 * @return mixed the record for the IP address
101 */
102 public function get(string $ipAddress)
103 {
104 if (\func_num_args() !== 1) {
105 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
106 }
107 [$record] = $this->getWithPrefixLen($ipAddress);
108 return $record;
109 }
110 /**
111 * Retrieves the record for the IP address and its associated network prefix length.
112 *
113 * @param string $ipAddress
114 * the IP address to look up
115 *
116 * @throws \BadMethodCallException if this method is called on a closed database
117 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
118 * @throws InvalidDatabaseException
119 * if the database is invalid or there is an error reading
120 * from it
121 *
122 * @return array an array where the first element is the record and the
123 * second the network prefix length for the record
124 */
125 public function getWithPrefixLen(string $ipAddress) : array
126 {
127 if (\func_num_args() !== 1) {
128 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
129 }
130 if (!\is_resource($this->fileHandle)) {
131 throw new \BadMethodCallException('Attempt to read from a closed MaxMind DB.');
132 }
133 [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
134 if ($pointer === 0) {
135 return [null, $prefixLen];
136 }
137 return [$this->resolveDataPointer($pointer), $prefixLen];
138 }
139 private function findAddressInTree(string $ipAddress) : array
140 {
141 $packedAddr = @\inet_pton($ipAddress);
142 if ($packedAddr === \false) {
143 throw new \InvalidArgumentException("The value \"{$ipAddress}\" is not a valid IP address.");
144 }
145 $rawAddress = \unpack('C*', $packedAddr);
146 if ($rawAddress === \false) {
147 throw new InvalidDatabaseException('Could not unpack the unsigned char of the packed in_addr representation.');
148 }
149 $bitCount = \count($rawAddress) * 8;
150 // The first node of the tree is always node 0, at the beginning of the
151 // value
152 $node = 0;
153 $metadata = $this->metadata;
154 // Check if we are looking up an IPv4 address in an IPv6 tree. If this
155 // is the case, we can skip over the first 96 nodes.
156 if ($metadata->ipVersion === 6) {
157 if ($bitCount === 32) {
158 $node = $this->ipV4Start;
159 }
160 } elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
161 throw new \InvalidArgumentException("Error looking up {$ipAddress}. You attempted to look up an" . ' IPv6 address in an IPv4-only database.');
162 }
163 $nodeCount = $metadata->nodeCount;
164 for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
165 $tempBit = 0xff & $rawAddress[($i >> 3) + 1];
166 $bit = 1 & $tempBit >> 7 - $i % 8;
167 $node = $this->readNode($node, $bit);
168 }
169 if ($node === $nodeCount) {
170 // Record is empty
171 return [0, $i];
172 }
173 if ($node > $nodeCount) {
174 // Record is a data pointer
175 return [$node, $i];
176 }
177 throw new InvalidDatabaseException('Invalid or corrupt database. Maximum search depth reached without finding a leaf node');
178 }
179 private function ipV4StartNode() : int
180 {
181 // If we have an IPv4 database, the start node is the first node
182 if ($this->metadata->ipVersion === 4) {
183 return 0;
184 }
185 $node = 0;
186 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
187 $node = $this->readNode($node, 0);
188 }
189 return $node;
190 }
191 private function readNode(int $nodeNumber, int $index) : int
192 {
193 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
194 switch ($this->metadata->recordSize) {
195 case 24:
196 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
197 $rc = \unpack('N', "\x00" . $bytes);
198 if ($rc === \false) {
199 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
200 }
201 [, $node] = $rc;
202 return $node;
203 case 28:
204 $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
205 if ($index === 0) {
206 $middle = (0xf0 & \ord($bytes[3])) >> 4;
207 } else {
208 $middle = 0xf & \ord($bytes[0]);
209 }
210 $rc = \unpack('N', \chr($middle) . \substr($bytes, $index, 3));
211 if ($rc === \false) {
212 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
213 }
214 [, $node] = $rc;
215 return $node;
216 case 32:
217 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
218 $rc = \unpack('N', $bytes);
219 if ($rc === \false) {
220 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
221 }
222 [, $node] = $rc;
223 return $node;
224 default:
225 throw new InvalidDatabaseException('Unknown record size: ' . $this->metadata->recordSize);
226 }
227 }
228 /**
229 * @return mixed
230 */
231 private function resolveDataPointer(int $pointer)
232 {
233 $resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize;
234 if ($resolved >= $this->fileSize) {
235 throw new InvalidDatabaseException("The MaxMind DB file's search tree is corrupt");
236 }
237 [$data] = $this->decoder->decode($resolved);
238 return $data;
239 }
240 /*
241 * This is an extremely naive but reasonably readable implementation. There
242 * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
243 * an issue, but I suspect it won't be.
244 */
245 private function findMetadataStart(string $filename) : int
246 {
247 $handle = $this->fileHandle;
248 $fstat = \fstat($handle);
249 if ($fstat === \false) {
250 throw new InvalidDatabaseException("Error getting file information ({$filename}).");
251 }
252 $fileSize = $fstat['size'];
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