From 6a1ee2b894a657eb49b50d3d14afe9c145d55e35 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 26 Sep 2021 13:25:02 +0200 Subject: [PATCH] Added new config to set custom defaults for QR codes --- config/autoload/qr-codes.global.php | 21 ++++++ config/constants.php | 4 ++ module/Core/config/dependencies.config.php | 3 + module/Core/src/Action/Model/QrCodeParams.php | 41 ++++++----- module/Core/src/Action/QrCodeAction.php | 4 +- module/Core/src/Options/QrCodeOptions.php | 60 ++++++++++++++++ module/Core/test/Action/QrCodeActionTest.php | 72 +++++++++++++++---- 7 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 config/autoload/qr-codes.global.php create mode 100644 module/Core/src/Options/QrCodeOptions.php diff --git a/config/autoload/qr-codes.global.php b/config/autoload/qr-codes.global.php new file mode 100644 index 00000000..1cf6fecb --- /dev/null +++ b/config/autoload/qr-codes.global.php @@ -0,0 +1,21 @@ + [ + 'size' => (int) env('DEFAULT_QR_CODE_SIZE', DEFAULT_QR_CODE_SIZE), + 'margin' => (int) env('DEFAULT_QR_CODE_MARGIN', DEFAULT_QR_CODE_MARGIN), + 'format' => env('DEFAULT_QR_CODE_FORMAT', DEFAULT_QR_CODE_FORMAT), + 'error_correction' => env('DEFAULT_QR_CODE_ERROR_CORRECTION', DEFAULT_QR_CODE_ERROR_CORRECTION), + ], + +]; diff --git a/config/constants.php b/config/constants.php index 5a270e3c..43de270a 100644 --- a/config/constants.php +++ b/config/constants.php @@ -14,3 +14,7 @@ const DEFAULT_REDIRECT_CACHE_LIFETIME = 30; const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory'; const CUSTOM_SLUGS_REGEXP = '/[^\pL\pN._~]/u'; // Any unicode letter or number, plus ".", "_" and "~" chars const TITLE_TAG_VALUE = '/]*>(.*?)<\/title>/i'; // Matches the value inside an html title tag +const DEFAULT_QR_CODE_SIZE = 300; +const DEFAULT_QR_CODE_MARGIN = 0; +const DEFAULT_QR_CODE_FORMAT = 'png'; +const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l'; diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 7f28b14d..66d854c3 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -25,6 +25,7 @@ return [ Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class, Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, Options\TrackingOptions::class => ConfigAbstractFactory::class, + Options\QrCodeOptions::class => ConfigAbstractFactory::class, Service\UrlShortener::class => ConfigAbstractFactory::class, Service\ShortUrlService::class => ConfigAbstractFactory::class, @@ -86,6 +87,7 @@ return [ Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], Options\TrackingOptions::class => ['config.tracking'], + Options\QrCodeOptions::class => ['config.qr_codes'], Service\UrlShortener::class => [ ShortUrl\Helper\ShortUrlTitleResolutionHelper::class, @@ -138,6 +140,7 @@ return [ Service\ShortUrl\ShortUrlResolver::class, ShortUrl\Helper\ShortUrlStringifier::class, 'Logger_Shlink', + Options\QrCodeOptions::class, ], Action\RobotsAction::class => [Crawling\CrawlingHelper::class], diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 39cd59a9..0e889c32 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -14,15 +14,17 @@ use Endroid\QrCode\Writer\SvgWriter; use Endroid\QrCode\Writer\WriterInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface as Request; +use Shlinkio\Shlink\Core\Options\QrCodeOptions; +use function Functional\contains; use function strtolower; use function trim; final class QrCodeParams { - private const DEFAULT_SIZE = 300; private const MIN_SIZE = 50; private const MAX_SIZE = 1000; + private const SUPPORTED_FORMATS = ['png', 'svg']; private function __construct( private int $size, @@ -32,22 +34,22 @@ final class QrCodeParams ) { } - public static function fromRequest(ServerRequestInterface $request): self + public static function fromRequest(ServerRequestInterface $request, QrCodeOptions $defaults): self { $query = $request->getQueryParams(); return new self( - self::resolveSize($request, $query), - self::resolveMargin($query), - self::resolveWriter($query), - self::resolveErrorCorrection($query), + self::resolveSize($request, $query, $defaults), + self::resolveMargin($query, $defaults), + self::resolveWriter($query, $defaults), + self::resolveErrorCorrection($query, $defaults), ); } - private static function resolveSize(Request $request, array $query): int + private static function resolveSize(Request $request, array $query, QrCodeOptions $defaults): int { // FIXME Size attribute is deprecated. After v3.0.0, always use the query param instead - $size = (int) $request->getAttribute('size', $query['size'] ?? self::DEFAULT_SIZE); + $size = (int) $request->getAttribute('size', $query['size'] ?? $defaults->size()); if ($size < self::MIN_SIZE) { return self::MIN_SIZE; } @@ -55,13 +57,9 @@ final class QrCodeParams return $size > self::MAX_SIZE ? self::MAX_SIZE : $size; } - private static function resolveMargin(array $query): int + private static function resolveMargin(array $query, QrCodeOptions $defaults): int { - $margin = $query['margin'] ?? null; - if ($margin === null) { - return 0; - } - + $margin = $query['margin'] ?? (string) $defaults->margin(); $intMargin = (int) $margin; if ($margin !== (string) $intMargin) { return 0; @@ -70,18 +68,20 @@ final class QrCodeParams return $intMargin < 0 ? 0 : $intMargin; } - private static function resolveWriter(array $query): WriterInterface + private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface { - $format = strtolower(trim($query['format'] ?? 'png')); + $qFormat = self::normalizeParam($query['format'] ?? ''); + $format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format()); + return match ($format) { 'svg' => new SvgWriter(), default => new PngWriter(), }; } - private static function resolveErrorCorrection(array $query): ErrorCorrectionLevelInterface + private static function resolveErrorCorrection(array $query, QrCodeOptions $defaults): ErrorCorrectionLevelInterface { - $errorCorrectionLevel = strtolower(trim($query['errorCorrection'] ?? 'l')); + $errorCorrectionLevel = self::normalizeParam($query['errorCorrection'] ?? $defaults->errorCorrection()); return match ($errorCorrectionLevel) { 'h' => new ErrorCorrectionLevelHigh(), 'q' => new ErrorCorrectionLevelQuartile(), @@ -90,6 +90,11 @@ final class QrCodeParams }; } + private static function normalizeParam(string $param): string + { + return strtolower(trim($param)); + } + public function size(): int { return $this->size; diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index 977e0864..f8d2e275 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -14,6 +14,7 @@ use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Core\Action\Model\QrCodeParams; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use Shlinkio\Shlink\Core\Options\QrCodeOptions; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; @@ -23,6 +24,7 @@ class QrCodeAction implements MiddlewareInterface private ShortUrlResolverInterface $urlResolver, private ShortUrlStringifierInterface $stringifier, private LoggerInterface $logger, + private QrCodeOptions $defaultOptions, ) { } @@ -37,7 +39,7 @@ class QrCodeAction implements MiddlewareInterface return $handler->handle($request); } - $params = QrCodeParams::fromRequest($request); + $params = QrCodeParams::fromRequest($request, $this->defaultOptions); $qrCodeBuilder = Builder::create() ->data($this->stringifier->stringify($shortUrl)) ->size($params->size()) diff --git a/module/Core/src/Options/QrCodeOptions.php b/module/Core/src/Options/QrCodeOptions.php new file mode 100644 index 00000000..80d6e456 --- /dev/null +++ b/module/Core/src/Options/QrCodeOptions.php @@ -0,0 +1,60 @@ +size; + } + + protected function setSize(int $size): void + { + $this->size = $size; + } + + public function margin(): int + { + return $this->margin; + } + + protected function setMargin(int $margin): void + { + $this->margin = $margin; + } + + public function format(): string + { + return $this->format; + } + + protected function setFormat(string $format): void + { + $this->format = $format; + } + + public function errorCorrection(): string + { + return $this->errorCorrection; + } + + protected function setErrorCorrection(string $errorCorrection): void + { + $this->errorCorrection = $errorCorrection; + } +} diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 0595734e..1fdc35ef 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -20,6 +20,7 @@ use Shlinkio\Shlink\Core\Action\QrCodeAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use Shlinkio\Shlink\Core\Options\QrCodeOptions; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; @@ -31,6 +32,7 @@ class QrCodeActionTest extends TestCase private QrCodeAction $action; private ObjectProphecy $urlResolver; + private QrCodeOptions $options; public function setUp(): void { @@ -38,11 +40,13 @@ class QrCodeActionTest extends TestCase $router->generateUri(Argument::cetera())->willReturn('/foo/bar'); $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); + $this->options = new QrCodeOptions(); $this->action = new QrCodeAction( $this->urlResolver->reveal(), new ShortUrlStringifier(['domain' => 'doma.in']), new NullLogger(), + $this->options, ); } @@ -85,9 +89,11 @@ class QrCodeActionTest extends TestCase * @dataProvider provideQueries */ public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat( + string $defaultFormat, array $query, string $expectedContentType, ): void { + $this->options->setFromArray(['format' => $defaultFormat]); $code = 'abc123'; $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn( ShortUrl::createEmpty(), @@ -102,18 +108,26 @@ class QrCodeActionTest extends TestCase public function provideQueries(): iterable { - yield 'no format' => [[], 'image/png']; - yield 'png format' => [['format' => 'png'], 'image/png']; - yield 'svg format' => [['format' => 'svg'], 'image/svg+xml']; - yield 'unsupported format' => [['format' => 'jpg'], 'image/png']; + yield 'no format, png default' => ['png', [], 'image/png']; + yield 'no format, svg default' => ['svg', [], 'image/svg+xml']; + yield 'png format, png default' => ['png', ['format' => 'png'], 'image/png']; + yield 'png format, svg default' => ['svg', ['format' => 'png'], 'image/png']; + yield 'svg format, png default' => ['png', ['format' => 'svg'], 'image/svg+xml']; + yield 'svg format, svg default' => ['svg', ['format' => 'svg'], 'image/svg+xml']; + yield 'unsupported format, png default' => ['png', ['format' => 'jpg'], 'image/png']; + yield 'unsupported format, svg default' => ['svg', ['format' => 'jpg'], 'image/svg+xml']; } /** * @test * @dataProvider provideRequestsWithSize */ - public function imageIsReturnedWithExpectedSize(ServerRequestInterface $req, int $expectedSize): void - { + public function imageIsReturnedWithExpectedSize( + array $defaults, + ServerRequestInterface $req, + int $expectedSize, + ): void { + $this->options->setFromArray($defaults); $code = 'abc123'; $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn( ShortUrl::createEmpty(), @@ -128,25 +142,59 @@ class QrCodeActionTest extends TestCase public function provideRequestsWithSize(): iterable { - yield 'no size' => [ServerRequestFactory::fromGlobals(), 300]; - yield 'size in attr' => [ServerRequestFactory::fromGlobals()->withAttribute('size', '400'), 400]; - yield 'size in query' => [ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 123]; + yield 'different margin and size defaults' => [ + ['size' => 660, 'margin' => 40], + ServerRequestFactory::fromGlobals(), + 740, + ]; + yield 'no size' => [[], ServerRequestFactory::fromGlobals(), 300]; + yield 'no size, different default' => [['size' => 500], ServerRequestFactory::fromGlobals(), 500]; + yield 'size in attr' => [[], ServerRequestFactory::fromGlobals()->withAttribute('size', '400'), 400]; + yield 'size in query' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 123]; + yield 'size in query, default margin' => [ + ['margin' => 25], + ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), + 173, + ]; yield 'size in query and attr' => [ + [], ServerRequestFactory::fromGlobals()->withAttribute('size', '350')->withQueryParams(['size' => '123']), 350, ]; - yield 'margin' => [ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 370]; + yield 'margin' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 370]; + yield 'margin and different default' => [ + ['size' => 400], + ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), + 470, + ]; yield 'margin and size' => [ + [], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '100', 'size' => '200']), 400, ]; - yield 'negative margin' => [ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), 300]; - yield 'non-numeric margin' => [ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo']), 300]; + yield 'negative margin' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), 300]; + yield 'negative margin, default margin' => [ + ['margin' => 10], + ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), + 300, + ]; + yield 'non-numeric margin' => [ + [], + ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo']), + 300, + ]; yield 'negative margin and size' => [ + [], + ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), + 150, + ]; + yield 'negative margin and size, default margin' => [ + ['margin' => 5], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), 150, ]; yield 'non-numeric margin and size' => [ + [], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo', 'size' => '538']), 538, ];