Compare commits

...

22 Commits

Author SHA1 Message Date
Alejandro Celaya
a30f796100 Merge pull request #743 from acelaya-forks/feature/geolite-license
Feature/geolite license
2020-04-30 19:34:44 +02:00
Alejandro Celaya
93a2c83652 Enabled GeoLite installer config option 2020-04-29 20:31:06 +02:00
Alejandro Celaya
4d4423413d Added GEOLITE_LICENSE_KEY env var to basic docker example, to encourage using it 2020-04-29 19:44:08 +02:00
Alejandro Celaya
a1c74c4038 Updated changelog 2020-04-29 19:31:10 +02:00
Alejandro Celaya
f71bb5e307 Added support for GEOLITE_LICENSE_KEY env var for docker image 2020-04-29 19:27:35 +02:00
Alejandro Celaya
9190996e54 Added support for geolite_license_key config option 2020-04-29 19:26:34 +02:00
Alejandro Celaya
af8b6b7f96 Documented how to pass a GEOLITE license key 2020-04-29 19:24:18 +02:00
Alejandro Celaya
e775b0f12f Merge pull request #722 from shlinkio/develop
Release 2.1.3
2020-04-09 12:50:46 +02:00
Alejandro Celaya
3ee5853b32 Merge pull request #721 from acelaya-forks/feature/qr-code-links
Feature/qr code links
2020-04-09 12:40:05 +02:00
Alejandro Celaya
832a24e4c7 Updated changelog 2020-04-09 12:33:00 +02:00
Alejandro Celaya
551368c30d Ensured QR code action respects configured domain 2020-04-09 12:31:03 +02:00
Alejandro Celaya
9f24b8eb76 Merge pull request #720 from acelaya-forks/feature/db-conn-recovery-task-workers
Feature/db conn recovery task workers
2020-04-09 12:01:47 +02:00
Alejandro Celaya
4c83ae2b22 Updated changelog 2020-04-09 11:55:47 +02:00
Alejandro Celaya
28e0fb049b Added check to ensure DB connection is gracefully recovered on swoole task workers 2020-04-09 11:54:54 +02:00
Alejandro Celaya
f79a369884 Merge pull request #719 from acelaya-forks/feature/handle-HEAD-requests
Feature/handle head requests
2020-04-09 00:06:28 +02:00
Alejandro Celaya
34c7b870a7 Removed unnecessary service registration, as it comes preregistered from third party config provider 2020-04-08 23:56:39 +02:00
Alejandro Celaya
ec9f874bb9 Updated changelog 2020-04-08 23:53:23 +02:00
Alejandro Celaya
1980d35691 Ensured redirect requests are not tracked when request is performed using method HEAD 2020-04-08 23:51:57 +02:00
Alejandro Celaya
ec8cbf82e5 Added HEAD request implicit handling 2020-04-08 17:27:26 +02:00
Alejandro Celaya
2b1011de52 Merge pull request #714 from acelaya-forks/feature/metadata-cache-clear
Feature/metadata cache clear
2020-04-06 21:08:46 +02:00
Alejandro Celaya
fa9ace83ad Fixed incorrect use of tilde 2020-04-06 20:59:10 +02:00
Alejandro Celaya
a9a53a9652 Ensured entities metadata cache is cleared during installation and docker start-up 2020-04-06 20:52:33 +02:00
19 changed files with 158 additions and 154 deletions

View File

