mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 21:27:44 +03:00
Added new config to set custom defaults for QR codes
This commit is contained in:
parent
cbec4a4e81
commit
6a1ee2b894
7 changed files with 174 additions and 31 deletions
21
config/autoload/qr-codes.global.php
Normal file
21
config/autoload/qr-codes.global.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
|
||||
|
||||
return [
|
||||
|
||||
'qr_codes' => [
|
||||
'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),
|
||||
],
|
||||
|
||||
];
|
|
@ -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[^>]*>(.*?)<\/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';
|
||||
|
|
|
@ -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],
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())
|
||||
|
|
60
module/Core/src/Options/QrCodeOptions.php
Normal file
60
module/Core/src/Options/QrCodeOptions.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Laminas\Stdlib\AbstractOptions;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
|
||||
|
||||
class QrCodeOptions extends AbstractOptions
|
||||
{
|
||||
private int $size = DEFAULT_QR_CODE_SIZE;
|
||||
private int $margin = DEFAULT_QR_CODE_MARGIN;
|
||||
private string $format = DEFAULT_QR_CODE_FORMAT;
|
||||
private string $errorCorrection = DEFAULT_QR_CODE_ERROR_CORRECTION;
|
||||
|
||||
public function size(): int
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue