mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-28 00:38:46 +03:00
Merge branch 'develop'
This commit is contained in:
commit
d54b823c88
24 changed files with 252 additions and 106 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -1,5 +1,35 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 1.10.1 - 2018-08-02
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* [#167](https://github.com/shlinkio/shlink/issues/167) Shlink version is now set at build time to avoid older version numbers to be kept in newer builds.
|
||||||
|
|
||||||
|
#### Deprecated
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* [#165](https://github.com/shlinkio/shlink/issues/165) Fixed custom slugs failing when they are longer than 10 characters.
|
||||||
|
* [#166](https://github.com/shlinkio/shlink/issues/166) Fixed unusual edge case in which visits were not properly counted when ordering by visit and filtering by search term in `[GET] /short-codes` API endpoint.
|
||||||
|
* [#174](https://github.com/shlinkio/shlink/issues/174) Fixed geolocation not working due to a deprecation on used service.
|
||||||
|
* [#172](https://github.com/shlinkio/shlink/issues/172) Documented missing filtering params for `[GET] /short-codes/{shortCode}/visits` API endpoint, which allow the list to be filtered by date range.
|
||||||
|
|
||||||
|
For example: `https://doma.in/rest/v1/short-urls/abc123/visits?startDate=2017-05-23&endDate=2017-10-05`
|
||||||
|
|
||||||
|
* [#169](https://github.com/shlinkio/shlink/issues/169) Fixed unhandled error when parsing `ShortUrlMeta` and date fields are already `DateTime` instances.
|
||||||
|
|
||||||
|
|
||||||
## 1.10.0 - 2018-07-09
|
## 1.10.0 - 2018-07-09
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
3
build.sh
3
build.sh
|
@ -46,6 +46,9 @@ rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
||||||
rm -rf config/params/{*,.gitignore}
|
rm -rf config/params/{*,.gitignore}
|
||||||
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
||||||
|
|
||||||
|
# Update shlink version in config
|
||||||
|
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||||
|
|
||||||
# Compressing file
|
# Compressing file
|
||||||
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
||||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
|
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
|
||||||
|
|
|
@ -7,7 +7,7 @@ return [
|
||||||
|
|
||||||
'app_options' => [
|
'app_options' => [
|
||||||
'name' => 'Shlink',
|
'name' => 'Shlink',
|
||||||
'version' => '1.7.0',
|
'version' => '%SHLINK_VERSION%',
|
||||||
'secret_key' => env('SECRET_KEY'),
|
'secret_key' => env('SECRET_KEY'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
45
data/migrations/Version20180801183328.php
Normal file
45
data/migrations/Version20180801183328.php
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\DBAL\Schema\SchemaException;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20180801183328 extends AbstractMigration
|
||||||
|
{
|
||||||
|
private const NEW_SIZE = 255;
|
||||||
|
private const OLD_SIZE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->setSize($schema, self::NEW_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->setSize($schema, self::OLD_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
* @param int $size
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
private function setSize(Schema $schema, int $size): void
|
||||||
|
{
|
||||||
|
$schema->getTable('short_urls')->getColumn('short_code')->setLength($size);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,24 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "startDate",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The date (in ISO-8601 format) from which we want to get visits.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "endDate",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The date (in ISO-8601 format) until which we want to get visits.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command;
|
use Shlinkio\Shlink\CLI\Command;
|
||||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
@ -51,7 +51,7 @@ return [
|
||||||
],
|
],
|
||||||
Command\Visit\ProcessVisitsCommand::class => [
|
Command\Visit\ProcessVisitsCommand::class => [
|
||||||
Service\VisitService::class,
|
Service\VisitService::class,
|
||||||
IpLocationResolver::class,
|
IpApiLocationResolver::class,
|
||||||
'translator',
|
'translator',
|
||||||
],
|
],
|
||||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||||
|
|
|
@ -15,8 +15,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
class ProcessVisitsCommand extends Command
|
class ProcessVisitsCommand extends Command
|
||||||
{
|
{
|
||||||
const LOCALHOST = '127.0.0.1';
|
private const LOCALHOST = '127.0.0.1';
|
||||||
const NAME = 'visit:process';
|
public const NAME = 'visit:process';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var VisitServiceInterface
|
* @var VisitServiceInterface
|
||||||
|
@ -57,10 +57,10 @@ class ProcessVisitsCommand extends Command
|
||||||
|
|
||||||
foreach ($visits as $visit) {
|
foreach ($visits as $visit) {
|
||||||
$ipAddr = $visit->getRemoteAddr();
|
$ipAddr = $visit->getRemoteAddr();
|
||||||
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
$io->write(\sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||||
if ($ipAddr === self::LOCALHOST) {
|
if ($ipAddr === self::LOCALHOST) {
|
||||||
$io->writeln(
|
$io->writeln(
|
||||||
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
\sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -73,12 +73,17 @@ class ProcessVisitsCommand extends Command
|
||||||
$visit->setVisitLocation($location);
|
$visit->setVisitLocation($location);
|
||||||
$this->visitService->saveVisit($visit);
|
$this->visitService->saveVisit($visit);
|
||||||
|
|
||||||
$io->writeln(sprintf(
|
$io->writeln(\sprintf(
|
||||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||||
$location->getCityName()
|
$location->getCityName()
|
||||||
));
|
));
|
||||||
} catch (WrongIpException $e) {
|
} catch (WrongIpException $e) {
|
||||||
continue;
|
$io->writeln(
|
||||||
|
\sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
||||||
|
);
|
||||||
|
if ($io->isVerbose()) {
|
||||||
|
$this->getApplication()->renderException($e, $output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
@ -37,7 +38,8 @@ class GenerateKeyCommandTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function noExpirationDateIsDefinedIfNotProvided()
|
public function noExpirationDateIsDefinedIfNotProvided()
|
||||||
{
|
{
|
||||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1);
|
$this->apiKeyService->create(null)->shouldBeCalledTimes(1)
|
||||||
|
->willReturn(new ApiKey());
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:generate',
|
'command' => 'api-key:generate',
|
||||||
]);
|
]);
|
||||||
|
@ -46,9 +48,10 @@ class GenerateKeyCommandTest extends TestCase
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function expirationDateIsDefinedIfWhenProvided()
|
public function expirationDateIsDefinedIfProvided()
|
||||||
{
|
{
|
||||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1);
|
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1)
|
||||||
|
->willReturn(new ApiKey());
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:generate',
|
'command' => 'api-key:generate',
|
||||||
'--expirationDate' => '2016-01-01',
|
'--expirationDate' => '2016-01-01',
|
||||||
|
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Prophecy\MethodProphecy;
|
use Prophecy\Prophecy\MethodProphecy;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
@ -52,7 +53,7 @@ class CreateTagCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
$tagNames = ['foo', 'bar'];
|
$tagNames = ['foo', 'bar'];
|
||||||
/** @var MethodProphecy $createTags */
|
/** @var MethodProphecy $createTags */
|
||||||
$createTags = $this->tagService->createTags($tagNames)->willReturn([]);
|
$createTags = $this->tagService->createTags($tagNames)->willReturn(new ArrayCollection());
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'--name' => $tagNames,
|
'--name' => $tagNames,
|
||||||
|
|
|
@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
|
@ -32,7 +32,7 @@ class ProcessVisitsCommandTest extends TestCase
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->visitService = $this->prophesize(VisitService::class);
|
$this->visitService = $this->prophesize(VisitService::class);
|
||||||
$this->ipResolver = $this->prophesize(IpLocationResolver::class);
|
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
||||||
$command = new ProcessVisitsCommand(
|
$command = new ProcessVisitsCommand(
|
||||||
$this->visitService->reveal(),
|
$this->visitService->reveal(),
|
||||||
$this->ipResolver->reveal(),
|
$this->ipResolver->reveal(),
|
||||||
|
|
|
@ -32,7 +32,7 @@ return [
|
||||||
|
|
||||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||||
|
|
||||||
Service\IpLocationResolver::class => ConfigAbstractFactory::class,
|
Service\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
||||||
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
|
@ -51,7 +51,7 @@ return [
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
TranslatorExtension::class => ['translator'],
|
TranslatorExtension::class => ['translator'],
|
||||||
LocaleMiddleware::class => ['translator'],
|
LocaleMiddleware::class => ['translator'],
|
||||||
Service\IpLocationResolver::class => ['httpClient'],
|
Service\IpApiLocationResolver::class => ['httpClient'],
|
||||||
Service\PreviewGenerator::class => [
|
Service\PreviewGenerator::class => [
|
||||||
ImageBuilder::class,
|
ImageBuilder::class,
|
||||||
Filesystem::class,
|
Filesystem::class,
|
||||||
|
|
|
@ -23,8 +23,7 @@ class EntityManagerFactory implements FactoryInterface
|
||||||
* @param null|array $options
|
* @param null|array $options
|
||||||
* @return object
|
* @return object
|
||||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
* @throws ServiceNotCreatedException if an exception is raised when
|
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||||
* creating a service.
|
|
||||||
* @throws ContainerException if any other error occurs
|
* @throws ContainerException if any other error occurs
|
||||||
*/
|
*/
|
||||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
|
@ -32,14 +31,14 @@ class EntityManagerFactory implements FactoryInterface
|
||||||
$globalConfig = $container->get('config');
|
$globalConfig = $container->get('config');
|
||||||
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
||||||
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||||
$emConfig = isset($globalConfig['entity_manager']) ? $globalConfig['entity_manager'] : [];
|
$emConfig = $globalConfig['entity_manager'] ?? [];
|
||||||
$connecitonConfig = isset($emConfig['connection']) ? $emConfig['connection'] : [];
|
$connectionConfig = $emConfig['connection'] ?? [];
|
||||||
$ormConfig = isset($emConfig['orm']) ? $emConfig['orm'] : [];
|
$ormConfig = $emConfig['orm'] ?? [];
|
||||||
|
|
||||||
return EntityManager::create($connecitonConfig, Setup::createAnnotationMetadataConfiguration(
|
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
|
||||||
isset($ormConfig['entities_paths']) ? $ormConfig['entities_paths'] : [],
|
$ormConfig['entities_paths'] ?? [],
|
||||||
$isDevMode,
|
$isDevMode,
|
||||||
isset($ormConfig['proxies_dir']) ? $ormConfig['proxies_dir'] : null,
|
$ormConfig['proxies_dir'] ?? null,
|
||||||
$cache,
|
$cache,
|
||||||
false
|
false
|
||||||
));
|
));
|
||||||
|
|
51
module/Common/src/Service/IpApiLocationResolver.php
Normal file
51
module/Common/src/Service/IpApiLocationResolver.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\Service;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $ipAddress
|
||||||
|
* @return array
|
||||||
|
* @throws WrongIpException
|
||||||
|
*/
|
||||||
|
public function resolveIpLocation(string $ipAddress): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->httpClient->get(\sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||||
|
return $this->mapFields(\json_decode((string) $response->getBody(), true));
|
||||||
|
} catch (GuzzleException $e) {
|
||||||
|
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapFields(array $entry): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'country_code' => $entry['countryCode'] ?? '',
|
||||||
|
'country_name' => $entry['country'] ?? '',
|
||||||
|
'region_name' => $entry['regionName'] ?? '',
|
||||||
|
'city' => $entry['city'] ?? '',
|
||||||
|
'latitude' => $entry['lat'] ?? '',
|
||||||
|
'longitude' => $entry['lon'] ?? '',
|
||||||
|
'time_zone' => $entry['timezone'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Service;
|
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
|
||||||
|
|
||||||
class IpLocationResolver implements IpLocationResolverInterface
|
|
||||||
{
|
|
||||||
const SERVICE_PATTERN = 'http://freegeoip.net/json/%s';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
private $httpClient;
|
|
||||||
|
|
||||||
public function __construct(Client $httpClient)
|
|
||||||
{
|
|
||||||
$this->httpClient = $httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $ipAddress
|
|
||||||
* @return array
|
|
||||||
* @throws WrongIpException
|
|
||||||
*/
|
|
||||||
public function resolveIpLocation(string $ipAddress): array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
|
||||||
return json_decode((string) $response->getBody(), true);
|
|
||||||
} catch (GuzzleException $e) {
|
|
||||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,12 +8,12 @@ use GuzzleHttp\Exception\TransferException;
|
||||||
use GuzzleHttp\Psr7\Response;
|
use GuzzleHttp\Psr7\Response;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||||
|
|
||||||
class IpLocationResolverTest extends TestCase
|
class IpApiLocationResolverTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var IpLocationResolver
|
* @var IpApiLocationResolver
|
||||||
*/
|
*/
|
||||||
protected $ipResolver;
|
protected $ipResolver;
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +24,7 @@ class IpLocationResolverTest extends TestCase
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->client = $this->prophesize(Client::class);
|
$this->client = $this->prophesize(Client::class);
|
||||||
$this->ipResolver = new IpLocationResolver($this->client->reveal());
|
$this->ipResolver = new IpApiLocationResolver($this->client->reveal());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,16 +32,26 @@ class IpLocationResolverTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function correctIpReturnsDecodedInfo()
|
public function correctIpReturnsDecodedInfo()
|
||||||
{
|
{
|
||||||
|
$actual = [
|
||||||
|
'countryCode' => 'bar',
|
||||||
|
'lat' => 5,
|
||||||
|
'lon' => 10,
|
||||||
|
];
|
||||||
$expected = [
|
$expected = [
|
||||||
'foo' => 'bar',
|
'country_code' => 'bar',
|
||||||
'baz' => 'foo',
|
'country_name' => '',
|
||||||
|
'region_name' => '',
|
||||||
|
'city' => '',
|
||||||
|
'latitude' => 5,
|
||||||
|
'longitude' => 10,
|
||||||
|
'time_zone' => '',
|
||||||
];
|
];
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
$response->getBody()->write(json_encode($expected));
|
$response->getBody()->write(\json_encode($actual));
|
||||||
$response->getBody()->rewind();
|
$response->getBody()->rewind();
|
||||||
|
|
||||||
$this->client->get('http://freegeoip.net/json/1.2.3.4')->willReturn($response)
|
$this->client->get('http://ip-api.com/json/1.2.3.4')->willReturn($response)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledTimes(1);
|
||||||
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
|
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +61,8 @@ class IpLocationResolverTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function guzzleExceptionThrowsShlinkException()
|
public function guzzleExceptionThrowsShlinkException()
|
||||||
{
|
{
|
||||||
$this->client->get('http://freegeoip.net/json/1.2.3.4')->willThrow(new TransferException())
|
$this->client->get('http://ip-api.com/json/1.2.3.4')->willThrow(new TransferException())
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledTimes(1);
|
||||||
$this->ipResolver->resolveIpLocation('1.2.3.4');
|
$this->ipResolver->resolveIpLocation('1.2.3.4');
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -29,7 +29,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||||
* name="short_code",
|
* name="short_code",
|
||||||
* type="string",
|
* type="string",
|
||||||
* nullable=false,
|
* nullable=false,
|
||||||
* length=10,
|
* length=255,
|
||||||
* unique=true
|
* unique=true
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -78,19 +78,34 @@ final class ShortUrlMeta
|
||||||
throw ValidationException::fromInputFilter($inputFilter);
|
throw ValidationException::fromInputFilter($inputFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validSince = $inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE);
|
$this->validSince = $this->parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||||
$this->validSince = $this->validSince !== null ? new \DateTime($this->validSince) : null;
|
$this->validUntil = $this->parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||||
$this->validUntil = $inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL);
|
|
||||||
$this->validUntil = $this->validUntil !== null ? new \DateTime($this->validUntil) : null;
|
|
||||||
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
||||||
$this->maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS);
|
$this->maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS);
|
||||||
$this->maxVisits = $this->maxVisits !== null ? (int) $this->maxVisits : null;
|
$this->maxVisits = $this->maxVisits !== null ? (int) $this->maxVisits : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param string|\DateTime|null $date
|
||||||
* @return \DateTime|null
|
* @return \DateTime|null
|
||||||
*/
|
*/
|
||||||
public function getValidSince()
|
private function parseDateField($date): ?\DateTime
|
||||||
|
{
|
||||||
|
if ($date === null || $date instanceof \DateTime) {
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_string($date)) {
|
||||||
|
return new \DateTime($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \DateTime|null
|
||||||
|
*/
|
||||||
|
public function getValidSince(): ?\DateTime
|
||||||
{
|
{
|
||||||
return $this->validSince;
|
return $this->validSince;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +118,7 @@ final class ShortUrlMeta
|
||||||
/**
|
/**
|
||||||
* @return \DateTime|null
|
* @return \DateTime|null
|
||||||
*/
|
*/
|
||||||
public function getValidUntil()
|
public function getValidUntil(): ?\DateTime
|
||||||
{
|
{
|
||||||
return $this->validUntil;
|
return $this->validUntil;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,16 +51,17 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||||
$order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
$order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||||
|
|
||||||
if (\in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) {
|
if (\in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) {
|
||||||
$qb->addSelect('COUNT(v) AS totalVisits')
|
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||||
->leftJoin('s.visits', 'v')
|
->leftJoin('s.visits', 'v')
|
||||||
->groupBy('s')
|
->groupBy('s')
|
||||||
->orderBy('totalVisits', $order);
|
->orderBy('totalVisits', $order);
|
||||||
|
|
||||||
return \array_column($qb->getQuery()->getResult(), 0);
|
return \array_column($qb->getQuery()->getResult(), 0);
|
||||||
} elseif (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) {
|
|
||||||
$qb->orderBy('s.' . $fieldName, $order);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) {
|
||||||
|
$qb->orderBy('s.' . $fieldName, $order);
|
||||||
|
}
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,16 +28,18 @@ class TagService implements TagServiceInterface
|
||||||
* @return Tag[]
|
* @return Tag[]
|
||||||
* @throws \UnexpectedValueException
|
* @throws \UnexpectedValueException
|
||||||
*/
|
*/
|
||||||
public function listTags()
|
public function listTags(): array
|
||||||
{
|
{
|
||||||
return $this->em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']);
|
/** @var Tag[] $tags */
|
||||||
|
$tags = $this->em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']);
|
||||||
|
return $tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $tagNames
|
* @param array $tagNames
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function deleteTags(array $tagNames)
|
public function deleteTags(array $tagNames): void
|
||||||
{
|
{
|
||||||
/** @var TagRepository $repo */
|
/** @var TagRepository $repo */
|
||||||
$repo = $this->em->getRepository(Tag::class);
|
$repo = $this->em->getRepository(Tag::class);
|
||||||
|
@ -50,7 +52,7 @@ class TagService implements TagServiceInterface
|
||||||
* @param string[] $tagNames
|
* @param string[] $tagNames
|
||||||
* @return Collection|Tag[]
|
* @return Collection|Tag[]
|
||||||
*/
|
*/
|
||||||
public function createTags(array $tagNames)
|
public function createTags(array $tagNames): Collection
|
||||||
{
|
{
|
||||||
$tags = $this->tagNamesToEntities($this->em, $tagNames);
|
$tags = $this->tagNamesToEntities($this->em, $tagNames);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
@ -65,7 +67,7 @@ class TagService implements TagServiceInterface
|
||||||
* @throws EntityDoesNotExistException
|
* @throws EntityDoesNotExistException
|
||||||
* @throws ORM\OptimisticLockException
|
* @throws ORM\OptimisticLockException
|
||||||
*/
|
*/
|
||||||
public function renameTag($oldName, $newName)
|
public function renameTag($oldName, $newName): Tag
|
||||||
{
|
{
|
||||||
$criteria = ['name' => $oldName];
|
$criteria = ['name' => $oldName];
|
||||||
/** @var Tag|null $tag */
|
/** @var Tag|null $tag */
|
||||||
|
|
|
@ -12,13 +12,13 @@ interface TagServiceInterface
|
||||||
/**
|
/**
|
||||||
* @return Tag[]
|
* @return Tag[]
|
||||||
*/
|
*/
|
||||||
public function listTags();
|
public function listTags(): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $tagNames
|
* @param string[] $tagNames
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function deleteTags(array $tagNames);
|
public function deleteTags(array $tagNames): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provided a list of tag names, creates all that do not exist yet
|
* Provided a list of tag names, creates all that do not exist yet
|
||||||
|
@ -26,7 +26,7 @@ interface TagServiceInterface
|
||||||
* @param string[] $tagNames
|
* @param string[] $tagNames
|
||||||
* @return Collection|Tag[]
|
* @return Collection|Tag[]
|
||||||
*/
|
*/
|
||||||
public function createTags(array $tagNames);
|
public function createTags(array $tagNames): Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $oldName
|
* @param string $oldName
|
||||||
|
@ -34,5 +34,5 @@ interface TagServiceInterface
|
||||||
* @return Tag
|
* @return Tag
|
||||||
* @throws EntityDoesNotExistException
|
* @throws EntityDoesNotExistException
|
||||||
*/
|
*/
|
||||||
public function renameTag($oldName, $newName);
|
public function renameTag($oldName, $newName): Tag;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
|
|
||||||
$bar = new ShortUrl();
|
$bar = new ShortUrl();
|
||||||
$bar->setOriginalUrl('bar')
|
$bar->setOriginalUrl('bar')
|
||||||
->setShortCode('bar')
|
->setShortCode('bar_very_long_text')
|
||||||
->setValidSince((new \DateTime())->add(new \DateInterval('P1M')));
|
->setValidSince((new \DateTime())->add(new \DateInterval('P1M')));
|
||||||
$this->getEntityManager()->persist($bar);
|
$this->getEntityManager()->persist($bar);
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||||
* @param \DateTime $expirationDate
|
* @param \DateTime $expirationDate
|
||||||
* @return ApiKey
|
* @return ApiKey
|
||||||
*/
|
*/
|
||||||
public function create(\DateTime $expirationDate = null)
|
public function create(\DateTime $expirationDate = null): ApiKey
|
||||||
{
|
{
|
||||||
$key = new ApiKey();
|
$key = new ApiKey();
|
||||||
if ($expirationDate !== null) {
|
if ($expirationDate !== null) {
|
||||||
|
@ -44,7 +44,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function check(string $key)
|
public function check(string $key): bool
|
||||||
{
|
{
|
||||||
/** @var ApiKey|null $apiKey */
|
/** @var ApiKey|null $apiKey */
|
||||||
$apiKey = $this->getByKey($key);
|
$apiKey = $this->getByKey($key);
|
||||||
|
@ -58,7 +58,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||||
* @return ApiKey
|
* @return ApiKey
|
||||||
* @throws InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function disable(string $key)
|
public function disable(string $key): ApiKey
|
||||||
{
|
{
|
||||||
/** @var ApiKey|null $apiKey */
|
/** @var ApiKey|null $apiKey */
|
||||||
$apiKey = $this->getByKey($key);
|
$apiKey = $this->getByKey($key);
|
||||||
|
@ -77,10 +77,12 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||||
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
||||||
* @return ApiKey[]
|
* @return ApiKey[]
|
||||||
*/
|
*/
|
||||||
public function listKeys(bool $enabledOnly = false)
|
public function listKeys(bool $enabledOnly = false): array
|
||||||
{
|
{
|
||||||
$conditions = $enabledOnly ? ['enabled' => true] : [];
|
$conditions = $enabledOnly ? ['enabled' => true] : [];
|
||||||
return $this->em->getRepository(ApiKey::class)->findBy($conditions);
|
/** @var ApiKey[] $apiKeys */
|
||||||
|
$apiKeys = $this->em->getRepository(ApiKey::class)->findBy($conditions);
|
||||||
|
return $apiKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,7 +91,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return ApiKey|null
|
* @return ApiKey|null
|
||||||
*/
|
*/
|
||||||
public function getByKey(string $key)
|
public function getByKey(string $key): ?ApiKey
|
||||||
{
|
{
|
||||||
/** @var ApiKey|null $apiKey */
|
/** @var ApiKey|null $apiKey */
|
||||||
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([
|
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface ApiKeyServiceInterface
|
||||||
* @param \DateTime $expirationDate
|
* @param \DateTime $expirationDate
|
||||||
* @return ApiKey
|
* @return ApiKey
|
||||||
*/
|
*/
|
||||||
public function create(\DateTime $expirationDate = null);
|
public function create(\DateTime $expirationDate = null): ApiKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if provided key is a valid api key
|
* Checks if provided key is a valid api key
|
||||||
|
@ -22,7 +22,7 @@ interface ApiKeyServiceInterface
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function check(string $key);
|
public function check(string $key): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables provided api key
|
* Disables provided api key
|
||||||
|
@ -31,7 +31,7 @@ interface ApiKeyServiceInterface
|
||||||
* @return ApiKey
|
* @return ApiKey
|
||||||
* @throws InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function disable(string $key);
|
public function disable(string $key): ApiKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all existing api keys
|
* Lists all existing api keys
|
||||||
|
@ -39,7 +39,7 @@ interface ApiKeyServiceInterface
|
||||||
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
||||||
* @return ApiKey[]
|
* @return ApiKey[]
|
||||||
*/
|
*/
|
||||||
public function listKeys(bool $enabledOnly = false);
|
public function listKeys(bool $enabledOnly = false): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to find one API key by its key string
|
* Tries to find one API key by its key string
|
||||||
|
@ -47,5 +47,5 @@ interface ApiKeyServiceInterface
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return ApiKey|null
|
* @return ApiKey|null
|
||||||
*/
|
*/
|
||||||
public function getByKey(string $key);
|
public function getByKey(string $key): ?ApiKey;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,3 @@ parameters:
|
||||||
- module/Rest/src/Util/RestUtils.php
|
- module/Rest/src/Util/RestUtils.php
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
- '#is not subtype of Throwable#'
|
- '#is not subtype of Throwable#'
|
||||||
- '#Cannot access offset#'
|
|
||||||
|
|
Loading…
Reference in a new issue