@@ -4,6 +4,56 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## 2.1.4 - 2020-04-30
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#742](https://github.com/shlinkio/shlink/issues/742) Allowed a custom GeoLite2 license key to be provided, in order to avoid download limits.
## 2.1.3 - 2020-04-09
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#712](https://github.com/shlinkio/shlink/issues/712) Fixed app set-up not clearing entities metadata cache.
* [#711](https://github.com/shlinkio/shlink/issues/711) Fixed `HEAD` requests returning a duplicated `Content-Length` header.
* [#716](https://github.com/shlinkio/shlink/issues/716) Fixed Twitter not properly displaying preview for final long URL.
* [#717](https://github.com/shlinkio/shlink/issues/717) Fixed DB connection expiring on task workers when using swoole.
* [#705](https://github.com/shlinkio/shlink/issues/705) Fixed how the short URL domain is inferred when generating QR codes, making sure the configured domain is respected even if the request is performed using a different one, and only when a custom domain is used, then that one is used instead.
## 2.1.2 - 2020-03-29 ## 2.1.2 - 2020-03-29
#### Added #### Added

View File

@@ -52,7 +52,7 @@
"shlinkio/shlink-common": "^3.0", "shlinkio/shlink-common": "^3.0",
"shlinkio/shlink-config": "^1.0", "shlinkio/shlink-config": "^1.0",
"shlinkio/shlink-event-dispatcher": "^1.4", "shlinkio/shlink-event-dispatcher": "^1.4",
"shlinkio/shlink-installer": "^4.3.1", "shlinkio/shlink-installer": "^4.4.0",
"shlinkio/shlink-ip-geolocation": "^1.4", "shlinkio/shlink-ip-geolocation": "^1.4",
"symfony/console": "^5.0", "symfony/console": "^5.0",
"symfony/filesystem": "^5.0", "symfony/filesystem": "^5.0",
@@ -65,7 +65,7 @@
"eaglewu/swoole-ide-helper": "dev-master", "eaglewu/swoole-ide-helper": "dev-master",
"infection/infection": "^0.16.1", "infection/infection": "^0.16.1",
"phpstan/phpstan": "^0.12.18", "phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^9.0.1", "phpunit/phpunit": "~9.0.1",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.1.0", "shlinkio/php-coding-standard": "~2.1.0",
"shlinkio/shlink-test-utils": "^1.4", "shlinkio/shlink-test-utils": "^1.4",

View File

@@ -31,6 +31,7 @@ return [
Option\WebWorkerNumConfigOption::class, Option\WebWorkerNumConfigOption::class,
Option\RedisServersConfigOption::class, Option\RedisServersConfigOption::class,
Option\ShortCodeLengthOption::class, Option\ShortCodeLengthOption::class,
Option\GeoLiteLicenseKeyConfigOption::class,
], ],
'installation_commands' => [ 'installation_commands' => [

View File

@@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink; namespace Shlinkio\Shlink;
use Laminas\Stratigility\Middleware\ErrorHandler; use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio; use Mezzio\Helper;
use Mezzio\ProblemDetails; use Mezzio\ProblemDetails;
use Mezzio\Router;
use PhpMiddleware\RequestId\RequestIdMiddleware; use PhpMiddleware\RequestId\RequestIdMiddleware;
return [ return [
@@ -14,7 +15,7 @@ return [
'middleware_pipeline' => [ 'middleware_pipeline' => [
'error-handler' => [ 'error-handler' => [
'middleware' => [ 'middleware' => [
Mezzio\Helper\ContentLengthMiddleware::class, Helper\ContentLengthMiddleware::class,
ErrorHandler::class, ErrorHandler::class,
], ],
], ],
@@ -35,14 +36,15 @@ return [
'routing' => [ 'routing' => [
'middleware' => [ 'middleware' => [
Mezzio\Router\Middleware\RouteMiddleware::class, Router\Middleware\RouteMiddleware::class,
Router\Middleware\ImplicitHeadMiddleware::class,
], ],
], ],
'rest' => [ 'rest' => [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
Mezzio\Router\Middleware\ImplicitOptionsMiddleware::class, Router\Middleware\ImplicitOptionsMiddleware::class,
Rest\Middleware\BodyParserMiddleware::class, Rest\Middleware\BodyParserMiddleware::class,
Rest\Middleware\AuthenticationMiddleware::class, Rest\Middleware\AuthenticationMiddleware::class,
], ],
@@ -50,7 +52,7 @@ return [
'dispatch' => [ 'dispatch' => [
'middleware' => [ 'middleware' => [
Mezzio\Router\Middleware\DispatchMiddleware::class, Router\Middleware\DispatchMiddleware::class,
], ],
], ],
@@ -67,4 +69,5 @@ return [
], ],
], ],
], ],
]; ];

View File

@@ -18,7 +18,7 @@ It also expects these two env vars to be provided, in order to properly generate
So based on this, to run shlink on a local docker service, you should run a command like this: So based on this, to run shlink on a local docker service, you should run a command like this:
```bash ```bash
docker run --name shlink -p 8080:8080 -e SHORT_DOMAIN_HOST=doma.in -e SHORT_DOMAIN_SCHEMA=https shlinkio/shlink:stable docker run --name shlink -p 8080:8080 -e SHORT_DOMAIN_HOST=doma.in -e SHORT_DOMAIN_SCHEMA=https -e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 shlinkio/shlink:stable
``` ```
### Interact with shlink's CLI on a running container. ### Interact with shlink's CLI on a running container.
@@ -121,6 +121,8 @@ This is the complete list of supported env vars:
In the future, these redis servers could be used for other caching operations performed by shlink. In the future, these redis servers could be used for other caching operations performed by shlink.
* `GEOLITE_LICENSE_KEY`: The license key used to download new GeoLite2 database files. This is not mandatory, as a default license key is provided, but it is **strongly recommended** that you provide your own. Go to [https://shlink.io/documentation/geolite-license-key](https://shlink.io/documentation/geolite-license-key) to know how to generate it.
An example using all env vars could look like this: An example using all env vars could look like this:
```bash ```bash
@@ -147,6 +149,7 @@ docker run \
-e TASK_WORKER_NUM=32 \ -e TASK_WORKER_NUM=32 \
-e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \ -e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \
-e DEFAULT_SHORT_CODES_LENGTH=6 \ -e DEFAULT_SHORT_CODES_LENGTH=6 \
-e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 \
shlinkio/shlink:stable shlinkio/shlink:stable
``` ```
@@ -187,7 +190,8 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc", "password": "123abc",
"host": "something.rds.amazonaws.com", "host": "something.rds.amazonaws.com",
"port": "3306" "port": "3306"
} },
"geolite_license_key": "kjh23ljkbndskj345"
} }
``` ```

View File

@@ -147,4 +147,8 @@ return [
], ],
], ],
'geolite2' => [
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'),
],
]; ];

