Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 3.7.2 #1963

Merged
merged 12 commits into from
Dec 26, 2023
2 changes: 1 addition & 1 deletion .github/workflows/ci-db-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Run tests
run: composer test:db:${{ inputs.platform }}
- name: Upload code coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: ${{ matrix.php-version == '8.2' && inputs.platform == 'sqlite:ci' }}
with:
name: coverage-db
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-mutation-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-22.1.0
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: coverage-${{ inputs.test-group }}
path: build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
php-extensions: openswoole-22.1.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- run: composer test:${{ inputs.test-group }}:ci
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: ${{ matrix.php-version == '8.2' }}
with:
name: coverage-${{ inputs.test-group }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ jobs:
php-version: ${{ matrix.php-version }}
coverage: pcov
ini-values: pcov.directory=module
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
path: build
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: ./build.sh ${GITHUB_REF#refs/tags/v}
- if: ${{ matrix.swoole == 'no' }}
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
path: build
Expand All @@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
path: build
- name: Publish release with assets
Expand Down
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@ 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).

## [3.7.2] - 2023-12-26
### Added
* *Nothing*

### Changed
* *Nothing*

### Deprecated
* *Nothing*

### Removed
* *Nothing*

### Fixed
* [#1960](https://github.com/shlinkio/shlink/issues/1960) Allow QR codes to be optionally resolved even when corresponding short URL is not enabled.


## [3.7.1] - 2023-12-17
### Added
* *Nothing*

### Changed
* Remove dependency on functional-php library
* [#1939](https://github.com/shlinkio/shlink/issues/1939) Fine-tune RoadRunner logs to avoid too many useless info.

### Deprecated
* *Nothing*
Expand All @@ -19,7 +37,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this

### Fixed
* [#1947](https://github.com/shlinkio/shlink/issues/1947) Fix error when importing short URLs while using Postgres.
* [#1939](https://github.com/shlinkio/shlink/issues/1939) Fine-tune RoadRunner logs to avoid too many useless info.


## [3.7.0] - 2023-11-25
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink)
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/)
[![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE)
[![Twitter](https://img.shields.io/badge/follow-shlinkio-blue.svg?style=flat-square&logo=twitter&color=blue)](https://twitter.com/shlinkio)
[![Twitter](https://img.shields.io/badge/follow-shlinkio-blue.svg?style=flat-square&logo=x&color=black)](https://twitter.com/shlinkio)
[![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio)
[![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate)

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"shlinkio/shlink-config": "^2.5",
"shlinkio/shlink-event-dispatcher": "^3.1",
"shlinkio/shlink-importer": "^5.2.1",
"shlinkio/shlink-installer": "^8.6.1",
"shlinkio/shlink-installer": "^8.7",
"shlinkio/shlink-ip-geolocation": "^3.4",
"shlinkio/shlink-json": "^1.1",
"spiral/roadrunner": "^2023.2",
Expand Down
1 change: 1 addition & 0 deletions config/autoload/installer.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
Option\QrCode\DefaultFormatConfigOption::class,
Option\QrCode\DefaultErrorCorrectionConfigOption::class,
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class,
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
Option\RabbitMq\RabbitMqHostConfigOption::class,
Option\RabbitMq\RabbitMqUseSslConfigOption::class,
Expand Down
4 changes: 4 additions & 0 deletions config/autoload/qr-codes.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Shlinkio\Shlink\Core\Config\EnvVars;

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;
Expand All @@ -22,6 +23,9 @@
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
),
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
),
],

];
2 changes: 2 additions & 0 deletions config/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@
const DEFAULT_QR_CODE_FORMAT = 'png';
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
// Deprecated. Shlink 4.0.0 should change default value to `true`
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = false;
const MIN_TASK_WORKERS = 4;
2 changes: 1 addition & 1 deletion config/roadrunner/.rr.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ logs:
http:
mode: 'off' # Disable logging as Shlink handles it internally
server:
level: debug
level: info
metrics:
level: debug
jobs:
Expand Down
10 changes: 6 additions & 4 deletions module/Core/src/Action/QrCodeAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;

class QrCodeAction implements MiddlewareInterface
readonly class QrCodeAction implements MiddlewareInterface
{
public function __construct(
private ShortUrlResolverInterface $urlResolver,
private ShortUrlStringifierInterface $stringifier,
private LoggerInterface $logger,
private QrCodeOptions $defaultOptions,
private QrCodeOptions $options,
) {
}

Expand All @@ -33,13 +33,15 @@ public function process(Request $request, RequestHandlerInterface $handler): Res
$identifier = ShortUrlIdentifier::fromRedirectRequest($request);

try {
$shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
$shortUrl = $this->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->defaultOptions);
$params = QrCodeParams::fromRequest($request, $this->options);
$qrCodeBuilder = Builder::create()
->data($this->stringifier->stringify($shortUrl))
->size($params->size)
Expand Down
1 change: 1 addition & 0 deletions module/Core/src/Config/EnvVars.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum EnvVars: string
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_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';
Expand Down
14 changes: 8 additions & 6 deletions module/Core/src/Options/QrCodeOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@

namespace Shlinkio\Shlink\Core\Options;

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;

final class QrCodeOptions
readonly final class QrCodeOptions
{
public function __construct(
public readonly int $size = DEFAULT_QR_CODE_SIZE,
public readonly int $margin = DEFAULT_QR_CODE_MARGIN,
public readonly string $format = DEFAULT_QR_CODE_FORMAT,
public readonly string $errorCorrection = DEFAULT_QR_CODE_ERROR_CORRECTION,
public readonly bool $roundBlockSize = DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
public int $size = DEFAULT_QR_CODE_SIZE,
public int $margin = DEFAULT_QR_CODE_MARGIN,
public string $format = DEFAULT_QR_CODE_FORMAT,
public string $errorCorrection = DEFAULT_QR_CODE_ERROR_CORRECTION,
public bool $roundBlockSize = DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
public bool $enabledForDisabledShortUrls = DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
) {
}
}
4 changes: 2 additions & 2 deletions module/Core/src/ShortUrl/Model/ShortUrlIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

use function sprintf;

final class ShortUrlIdentifier
final readonly class ShortUrlIdentifier
{
private function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
private function __construct(public string $shortCode, public ?string $domain = null)
{
}

Expand Down
18 changes: 14 additions & 4 deletions module/Core/src/ShortUrl/ShortUrlResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Rest\Entity\ApiKey;

class ShortUrlResolver implements ShortUrlResolverInterface
readonly class ShortUrlResolver implements ShortUrlResolverInterface
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly UrlShortenerOptions $urlShortenerOptions,
private EntityManagerInterface $em,
private UrlShortenerOptions $urlShortenerOptions,
) {
}

Expand All @@ -39,11 +39,21 @@ public function resolveShortUrl(ShortUrlIdentifier $identifier, ?ApiKey $apiKey
* @throws ShortUrlNotFoundException
*/
public function resolveEnabledShortUrl(ShortUrlIdentifier $identifier): ShortUrl
{
$shortUrl = $this->resolvePublicShortUrl($identifier);
if (! $shortUrl->isEnabled()) {
throw ShortUrlNotFoundException::fromNotFound($identifier);
}

return $shortUrl;
}

public function resolvePublicShortUrl(ShortUrlIdentifier $identifier): ShortUrl
{
/** @var ShortUrlRepository $shortUrlRepo */
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
$shortUrl = $shortUrlRepo->findOneWithDomainFallback($identifier, $this->urlShortenerOptions->mode);
if (! $shortUrl?->isEnabled()) {
if ($shortUrl === null) {
throw ShortUrlNotFoundException::fromNotFound($identifier);
}

Expand Down
13 changes: 13 additions & 0 deletions module/Core/src/ShortUrl/ShortUrlResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ interface ShortUrlResolverInterface
public function resolveShortUrl(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): ShortUrl;

/**
* Resolves a public short URL matching provided identifier.
* When trying to match public short URLs, if provided domain is default one, it gets ignored.
* If provided domain is not default, but the short code is found in default domain, we fall back to that short URL.
*
* @throws ShortUrlNotFoundException
*/
public function resolvePublicShortUrl(ShortUrlIdentifier $identifier): ShortUrl;

/**
* Resolves a public short URL matching provided identifier, only if it's not disabled.
* Disabled short URLs are those which received the max amount of visits, have a `validSince` in the future or have
* a `validUntil` in the past.
*
* @throws ShortUrlNotFoundException
*/
public function resolveEnabledShortUrl(ShortUrlIdentifier $identifier): ShortUrl;
Expand Down
27 changes: 27 additions & 0 deletions module/Core/test-api/Action/QrCodeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace ShlinkioApiTest\Shlink\Core\Action;

use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;

class QrCodeTest extends ApiTestCase
{
#[Test]
public function returnsNotFoundWhenShortUrlIsNotEnabled(): void
{
// The QR code successfully resolves at first
$response = $this->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 QR code should return a 404
$response = $this->callShortUrl('custom/qr-code');
self::assertEquals(404, $response->getStatusCode());
}
}
29 changes: 29 additions & 0 deletions module/Core/test/Action/QrCodeActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,35 @@ public static function provideRoundBlockSize(): iterable
];
}

#[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 $options = null): QrCodeAction
{
return new QrCodeAction(
Expand Down
Loading
Loading