Code

< 40 %
40-60 %
> 60 %
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