GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 89afed...81a6bd )
by Tobias
15:14 queued 06:27
created

GoogleMaps::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 3
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\GoogleMaps;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidCredentials;
17
use Geocoder\Exception\InvalidServerResponse;
18
use Geocoder\Exception\QuotaExceeded;
19
use Geocoder\Exception\UnsupportedOperation;
20
use Geocoder\Model\AddressCollection;
21
use Geocoder\Model\AddressBuilder;
22
use Geocoder\Query\GeocodeQuery;
23
use Geocoder\Query\ReverseQuery;
24
use Geocoder\Http\Provider\AbstractHttpProvider;
25
use Geocoder\Provider\GoogleMaps\Model\GoogleAddress;
26
use Geocoder\Provider\Provider;
27
use Http\Client\HttpClient;
28
29
/**
30
 * @author William Durand <[email protected]>
31
 */
32
final class GoogleMaps extends AbstractHttpProvider implements Provider
33
{
34
    /**
35
     * @var string
36
     */
37
    const GEOCODE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?address=%s';
38
39
    /**
40
     * @var string
41
     */
42
    const REVERSE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%F,%F';
43
44
    /**
45
     * @var string|null
46
     */
47
    private $region;
48
49
    /**
50
     * @var string|null
51
     */
52
    private $apiKey;
53
54
    /**
55
     * @var string
56
     */
57
    private $clientId;
58
59
    /**
60
     * @var string|null
61
     */
62
    private $privateKey;
63
64
    /**
65
     * @var string|null
66
     */
67
    private $channel;
68
69
    /**
70
     * Google Maps for Business
71
     * https://developers.google.com/maps/documentation/business/.
72
     *
73
     * @param HttpClient $client     An HTTP adapter
74
     * @param string     $clientId   Your Client ID
75
     * @param string     $privateKey Your Private Key (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $privateKey not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
76
     * @param string     $region     Region biasing (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $region not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
77
     * @param string     $apiKey     Google Geocoding API key (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $apiKey not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
78
     * @param string     $channel    Google Channel parameter (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $channel not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
79
     *
80
     * @return GoogleMaps
81
     */
82
    public static function business(HttpClient $client, string $clientId, string $privateKey = null, string $region = null, string $apiKey = null, string $channel = null)
83
    {
84
        $provider = new self($client, $region, $apiKey);
85
        $provider->clientId = $clientId;
86
        $provider->privateKey = $privateKey;
87
        $provider->channel = $channel;
88
89
        return $provider;
90
    }
91
92
    /**
93
     * @param HttpClient $client An HTTP adapter
94
     * @param string     $region Region biasing (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $region not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
95
     * @param string     $apiKey Google Geocoding API key (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $apiKey not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
96
     */
97
    public function __construct(HttpClient $client, string $region = null, string $apiKey = null)
98
    {
99
        parent::__construct($client);
100
101
        $this->region = $region;
102
        $this->apiKey = $apiKey;
103
    }
104
105
    public function geocodeQuery(GeocodeQuery $query): Collection
106
    {
107
        // Google API returns invalid data if IP address given
108
        // This API doesn't handle IPs
109
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
110
            throw new UnsupportedOperation('The GoogleMaps provider does not support IP addresses, only street addresses.');
111
        }
112
113
        $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, rawurlencode($query->getText()));
114
        if (null !== $bounds = $query->getBounds()) {
115
            $url .= sprintf('&bounds=%s,%s|%s,%s', $bounds->getSouth(), $bounds->getWest(), $bounds->getNorth(), $bounds->getEast());
116
        }
117
118
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
119
    }
120
121
    public function reverseQuery(ReverseQuery $query): Collection
