From b15e832cf4d388b8642cfe186a3f165654224863 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 22 Apr 2025 08:41:58 +0200 Subject: [PATCH] Deprecate QR code generation endpoint --- CHANGELOG.md | 18 +++++++++++++ composer.json | 2 +- config/constants.php | 25 +++++++++++------ docs/swagger/paths/{shortCode}_qr-code.json | 5 ++-- module/Core/src/Action/Model/QrCodeParams.php | 1 + module/Core/src/Action/QrCodeAction.php | 1 + module/Core/src/Config/EnvVars.php | 27 ++++++++++++------- .../Core/src/Config/Options/QrCodeOptions.php | 1 + module/Core/test-api/Action/QrCodeTest.php | 1 + module/Core/test/Action/QrCodeActionTest.php | 25 ----------------- 10 files changed, 61 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 818a25f1..2807ddc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ 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 +* [#2408](https://github.com/shlinkio/shlink/issues/2408) Generating QR codes via `/{short-code}/qr-code` is now deprecated and will be removed in Shlink 5.0. Use the equivalent capability from web clients instead. + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [4.4.6] - 2025-03-20 ### Added * *Nothing* diff --git a/composer.json b/composer.json index 1123084d..e3548394 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "pagerfanta/core": "^3.8", "ramsey/uuid": "^4.7", "shlinkio/doctrine-specification": "^2.2", - "shlinkio/shlink-common": "^7.0", + "shlinkio/shlink-common": "dev-main#e601317 as 7.1", "shlinkio/shlink-config": "^4.0", "shlinkio/shlink-event-dispatcher": "^4.2", "shlinkio/shlink-importer": "^5.6", diff --git a/config/constants.php b/config/constants.php index 09df0e60..f5dff4d5 100644 --- a/config/constants.php +++ b/config/constants.php @@ -13,13 +13,22 @@ const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; const DEFAULT_REDIRECT_CACHE_LIFETIME = 30; const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory'; const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema. -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'; -const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true; -const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true; -const DEFAULT_QR_CODE_COLOR = '#000000'; // Black -const DEFAULT_QR_CODE_BG_COLOR = '#ffffff'; // White const IP_ADDRESS_REQUEST_ATTRIBUTE = 'remote_address'; const REDIRECT_URL_REQUEST_ATTRIBUTE = 'redirect_url'; + +/** @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 index bb95f7ef..dc0f2d81 100644 --- a/docs/swagger/paths/{shortCode}_qr-code.json +++ b/docs/swagger/paths/{shortCode}_qr-code.json @@ -1,11 +1,12 @@ { "get": { + "deprecated": true, "operationId": "shortUrlQrCode", "tags": [ "URL Shortener" ], - "summary": "Short URL QR code", - "description": "Generates a QR code image pointing to a short URL.
Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.", + "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" diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 3b25b611..9bdaf87e 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -28,6 +28,7 @@ use function trim; use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; +/** @deprecated */ final readonly class QrCodeParams { private const int MIN_SIZE = 50; diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index 889d1d04..0ab126bc 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -19,6 +19,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface; +/** @deprecated */ readonly class QrCodeAction implements MiddlewareInterface { public function __construct( diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index f8042feb..2a835932 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -56,15 +56,6 @@ enum EnvVars: string case MATOMO_BASE_URL = 'MATOMO_BASE_URL'; case MATOMO_SITE_ID = 'MATOMO_SITE_ID'; case MATOMO_API_TOKEN = 'MATOMO_API_TOKEN'; - case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE'; - case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN'; - case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT'; - case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION'; - case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE'; - case QR_CODE_FOR_DISABLED_SHORT_URLS = 'QR_CODE_FOR_DISABLED_SHORT_URLS'; - case DEFAULT_QR_CODE_COLOR = 'DEFAULT_QR_CODE_COLOR'; - case DEFAULT_QR_CODE_BG_COLOR = 'DEFAULT_QR_CODE_BG_COLOR'; - case DEFAULT_QR_CODE_LOGO_URL = 'DEFAULT_QR_CODE_LOGO_URL'; case DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT'; case DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT'; case DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT'; @@ -95,6 +86,24 @@ enum EnvVars: string case SKIP_INITIAL_GEOLITE_DOWNLOAD = 'SKIP_INITIAL_GEOLITE_DOWNLOAD'; /** @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 { diff --git a/module/Core/src/Config/Options/QrCodeOptions.php b/module/Core/src/Config/Options/QrCodeOptions.php index ac864851..e36a4285 100644 --- a/module/Core/src/Config/Options/QrCodeOptions.php +++ b/module/Core/src/Config/Options/QrCodeOptions.php @@ -15,6 +15,7 @@ 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; +/** @deprecated */ final readonly class QrCodeOptions { public function __construct( diff --git a/module/Core/test-api/Action/QrCodeTest.php b/module/Core/test-api/Action/QrCodeTest.php index 21fd5147..c8285198 100644 --- a/module/Core/test-api/Action/QrCodeTest.php +++ b/module/Core/test-api/Action/QrCodeTest.php @@ -7,6 +7,7 @@ namespace ShlinkioApiTest\Shlink\Core\Action; use PHPUnit\Framework\Attributes\Test; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; +/** @deprecated */ class QrCodeTest extends ApiTestCase { #[Test] diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 4e987277..5ea4cc47 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -9,7 +9,6 @@ use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -286,30 +285,6 @@ class QrCodeActionTest extends TestCase ); } - #[Test] - #[TestWith([[], '4696E5'])] // The logo has Shlink's brand color - #[TestWith([['logo' => 'invalid'], '4696E5'])] // Invalid `logo` values are ignored. Default logo is still rendered - #[TestWith([['logo' => 'disable'], '000000'])] // No logo will be added if explicitly disabled - public function logoIsAddedToQrCodeIfOptionIsDefined(array $query, string $expectedColor): void - { - $logoUrl = 'https://avatars.githubusercontent.com/u/20341790?v=4'; // Shlink's logo - $code = 'abc123'; - $req = ServerRequestFactory::fromGlobals() - ->withAttribute('shortCode', $code) - ->withQueryParams($query); - - $this->urlResolver->method('resolveEnabledShortUrl')->willReturn(ShortUrl::withLongUrl('https://shlink.io')); - $handler = $this->createMock(RequestHandlerInterface::class); - - $resp = $this->action(new QrCodeOptions(size: 250, logoUrl: $logoUrl))->process($req, $handler); - $image = imagecreatefromstring($resp->getBody()->__toString()); - self::assertNotFalse($image); - - // At around 100x100 px we can already find the logo, if present - $resultingColor = imagecolorat($image, 100, 100); - self::assertEquals(hexdec($expectedColor), $resultingColor); - } - public static function provideEnabled(): iterable { yield 'always enabled' => [true];