Skip to content

Commit c67807e

Browse files
committed
Introduce ip-api Provider
1 parent 9c2224f commit c67807e

20 files changed

+574
-0
lines changed

.github/workflows/provider.yml

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
- HostIp
3535
- IP2Location
3636
# - IP2LocationBinary
37+
- IpApi
3738
- IpInfo
3839
- IpInfoDb
3940
- Ipstack

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ composer.phar
44
phpunit.xml
55
.phpunit.result.cache
66
.php-cs-fixer.cache
7+
.php-cs-fixer.php
78
.puli/

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ Provider | Package | Features | Stats
174174
[IpInfo](https://github.com/geocoder-php/ip-info-provider) | `geocoder-php/ip-info-provider` | IPv4, IPv6 <br> [Website](https://ipinfo.io/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/ip-info-provider/v/stable)](https://packagist.org/packages/geocoder-php/ip-info-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/ip-info-provider/downloads)](https://packagist.org/packages/geocoder-php/ip-info-provider)
175175
[IpInfoDB](https://github.com/geocoder-php/ip-info-db-provider) | `geocoder-php/ip-info-db-provider` | IPv4 <br> [Website](http://ipinfodb.com/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/ip-info-db-provider/v/stable)](https://packagist.org/packages/geocoder-php/ip-info-db-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/ip-info-db-provider/downloads)](https://packagist.org/packages/geocoder-php/ip-info-db-provider)
176176
[ipstack](https://github.com/geocoder-php/ipstack-provider) | `geocoder-php/ipstack-provider` | IPv4, IPv6 <br> [Website](https://ipstack.com/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/ipstack-provider/v/stable)](https://packagist.org/packages/geocoder-php/ipstack-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/ipstack-provider/downloads)](https://packagist.org/packages/geocoder-php/ipstack-provider)
177+
[ip-api](https://github.com/geocoder-php/ip-api-provider) | `geocoder-php/ip-api-provider` | IPv4, IPv6 <br> [Website](https://ip-api.com/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/ip-api-provider/v/stable)](https://packagist.org/packages/geocoder-php/ip-api-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/ip-api-provider/downloads)](https://packagist.org/packages/geocoder-php/ip-api-provider)
177178
[MaxMind](https://github.com/geocoder-php/maxmind-provider) | `geocoder-php/maxmind-provider` | IPv4, IPv6 <br> [Website](https://www.maxmind.com/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/maxmind-provider/v/stable)](https://packagist.org/packages/geocoder-php/maxmind-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/maxmind-provider/downloads)](https://packagist.org/packages/geocoder-php/maxmind-provider)
178179
[MaxMind Binary](https://github.com/geocoder-php/maxmind-binary-provider) | `geocoder-php/maxmind-binary-provider` | IPv4, IPv6 <br> [Website](https://www.maxmind.com/) | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/maxmind-binary-provider/v/stable)](https://packagist.org/packages/geocoder-php/maxmind-binary-provider) <br>[![Total Downloads](https://poser.pugx.org/geocoder-php/maxmind-binary-provider/downloads)](https://packagist.org/packages/geocoder-php/maxmind-binary-provider)
179180

src/Provider/IpApi/.gitattributes

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.gitattributes export-ignore
2+
.travis.yml export-ignore
3+
phpunit.xml.dist export-ignore
4+
Tests/ export-ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Provider
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
test:
11+
name: PHP ${{ matrix.php-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
php-version: ['8.0', '8.1', '8.2']
17+
steps:
18+
- uses: actions/checkout@v3
19+
- name: Use PHP ${{ matrix.php-version }}
20+
uses: shivammathur/setup-php@v2
21+
with:
22+
php-version: ${{ matrix.php-version }}
23+
extensions: curl
24+
- name: Validate composer.json and composer.lock
25+
run: composer validate --strict
26+
- name: Install dependencies
27+
run: composer update --prefer-stable --prefer-dist --no-progress
28+
- name: Run test suite
29+
run: composer run-script test-ci
30+
- name: Upload Coverage report
31+
run: |
32+
wget https://scrutinizer-ci.com/ocular.phar
33+
php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml

src/Provider/IpApi/.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
4+
.phpunit.result.cache

src/Provider/IpApi/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Change Log
2+
3+
The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.
4+
5+
## 0.1.0
6+
7+
First release of this library.

src/Provider/IpApi/IpApi.php

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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\IpApi;
14+
15+
use Geocoder\Collection;
16+
use Geocoder\Exception\InvalidArgument;
17+
use Geocoder\Exception\InvalidCredentials;
18+
use Geocoder\Exception\InvalidServerResponse;
19+
use Geocoder\Exception\UnsupportedOperation;
20+
use Geocoder\Http\Provider\AbstractHttpProvider;
21+
use Geocoder\Model\AddressBuilder;
22+
use Geocoder\Model\AddressCollection;
23+
use Geocoder\Provider\IpApi\Model\IpApiLocation;
24+
use Geocoder\Query\GeocodeQuery;
25+
use Geocoder\Query\ReverseQuery;
26+
use Psr\Http\Client\ClientInterface;
27+
28+
final class IpApi extends AbstractHttpProvider
29+
{
30+
private const URL = '{host_prefix}ip-api.com/json/{ip}';
31+
32+
private const FIELDS = 'status,message,lat,lon,city,district,zip,country,countryCode,timezone,regionName,region,proxy,hosting';
33+
34+
private string|null $apiKey;
35+
36+
public function __construct(ClientInterface $client, string $apiKey = null)
37+
{
38+
$this->apiKey = $apiKey;
39+
parent::__construct($client);
40+
}
41+
42+
#[\Override]
43+
public function geocodeQuery(GeocodeQuery $query): Collection
44+
{
45+
$ip = $query->getText();
46+
47+
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
48+
throw new UnsupportedOperation('The ip-api provider does not support street addresses.');
49+
}
50+
51+
if (in_array($ip, ['127.0.0.1', '::1'])) {
52+
return new AddressCollection([$this->getLocationForLocalhost()]);
53+
}
54+
55+
$url = $this->buildUrl($ip, $query->getLocale());
56+
57+
$body = $this->getUrlContents($url);
58+
59+
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
60+
if ('fail' === $data['status']) {
61+
$this->throwError($data['message']);
62+
}
63+
64+
$location = $this->buildLocation($data);
65+
66+
return new AddressCollection([$location]);
67+
}
68+
69+
#[\Override]
70+
public function reverseQuery(ReverseQuery $query): Collection
71+
{
72+
throw new UnsupportedOperation('The ip-api provider is not able to do reverse geocoding.');
73+
}
74+
75+
#[\Override]
76+
public function getName(): string
77+
{
78+
return 'ip-api';
79+
}
80+
81+
public function buildUrl(string $ip, string|null $locale): string
82+
{
83+
$baseUrl = strtr(self::URL, [
84+
'{host_prefix}' => $this->apiKey ? 'https://pro.' : 'http://',
85+
'{ip}' => $ip,
86+
]);
87+
88+
$query = http_build_query(array_filter([
89+
'key' => $this->apiKey,
90+
'lang' => $locale,
91+
'fields' => self::FIELDS,
92+
]));
93+
94+
return $baseUrl.'?'.$query;
95+
}
96+
97+
/**
98+
* @param array<string, scalar> $data
99+
*/
100+
private function buildLocation(array $data): IpApiLocation
101+
{
102+
$data = array_map(
103+
static fn ($value) => '' === $value ? null : $value,
104+
$data,
105+
);
106+
107+
$builder = new AddressBuilder($this->getName());
108+
$builder->setCoordinates($data['lat'], $data['lon']);
109+
$builder->setLocality($data['city']);
110+
$builder->setSubLocality($data['district']);
111+
$builder->setPostalCode($data['zip']);
112+
$builder->setCountry($data['country']);
113+
$builder->setCountryCode($data['countryCode']);
114+
$builder->setTimezone($data['timezone']);
115+
116+
if ($data['regionName']) {
117+
$builder->addAdminLevel(1, $data['regionName'], $data['region']);
118+
}
119+
120+
/** @var IpApiLocation $location */
121+
$location = $builder->build(IpApiLocation::class);
122+
123+
return $location
124+
->withIsProxy($data['proxy'])
125+
->withIsHosting($data['hosting']);
126+
}
127+
128+
/**
129+
* @see https://members.ip-api.com/faq#errors
130+
*
131+
* @return never
132+
*/
133+
private function throwError(string $message)
134+
{
135+
if (
136+
in_array($message, ['private range', 'reserved range', 'invalid query'], true)
137+
|| str_contains('Origin restriction', $message)
138+
|| str_contains('IP range restriction', $message)
139+
|| str_contains('Calling IP restriction', $message)
140+
) {
141+
throw new InvalidArgument($message);
142+
}
143+
144+
if (
145+
str_contains('invalid/expired ke', $message)
146+
|| str_contains('no API key supplied', $message)
147+
) {
148+
throw new InvalidCredentials($message);
149+
}
150+
151+
throw new InvalidServerResponse($message);
152+
}
153+
}

src/Provider/IpApi/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2011 — William Durand <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\IpApi\Model;
14+
15+
use Geocoder\Model\Address;
16+
17+
final class IpApiLocation extends Address
18+
{
19+
private bool $isProxy;
20+
21+
private bool $isHosting;
22+
23+
public function isProxy(): bool
24+
{
25+
return $this->isProxy;
26+
}
27+
28+
public function withIsProxy(bool $isProxy): self
29+
{
30+
$new = clone $this;
31+
$new->isProxy = $isProxy;
32+
33+
return $new;
34+
}
35+
36+
public function isHosting(): bool
37+
{
38+
return $this->isHosting;
39+
}
40+
41+
public function withIsHosting(bool $isHosting): self
42+
{
43+
$new = clone $this;
44+
$new->isHosting = $isHosting;
45+
46+
return $new;
47+
}
48+
}

src/Provider/IpApi/Readme.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# ip-api Geocoder provider
2+
[![Build Status](https://travis-ci.org/geocoder-php/ip-api-provider.svg?branch=master)](http://travis-ci.org/geocoder-php/ip-api-provider)
3+
[![Latest Stable Version](https://poser.pugx.org/geocoder-php/ip-api-provider/v/stable)](https://packagist.org/packages/geocoder-php/ip-api-provider)
4+
[![Total Downloads](https://poser.pugx.org/geocoder-php/ip-api-provider/downloads)](https://packagist.org/packages/geocoder-php/ip-api-provider)
5+
[![Monthly Downloads](https://poser.pugx.org/geocoder-php/ip-api-provider/d/monthly.png)](https://packagist.org/packages/geocoder-php/ip-api-provider)
6+
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/ip-api-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/ip-api-provider)
7+
[![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/ip-api-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/ip-api-provider)
8+
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
9+
10+
This is the IpApi provider from the PHP Geocoder. This is a **READ ONLY** repository. See the
11+
[main repo](https://github.com/geocoder-php/Geocoder) for information and documentation.
12+
13+
### Install
14+
15+
```bash
16+
composer require geocoder-php/ip-api-provider
17+
```
18+
19+
### Note
20+
21+
The default language-locale is `en`, you can choose between `de`, `es`, `pt-BR`, `fr`, `ja`, `zh-CN`, `ru`.
22+
23+
### Contribute
24+
25+
Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or
26+
report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:230:"{"status":"success","country":"United States","countryCode":"US","region":"OK","regionName":"Oklahoma","city":"Tulsa","district":"","zip":"","lat":36.15398,"lon":-95.99277,"timezone":"America/Chicago","proxy":false,"hosting":true}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:230:"{"status":"success","country":"United States","countryCode":"US","region":"OK","regionName":"Oklahoma","city":"Tulsa","district":"","zip":"","lat":36.15398,"lon":-95.99277,"timezone":"America/Chicago","proxy":false,"hosting":true}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:230:"{"city":"Tulsa","country":"United States","countryCode":"US","district":"","hosting":true,"lat":36.15398,"lon":-95.99277,"proxy":false,"region":"OK","regionName":"Oklahoma","status":"success","timezone":"America/Chicago","zip":""}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:230:"{"city":"Tulsa","country":"United States","countryCode":"US","district":"","hosting":true,"lat":36.15398,"lon":-95.99277,"proxy":false,"region":"OK","regionName":"Oklahoma","status":"success","timezone":"America/Chicago","zip":""}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s:239:"{"city":"Karlskrona","country":"Sweden","countryCode":"SE","district":"","hosting":false,"lat":56.1625,"lon":15.5801,"proxy":false,"region":"K","regionName":"Blekinge County","status":"success","timezone":"Europe/Stockholm","zip":"371 37"}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\IpApi\Tests;
14+
15+
use Geocoder\IntegrationTest\ProviderIntegrationTest;
16+
use Geocoder\Provider\IpApi\IpApi;
17+
use Psr\Http\Client\ClientInterface;
18+
19+
class IntegrationTest extends ProviderIntegrationTest
20+
{
21+
protected bool $testAddress = false;
22+
23+
protected bool $testReverse = false;
24+
25+
protected bool $testIpv6 = false;
26+
27+
protected function createProvider(ClientInterface $httpClient): IpApi
28+
{
29+
return new IpApi($httpClient, $this->getApiKey());
30+
}
31+
32+
protected function getCacheDir(): string
33+
{
34+
return __DIR__.'/.cached_responses';
35+
}
36+
37+
protected function getApiKey(): string
38+
{
39+
if (!isset($_SERVER['IP_API_KEY'])) {
40+
$this->markTestSkipped('No ip-api API key');
41+
}
42+
43+
return $_SERVER['IP_API_KEY'];
44+
}
45+
}

0 commit comments

Comments
 (0)