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