View File

@@ -12,6 +12,9 @@ php bin/cli db:migrate -n -q
echo "Generating proxies..." echo "Generating proxies..."
php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q
echo "Clearing entities cache..."
php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n -q
# When restarting the container, swoole might think it is already in execution # When restarting the container, swoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0 # This forces the app to be started every second until the exit code is 0
until php vendor/mezzio/mezzio-swoole/bin/mezzio-swoole start; do sleep 1 ; done until php vendor/mezzio/mezzio-swoole/bin/mezzio-swoole start; do sleep 1 ; done

View File

@@ -4,9 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core; namespace Shlinkio\Shlink\Core;
use Doctrine\Common\Cache\Cache;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Mezzio\Router\RouterInterface;
use Mezzio\Template\TemplateRendererInterface; use Mezzio\Template\TemplateRendererInterface;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
use Shlinkio\Shlink\Core\Domain\Resolver; use Shlinkio\Shlink\Core\Domain\Resolver;
@@ -39,8 +37,6 @@ return [
Action\PixelAction::class => ConfigAbstractFactory::class, Action\PixelAction::class => ConfigAbstractFactory::class,
Action\QrCodeAction::class => ConfigAbstractFactory::class, Action\QrCodeAction::class => ConfigAbstractFactory::class,
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class, Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class,
], ],
], ],
@@ -81,13 +77,11 @@ return [
'Logger_Shlink', 'Logger_Shlink',
], ],
Action\QrCodeAction::class => [ Action\QrCodeAction::class => [
RouterInterface::class,
Service\ShortUrl\ShortUrlResolver::class, Service\ShortUrl\ShortUrlResolver::class,
'config.url_shortener.domain',
'Logger_Shlink', 'Logger_Shlink',
], ],
Middleware\QrCodeCacheMiddleware::class => [Cache::class],
Resolver\PersistenceDomainResolver::class => ['em'], Resolver\PersistenceDomainResolver::class => ['em'],
], ],

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
use Fig\Http\Message\RequestMethodInterface as RequestMethod; use Fig\Http\Message\RequestMethodInterface as RequestMethod;
use RKA\Middleware\IpAddress; use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Core\Action; use Shlinkio\Shlink\Core\Action;
use Shlinkio\Shlink\Core\Middleware;
return [ return [
@@ -32,7 +31,6 @@ return [
'name' => Action\QrCodeAction::class, 'name' => Action\QrCodeAction::class,
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]', 'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',
'middleware' => [ 'middleware' => [
Middleware\QrCodeCacheMiddleware::class,
Action\QrCodeAction::class, Action\QrCodeAction::class,
], ],
'allowed_methods' => [RequestMethod::METHOD_GET], 'allowed_methods' => [RequestMethod::METHOD_GET],

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
use Fig\Http\Message\RequestMethodInterface;
use Laminas\Diactoros\Uri; use Laminas\Diactoros\Uri;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@@ -24,7 +26,7 @@ use function array_merge;
use function GuzzleHttp\Psr7\build_query; use function GuzzleHttp\Psr7\build_query;
use function GuzzleHttp\Psr7\parse_query; use function GuzzleHttp\Psr7\parse_query;
abstract class AbstractTrackingAction implements MiddlewareInterface abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
{ {
private ShortUrlResolverInterface $urlResolver; private ShortUrlResolverInterface $urlResolver;
private VisitsTrackerInterface $visitTracker; private VisitsTrackerInterface $visitTracker;
@@ -50,14 +52,13 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
$disableTrackParam = $this->appOptions->getDisableTrackParam(); $disableTrackParam = $this->appOptions->getDisableTrackParam();
try { try {
$url = $this->urlResolver->resolveEnabledShortUrl($identifier); $shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
// Track visit to this short code if ($this->shouldTrackRequest($request, $query, $disableTrackParam)) {
if ($disableTrackParam === null || ! array_key_exists($disableTrackParam, $query)) { $this->visitTracker->track($shortUrl, Visitor::fromRequest($request));
$this->visitTracker->track($url, Visitor::fromRequest($request));
} }
return $this->createSuccessResp($this->buildUrlToRedirectTo($url, $query, $disableTrackParam)); return $this->createSuccessResp($this->buildUrlToRedirectTo($shortUrl, $query, $disableTrackParam));
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]); $this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]);
return $this->createErrorResp($request, $handler); return $this->createErrorResp($request, $handler);
@@ -76,6 +77,16 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
return (string) $uri->withQuery(build_query($mergedQuery)); return (string) $uri->withQuery(build_query($mergedQuery));
} }
private function shouldTrackRequest(ServerRequestInterface $request, array $query, ?string $disableTrackParam): bool
{
$forwardedMethod = $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE);
if ($forwardedMethod === self::METHOD_HEAD) {
return false;
}
return $disableTrackParam === null || ! array_key_exists($disableTrackParam, $query);
}
abstract protected function createSuccessResp(string $longUrl): ResponseInterface; abstract protected function createSuccessResp(string $longUrl): ResponseInterface;
abstract protected function createErrorResp( abstract protected function createErrorResp(

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
use Endroid\QrCode\QrCode; use Endroid\QrCode\QrCode;
use Mezzio\Router\RouterInterface;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@@ -23,17 +22,17 @@ class QrCodeAction implements MiddlewareInterface
private const MIN_SIZE = 50; private const MIN_SIZE = 50;
private const MAX_SIZE = 1000; private const MAX_SIZE = 1000;
private RouterInterface $router;
private ShortUrlResolverInterface $urlResolver; private ShortUrlResolverInterface $urlResolver;
private array $domainConfig;
private LoggerInterface $logger; private LoggerInterface $logger;
public function __construct( public function __construct(
RouterInterface $router,
ShortUrlResolverInterface $urlResolver, ShortUrlResolverInterface $urlResolver,
array $domainConfig,
?LoggerInterface $logger = null ?LoggerInterface $logger = null
) { ) {
$this->router = $router;
$this->urlResolver = $urlResolver; $this->urlResolver = $urlResolver;
$this->domainConfig = $domainConfig;
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
@@ -42,23 +41,19 @@ class QrCodeAction implements MiddlewareInterface
$identifier = ShortUrlIdentifier::fromRedirectRequest($request); $identifier = ShortUrlIdentifier::fromRedirectRequest($request);
try { try {
$this->urlResolver->resolveEnabledShortUrl($identifier); $shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]); $this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]);
return $handler->handle($request); return $handler->handle($request);
} }
$path = $this->router->generateUri(RedirectAction::class, ['shortCode' => $identifier->shortCode()]); $qrCode = new QrCode($shortUrl->toString($this->domainConfig));
$size = $this->getSizeParam($request); $qrCode->setSize($this->getSizeParam($request));
$qrCode = new QrCode((string) $request->getUri()->withPath($path)->withQuery(''));
$qrCode->setSize($size);
$qrCode->setMargin(0); $qrCode->setMargin(0);
return new QrCodeResponse($qrCode); return new QrCodeResponse($qrCode);
} }
/**
*/
private function getSizeParam(Request $request): int private function getSizeParam(Request $request): int
{ {
$size = (int) $request->getAttribute('size', self::DEFAULT_SIZE); $size = (int) $request->getAttribute('size', self::DEFAULT_SIZE);

View File

@@ -33,6 +33,7 @@ class SimplifiedConfigParser
'task_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'task_worker_num'], 'task_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'task_worker_num'],
'visits_webhooks' => ['url_shortener', 'visits_webhooks'], 'visits_webhooks' => ['url_shortener', 'visits_webhooks'],
'default_short_codes_length' => ['url_shortener', 'default_short_codes_length'], 'default_short_codes_length' => ['url_shortener', 'default_short_codes_length'],
'geolite_license_key' => ['geolite2', 'license_key'],
]; ];
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [ private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
'delete_short_url_threshold' => [ 'delete_short_url_threshold' => [

View File

@@ -9,6 +9,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManager;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
@@ -41,22 +42,35 @@ class LocateShortUrlVisit
public function __invoke(ShortUrlVisited $shortUrlVisited): void public function __invoke(ShortUrlVisited $shortUrlVisited): void
{ {
// FIXME Temporarily handling DB connection reset here to fix https://github.com/shlinkio/shlink/issues/717
// Remove when https://github.com/shlinkio/shlink-event-dispatcher/issues/23 is implemented
if ($this->em instanceof ReopeningEntityManager) {
$this->em->open();
}
$visitId = $shortUrlVisited->visitId(); $visitId = $shortUrlVisited->visitId();
/** @var Visit|null $visit */ try {
$visit = $this->em->find(Visit::class, $visitId); /** @var Visit|null $visit */
if ($visit === null) { $visit = $this->em->find(Visit::class, $visitId);
$this->logger->warning('Tried to locate visit with id "{visitId}", but it does not exist.', [ if ($visit === null) {
'visitId' => $visitId, $this->logger->warning('Tried to locate visit with id "{visitId}", but it does not exist.', [
]); 'visitId' => $visitId,
return; ]);
} return;
}
if ($this->downloadOrUpdateGeoLiteDb($visitId)) { if ($this->downloadOrUpdateGeoLiteDb($visitId)) {
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit); $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
} }
$this->eventDispatcher->dispatch(new VisitLocated($visitId)); $this->eventDispatcher->dispatch(new VisitLocated($visitId));
} finally {
// FIXME Temporarily handling DB connection reset here to fix https://github.com/shlinkio/shlink/issues/717
// Remove when https://github.com/shlinkio/shlink-event-dispatcher/issues/23 is implemented
$this->em->getConnection()->close();
$this->em->clear();
}
} }
private function downloadOrUpdateGeoLiteDb(string $visitId): bool private function downloadOrUpdateGeoLiteDb(string $visitId): bool

View File

@@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Middleware;
use Doctrine\Common\Cache\Cache;
use Laminas\Diactoros\Response as DiactResp;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class QrCodeCacheMiddleware implements MiddlewareInterface
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
*
*/
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$cacheKey = $request->getUri()->getPath();
// If this QR code is already cached, just return it
if ($this->cache->contains($cacheKey)) {
$qrData = $this->cache->fetch($cacheKey);
$response = new DiactResp();
$response->getBody()->write($qrData['body']);
return $response->withHeader('Content-Type', $qrData['content-type']);
}
// If not, call the next middleware and cache it
/** @var Response $resp */
$resp = $handler->handle($request);
$this->cache->save($cacheKey, [
'body' => $resp->getBody()->__toString(),
'content-type' => $resp->getHeaderLine('Content-Type'),
]);
return $resp;
}
}

