diff --git a/.gitignore b/.gitignore
index 9426e5f3..bf3d5ae0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@ composer.phar
vendor/
.env
data/database.sqlite
+data/GeoLite2-City.mmdb
docs/swagger-ui
docker-compose.override.yml
diff --git a/config/autoload/geolite2.global.php b/config/autoload/geolite2.global.php
new file mode 100644
index 00000000..0536a0be
--- /dev/null
+++ b/config/autoload/geolite2.global.php
@@ -0,0 +1,10 @@
+ [
+ 'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
+ ],
+
+];
diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php
index 371c9175..2976e5b0 100644
--- a/module/CLI/config/dependencies.config.php
+++ b/module/CLI/config/dependencies.config.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
-use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
+use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
@@ -65,7 +65,7 @@ return [
Command\Visit\ProcessVisitsCommand::class => [
Service\VisitService::class,
- IpApiLocationResolver::class,
+ IpLocationResolverInterface::class,
'translator',
],
diff --git a/module/CLI/src/Command/Visit/ProcessVisitsCommand.php b/module/CLI/src/Command/Visit/ProcessVisitsCommand.php
index f1f70e35..3d36695c 100644
--- a/module/CLI/src/Command/Visit/ProcessVisitsCommand.php
+++ b/module/CLI/src/Command/Visit/ProcessVisitsCommand.php
@@ -68,10 +68,10 @@ class ProcessVisitsCommand extends Command
}
$ipAddr = $visit->getRemoteAddr();
- $io->write(sprintf('%s %s', $this->translator->translate('Processing IP'), $ipAddr));
+ $io->write(sprintf('%s %s>', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) {
$io->writeln(
- sprintf(' (%s)', $this->translator->translate('Ignored localhost address'))
+ sprintf(' [%s]', $this->translator->translate('Ignored localhost address'))
);
continue;
}
@@ -85,12 +85,15 @@ class ProcessVisitsCommand extends Command
$this->visitService->saveVisit($visit);
$io->writeln(sprintf(
- ' (' . $this->translator->translate('Address located at "%s"') . ')',
+ ' [' . $this->translator->translate('Address located at "%s"') . ']',
$location->getCityName()
));
} catch (WrongIpException $e) {
$io->writeln(
- sprintf(' %s', $this->translator->translate('An error occurred while locating IP'))
+ sprintf(
+ ' [%s>]',
+ $this->translator->translate('An error occurred while locating IP. Skipped')
+ )
);
if ($io->isVerbose()) {
$this->getApplication()->renderException($e, $output);
diff --git a/module/Common/config/dependencies.config.php b/module/Common/config/dependencies.config.php
index 6278d63b..d8df79aa 100644
--- a/module/Common/config/dependencies.config.php
+++ b/module/Common/config/dependencies.config.php
@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
+use GeoIp2\Database\Reader;
use GuzzleHttp\Client as GuzzleClient;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
@@ -23,6 +24,7 @@ return [
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Filesystem::class => InvokableFactory::class,
+ Reader::class => ConfigAbstractFactory::class,
Translator::class => Factory\TranslatorFactory::class,
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
@@ -33,6 +35,7 @@ return [
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
Service\IpApiLocationResolver::class => ConfigAbstractFactory::class,
+ Service\GeoLite2LocationResolver::class => ConfigAbstractFactory::class,
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
],
'aliases' => [
@@ -42,6 +45,7 @@ return [
'logger' => LoggerInterface::class,
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
+ Service\IpLocationResolverInterface::class => Service\GeoLite2LocationResolver::class,
],
'abstract_factories' => [
Factory\DottedAccessConfigAbstractFactory::class,
@@ -49,9 +53,12 @@ return [
],
ConfigAbstractFactory::class => [
+ Reader::class => ['config.geolite2.db_location'],
+
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Service\IpApiLocationResolver::class => ['httpClient'],
+ Service\GeoLite2LocationResolver::class => [Reader::class],
Service\PreviewGenerator::class => [
Image\ImageBuilder::class,
Filesystem::class,
diff --git a/module/Common/src/Service/GeoLite2LocationResolver.php b/module/Common/src/Service/GeoLite2LocationResolver.php
new file mode 100644
index 00000000..104a1ab3
--- /dev/null
+++ b/module/Common/src/Service/GeoLite2LocationResolver.php
@@ -0,0 +1,73 @@
+geoLiteDbReader = $geoLiteDbReader;
+ }
+
+ /**
+ * @param string $ipAddress
+ * @return array
+ * @throws WrongIpException
+ */
+ public function resolveIpLocation(string $ipAddress): array
+ {
+ 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): array
+ {
+ return [
+ 'country_code' => $city->country->isoCode ?? '',
+ 'country_name' => $city->country->name ?? '',
+ 'region_name' => $city->mostSpecificSubdivision->name ?? '',
+ 'city' => $city->city->name ?? '',
+ 'latitude' => (string) $city->location->latitude, // FIXME Cast to string for BC compatibility
+ 'longitude' => (string) $city->location->longitude, // FIXME Cast to string for BC compatibility
+ 'time_zone' => $city->location->timeZone ?? '',
+ ];
+ }
+
+ /**
+ * Returns the interval in seconds that needs to be waited when the API limit is reached
+ *
+ * @return int
+ */
+ public function getApiInterval(): int
+ {
+ return 0;
+ }
+
+ /**
+ * Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
+ *
+ * @return int|null
+ */
+ public function getApiLimit(): ?int
+ {
+ return null;
+ }
+}