1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /* |
||
6 | * This file is part of the Geocoder package. |
||
7 | * For the full copyright and license information, please view the LICENSE |
||
8 | * file that was distributed with this source code. |
||
9 | * |
||
10 | * @license MIT License |
||
11 | */ |
||
12 | |||
13 | namespace Geocoder\Provider\MaxMind; |
||
14 | |||
15 | use Geocoder\Collection; |
||
16 | use Geocoder\Exception\InvalidCredentials; |
||
17 | use Geocoder\Exception\InvalidServerResponse; |
||
18 | use Geocoder\Exception\UnsupportedOperation; |
||
19 | use Geocoder\Model\Address; |
||
20 | use Geocoder\Model\AddressCollection; |
||
21 | use Geocoder\Query\GeocodeQuery; |
||
22 | use Geocoder\Query\ReverseQuery; |
||
23 | use Geocoder\Http\Provider\AbstractHttpProvider; |
||
24 | use Geocoder\Provider\Provider; |
||
25 | use Http\Client\HttpClient; |
||
26 | |||
27 | /** |
||
28 | * @author Andrea Cristaudo <[email protected]> |
||
29 | */ |
||
30 | final class MaxMind extends AbstractHttpProvider implements Provider |
||
31 | { |
||
32 | /** |
||
33 | * @var string Country, City, ISP and Organization |
||
34 | */ |
||
35 | const CITY_EXTENDED_SERVICE = 'f'; |
||
36 | |||
37 | /** |
||
38 | * @var string Extended |
||
39 | */ |
||
40 | const OMNI_SERVICE = 'e'; |
||
41 | |||
42 | /** |
||
43 | * @var string |
||
44 | */ |
||
45 | const GEOCODE_ENDPOINT_URL_SSL = 'https://geoip.maxmind.com/%s?l=%s&i=%s'; |
||
46 | |||
47 | /** |
||
48 | * @var string |
||
49 | */ |
||
50 | private $apiKey = null; |
||
51 | |||
52 | /** |
||
53 | * @var string |
||
54 | */ |
||
55 | private $service = null; |
||
56 | |||
57 | /** |
||
58 | * @param HttpClient $client an HTTP adapter |
||
59 | * @param string $apiKey an API key |
||
60 | * @param string $service the specific Maxmind service to use (optional) |
||
61 | */ |
||
62 | 17 | public function __construct(HttpClient $client, string $apiKey, string $service = self::CITY_EXTENDED_SERVICE) |
|
63 | { |
||
64 | 17 | if (empty($apiKey)) { |
|
65 | throw new InvalidCredentials('No API key provided.'); |
||
66 | } |
||
67 | |||
68 | 17 | $this->apiKey = $apiKey; |
|
69 | 17 | $this->service = $service; |
|
70 | 17 | parent::__construct($client); |
|
71 | 17 | } |
|
72 | |||
73 | /** |
||
74 | * {@inheritdoc} |
||
75 | */ |
||
76 | 15 | public function geocodeQuery(GeocodeQuery $query): Collection |
|
77 | { |
||
78 | 15 | $address = $query->getText(); |
|
79 | |||
80 | 15 | if (!filter_var($address, FILTER_VALIDATE_IP)) { |
|
81 | 1 | throw new UnsupportedOperation('The MaxMind provider does not support street addresses, only IP addresses.'); |
|
82 | } |
||
83 | |||
84 | 14 | if (in_array($address, ['127.0.0.1', '::1'])) { |
|
85 | 2 | return new AddressCollection([$this->getLocationForLocalhost()]); |
|
86 | } |
||
87 | |||
88 | 12 | $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, $this->service, $this->apiKey, $address); |
|
89 | |||
90 | 12 | return $this->executeQuery($url); |
|
91 | } |
||
92 | |||
93 | /** |
||
94 | * {@inheritdoc} |
||
95 | */ |
||
96 | 1 | public function reverseQuery(ReverseQuery $query): Collection |
|
97 | { |
||
98 | 1 | throw new UnsupportedOperation('The MaxMind provider is not able to do reverse geocoding.'); |
|
99 | } |
||
100 | |||
101 | /** |
||
102 | * {@inheritdoc} |
||
103 | */ |
||
104 | 5 | public function getName(): string |
|
105 | { |
||
106 | 5 | return 'maxmind'; |
|
107 | } |
||
108 | |||
109 | /** |
||
110 | * @param string $url |
||
111 | * |
||
112 | * @return Collection |
||
113 | */ |
||
114 | 12 | private function executeQuery(string $url): AddressCollection |
|
115 | { |
||
116 | 12 | $fields = $this->fieldsForService($this->service); |
|
117 | 10 | $content = $this->getUrlContents($url); |
|
118 | 10 | $data = str_getcsv($content); |
|
119 | |||
120 | 10 | if (in_array(end($data), ['INVALID_LICENSE_KEY', 'LICENSE_REQUIRED'])) { |
|
121 | 5 | throw new InvalidCredentials('API Key provided is not valid.'); |
|
122 | } |
||
123 | |||
124 | 5 | if ('IP_NOT_FOUND' === end($data)) { |
|
125 | 2 | return new AddressCollection([]); |
|
126 | } |
||
127 | |||
128 | 3 | if (count($fields) !== count($data)) { |
|
129 | 1 | throw InvalidServerResponse::create($url); |
|
130 | } |
||
131 | |||
132 | 2 | $data = array_combine($fields, $data); |
|
133 | 2 | $data = array_map(function ($value) { |
|
134 | 2 | return '' === $value ? null : $value; |
|
135 | 2 | }, $data); |
|
136 | |||
137 | 2 | if (empty($data['country']) && !empty($data['countryCode'])) { |
|
138 | 1 | $data['country'] = $this->countryCodeToCountryName($data['countryCode']); |
|
139 | } |
||
140 | |||
141 | 2 | $data = $this->replaceAdmins($data); |
|
142 | 2 | $data['providedBy'] = $this->getName(); |
|
143 | |||
144 | 2 | return new AddressCollection([Address::createFromArray($data)]); |
|
145 | } |
||
146 | |||
147 | 1 | private function countryCodeToCountryName(string $code): string |
|
148 | { |
||
149 | 1 | $countryNames = $this->getCountryNames(); |
|
150 | |||
151 | 1 | return $countryNames[$code]; |
|
152 | } |
||
153 | |||
154 | 2 | private function replaceAdmins($data) |
|
155 | { |
||
156 | 2 | $adminLevels = []; |
|
157 | |||
158 | 2 | $region = \igorw\get_in($data, ['region']); |
|
159 | 2 | $regionCode = \igorw\get_in($data, ['regionCode']); |
|
160 | 2 | unset($data['region'], $data['regionCode']); |
|
161 | |||
162 | 2 | if (null !== $region || null !== $regionCode) { |
|
163 | 1 | $adminLevels[] = ['name' => $region, 'code' => $regionCode, 'level' => 1]; |
|
164 | } |
||
165 | |||
166 | 2 | $data['adminLevels'] = $adminLevels; |
|
167 | |||
168 | 2 | return $data; |
|
169 | } |
||
170 | |||
171 | /** |
||
172 | * We do not support Country and City services because they do not return much fields. |
||
173 | * |
||
174 | * @see http://dev.maxmind.com/geoip/web-services |
||
175 | * |
||
176 | * @param string $service |
||
177 | * |
||
178 | * @return string[] |
||
179 | */ |
||
180 | 12 | private function fieldsForService(string $service): array |
|
181 | { |
||
182 | switch ($service) { |
||
183 | 12 | case self::CITY_EXTENDED_SERVICE: |
|
184 | return [ |
||
185 | 7 | 'countryCode', |
|
186 | 'regionCode', |
||
187 | 'locality', |
||
188 | 'postalCode', |
||
189 | 'latitude', |
||
190 | 'longitude', |
||
191 | 'metroCode', |
||
192 | 'areaCode', |
||
193 | 'isp', |
||
194 | 'organization', |
||
195 | ]; |
||
196 | 5 | case self::OMNI_SERVICE: |
|
197 | return [ |
||
198 | 3 | 'countryCode', |
|
199 | 'countryName', |
||
200 | 'regionCode', |
||
201 | 'region', |
||
202 | 'locality', |
||
203 | 'latitude', |
||
204 | 'longitude', |
||
205 | 'metroCode', |
||
206 | 'areaCode', |
||
207 | 'timezone', |
||
208 | 'continentCode', |
||
209 | 'postalCode', |
||
210 | 'isp', |
||
211 | 'organization', |
||
212 | 'domain', |
||
213 | 'asNumber', |
||
214 | 'netspeed', |
||
215 | 'userType', |
||
216 | 'accuracyRadius', |
||
217 | 'countryConfidence', |
||
218 | 'cityConfidence', |
||
219 | 'regionConfidence', |
||
220 | 'postalConfidence', |
||
221 | 'error', |
||
222 | ]; |
||
223 | default: |
||
224 | 2 | throw new UnsupportedOperation(sprintf('Unknown MaxMind service %s', $service)); |
|
225 | } |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * @return array |
||
230 | */ |
||
231 | 1 | private function getCountryNames(): array |
|
232 | { |
||
233 | return [ |
||
234 | 1 | 'A1' => 'Anonymous Proxy', |
|
235 | 'A2' => 'Satellite Provider', |
||
236 | 'O1' => 'Other Country', |
||
237 | 'AD' => 'Andorra', |
||
238 | 'AE' => 'United Arab Emirates', |
||
239 | 'AF' => 'Afghanistan', |
||
240 | 'AG' => 'Antigua and Barbuda', |
||
241 | 'AI' => 'Anguilla', |
||
242 | 'AL' => 'Albania', |
||
243 | 'AM' => 'Armenia', |
||
244 | 'AO' => 'Angola', |
||
245 | 'AP' => 'Asia/Pacific Region', |
||
246 | 'AQ' => 'Antarctica', |
||
247 | 'AR' => 'Argentina', |
||
248 | 'AS' => 'American Samoa', |
||
249 | 'AT' => 'Austria', |
||
250 | 'AU' => 'Australia', |
||
251 | 'AW' => 'Aruba', |
||
252 | 'AX' => 'Aland Islands', |
||
253 | 'AZ' => 'Azerbaijan', |
||
254 | 'BA' => 'Bosnia and Herzegovina', |
||
255 | 'BB' => 'Barbados', |
||
256 | 'BD' => 'Bangladesh', |
||
257 | 'BE' => 'Belgium', |
||
258 | 'BF' => 'Burkina Faso', |
||
259 | 'BG' => 'Bulgaria', |
||
260 | 'BH' => 'Bahrain', |
||
261 | 'BI' => 'Burundi', |
||
262 | 'BJ' => 'Benin', |
||
263 | 'BL' => 'Saint Bartelemey', |
||
264 | 'BM' => 'Bermuda', |
||
265 | 'BN' => 'Brunei Darussalam', |
||
266 | 'BO' => 'Bolivia', |
||
267 | 'BQ' => 'Bonaire, Saint Eustatius and Saba', |
||
268 | 'BR' => 'Brazil', |
||
269 | 'BS' => 'Bahamas', |
||
270 | 'BT' => 'Bhutan', |
||
271 | 'BV' => 'Bouvet Island', |
||
272 | 'BW' => 'Botswana', |
||
273 | 'BY' => 'Belarus', |
||
274 | 'BZ' => 'Belize', |
||
275 | 'CA' => 'Canada', |
||
276 | 'CC' => 'Cocos (Keeling) Islands', |
||
277 | 'CD' => 'Congo, The Democratic Republic of the', |
||
278 | 'CF' => 'Central African Republic', |
||
279 | 'CG' => 'Congo', |
||
280 | 'CH' => 'Switzerland', |
||
281 | 'CI' => 'Cote d\'Ivoire', |
||
282 | 'CK' => 'Cook Islands', |
||
283 | 'CL' => 'Chile', |
||
284 | 'CM' => 'Cameroon', |
||
285 | 'CN' => 'China', |
||
286 | 'CO' => 'Colombia', |
||
287 | 'CR' => 'Costa Rica', |
||
288 | 'CU' => 'Cuba', |
||
289 | 'CV' => 'Cape Verde', |
||
290 | 'CW' => 'Curacao', |
||
291 | 'CX' => 'Christmas Island', |
||
292 | 'CY' => 'Cyprus', |
||
293 | 'CZ' => 'Czech Republic', |
||
294 | 'DE' => 'Germany', |
||
295 | 'DJ' => 'Djibouti', |
||
296 | 'DK' => 'Denmark', |
||
297 | 'DM' => 'Dominica', |
||
298 | 'DO' => 'Dominican Republic', |
||
299 | 'DZ' => 'Algeria', |
||
300 | 'EC' => 'Ecuador', |
||
301 | 'EE' => 'Estonia', |
||
302 | 'EG' => 'Egypt', |
||
303 | 'EH' => 'Western Sahara', |
||
304 | 'ER' => 'Eritrea', |
||
305 | 'ES' => 'Spain', |
||
306 | 'ET' => 'Ethiopia', |
||
307 | 'EU' => 'Europe', |
||
308 | 'FI' => 'Finland', |
||
309 | 'FJ' => 'Fiji', |
||
310 | 'FK' => 'Falkland Islands (Malvinas)', |
||
311 | 'FM' => 'Micronesia, Federated States of', |
||
312 | 'FO' => 'Faroe Islands', |
||
313 | 'FR' => 'France', |
||
314 | 'GA' => 'Gabon', |
||
315 | 'GB' => 'United Kingdom', |
||
316 | 'GD' => 'Grenada', |
||
317 | 'GE' => 'Georgia', |
||
318 | 'GF' => 'French Guiana', |
||
319 | 'GG' => 'Guernsey', |
||
320 | 'GH' => 'Ghana', |
||
321 | 'GI' => 'Gibraltar', |
||
322 | 'GL' => 'Greenland', |
||
323 | 'GM' => 'Gambia', |
||
324 | 'GN' => 'Guinea', |
||
325 | 'GP' => 'Guadeloupe', |
||
326 | 'GQ' => 'Equatorial Guinea', |
||
327 | 'GR' => 'Greece', |
||
328 | 'GS' => 'South Georgia and the South Sandwich Islands', |
||
329 | 'GT' => 'Guatemala', |
||
330 | 'GU' => 'Guam', |
||
331 | 'GW' => 'Guinea-Bissau', |
||
332 | 'GY' => 'Guyana', |
||
333 | 'HK' => 'Hong Kong', |
||
334 | 'HM' => 'Heard Island and McDonald Islands', |
||
335 | 'HN' => 'Honduras', |
||
336 | 'HR' => 'Croatia', |
||
337 | 'HT' => 'Haiti', |
||
338 | 'HU' => 'Hungary', |
||
339 | 'ID' => 'Indonesia', |
||
340 | 'IE' => 'Ireland', |
||
341 | 'IL' => 'Israel', |
||
342 | 'IM' => 'Isle of Man', |
||
343 | 'IN' => 'India', |
||
344 | 'IO' => 'British Indian Ocean Territory', |
||
345 | 'IQ' => 'Iraq', |
||
346 | 'IR' => 'Iran, Islamic Republic of', |
||
347 | 'IS' => 'Iceland', |
||
348 | 'IT' => 'Italy', |
||
349 | 'JE' => 'Jersey', |
||
350 | 'JM' => 'Jamaica', |
||
351 | 'JO' => 'Jordan', |
||
352 | 'JP' => 'Japan', |
||
353 | 'KE' => 'Kenya', |
||
354 | 'KG' => 'Kyrgyzstan', |
||
355 | 'KH' => 'Cambodia', |
||
356 | 'KI' => 'Kiribati', |
||
357 | 'KM' => 'Comoros', |
||
358 | 'KN' => 'Saint Kitts and Nevis', |
||
359 | 'KP' => 'Korea, Democratic People\'s Republic of', |
||
360 | 'KR' => 'Korea, Republic of', |
||
361 | 'KW' => 'Kuwait', |
||
362 | 'KY' => 'Cayman Islands', |
||
363 | 'KZ' => 'Kazakhstan', |
||
364 | 'LA' => 'Lao People\'s Democratic Republic', |
||
365 | 'LB' => 'Lebanon', |
||
366 | 'LC' => 'Saint Lucia', |
||
367 | 'LI' => 'Liechtenstein', |
||
368 | 'LK' => 'Sri Lanka', |
||
369 | 'LR' => 'Liberia', |
||
370 | 'LS' => 'Lesotho', |
||
371 | 'LT' => 'Lithuania', |
||
372 | 'LU' => 'Luxembourg', |
||
373 | 'LV' => 'Latvia', |
||
374 | 'LY' => 'Libyan Arab Jamahiriya', |
||
375 | 'MA' => 'Morocco', |
||
376 | 'MC' => 'Monaco', |
||
377 | 'MD' => 'Moldova, Republic of', |
||
378 | 'ME' => 'Montenegro', |
||
379 | 'MF' => 'Saint Martin', |
||
380 | 'MG' => 'Madagascar', |
||
381 | 'MH' => 'Marshall Islands', |
||
382 | 'MK' => 'Macedonia', |
||
383 | 'ML' => 'Mali', |
||
384 | 'MM' => 'Myanmar', |
||
385 | 'MN' => 'Mongolia', |
||
386 | 'MO' => 'Macao', |
||
387 | 'MP' => 'Northern Mariana Islands', |
||
388 | 'MQ' => 'Martinique', |
||
389 | 'MR' => 'Mauritania', |
||
390 | 'MS' => 'Montserrat', |
||
391 | 'MT' => 'Malta', |
||
392 | 'MU' => 'Mauritius', |
||
393 | 'MV' => 'Maldives', |
||
394 | 'MW' => 'Malawi', |
||
395 | 'MX' => 'Mexico', |
||
396 | 'MY' => 'Malaysia', |
||
397 | 'MZ' => 'Mozambique', |
||
398 | 'NA' => 'Namibia', |
||
399 | 'NC' => 'New Caledonia', |
||
400 | 'NE' => 'Niger', |
||
401 | 'NF' => 'Norfolk Island', |
||
402 | 'NG' => 'Nigeria', |
||
403 | 'NI' => 'Nicaragua', |
||
404 | 'NL' => 'Netherlands', |
||
405 | 'NO' => 'Norway', |
||
406 | 'NP' => 'Nepal', |
||
407 | 'NR' => 'Nauru', |
||
408 | 'NU' => 'Niue', |
||
409 | 'NZ' => 'New Zealand', |
||
410 | 'OM' => 'Oman', |
||
411 | 'PA' => 'Panama', |
||
412 | 'PE' => 'Peru', |
||
413 | 'PF' => 'French Polynesia', |
||
414 | 'PG' => 'Papua New Guinea', |
||
415 | 'PH' => 'Philippines', |
||
416 | 'PK' => 'Pakistan', |
||
417 | 'PL' => 'Poland', |
||
418 | 'PM' => 'Saint Pierre and Miquelon', |
||
419 | 'PN' => 'Pitcairn', |
||
420 | 'PR' => 'Puerto Rico', |
||
421 | 'PS' => 'Palestinian Territory', |
||
422 | 'PT' => 'Portugal', |
||
423 | 'PW' => 'Palau', |
||
424 | 'PY' => 'Paraguay', |
||
425 | 'QA' => 'Qatar', |
||
426 | 'RE' => 'Reunion', |
||
427 | 'RO' => 'Romania', |
||
428 | 'RS' => 'Serbia', |
||
429 | 'RU' => 'Russian Federation', |
||
430 | 'RW' => 'Rwanda', |
||
431 | 'SA' => 'Saudi Arabia', |
||
432 | 'SB' => 'Solomon Islands', |
||
433 | 'SC' => 'Seychelles', |
||
434 | 'SD' => 'Sudan', |
||
435 | 'SE' => 'Sweden', |
||
436 | 'SG' => 'Singapore', |
||
437 | 'SH' => 'Saint Helena', |
||
438 | 'SI' => 'Slovenia', |
||
439 | 'SJ' => 'Svalbard and Jan Mayen', |
||
440 | 'SK' => 'Slovakia', |
||
441 | 'SL' => 'Sierra Leone', |
||
442 | 'SM' => 'San Marino', |
||
443 | 'SN' => 'Senegal', |
||
444 | 'SO' => 'Somalia', |
||
445 | 'SR' => 'Suriname', |
||
446 | 'ST' => 'Sao Tome and Principe', |
||
447 | 'SV' => 'El Salvador', |
||
448 | 'SX' => 'Sint Maarten', |
||
449 | 'SY' => 'Syrian Arab Republic', |
||
450 | 'SZ' => 'Swaziland', |
||
451 | 'TC' => 'Turks and Caicos Islands', |
||
452 | 'TD' => 'Chad', |
||
453 | 'TF' => 'French Southern Territories', |
||
454 | 'TG' => 'Togo', |
||
455 | 'TH' => 'Thailand', |
||
456 | 'TJ' => 'Tajikistan', |
||
457 | 'TK' => 'Tokelau', |
||
458 | 'TL' => 'Timor-Leste', |
||
459 | 'TM' => 'Turkmenistan', |
||
460 | 'TN' => 'Tunisia', |
||
461 | 'TO' => 'Tonga', |
||
462 | 'TR' => 'Turkey', |
||
463 | 'TT' => 'Trinidad and Tobago', |
||
464 | 'TV' => 'Tuvalu', |
||
465 | 'TW' => 'Taiwan', |
||
466 | 'TZ' => 'Tanzania, United Republic of', |
||
467 | 'UA' => 'Ukraine', |
||
468 | 'UG' => 'Uganda', |
||
469 | 'UM' => 'United States Minor Outlying Islands', |
||
470 | 'US' => 'United States', |
||
471 | 'UY' => 'Uruguay', |
||
472 | 'UZ' => 'Uzbekistan', |
||
473 | 'VA' => 'Holy See (Vatican City State)', |
||
474 | 'VC' => 'Saint Vincent and the Grenadines', |
||
475 | 'VE' => 'Venezuela', |
||
476 | 'VG' => 'Virgin Islands, British', |
||
477 | 'VI' => 'Virgin Islands, U.S.', |
||
478 | 'VN' => 'Vietnam', |
||
479 | 'VU' => 'Vanuatu', |
||
480 | 'WF' => 'Wallis and Futuna', |
||
481 | 'WS' => 'Samoa', |
||
482 | 'YE' => 'Yemen', |
||
483 | 'YT' => 'Mayotte', |
||
484 | 'ZA' => 'South Africa', |
||
485 | 'ZM' => 'Zambia', |
||
486 | 'ZW' => 'Zimbabwe', |
||
487 | ]; |
||
488 | } |
||
489 | } |
||
490 |