mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-28 00:38:46 +03:00
Merge pull request #464 from acelaya/feature/external-ip-geolocation-module
Moved IpGeolocation module to external library
This commit is contained in:
commit
b3a4adeba4
28 changed files with 1 additions and 1049 deletions
|
@ -35,6 +35,7 @@
|
|||
"predis/predis": "^1.1",
|
||||
"shlinkio/shlink-common": "^1.0",
|
||||
"shlinkio/shlink-installer": "^1.2.1",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.0",
|
||||
"symfony/console": "^4.3",
|
||||
"symfony/filesystem": "^4.3",
|
||||
"symfony/lock": "^4.3",
|
||||
|
@ -76,7 +77,6 @@
|
|||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||
"Shlinkio\\Shlink\\EventDispatcher\\": "module/EventDispatcher/src",
|
||||
"Shlinkio\\Shlink\\IpGeolocation\\": "module/IpGeolocation/src/",
|
||||
"Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/"
|
||||
},
|
||||
"files": [
|
||||
|
@ -93,7 +93,6 @@
|
|||
"module/Core/test-db"
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\EventDispatcher\\": "module/EventDispatcher/test",
|
||||
"ShlinkioTest\\Shlink\\IpGeolocation\\": "module/IpGeolocation/test",
|
||||
"ShlinkioTest\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/test"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Alejandro Celaya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,30 +0,0 @@
|
|||
# Shlink IP Address Geolocation module
|
||||
|
||||
Shlink module with tools to geolocate an IP address using different strategies.
|
||||
|
||||
Most of the elements it provides require a [PSR-11] container, and it's easy to integrate on [expressive] applications thanks to the `ConfigProvider` it includes.
|
||||
|
||||
## Install
|
||||
|
||||
Install this library using composer:
|
||||
|
||||
composer require shlinkio/shlink-ip-geolocation
|
||||
|
||||
> This library is also an expressive module which provides its own `ConfigProvider`. Add it to your configuration to get everything automatically set up.
|
||||
|
||||
## *TODO*
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'geolite2' => [
|
||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
// 'download_from' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz',
|
||||
],
|
||||
|
||||
];
|
||||
```
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Resolver\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
||||
Resolver\GeoLite2LocationResolver::class => ConfigAbstractFactory::class,
|
||||
Resolver\EmptyIpLocationResolver::class => InvokableFactory::class,
|
||||
Resolver\ChainIpLocationResolver::class => ConfigAbstractFactory::class,
|
||||
|
||||
GeoLite2\GeoLite2Options::class => ConfigAbstractFactory::class,
|
||||
GeoLite2\DbUpdater::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
Resolver\IpLocationResolverInterface::class => Resolver\ChainIpLocationResolver::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Resolver\IpApiLocationResolver::class => [GuzzleClient::class],
|
||||
Resolver\GeoLite2LocationResolver::class => [Reader::class],
|
||||
Resolver\ChainIpLocationResolver::class => [
|
||||
Resolver\GeoLite2LocationResolver::class,
|
||||
Resolver\IpApiLocationResolver::class,
|
||||
Resolver\EmptyIpLocationResolver::class,
|
||||
],
|
||||
|
||||
GeoLite2\GeoLite2Options::class => ['config.geolite2'],
|
||||
GeoLite2\DbUpdater::class => [
|
||||
GuzzleClient::class,
|
||||
Filesystem::class,
|
||||
GeoLite2\GeoLite2Options::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Proxy\LazyServiceFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Reader::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'delegators' => [
|
||||
// The GeoLite2 db reader has to be lazy so that it does not try to load the DB file at app bootstrapping.
|
||||
// By doing so, it would fail the first time shlink tries to download it.
|
||||
Reader::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'lazy_services' => [
|
||||
'class_map' => [
|
||||
Reader::class => Reader::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Reader::class => ['config.geolite2.db_location'],
|
||||
],
|
||||
|
||||
];
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation;
|
||||
|
||||
use function Shlinkio\Shlink\Common\loadConfigFromGlob;
|
||||
|
||||
class ConfigProvider
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return loadConfigFromGlob(__DIR__ . '/../config/{,*.}config.php');
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Exception;
|
||||
|
||||
use RuntimeException as SplRuntimeException;
|
||||
|
||||
class RuntimeException extends SplRuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Exception;
|
||||
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class WrongIpException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
public static function fromIpAddress($ipAddress, ?Throwable $prev = null): self
|
||||
{
|
||||
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\GeoLite2;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use PharData;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
use Symfony\Component\Filesystem\Exception as FilesystemException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DbUpdater implements DbUpdaterInterface
|
||||
{
|
||||
private const DB_COMPRESSED_FILE = 'GeoLite2-City.tar.gz';
|
||||
private const DB_DECOMPRESSED_FILE = 'GeoLite2-City.mmdb';
|
||||
|
||||
/** @var ClientInterface */
|
||||
private $httpClient;
|
||||
/** @var Filesystem */
|
||||
private $filesystem;
|
||||
/** @var GeoLite2Options */
|
||||
private $options;
|
||||
|
||||
public function __construct(ClientInterface $httpClient, Filesystem $filesystem, GeoLite2Options $options)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function downloadFreshCopy(?callable $handleProgress = null): void
|
||||
{
|
||||
$tempDir = $this->options->getTempDir();
|
||||
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
|
||||
|
||||
$this->downloadDbFile($compressedFile, $handleProgress);
|
||||
$tempFullPath = $this->extractDbFile($compressedFile, $tempDir);
|
||||
$this->copyNewDbFile($tempFullPath);
|
||||
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
|
||||
}
|
||||
|
||||
private function downloadDbFile(string $dest, ?callable $handleProgress = null): void
|
||||
{
|
||||
try {
|
||||
$this->httpClient->request(RequestMethod::METHOD_GET, $this->options->getDownloadFrom(), [
|
||||
RequestOptions::SINK => $dest,
|
||||
RequestOptions::PROGRESS => $handleProgress,
|
||||
]);
|
||||
} catch (Throwable | GuzzleException $e) {
|
||||
throw new RuntimeException(
|
||||
'An error occurred while trying to download a fresh copy of the GeoLite2 database',
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractDbFile(string $compressedFile, string $tempDir): string
|
||||
{
|
||||
try {
|
||||
$phar = new PharData($compressedFile);
|
||||
$internalPathToDb = sprintf('%s/%s', $phar->getBasename(), self::DB_DECOMPRESSED_FILE);
|
||||
$phar->extractTo($tempDir, $internalPathToDb, true);
|
||||
|
||||
return sprintf('%s/%s', $tempDir, $internalPathToDb);
|
||||
} catch (Throwable $e) {
|
||||
throw new RuntimeException(
|
||||
sprintf('An error occurred while trying to extract the GeoLite2 database from %s', $compressedFile),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function copyNewDbFile(string $from): void
|
||||
{
|
||||
try {
|
||||
$this->filesystem->copy($from, $this->options->getDbLocation(), true);
|
||||
} catch (FilesystemException\FileNotFoundException | FilesystemException\IOException $e) {
|
||||
throw new RuntimeException('An error occurred while trying to copy GeoLite2 db file to destination', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteTempFiles(array $files): void
|
||||
{
|
||||
try {
|
||||
$this->filesystem->remove($files);
|
||||
} catch (FilesystemException\IOException $e) {
|
||||
// Ignore any error produced when trying to delete temp files
|
||||
}
|
||||
}
|
||||
|
||||
public function databaseFileExists(): bool
|
||||
{
|
||||
return $this->filesystem->exists($this->options->getDbLocation());
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\GeoLite2;
|
||||
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
|
||||
interface DbUpdaterInterface
|
||||
{
|
||||
public function databaseFileExists(): bool;
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function downloadFreshCopy(?callable $handleProgress = null): void;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\GeoLite2;
|
||||
|
||||
use Zend\Stdlib\AbstractOptions;
|
||||
|
||||
class GeoLite2Options extends AbstractOptions
|
||||
{
|
||||
private $dbLocation = '';
|
||||
private $tempDir = '';
|
||||
private $downloadFrom = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz';
|
||||
|
||||
public function getDbLocation(): string
|
||||
{
|
||||
return $this->dbLocation;
|
||||
}
|
||||
|
||||
protected function setDbLocation(string $dbLocation): self
|
||||
{
|
||||
$this->dbLocation = $dbLocation;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTempDir(): string
|
||||
{
|
||||
return $this->tempDir;
|
||||
}
|
||||
|
||||
protected function setTempDir(string $tempDir): self
|
||||
{
|
||||
$this->tempDir = $tempDir;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDownloadFrom(): string
|
||||
{
|
||||
return $this->downloadFrom;
|
||||
}
|
||||
|
||||
protected function setDownloadFrom(string $downloadFrom): self
|
||||
{
|
||||
$this->downloadFrom = $downloadFrom;
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Model;
|
||||
|
||||
final class Location
|
||||
{
|
||||
/** @var string */
|
||||
private $countryCode;
|
||||
/** @var string */
|
||||
private $countryName;
|
||||
/** @var string */
|
||||
private $regionName;
|
||||
/** @var string */
|
||||
private $city;
|
||||
/** @var float */
|
||||
private $latitude;
|
||||
/** @var float */
|
||||
private $longitude;
|
||||
/** @var string */
|
||||
private $timeZone;
|
||||
|
||||
public function __construct(
|
||||
string $countryCode,
|
||||
string $countryName,
|
||||
string $regionName,
|
||||
string $city,
|
||||
float $latitude,
|
||||
float $longitude,
|
||||
string $timeZone
|
||||
) {
|
||||
$this->countryCode = $countryCode;
|
||||
$this->countryName = $countryName;
|
||||
$this->regionName = $regionName;
|
||||
$this->city = $city;
|
||||
$this->latitude = $latitude;
|
||||
$this->longitude = $longitude;
|
||||
$this->timeZone = $timeZone;
|
||||
}
|
||||
|
||||
public static function emptyInstance(): self
|
||||
{
|
||||
return new self('', '', '', '', 0.0, 0.0, '');
|
||||
}
|
||||
|
||||
public function countryCode(): string
|
||||
{
|
||||
return $this->countryCode;
|
||||
}
|
||||
|
||||
public function countryName(): string
|
||||
{
|
||||
return $this->countryName;
|
||||
}
|
||||
|
||||
public function regionName(): string
|
||||
{
|
||||
return $this->regionName;
|
||||
}
|
||||
|
||||
public function city(): string
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
public function latitude(): float
|
||||
{
|
||||
return $this->latitude;
|
||||
}
|
||||
|
||||
public function longitude(): float
|
||||
{
|
||||
return $this->longitude;
|
||||
}
|
||||
|
||||
public function timeZone(): string
|
||||
{
|
||||
return $this->timeZone;
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model;
|
||||
|
||||
class ChainIpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/** @var IpLocationResolverInterface[] */
|
||||
private $resolvers;
|
||||
|
||||
public function __construct(IpLocationResolverInterface ...$resolvers)
|
||||
{
|
||||
$this->resolvers = $resolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
$error = null;
|
||||
|
||||
foreach ($this->resolvers as $resolver) {
|
||||
try {
|
||||
return $resolver->resolveIpLocation($ipAddress);
|
||||
} catch (WrongIpException $e) {
|
||||
$error = $e;
|
||||
}
|
||||
}
|
||||
|
||||
// If this instruction is reached, it means no resolver was capable of resolving the address
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $error);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model;
|
||||
|
||||
class EmptyIpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
return Model\Location::emptyInstance();
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use GeoIp2\Exception\AddressNotFoundException;
|
||||
use GeoIp2\Model\City;
|
||||
use GeoIp2\Record\Subdivision;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model;
|
||||
|
||||
use function Functional\first;
|
||||
|
||||
class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/** @var Reader */
|
||||
private $geoLiteDbReader;
|
||||
|
||||
public function __construct(Reader $geoLiteDbReader)
|
||||
{
|
||||
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
try {
|
||||
$city = $this->geoLiteDbReader->city($ipAddress);
|
||||
return $this->mapFields($city);
|
||||
} catch (AddressNotFoundException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
} catch (InvalidDatabaseException $e) {
|
||||
throw new WrongIpException('Provided GeoLite2 db file is invalid', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFields(City $city): Model\Location
|
||||
{
|
||||
/** @var Subdivision $region */
|
||||
$region = first($city->subdivisions);
|
||||
|
||||
return new Model\Location(
|
||||
$city->country->isoCode ?? '',
|
||||
$city->country->name ?? '',
|
||||
$region->name ?? '',
|
||||
$city->city->name ?? '',
|
||||
(float) ($city->location->latitude ?? ''),
|
||||
(float) ($city->location->longitude ?? ''),
|
||||
$city->location->timeZone ?? ''
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model;
|
||||
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function sprintf;
|
||||
|
||||
class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
private const SERVICE_PATTERN = 'http://ip-api.com/json/%s';
|
||||
|
||||
/** @var Client */
|
||||
private $httpClient;
|
||||
|
||||
public function __construct(Client $httpClient)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return $this->mapFields(json_decode((string) $response->getBody()));
|
||||
} catch (GuzzleException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new WrongIpException('IP-API returned invalid body while locating IP address', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFields(array $entry): Model\Location
|
||||
{
|
||||
return new Model\Location(
|
||||
(string) ($entry['countryCode'] ?? ''),
|
||||
(string) ($entry['country'] ?? ''),
|
||||
(string) ($entry['regionName'] ?? ''),
|
||||
(string) ($entry['city'] ?? ''),
|
||||
(float) ($entry['lat'] ?? 0.0),
|
||||
(float) ($entry['lon'] ?? 0.0),
|
||||
(string) ($entry['timezone'] ?? '')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model;
|
||||
|
||||
interface IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): Model\Location;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
geolite2-testing-db
|
Binary file not shown.
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\IpGeolocation\ConfigProvider;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
/** @var ConfigProvider */
|
||||
private $configProvider;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->configProvider = new ConfigProvider();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function configIsReturned(): void
|
||||
{
|
||||
$config = $this->configProvider->__invoke();
|
||||
|
||||
$this->assertArrayHasKey('dependencies', $config);
|
||||
$this->assertArrayHasKey(ConfigAbstractFactory::class, $config);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation\Exception;
|
||||
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
|
||||
class WrongIpExceptionTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function fromIpAddressProperlyCreatesExceptionWithoutPrev(): void
|
||||
{
|
||||
$e = WrongIpException::fromIpAddress('1.2.3.4');
|
||||
|
||||
$this->assertEquals('Provided IP "1.2.3.4" is invalid', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function fromIpAddressProperlyCreatesExceptionWithPrev(): void
|
||||
{
|
||||
$prev = new Exception('Previous error');
|
||||
$e = WrongIpException::fromIpAddress('1.2.3.4', $prev);
|
||||
|
||||
$this->assertEquals('Provided IP "1.2.3.4" is invalid', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertSame($prev, $e->getPrevious());
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation\GeoLite2;
|
||||
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
use Symfony\Component\Filesystem\Exception as FilesystemException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\Diactoros\Response;
|
||||
|
||||
class DbUpdaterTest extends TestCase
|
||||
{
|
||||
/** @var DbUpdater */
|
||||
private $dbUpdater;
|
||||
/** @var ObjectProphecy */
|
||||
private $httpClient;
|
||||
/** @var ObjectProphecy */
|
||||
private $filesystem;
|
||||
/** @var GeoLite2Options */
|
||||
private $options;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->httpClient = $this->prophesize(ClientInterface::class);
|
||||
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||
$this->options = new GeoLite2Options([
|
||||
'temp_dir' => __DIR__ . '/../../test-resources',
|
||||
'db_location' => 'db_location',
|
||||
'download_from' => '',
|
||||
]);
|
||||
|
||||
$this->dbUpdater = new DbUpdater($this->httpClient->reveal(), $this->filesystem->reveal(), $this->options);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function anExceptionIsThrownIfFreshDbCannotBeDownloaded(): void
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionCode(0);
|
||||
$this->expectExceptionMessage(
|
||||
'An error occurred while trying to download a fresh copy of the GeoLite2 database'
|
||||
);
|
||||
$request->shouldBeCalledOnce();
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function anExceptionIsThrownIfFreshDbCannotBeExtracted(): void
|
||||
{
|
||||
$this->options->tempDir = '__invalid__';
|
||||
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionCode(0);
|
||||
$this->expectExceptionMessage(
|
||||
'An error occurred while trying to extract the GeoLite2 database from __invalid__/GeoLite2-City.tar.gz'
|
||||
);
|
||||
$request->shouldBeCalledOnce();
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFilesystemExceptions
|
||||
*/
|
||||
public function anExceptionIsThrownIfFreshDbCannotBeCopiedToDestination(string $e): void
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->willThrow($e);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionCode(0);
|
||||
$this->expectExceptionMessage('An error occurred while trying to copy GeoLite2 db file to destination');
|
||||
$request->shouldBeCalledOnce();
|
||||
$copy->shouldBeCalledOnce();
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
}
|
||||
|
||||
public function provideFilesystemExceptions(): iterable
|
||||
{
|
||||
yield 'file not found' => [FilesystemException\FileNotFoundException::class];
|
||||
yield 'IO error' => [FilesystemException\IOException::class];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function noExceptionsAreThrownIfEverythingWorksFine(): void
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->will(function () {
|
||||
});
|
||||
$remove = $this->filesystem->remove(Argument::cetera())->will(function () {
|
||||
});
|
||||
|
||||
$this->dbUpdater->downloadFreshCopy();
|
||||
|
||||
$request->shouldHaveBeenCalledOnce();
|
||||
$copy->shouldHaveBeenCalledOnce();
|
||||
$remove->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExists
|
||||
*/
|
||||
public function databaseFileExistsChecksIfTheFilesExistsInTheFilesystem(bool $expected): void
|
||||
{
|
||||
$exists = $this->filesystem->exists('db_location')->willReturn($expected);
|
||||
|
||||
$result = $this->dbUpdater->databaseFileExists();
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
$exists->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExists(): iterable
|
||||
{
|
||||
return [[true], [false]];
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\ChainIpLocationResolver;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
class ChainIpLocationResolverTest extends TestCase
|
||||
{
|
||||
/** @var ChainIpLocationResolver */
|
||||
private $resolver;
|
||||
/** @var ObjectProphecy */
|
||||
private $firstInnerResolver;
|
||||
/** @var ObjectProphecy */
|
||||
private $secondInnerResolver;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->firstInnerResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
$this->secondInnerResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
|
||||
$this->resolver = new ChainIpLocationResolver(
|
||||
$this->firstInnerResolver->reveal(),
|
||||
$this->secondInnerResolver->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function throwsExceptionWhenNoInnerResolverCanHandleTheResolution()
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
|
||||
$this->expectException(WrongIpException::class);
|
||||
$firstResolve->shouldBeCalledOnce();
|
||||
$secondResolve->shouldBeCalledOnce();
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function returnsResultOfFirstInnerResolver(): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willReturn(Location::emptyInstance());
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$firstResolve->shouldHaveBeenCalledOnce();
|
||||
$secondResolve->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function returnsResultOfSecondInnerResolver(): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$firstResolve = $this->firstInnerResolver->resolveIpLocation($ipAddress)->willThrow(WrongIpException::class);
|
||||
$secondResolve = $this->secondInnerResolver->resolveIpLocation($ipAddress)->willReturn(
|
||||
Location::emptyInstance()
|
||||
);
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$firstResolve->shouldHaveBeenCalledOnce();
|
||||
$secondResolve->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\EmptyIpLocationResolver;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
class EmptyIpLocationResolverTest extends TestCase
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/** @var EmptyIpLocationResolver */
|
||||
private $resolver;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->resolver = new EmptyIpLocationResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideEmptyResponses
|
||||
*/
|
||||
public function alwaysReturnsAnEmptyLocation(string $ipAddress): void
|
||||
{
|
||||
$this->assertEquals(Location::emptyInstance(), $this->resolver->resolveIpLocation($ipAddress));
|
||||
}
|
||||
|
||||
public function provideEmptyResponses(): array
|
||||
{
|
||||
return map(range(0, 5), function () {
|
||||
return [$this->generateRandomString(15)];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use GeoIp2\Exception\AddressNotFoundException;
|
||||
use GeoIp2\Model\City;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\GeoLite2LocationResolver;
|
||||
|
||||
class GeoLite2LocationResolverTest extends TestCase
|
||||
{
|
||||
/** @var GeoLite2LocationResolver */
|
||||
private $resolver;
|
||||
/** @var ObjectProphecy */
|
||||
private $reader;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->reader = $this->prophesize(Reader::class);
|
||||
$this->resolver = new GeoLite2LocationResolver($this->reader->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideReaderExceptions
|
||||
*/
|
||||
public function exceptionIsThrownIfReaderThrowsException(string $e, string $message): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
|
||||
$cityMethod = $this->reader->city($ipAddress)->willThrow($e);
|
||||
|
||||
$this->expectException(WrongIpException::class);
|
||||
$this->expectExceptionMessage($message);
|
||||
$this->expectExceptionCode(0);
|
||||
$cityMethod->shouldBeCalledOnce();
|
||||
|
||||
$this->resolver->resolveIpLocation($ipAddress);
|
||||
}
|
||||
|
||||
public function provideReaderExceptions(): iterable
|
||||
{
|
||||
yield 'invalid IP address' => [AddressNotFoundException::class, 'Provided IP "1.2.3.4" is invalid'];
|
||||
yield 'invalid geolite DB' => [InvalidDatabaseException::class, 'Provided GeoLite2 db file is invalid'];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function resolvedCityIsProperlyMapped(): void
|
||||
{
|
||||
$ipAddress = '1.2.3.4';
|
||||
$city = new City([]);
|
||||
|
||||
$cityMethod = $this->reader->city($ipAddress)->willReturn($city);
|
||||
|
||||
$result = $this->resolver->resolveIpLocation($ipAddress);
|
||||
|
||||
$this->assertEquals(Location::emptyInstance(), $result);
|
||||
$cityMethod->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\IpGeolocation\Resolver;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver;
|
||||
|
||||
use function json_encode;
|
||||
|
||||
class IpApiLocationResolverTest extends TestCase
|
||||
{
|
||||
/** @var IpApiLocationResolver */
|
||||
private $ipResolver;
|
||||
/** @var ObjectProphecy */
|
||||
private $client;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->client = $this->prophesize(Client::class);
|
||||
$this->ipResolver = new IpApiLocationResolver($this->client->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function correctIpReturnsDecodedInfo(): void
|
||||
{
|
||||
$actual = [
|
||||
'countryCode' => 'bar',
|
||||
'lat' => 5,
|
||||
'lon' => 10,
|
||||
];
|
||||
$expected = new Location('bar', '', '', '', 5, 10, '');
|
||||
$response = new Response();
|
||||
$response->getBody()->write(json_encode($actual));
|
||||
$response->getBody()->rewind();
|
||||
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willReturn($response)
|
||||
->shouldBeCalledOnce();
|
||||
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function guzzleExceptionThrowsShlinkException(): void
|
||||
{
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willThrow(new TransferException())
|
||||
->shouldBeCalledOnce();
|
||||
$this->expectException(WrongIpException::class);
|
||||
$this->ipResolver->resolveIpLocation('1.2.3.4');
|
||||
}
|
||||
}
|
|
@ -18,9 +18,6 @@
|
|||
<testsuite name="EventDispatcher">
|
||||
<directory>./module/EventDispatcher/test</directory>
|
||||
</testsuite>
|
||||
<testsuite name="IpGeolocation">
|
||||
<directory>./module/IpGeolocation/test</directory>
|
||||
</testsuite>
|
||||
<testsuite name="PreviewGenerator">
|
||||
<directory>./module/PreviewGenerator/test</directory>
|
||||
</testsuite>
|
||||
|
|
Loading…
Reference in a new issue