View File

@@ -30,7 +30,7 @@ class QrCodeActionTest extends TestCase
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->action = new QrCodeAction($router->reveal(), $this->urlResolver->reveal()); $this->action = new QrCodeAction($this->urlResolver->reveal(), ['domain' => 'doma.in']);
} }
/** @test */ /** @test */

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use Fig\Http\Message\RequestMethodInterface;
use Laminas\Diactoros\Response; use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequest;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@@ -89,4 +91,23 @@ class RedirectActionTest extends TestCase
$handle->shouldHaveBeenCalledOnce(); $handle->shouldHaveBeenCalledOnce();
} }
/** @test */
public function trackingIsDisabledWhenRequestIsForwardedFromHead(): void
{
$shortCode = 'abc123';
$shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing');
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn($shortUrl);
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
});
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)
->withAttribute(
ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE,
RequestMethodInterface::METHOD_HEAD,
);
$this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
$track->shouldNotHaveBeenCalled();
}
} }

View File

@@ -60,6 +60,7 @@ class SimplifiedConfigParserTest extends TestCase
'https://third-party.io/foo', 'https://third-party.io/foo',
], ],
'default_short_codes_length' => 8, 'default_short_codes_length' => 8,
'geolite_license_key' => 'kjh23ljkbndskj345',
]; ];
$expected = [ $expected = [
'app_options' => [ 'app_options' => [
@@ -127,6 +128,10 @@ class SimplifiedConfigParserTest extends TestCase
], ],
], ],
], ],
'geolite2' => [
'license_key' => 'kjh23ljkbndskj345',
],
]; ];
$result = ($this->postProcessor)(array_merge($config, $simplified)); $result = ($this->postProcessor)(array_merge($config, $simplified));

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@@ -37,6 +38,10 @@ class LocateShortUrlVisitTest extends TestCase
{ {
$this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class); $this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class);
$conn = $this->prophesize(Connection::class);
$this->em->getConnection()->willReturn($conn->reveal());
$this->em->clear()->will(function (): void {
});
$this->logger = $this->prophesize(LoggerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class);
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class); $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);

View File

@@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Middleware;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Uri;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Middleware\QrCodeCacheMiddleware;
class QrCodeCacheMiddlewareTest extends TestCase
{
private QrCodeCacheMiddleware $middleware;
private Cache $cache;
public function setUp(): void
{
$this->cache = new ArrayCache();
$this->middleware = new QrCodeCacheMiddleware($this->cache);
}
/** @test */
public function noCachedPathFallsBackToNextMiddleware(): void
{
$delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->handle(Argument::any())->willReturn(new Response())->shouldBeCalledOnce();
$this->middleware->process((new ServerRequest())->withUri(new Uri('/foo/bar')), $delegate->reveal());
$this->assertTrue($this->cache->contains('/foo/bar'));
}
/** @test */
public function cachedPathReturnsCacheContent(): void
{
$isCalled = false;
$uri = (new Uri())->withPath('/foo');
$this->cache->save('/foo', ['body' => 'the body', 'content-type' => 'image/png']);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->middleware->process((new ServerRequest())->withUri($uri), $delegate->reveal());
$this->assertFalse($isCalled);
$resp->getBody()->rewind();
$this->assertEquals('the body', $resp->getBody()->getContents());
$this->assertEquals('image/png', $resp->getHeaderLine('Content-Type'));
$delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
}
}