122
    {
123
        $coordinate = $query->getCoordinates();
124
        $url = sprintf(self::REVERSE_ENDPOINT_URL_SSL, $coordinate->getLatitude(), $coordinate->getLongitude());
125
126
        if (null !== $locationType = $query->getData('location_type')) {
127
            $url .= '&location_type='.urlencode($locationType);
128
        }
129
130
        if (null !== $resultType = $query->getData('result_type')) {
131
            $url .= '&result_type='.urlencode($resultType);
132
        }
133
134
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function getName(): string
141
    {
142
        return 'google_maps';
143
    }
144
145
    /**
146
     * @param string $url
147
     * @param string $locale
0 ignored issues
show
Documentation introduced by
Should the type for parameter $locale not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
148
     *
149
     * @return string query with extra params
150
     */
151
    private function buildQuery(string $url, string $locale = null, string $region = null): string
152
    {
153
        if (null !== $locale) {
154
            $url = sprintf('%s&language=%s', $url, $locale);
155
        }
156
157
        if (null !== $region) {
158
            $url = sprintf('%s&region=%s', $url, $region);
159
        }
160
161
        if (null !== $this->apiKey) {
162
            $url = sprintf('%s&key=%s', $url, $this->apiKey);
163
        }
164
165
        if (null !== $this->clientId) {
166
            $url = sprintf('%s&client=%s', $url, $this->clientId);
167
168
            if (null !== $this->channel) {
169
                $url = sprintf('%s&channel=%s', $url, $this->channel);
170
            }
171
172
            if (null !== $this->privateKey) {
173
                $url = $this->signQuery($url);
174
            }
175
        }
176
177
        return $url;
178
    }
179
180
    /**
181
     * @param string $url
182
     * @param string $locale
0 ignored issues
show
Documentation introduced by
Should the type for parameter $locale not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
183
     * @param int    $limit
184
     * @param string $region
0 ignored issues
show
Documentation introduced by
Should the type for parameter $region not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
185
     *
186
     * @return AddressCollection
187
     *
188
     * @throws InvalidServerResponse
189
     * @throws InvalidCredentials
190
     */
191
    private function fetchUrl(string $url, string $locale = null, int $limit, string $region = null): AddressCollection
192
    {
193
        $url = $this->buildQuery($url, $locale, $region);
194
        $content = $this->getUrlContents($url);
195
        $json = $this->validateResponse($url, $content);
196
197
        // no result
198
        if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) {
199
            return new AddressCollection([]);
200
        }
201
202
        $results = [];
203
        foreach ($json->results as $result) {
204
            $builder = new AddressBuilder($this->getName());
205
            $this->parseCoordinates($builder, $result);
206
207
            // set official Google place id
208
            if (isset($result->place_id)) {
209
                $builder->setValue('id', $result->place_id);
210
            }
211
212
            // update address components
213
            foreach ($result->address_components as $component) {
214
                foreach ($component->types as $type) {
215
                    $this->updateAddressComponent($builder, $type, $component);
216
                }
217
            }
218
219
            /** @var GoogleAddress $address */
220
            $address = $builder->build(GoogleAddress::class);
221
            $address = $address->withId($builder->getValue('id'));
222
            if (isset($result->geometry->location_type)) {
223
                $address = $address->withLocationType($result->geometry->location_type);
224
            }
225
            if (isset($result->types)) {
226
                $address = $address->withResultType($result->types);
227
            }
228
            if (isset($result->formatted_address)) {
229
                $address = $address->withFormattedAddress($result->formatted_address);
230
            }
231
            $address = $address->withStreetAddress($builder->getValue('street_address'));
232
            $address = $address->withIntersection($builder->getValue('intersection'));
233
            $address = $address->withPolitical($builder->getValue('political'));
234
            $address = $address->withColloquialArea($builder->getValue('colloquial_area'));
235
            $address = $address->withWard($builder->getValue('ward'));
236
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
237
            $address = $address->withPremise($builder->getValue('premise'));
238
            $address = $address->withSubpremise($builder->getValue('subpremise'));
239
            $address = $address->withNaturalFeature($builder->getValue('natural_feature'));
240
            $address = $address->withAirport($builder->getValue('airport'));
241
            $address = $address->withPark($builder->getValue('park'));
242
            $address = $address->withPointOfInterest($builder->getValue('point_of_interest'));
243
            $address = $address->withEstablishment($builder->getValue('establishment'));
244
            $address = $address->withSubLocalityLevels($builder->getValue('subLocalityLevel', []));
245
            $results[] = $address;
246
247
            if (count($results) >= $limit) {
248
                break;
249
            }
250
        }
251
252
        return new AddressCollection($results);
253
    }
254
255
    /**
256
     * Update current resultSet with given key/value.
257
     *
258
     * @param AddressBuilder $builder
259
     * @param string         $type    Component type
260
     * @param object         $values  The component values
261
     */
262
    private function updateAddressComponent(AddressBuilder $builder, string $type, $values)
