From b4043be7fadf8327bcdcef9a490e3e4b782de623 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 7 Nov 2025 16:58:19 +0100 Subject: [PATCH] Drop support for QR code generation --- CHANGELOG.md | 17 + Dockerfile | 5 +- README.md | 3 +- composer.json | 1 - config/autoload/installer.global.php | 9 - config/autoload/routes.config.php | 8 - config/constants.php | 17 - docs/swagger/paths/{shortCode}_qr-code.json | 121 ------- docs/swagger/swagger.json | 3 - module/Core/config/dependencies.config.php | 8 - module/Core/src/Action/Model/QrCodeParams.php | 161 ---------- module/Core/src/Action/QrCodeAction.php | 74 ----- module/Core/src/Config/EnvVars.php | 35 -- .../Core/src/Config/Options/QrCodeOptions.php | 48 --- module/Core/test-api/Action/QrCodeTest.php | 28 -- module/Core/test/Action/QrCodeActionTest.php | 303 ------------------ 16 files changed, 20 insertions(+), 821 deletions(-) delete mode 100644 docs/swagger/paths/{shortCode}_qr-code.json delete mode 100644 module/Core/src/Action/Model/QrCodeParams.php delete mode 100644 module/Core/src/Action/QrCodeAction.php delete mode 100644 module/Core/src/Config/Options/QrCodeOptions.php delete mode 100644 module/Core/test-api/Action/QrCodeTest.php delete mode 100644 module/Core/test/Action/QrCodeActionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ae2865..8719b66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ 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). +## [Unreleased] +### Added +* *Nothing* + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* [#2514](https://github.com/shlinkio/shlink/issues/2514) Remove support to generate QR codes. This functionality is now handled by Shlink Web Client and Shlink Dashboard. + +### Fixed +* *Nothing* + + ## [4.6.0] - 2025-11-01 ### Added * [#2327](https://github.com/shlinkio/shlink/issues/2327) Allow filtering short URL lists by those not including certain tags. diff --git a/Dockerfile b/Dockerfile index 1b69a441..d741d15c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,9 +16,8 @@ WORKDIR /etc/shlink # Install required PHP extensions RUN \ # Temp install dev dependencies needed to compile the extensions - # FIXME Deprecated image-related extensions. They can be removed with QR-code support - apk add --no-cache --virtual .dev-deps sqlite-dev postgresql-dev icu-dev libzip-dev zlib-dev libpng-dev linux-headers && \ - docker-php-ext-install -j"$(nproc)" pdo_mysql pdo_pgsql intl calendar sockets bcmath zip gd && \ + apk add --no-cache --virtual .dev-deps sqlite-dev postgresql-dev icu-dev libzip-dev zlib-dev linux-headers && \ + docker-php-ext-install -j"$(nproc)" pdo_mysql pdo_pgsql intl calendar sockets bcmath zip && \ apk add --no-cache sqlite-libs && \ docker-php-ext-install -j"$(nproc)" pdo_sqlite && \ # Remove temp dev extensions, and install prod equivalents that are required at runtime diff --git a/README.md b/README.md index dc23d7f6..b922144e 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,9 @@ The idea is that you can just generate a container using the image and provide t First, make sure the host where you are going to run shlink fulfills these requirements: -* PHP 8.3 or 8.4 +* PHP 8.4 or 8.5 * The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath. * apcu extension is recommended if you don't plan to use RoadRunner. - * xml extension is required if you want to generate QR codes in svg format. * sockets and bcmath extensions are required if you want to integrate with a RabbitMQ instance. * MySQL, MariaDB, PostgreSQL, MicrosoftSQL or SQLite. * You will also need the corresponding pdo variation for the database you are planning to use: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv` or `pdo_sqlite`. diff --git a/composer.json b/composer.json index dc851009..a4841d99 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "doctrine/migrations": "^3.9", "doctrine/orm": "^3.5", "donatj/phpuseragentparser": "^1.10", - "endroid/qr-code": "^6.0.5", "friendsofphp/proxy-manager-lts": "^1.0", "geoip2/geoip2": "^3.1", "guzzlehttp/guzzle": "^7.9", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index a3918aff..c3c63828 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -60,15 +60,6 @@ return [ Option\Tracking\DisableIpTrackingConfigOption::class, Option\Tracking\DisableReferrerTrackingConfigOption::class, Option\Tracking\DisableUaTrackingConfigOption::class, - Option\QrCode\DefaultSizeConfigOption::class, - Option\QrCode\DefaultMarginConfigOption::class, - Option\QrCode\DefaultFormatConfigOption::class, - Option\QrCode\DefaultErrorCorrectionConfigOption::class, - Option\QrCode\DefaultRoundBlockSizeConfigOption::class, - Option\QrCode\DefaultColorConfigOption::class, - Option\QrCode\DefaultBgColorConfigOption::class, - Option\QrCode\DefaultLogoUrlConfigOption::class, - Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class, Option\RabbitMq\RabbitMqEnabledConfigOption::class, Option\RabbitMq\RabbitMqHostConfigOption::class, Option\RabbitMq\RabbitMqUseSslConfigOption::class, diff --git a/config/autoload/routes.config.php b/config/autoload/routes.config.php index 1f5425b5..45527697 100644 --- a/config/autoload/routes.config.php +++ b/config/autoload/routes.config.php @@ -94,14 +94,6 @@ return (static function (): array { ], 'allowed_methods' => [RequestMethodInterface::METHOD_GET], ], - [ - 'name' => CoreAction\QrCodeAction::class, - 'path' => '/{shortCode}/qr-code', - 'middleware' => [ - CoreAction\QrCodeAction::class, - ], - 'allowed_methods' => [RequestMethodInterface::METHOD_GET], - ], [ 'name' => CoreAction\RedirectAction::class, 'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix), diff --git a/config/constants.php b/config/constants.php index 664c8c9f..6ed765e3 100644 --- a/config/constants.php +++ b/config/constants.php @@ -38,20 +38,3 @@ const ISO_COUNTRY_CODES = [ 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW', ]; - -/** @deprecated */ -const DEFAULT_QR_CODE_SIZE = 300; -/** @deprecated */ -const DEFAULT_QR_CODE_MARGIN = 0; -/** @deprecated */ -const DEFAULT_QR_CODE_FORMAT = 'png'; -/** @deprecated */ -const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l'; -/** @deprecated */ -const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true; -/** @deprecated */ -const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true; -/** @deprecated */ -const DEFAULT_QR_CODE_COLOR = '#000000'; // Black -/** @deprecated */ -const DEFAULT_QR_CODE_BG_COLOR = '#ffffff'; // White diff --git a/docs/swagger/paths/{shortCode}_qr-code.json b/docs/swagger/paths/{shortCode}_qr-code.json deleted file mode 100644 index dc0f2d81..00000000 --- a/docs/swagger/paths/{shortCode}_qr-code.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "get": { - "deprecated": true, - "operationId": "shortUrlQrCode", - "tags": [ - "URL Shortener" - ], - "summary": "[Deprecated] Short URL QR code", - "description": "**[Deprecated]** Use an external mechanism to generate QR codes. Shlink dashboard and shlink-web-client provide their own.", - "parameters": [ - { - "$ref": "../parameters/shortCode.json" - }, - { - "name": "size", - "in": "query", - "description": "The size of the image to be returned.", - "required": false, - "schema": { - "type": "integer", - "minimum": 50, - "maximum": 1000, - "default": 300 - } - }, - { - "name": "format", - "in": "query", - "description": "The format for the QR code image, being valid values png and svg. Not providing the param or providing any other value will fall back to png.", - "required": false, - "schema": { - "type": "string", - "enum": ["png", "svg"], - "default": "png" - } - }, - { - "name": "margin", - "in": "query", - "description": "The margin around the QR code image.", - "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0 - } - }, - { - "name": "errorCorrection", - "in": "query", - "description": "The error correction level to apply to the QR code: **[L]**ow, **[M]**edium, **[Q]**uartile or **[H]**igh. See [docs](https://www.qrcode.com/en/about/error_correction.html).", - "required": false, - "schema": { - "type": "string", - "enum": ["L", "M", "Q", "H"], - "default": "L" - } - }, - { - "name": "roundBlockSize", - "in": "query", - "description": "Allows to disable block size rounding, which might reduce the readability of the QR code, but ensures no extra margin is added.", - "required": false, - "schema": { - "type": "string", - "enum": ["true", "false"], - "default": "false" - } - }, - { - "name": "color", - "in": "query", - "description": "The QR code foreground color. It should be an hex representation of a color, in 3 or 6 characters, optionally preceded by the \"#\" character.", - "required": false, - "schema": { - "type": "string", - "default": "#000000" - } - }, - { - "name": "bgColor", - "in": "query", - "description": "The QR code background color. It should be an hex representation of a color, in 3 or 6 characters, optionally preceded by the \"#\" character.", - "required": false, - "schema": { - "type": "string", - "default": "#ffffff" - } - }, - { - "name": "logo", - "in": "query", - "description": "Currently used to disable the logo that was set via configuration options. It may be used in future to dynamically choose from multiple logos.", - "required": false, - "schema": { - "type": "string", - "enum": ["disable"] - } - } - ], - "responses": { - "200": { - "description": "QR code in PNG format", - "content": { - "image/png": { - "schema": { - "type": "string", - "format": "binary" - } - }, - "image/svg+xml": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - } - } - } -} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 1b34b470..bced7510 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -133,9 +133,6 @@ }, "/{shortCode}/track": { "$ref": "paths/{shortCode}_track.json" - }, - "/{shortCode}/qr-code": { - "$ref": "paths/{shortCode}_qr-code.json" } } } diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 5bb534c2..6ccff061 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -33,7 +33,6 @@ return [ Config\Options\RedirectOptions::class => [Config\Options\RedirectOptions::class, 'fromEnv'], Config\Options\UrlShortenerOptions::class => [Config\Options\UrlShortenerOptions::class, 'fromEnv'], Config\Options\TrackingOptions::class => [Config\Options\TrackingOptions::class, 'fromEnv'], - Config\Options\QrCodeOptions::class => [Config\Options\QrCodeOptions::class, 'fromEnv'], Config\Options\RabbitMqOptions::class => [Config\Options\RabbitMqOptions::class, 'fromEnv'], Config\Options\RobotsOptions::class => [Config\Options\RobotsOptions::class, 'fromEnv'], Config\Options\RealTimeUpdatesOptions::class => [Config\Options\RealTimeUpdatesOptions::class, 'fromEnv'], @@ -103,7 +102,6 @@ return [ Action\RedirectAction::class => ConfigAbstractFactory::class, Action\PixelAction::class => ConfigAbstractFactory::class, - Action\QrCodeAction::class => ConfigAbstractFactory::class, Action\RobotsAction::class => ConfigAbstractFactory::class, EventDispatcher\PublishingUpdatesGenerator::class => ConfigAbstractFactory::class, @@ -209,12 +207,6 @@ return [ Util\RedirectResponseHelper::class, ], Action\PixelAction::class => [ShortUrl\ShortUrlResolver::class, Visit\RequestTracker::class], - Action\QrCodeAction::class => [ - ShortUrl\ShortUrlResolver::class, - ShortUrl\Helper\ShortUrlStringifier::class, - 'Logger_Shlink', - Config\Options\QrCodeOptions::class, - ], Action\RobotsAction::class => [Crawling\CrawlingHelper::class, Config\Options\RobotsOptions::class], ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => [ diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php deleted file mode 100644 index 9bdaf87e..00000000 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ /dev/null @@ -1,161 +0,0 @@ -getQueryParams(); - [$writer, $writerOptions] = self::resolveWriterAndWriterOptions($query, $defaults); - - return new self( - size: self::resolveSize($query, $defaults), - margin: self::resolveMargin($query, $defaults), - writer: $writer, - writerOptions: $writerOptions, - errorCorrectionLevel: self::resolveErrorCorrection($query, $defaults), - roundBlockSizeMode: self::resolveRoundBlockSize($query, $defaults), - color: self::resolveColor($query, $defaults), - bgColor: self::resolveBackgroundColor($query, $defaults), - disableLogo: isset($query['logo']) && $query['logo'] === 'disable', - ); - } - - private static function resolveSize(array $query, QrCodeOptions $defaults): int - { - $size = (int) ($query['size'] ?? $defaults->size); - if ($size < self::MIN_SIZE) { - return self::MIN_SIZE; - } - - return min($size, self::MAX_SIZE); - } - - private static function resolveMargin(array $query, QrCodeOptions $defaults): int - { - $margin = $query['margin'] ?? (string) $defaults->margin; - $intMargin = (int) $margin; - if ($margin !== (string) $intMargin) { - return 0; - } - - return max($intMargin, 0); - } - - /** - * @return array{WriterInterface, array} - */ - private static function resolveWriterAndWriterOptions(array $query, QrCodeOptions $defaults): array - { - $qFormat = self::normalizeParam($query['format'] ?? ''); - $format = contains($qFormat, self::SUPPORTED_FORMATS) ? $qFormat : self::normalizeParam($defaults->format); - - return match ($format) { - 'svg' => [new SvgWriter(), []], - default => [new PngWriter(), [PngWriter::WRITER_OPTION_NUMBER_OF_COLORS => null]], - }; - } - - private static function resolveErrorCorrection(array $query, QrCodeOptions $defaults): ErrorCorrectionLevel - { - $errorCorrectionLevel = self::normalizeParam($query['errorCorrection'] ?? $defaults->errorCorrection); - return match ($errorCorrectionLevel) { - 'h' => ErrorCorrectionLevel::High, - 'q' => ErrorCorrectionLevel::Quartile, - 'm' => ErrorCorrectionLevel::Medium, - default => ErrorCorrectionLevel::Low, // 'l' - }; - } - - private static function resolveRoundBlockSize(array $query, QrCodeOptions $defaults): RoundBlockSizeMode - { - $doNotRoundBlockSize = isset($query['roundBlockSize']) - ? $query['roundBlockSize'] === 'false' - : ! $defaults->roundBlockSize; - return $doNotRoundBlockSize ? RoundBlockSizeMode::None : RoundBlockSizeMode::Margin; - } - - private static function resolveColor(array $query, QrCodeOptions $defaults): ColorInterface - { - $color = self::normalizeParam($query['color'] ?? $defaults->color); - return self::parseHexColor($color, DEFAULT_QR_CODE_COLOR); - } - - private static function resolveBackgroundColor(array $query, QrCodeOptions $defaults): ColorInterface - { - $bgColor = self::normalizeParam($query['bgColor'] ?? $defaults->bgColor); - return self::parseHexColor($bgColor, DEFAULT_QR_CODE_BG_COLOR); - } - - private static function parseHexColor(string $hexColor, string|null $fallback): Color - { - $hexColor = ltrim($hexColor, '#'); - if (! ctype_xdigit($hexColor) && $fallback !== null) { - return self::parseHexColor($fallback, null); - } - - if (strlen($hexColor) === 3) { - return new Color( - (int) hexdec(substr($hexColor, 0, 1) . substr($hexColor, 0, 1)), - (int) hexdec(substr($hexColor, 1, 1) . substr($hexColor, 1, 1)), - (int) hexdec(substr($hexColor, 2, 1) . substr($hexColor, 2, 1)), - ); - } - - return new Color( - (int) hexdec(substr($hexColor, 0, 2)), - (int) hexdec(substr($hexColor, 2, 2)), - (int) hexdec(substr($hexColor, 4, 2)), - ); - } - - private static function normalizeParam(string $param): string - { - return strtolower(trim($param)); - } -} diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php deleted file mode 100644 index 0ab126bc..00000000 --- a/module/Core/src/Action/QrCodeAction.php +++ /dev/null @@ -1,74 +0,0 @@ -options->enabledForDisabledShortUrls - ? $this->urlResolver->resolvePublicShortUrl($identifier) - : $this->urlResolver->resolveEnabledShortUrl($identifier); - } catch (ShortUrlNotFoundException $e) { - $this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]); - return $handler->handle($request); - } - - $params = QrCodeParams::fromRequest($request, $this->options); - $qrCodeBuilder = new Builder( - writer: $params->writer, - writerOptions: $params->writerOptions, - data: $this->stringifier->stringify($shortUrl), - errorCorrectionLevel: $params->errorCorrectionLevel, - size: $params->size, - margin: $params->margin, - roundBlockSizeMode: $params->roundBlockSizeMode, - foregroundColor: $params->color, - backgroundColor: $params->bgColor, - ); - - return new QrCodeResponse($this->buildQrCode($qrCodeBuilder, $params)); - } - - private function buildQrCode(Builder $qrCodeBuilder, QrCodeParams $params): ResultInterface - { - $logoUrl = $this->options->logoUrl; - if ($logoUrl === null || $params->disableLogo) { - return $qrCodeBuilder->build(); - } - - return $qrCodeBuilder->build( - logoPath: $logoUrl, - logoResizeToHeight: (int) ($params->size / 4), - ); - } -} diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 8613b53f..17b6dd60 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -13,14 +13,6 @@ use function Shlinkio\Shlink\Config\env; use function Shlinkio\Shlink\Config\parseEnvVar; use function sprintf; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS; -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_ROUND_BLOCK_SIZE; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE; use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME; use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; @@ -97,24 +89,6 @@ enum EnvVars: string /** @deprecated Use REDIRECT_EXTRA_PATH */ case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH'; - /** @deprecated */ - case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE'; - /** @deprecated */ - case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN'; - /** @deprecated */ - case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT'; - /** @deprecated */ - case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION'; - /** @deprecated */ - case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE'; - /** @deprecated */ - case QR_CODE_FOR_DISABLED_SHORT_URLS = 'QR_CODE_FOR_DISABLED_SHORT_URLS'; - /** @deprecated */ - case DEFAULT_QR_CODE_COLOR = 'DEFAULT_QR_CODE_COLOR'; - /** @deprecated */ - case DEFAULT_QR_CODE_BG_COLOR = 'DEFAULT_QR_CODE_BG_COLOR'; - /** @deprecated */ - case DEFAULT_QR_CODE_LOGO_URL = 'DEFAULT_QR_CODE_LOGO_URL'; public function loadFromEnv(): mixed { @@ -173,15 +147,6 @@ enum EnvVars: string self::MERCURE_ENABLED => self::MERCURE_PUBLIC_HUB_URL->existsInEnv(), self::MERCURE_INTERNAL_HUB_URL => self::MERCURE_PUBLIC_HUB_URL->loadFromEnv(), - self::DEFAULT_QR_CODE_SIZE, => DEFAULT_QR_CODE_SIZE, - self::DEFAULT_QR_CODE_MARGIN, => DEFAULT_QR_CODE_MARGIN, - self::DEFAULT_QR_CODE_FORMAT, => DEFAULT_QR_CODE_FORMAT, - self::DEFAULT_QR_CODE_ERROR_CORRECTION, => DEFAULT_QR_CODE_ERROR_CORRECTION, - self::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, => DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, - self::QR_CODE_FOR_DISABLED_SHORT_URLS, => DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS, - self::DEFAULT_QR_CODE_COLOR, => DEFAULT_QR_CODE_COLOR, - self::DEFAULT_QR_CODE_BG_COLOR, => DEFAULT_QR_CODE_BG_COLOR, - self::RABBITMQ_ENABLED, self::RABBITMQ_USE_SSL => false, self::RABBITMQ_PORT => 5672, self::RABBITMQ_VHOST => '/', diff --git a/module/Core/src/Config/Options/QrCodeOptions.php b/module/Core/src/Config/Options/QrCodeOptions.php deleted file mode 100644 index e36a4285..00000000 --- a/module/Core/src/Config/Options/QrCodeOptions.php +++ /dev/null @@ -1,48 +0,0 @@ -loadFromEnv(), - margin: (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(), - format: EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(), - errorCorrection: EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(), - roundBlockSize: (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(), - enabledForDisabledShortUrls: (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(), - color: EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(), - bgColor: EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(), - logoUrl: EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(), - ); - } -} diff --git a/module/Core/test-api/Action/QrCodeTest.php b/module/Core/test-api/Action/QrCodeTest.php deleted file mode 100644 index c8285198..00000000 --- a/module/Core/test-api/Action/QrCodeTest.php +++ /dev/null @@ -1,28 +0,0 @@ -callShortUrl('custom/qr-code'); - self::assertEquals(200, $response->getStatusCode()); - - // This short URL allow max 2 visits - $this->callShortUrl('custom'); - $this->callShortUrl('custom'); - - // After 2 visits, the short URL returns a 404, but the QR code should still work - self::assertEquals(404, $this->callShortUrl('custom')->getStatusCode()); - self::assertEquals(200, $this->callShortUrl('custom/qr-code')->getStatusCode()); - } -} diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php deleted file mode 100644 index 5ea4cc47..00000000 --- a/module/Core/test/Action/QrCodeActionTest.php +++ /dev/null @@ -1,303 +0,0 @@ -urlResolver = $this->createMock(ShortUrlResolverInterface::class); - } - - #[Test] - public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void - { - $shortCode = 'abc123'; - $this->urlResolver->expects($this->once())->method('resolveEnabledShortUrl')->with( - ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''), - )->willThrowException(ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain(''))); - $handler = $this->createMock(RequestHandlerInterface::class); - $handler->expects($this->once())->method('handle')->withAnyParameters()->willReturn(new Response()); - - $this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $handler); - } - - #[Test] - public function aCorrectRequestReturnsTheQrCodeResponse(): void - { - $shortCode = 'abc123'; - $this->urlResolver->expects($this->once())->method('resolveEnabledShortUrl')->with( - ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''), - )->willReturn(ShortUrl::createFake()); - $handler = $this->createMock(RequestHandlerInterface::class); - $handler->expects($this->never())->method('handle'); - - $resp = $this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $handler); - - self::assertInstanceOf(QrCodeResponse::class, $resp); - self::assertEquals(200, $resp->getStatusCode()); - } - - #[Test, DataProvider('provideQueries')] - public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat( - string $defaultFormat, - array $query, - string $expectedContentType, - ): void { - $code = 'abc123'; - $this->urlResolver->method('resolveEnabledShortUrl')->willReturn(ShortUrl::createFake()); - $handler = $this->createMock(RequestHandlerInterface::class); - $req = (new ServerRequest())->withAttribute('shortCode', $code)->withQueryParams($query); - - $resp = $this->action(new QrCodeOptions(format: $defaultFormat))->process($req, $handler); - - self::assertEquals($expectedContentType, $resp->getHeaderLine('Content-Type')); - } - - public static function provideQueries(): iterable - { - 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( - QrCodeOptions $defaultOptions, - ServerRequestInterface $req, - int $expectedSize, - ): void { - $code = 'abc123'; - $this->urlResolver->method('resolveEnabledShortUrl')->willReturn(ShortUrl::createFake()); - $handler = $this->createMock(RequestHandlerInterface::class); - - $resp = $this->action($defaultOptions)->process($req->withAttribute('shortCode', $code), $handler); - $result = getimagesizefromstring($resp->getBody()->__toString()); - self::assertNotFalse($result); - - [$size] = $result; - self::assertEquals($expectedSize, $size); - } - - public static function provideRequestsWithSize(): iterable - { - yield 'different margin and size defaults' => [ - new QrCodeOptions(size: 660, margin: 40), - ServerRequestFactory::fromGlobals(), - 740, - ]; - yield 'no size' => [new QrCodeOptions(), ServerRequestFactory::fromGlobals(), 300]; - yield 'no size, different default' => [new QrCodeOptions(size: 500), ServerRequestFactory::fromGlobals(), 500]; - yield 'size in query' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), - 123, - ]; - yield 'size in query, default margin' => [ - new QrCodeOptions(margin: 25), - ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), - 173, - ]; - yield 'margin' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), - 370, - ]; - yield 'margin and different default' => [ - new QrCodeOptions(size: 400), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), - 470, - ]; - yield 'margin and size' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '100', 'size' => '200']), - 400, - ]; - yield 'negative margin' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), - 300, - ]; - yield 'negative margin, default margin' => [ - new QrCodeOptions(margin: 10), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), - 300, - ]; - yield 'non-numeric margin' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo']), - 300, - ]; - yield 'negative margin and size' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), - 150, - ]; - yield 'negative margin and size, default margin' => [ - new QrCodeOptions(margin: 5), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), - 150, - ]; - yield 'non-numeric margin and size' => [ - new QrCodeOptions(), - ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo', 'size' => '538']), - 538, - ]; - } - - #[Test, DataProvider('provideRoundBlockSize')] - public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled( - QrCodeOptions $defaultOptions, - string|null $roundBlockSize, - int $expectedColor, - ): void { - $code = 'abc123'; - $req = ServerRequestFactory::fromGlobals() - ->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize]) - ->withAttribute('shortCode', $code); - - $this->urlResolver->method('resolveEnabledShortUrl')->willReturn(ShortUrl::withLongUrl('https://shlink.io')); - $handler = $this->createMock(RequestHandlerInterface::class); - - $resp = $this->action($defaultOptions)->process($req, $handler); - $image = imagecreatefromstring($resp->getBody()->__toString()); - self::assertNotFalse($image); - - $color = imagecolorat($image, 1, 1); - self::assertEquals($expectedColor, $color); - } - - public static function provideRoundBlockSize(): iterable - { - yield 'no round block param' => [new QrCodeOptions(), null, self::WHITE]; - yield 'no round block param, but disabled by default' => [ - new QrCodeOptions(roundBlockSize: false), - null, - self::BLACK, - ]; - yield 'round block: "true"' => [new QrCodeOptions(), 'true', self::WHITE]; - yield 'round block: "true", but disabled by default' => [ - new QrCodeOptions(roundBlockSize: false), - 'true', - self::WHITE, - ]; - yield 'round block: "false"' => [new QrCodeOptions(), 'false', self::BLACK]; - yield 'round block: "false", but enabled by default' => [ - new QrCodeOptions(roundBlockSize: true), - 'false', - self::BLACK, - ]; - } - - #[Test, DataProvider('provideColors')] - public function properColorsAreUsed(string|null $queryColor, string|null $optionsColor, int $expectedColor): void - { - $code = 'abc123'; - $req = ServerRequestFactory::fromGlobals() - ->withQueryParams(['color' => $queryColor]) - ->withAttribute('shortCode', $code); - - $this->urlResolver->method('resolveEnabledShortUrl')->willReturn(ShortUrl::withLongUrl('https://shlink.io')); - $handler = $this->createMock(RequestHandlerInterface::class); - - $resp = $this->action( - new QrCodeOptions(size: 250, roundBlockSize: false, color: $optionsColor ?? DEFAULT_QR_CODE_COLOR), - )->process($req, $handler); - $image = imagecreatefromstring($resp->getBody()->__toString()); - self::assertNotFalse($image); - - $resultingColor = imagecolorat($image, 1, 1); - self::assertEquals($expectedColor, $resultingColor); - } - - public static function provideColors(): iterable - { - yield 'no query, no default' => [null, null, self::BLACK]; - yield '6-char-query black' => ['000000', null, self::BLACK]; - yield '6-char-query white' => ['ffffff', null, self::WHITE]; - yield '6-char-query red' => ['ff0000', null, (int) hexdec('ff0000')]; - yield '3-char-query black' => ['000', null, self::BLACK]; - yield '3-char-query white' => ['fff', null, self::WHITE]; - yield '3-char-query red' => ['f00', null, (int) hexdec('ff0000')]; - yield '3-char-default red' => [null, 'f00', (int) hexdec('ff0000')]; - yield 'invalid color in query' => ['zzzzzzzz', null, self::BLACK]; - yield 'invalid color in query with default' => ['zzzzzzzz', 'aa88cc', self::BLACK]; - yield 'invalid color in default' => [null, 'zzzzzzzz', self::BLACK]; - } - - #[Test, DataProvider('provideEnabled')] - public function qrCodeIsResolvedBasedOnOptions(bool $enabledForDisabledShortUrls): void - { - if ($enabledForDisabledShortUrls) { - $this->urlResolver->expects($this->once())->method('resolvePublicShortUrl')->willThrowException( - ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain('')), - ); - $this->urlResolver->expects($this->never())->method('resolveEnabledShortUrl'); - } else { - $this->urlResolver->expects($this->once())->method('resolveEnabledShortUrl')->willThrowException( - ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain('')), - ); - $this->urlResolver->expects($this->never())->method('resolvePublicShortUrl'); - } - - $options = new QrCodeOptions(enabledForDisabledShortUrls: $enabledForDisabledShortUrls); - $this->action($options)->process( - ServerRequestFactory::fromGlobals(), - $this->createMock(RequestHandlerInterface::class), - ); - } - - public static function provideEnabled(): iterable - { - yield 'always enabled' => [true]; - yield 'only enabled short URLs' => [false]; - } - - public function action(QrCodeOptions|null $options = null): QrCodeAction - { - return new QrCodeAction( - $this->urlResolver, - new ShortUrlStringifier(), - new NullLogger(), - $options ?? new QrCodeOptions(enabledForDisabledShortUrls: false), - ); - } -}