From 68db52679b3b78a0cbcca7d0e8c2038caeb87039 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya <alejandro@alejandrocelaya.com> Date: Wed, 17 Jun 2020 19:01:56 +0200 Subject: [PATCH] Added support to serve redirects with status 301 and Cache-Control --- config/autoload/url-shortener.global.php | 3 ++ config/cli-config.php | 3 +- module/Core/config/dependencies.config.php | 1 + module/Core/functions/functions.php | 3 ++ module/Core/src/Action/RedirectAction.php | 30 +++++++++++++--- .../Core/src/Options/UrlShortenerOptions.php | 34 +++++++++++++++++-- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 5ad66bea..ba1f473a 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; return [ @@ -15,6 +16,8 @@ return [ 'anonymize_remote_addr' => true, 'visits_webhooks' => [], 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, + 'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE, + 'redirect_cache_lifetime' => 30, ], ]; diff --git a/config/cli-config.php b/config/cli-config.php index c0e80687..71c7a75e 100644 --- a/config/cli-config.php +++ b/config/cli-config.php @@ -4,11 +4,10 @@ declare(strict_types=1); use Doctrine\ORM\EntityManager; use Doctrine\ORM\Tools\Console\ConsoleRunner; -use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerInterface; return (function () { - /** @var ContainerInterface|ServiceManager $container */ + /** @var ContainerInterface $container */ $container = include __DIR__ . '/container.php'; $em = $container->get(EntityManager::class); diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index debf021f..46bf1735 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -76,6 +76,7 @@ return [ Service\ShortUrl\ShortUrlResolver::class, Service\VisitsTracker::class, Options\AppOptions::class, + Options\UrlShortenerOptions::class, 'Logger_Shlink', ], Action\PixelAction::class => [ diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 3016b18c..1fac0bd5 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -6,12 +6,15 @@ namespace Shlinkio\Shlink\Core; use Cake\Chronos\Chronos; use DateTimeInterface; +use Fig\Http\Message\StatusCodeInterface; use PUGX\Shortid\Factory as ShortIdFactory; use function sprintf; const DEFAULT_SHORT_CODES_LENGTH = 5; const MIN_SHORT_CODES_LENGTH = 4; +const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND; +const DEFAULT_REDIRECT_CACHE_LIFETIME = 30; const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory'; function generateRandomShortCode(int $length): string diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index e6e1ec4c..72a56096 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -4,18 +4,40 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; +use Fig\Http\Message\StatusCodeInterface; use Laminas\Diactoros\Response\RedirectResponse; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; +use Shlinkio\Shlink\Core\Options; +use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; +use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; +use function sprintf; -class RedirectAction extends AbstractTrackingAction +class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface { + private Options\UrlShortenerOptions $urlShortenerOptions; + + public function __construct( + ShortUrlResolverInterface $urlResolver, + VisitsTrackerInterface $visitTracker, + Options\AppOptions $appOptions, + Options\UrlShortenerOptions $urlShortenerOptions, + ?LoggerInterface $logger = null + ) { + parent::__construct($urlResolver, $visitTracker, $appOptions, $logger); + $this->urlShortenerOptions = $urlShortenerOptions; + } + protected function createSuccessResp(string $longUrl): Response { - // Return a redirect response to the long URL. - // Use a temporary redirect to make sure browsers always hit the server for analytics purposes - return new RedirectResponse($longUrl); + $statusCode = $this->urlShortenerOptions->redirectStatusCode(); + $headers = $statusCode === self::STATUS_FOUND ? [] : [ + 'Cache-Control' => sprintf('private,max-age=%s', $this->urlShortenerOptions->redirectCacheLifetime()), + ]; + + return new RedirectResponse($longUrl, $statusCode, $headers); } protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response diff --git a/module/Core/src/Options/UrlShortenerOptions.php b/module/Core/src/Options/UrlShortenerOptions.php index 19267ea1..69a06f1f 100644 --- a/module/Core/src/Options/UrlShortenerOptions.php +++ b/module/Core/src/Options/UrlShortenerOptions.php @@ -6,20 +6,50 @@ namespace Shlinkio\Shlink\Core\Options; use Laminas\Stdlib\AbstractOptions; +use function Functional\contains; + +use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; + class UrlShortenerOptions extends AbstractOptions { protected $__strictMode__ = false; // phpcs:ignore private bool $validateUrl = true; + private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE; + private int $redirectCacheLifetime = 30; public function isUrlValidationEnabled(): bool { return $this->validateUrl; } - protected function setValidateUrl(bool $validateUrl): self + protected function setValidateUrl(bool $validateUrl): void { $this->validateUrl = $validateUrl; - return $this; + } + + public function redirectStatusCode(): int + { + return $this->redirectStatusCode; + } + + protected function setRedirectStatusCode(int $redirectStatusCode): void + { + $this->redirectStatusCode = $this->normalizeRedirectStatusCode($redirectStatusCode); + } + + private function normalizeRedirectStatusCode(int $statusCode): int + { + return contains([301, 302], $statusCode) ? $statusCode : 302; + } + + public function redirectCacheLifetime(): int + { + return $this->redirectCacheLifetime; + } + + protected function setRedirectCacheLifetime(int $redirectCacheLifetime): void + { + $this->redirectCacheLifetime = $redirectCacheLifetime; } }