263
    {
264
        switch ($type) {
265
            case 'postal_code':
266
                $builder->setPostalCode($values->long_name);
267
268
                break;
269
270
            case 'locality':
271
            case 'postal_town':
272
                $builder->setLocality($values->long_name);
273
274
                break;
275
276
            case 'administrative_area_level_1':
277
            case 'administrative_area_level_2':
278
            case 'administrative_area_level_3':
279
            case 'administrative_area_level_4':
280
            case 'administrative_area_level_5':
281
                $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name, $values->short_name);
282
283
                break;
284
285
            case 'sublocality_level_1':
286
            case 'sublocality_level_2':
287
            case 'sublocality_level_3':
288
            case 'sublocality_level_4':
289
            case 'sublocality_level_5':
290
                $subLocalityLevel = $builder->getValue('subLocalityLevel', []);
291
                $subLocalityLevel[] = [
292
                    'level' => intval(substr($type, -1)),
293
                    'name' => $values->long_name,
294
                    'code' => $values->short_name,
295
                ];
296
                $builder->setValue('subLocalityLevel', $subLocalityLevel);
297
298
                break;
299
300
            case 'country':
301
                $builder->setCountry($values->long_name);
302
                $builder->setCountryCode($values->short_name);
303
304
                break;
305
306
            case 'street_number':
307
                $builder->setStreetNumber($values->long_name);
308
309
                break;
310
311
            case 'route':
312
                $builder->setStreetName($values->long_name);
313
314
                break;
315
316
            case 'sublocality':
317
                $builder->setSubLocality($values->long_name);
318
319
                break;
320
321
            case 'street_address':
322
            case 'intersection':
323
            case 'political':
324
            case 'colloquial_area':
325
            case 'ward':
326
            case 'neighborhood':
327
            case 'premise':
328
            case 'subpremise':
329
            case 'natural_feature':
330
            case 'airport':
331
            case 'park':
332
            case 'point_of_interest':
333
            case 'establishment':
334
                $builder->setValue($type, $values->long_name);
335
336
                break;
337
338
            default:
339
        }
340
    }
341
342
    /**
343
     * Sign a URL with a given crypto key
344
     * Note that this URL must be properly URL-encoded
345
     * src: http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/UrlSigner.php-source.
346
     *
347
     * @param string $query Query to be signed
348
     *
349
     * @return string $query query with signature appended
350
     */
351
    private function signQuery(string $query): string
352
    {
353
        $url = parse_url($query);
354
355
        $urlPartToSign = $url['path'].'?'.$url['query'];
356
357
        // Decode the private key into its binary format
358
        $decodedKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $this->privateKey));
359
360
        // Create a signature using the private key and the URL-encoded
361
        // string using HMAC SHA1. This signature will be binary.
362
        $signature = hash_hmac('sha1', $urlPartToSign, $decodedKey, true);
363
364
        $encodedSignature = str_replace(['+', '/'], ['-', '_'], base64_encode($signature));
365
366
        return sprintf('%s&signature=%s', $query, $encodedSignature);
367
    }
368
369
    /**
370
     * Decode the response content and validate it to make sure it does not have any errors.
371
     *
372
     * @param string $url
373
     * @param string $content
374
     *
375
     * @return mixed result form json_decode()
376
     *
377
     * @throws InvalidCredentials
378
     * @throws InvalidServerResponse
379
     * @throws QuotaExceeded
380
     */
381
    private function validateResponse(string $url, $content)
382
    {
383
        // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider
384
        if (false !== strpos($content, "Provided 'signature' is not valid for the provided client ID")) {
385
            throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s', $url));
386
        }
387
388
        $json = json_decode($content);
389
390
        // API error
391
        if (!isset($json)) {
392
            throw InvalidServerResponse::create($url);
393
        }
394
395
        if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) {
396
            throw new InvalidCredentials(sprintf('API key is invalid %s', $url));
397
        }
398
399
        if ('REQUEST_DENIED' === $json->status) {
400
            throw new InvalidServerResponse(
401
                sprintf('API access denied. Request: %s - Message: %s', $url, $json->error_message)
402
            );
403
        }
404
405
        // you are over your quota
406
        if ('OVER_QUERY_LIMIT' === $json->status) {
407
            throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url));
408
        }
409
410
        return $json;
411
    }
412
413
    /**
414
     * Parse coordinats and bounds.
415
     *
416
     * @param AddressBuilder $builder
417
     * @param $result
418
     */
419
    private function parseCoordinates(AddressBuilder $builder, $result)
420
    {
421
        $coordinates = $result->geometry->location;
422
        $builder->setCoordinates($coordinates->lat, $coordinates->lng);
423
424
        if (isset($result->geometry->bounds)) {
425
            $builder->setBounds(
426
                $result->geometry->bounds->southwest->lat,
427
                $result->geometry->bounds->southwest->lng,
428
                $result->geometry->bounds->northeast->lat,
429
                $result->geometry->bounds->northeast->lng
430
            );
431
        } elseif (isset($result->geometry->viewport)) {
432
            $builder->setBounds(
433
                $result->geometry->viewport->southwest->lat,
434
                $result->geometry->viewport->southwest->lng,
435
                $result->geometry->viewport->northeast->lat,
436
                $result->geometry->viewport->northeast->lng
437
            );
438
        } elseif ('ROOFTOP' === $result->geometry->location_type) {
439
            // Fake bounds
440
            $builder->setBounds(
441
                $coordinates->lat,
442
                $coordinates->lng,
443
                $coordinates->lat,
444
                $coordinates->lng
445
            );
446
        }
447
    }
448
}
449