From b591c918457eea50a94df4e31a63fda1cdee3c29 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Sep 2026 22:28:40 +0200 Subject: [PATCH 1/7] Update to doctrine/orm 3.7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ac46d4817..231189f7e 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "cuyz/valinor": "~2.5.0", "doctrine/dbal": "^4.4", "doctrine/migrations": "^3.9", - "doctrine/orm": "^3.6", + "doctrine/orm": "^3.7", "donatj/phpuseragentparser": "^1.11", "friendsofphp/proxy-manager-lts": "^1.0", "geoip2/geoip2": "^3.3", From 182a430b9e6a3e3ad31c69ffd0e59a77e0c54f3a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Sep 2026 22:31:20 +0200 Subject: [PATCH 2/7] Add PHP 8.6 to CI --- .github/workflows/ci-db-tests.yml | 3 ++- .github/workflows/ci-tests.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index 8675d1bfa..53d187cda 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -13,9 +13,10 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - php-version: ['8.4', '8.5'] + php-version: ['8.4', '8.5', '8.6'] env: LC_ALL: C + continue-on-error: ${{ matrix.php-version == '8.6' }} steps: - uses: actions/checkout@v5 - name: Install MSSQL ODBC diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1a45651b1..1b1c2d232 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -13,9 +13,10 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - php-version: ['8.4', '8.5'] + php-version: ['8.4', '8.5', '8.6'] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically + continue-on-error: ${{ matrix.php-version == '8.6' }} steps: - uses: actions/checkout@v5 - name: Start postgres database server From cef79844f8755083152f23187ef09a3c8267c580 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Sep 2026 22:35:11 +0200 Subject: [PATCH 3/7] Fix static analysis after update to doctrine/orm 3.7 --- module/Core/src/ShortUrl/Entity/ShortUrl.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php index 81483d9fb..187c9d5fc 100644 --- a/module/Core/src/ShortUrl/Entity/ShortUrl.php +++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php @@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Order; use Doctrine\Common\Collections\Selectable; use Shlinkio\Shlink\Common\Entity\AbstractEntity; use Shlinkio\Shlink\Core\Domain\Entity\Domain; @@ -186,7 +187,7 @@ public function mostRecentImportedVisitDate(): Chronos|null { $criteria = Criteria::create() ->where(Criteria::expr()->eq('type', VisitType::IMPORTED)) - ->orderBy(['id' => 'DESC']) + ->orderBy(['id' => Order::Descending]) ->setMaxResults(1); $visit = $this->visits->matching($criteria)->last(); From 3f08c5e638a774e0ace7cc0fb701139e3e99b4fc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Sep 2026 22:37:08 +0200 Subject: [PATCH 4/7] Ingore PHP platform req when installing deps in CI for PHP 8.6 --- .github/actions/ci-setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index 3a6a86423..720a35e05 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -43,5 +43,5 @@ runs: coverage: xdebug - name: Install dependencies if: ${{ inputs.install-deps == 'yes' }} - run: composer install --no-interaction --prefer-dist + run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.6' && '--ignore-platform-req=php' || '' }} shell: bash From c303aff18f8108ca1e17ec5c697a63afd8efd4fe Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 10 Sep 2026 16:18:37 +0200 Subject: [PATCH 5/7] Update SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index f4a2882db..134a68a57 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,4 @@ The only exception is when a major version has just been released and the securi ## Reporting a Vulnerability -Do not report potential vulnerabilities as public issues. Instead, do it through https://github.com/shlinkio/shlink/security +Do not report potential vulnerabilities as public issues. ~Instead, do it through https://github.com/shlinkio/shlink/security~ Every single report this project has received was AI slop, so I have disabled the capability. From f467af2a01391b2d8df2cdfa33ab58903c6353ec Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 21 Sep 2026 17:31:46 +0200 Subject: [PATCH 6/7] Fix redis connections left open sometimes --- CHANGELOG.md | 17 +++++++++++++++++ composer.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6630cf606..403bb86c3 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). +## [5.1.7] - 2026-09-21 +### Added +* *Nothing* + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* [#2658](https://github.com/shlinkio/shlink/issues/2658) Fix redis connections left open sometimes. + + ## [5.1.6] - 2026-09-06 ### Added * *Nothing* diff --git a/composer.json b/composer.json index 231189f7e..e4e952276 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "pagerfanta/core": "^3.8", "ramsey/uuid": "^4.9", "shlinkio/doctrine-specification": "^2.3", - "shlinkio/shlink-common": "^9.0.1", + "shlinkio/shlink-common": "^9.0.2", "shlinkio/shlink-config": "^4.1.0", "shlinkio/shlink-event-dispatcher": "^4.4.0", "shlinkio/shlink-importer": "^5.8", From 951daf6e6e5c356649af21f656b5f20f718b06d5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 21 Sep 2026 17:38:59 +0200 Subject: [PATCH 7/7] Fix static analysis --- mago-analyze-baseline.toml | 100 +----------------- module/CLI/src/Util/PhpProcessRunner.php | 3 + module/CLI/src/Util/ProcessRunner.php | 16 ++- .../CLI/src/Util/ProcessRunnerInterface.php | 3 + .../Helper/ShortUrlTitleResolutionHelper.php | 6 +- .../Persistence/ShortUrlsCountFiltering.php | 13 +-- 6 files changed, 32 insertions(+), 109 deletions(-) diff --git a/mago-analyze-baseline.toml b/mago-analyze-baseline.toml index c0594ae87..6149e14b2 100644 --- a/mago-analyze-baseline.toml +++ b/mago-analyze-baseline.toml @@ -450,54 +450,6 @@ code = "less-specific-nested-argument-type" message = 'Argument type mismatch for argument #1 of `Symfony\Component\Process\Process::__construct`: expected `array`, but provided type `array` is less specific.' count = 1 -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-argument" -message = "Invalid argument type for argument #1 of `spl_object_hash`: expected `object`, but found `mixed`." -count = 2 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #2 of `Symfony\Component\Console\Helper\ProcessHelper::wrapCallback`: expected `Symfony\Component\Process\Process`, but found `mixed`.' -count = 1 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-argument" -message = "Invalid argument type for argument #2 of `sprintf`: expected `Stringable|null|scalar`, but found `mixed`." -count = 1 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #3 of `Symfony\Component\Console\Helper\DebugFormatterHelper::stop`: expected `bool`, but found `mixed`.' -count = 1 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-argument" -message = "Invalid argument type for argument #3 of `str_replace`: expected `array|string`, but found `mixed`." -count = 1 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "mixed-method-access" -message = "Attempting to access a method on a non-object type (`mixed`)." -count = 5 - -[[issues]] -file = "module/CLI/src/Util/ProcessRunner.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #2 of `Symfony\Component\Console\Helper\DebugFormatterHelper::start`: expected `string`, but possibly received `array|string`.' -count = 1 - [[issues]] file = "module/CLI/src/Util/ShlinkTable.php" code = "mixed-assignment" @@ -642,12 +594,6 @@ code = "mixed-argument" message = "Invalid argument type for argument #6 of `sprintf`: expected `Stringable|null|scalar`, but found `mixed`." count = 1 -[[issues]] -file = "module/Core/functions/functions.php" -code = "mixed-array-access" -message = "Unsafe array access on type `nonnull`." -count = 1 - [[issues]] file = "module/Core/functions/functions.php" code = "mixed-array-assignment" @@ -723,7 +669,7 @@ count = 1 [[issues]] file = "module/Core/functions/functions.php" code = "possibly-null-argument" -message = "Argument #3 of function `preg_replace` is possibly `null`, but parameter type `array|string` does not accept it." +message = "Argument #3 of function `preg_replace` is possibly `null`, but parameter type `array<('K.preg_replace() extends array-key), string>|string` does not accept it." count = 1 [[issues]] @@ -1098,17 +1044,11 @@ code = "mixed-array-access" message = "Unsafe array access on type `mixed`." count = 1 -[[issues]] -file = "module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php" -code = "mixed-array-assignment" -message = "Unsafe array assignment on type `nonnull`." -count = 1 - [[issues]] file = "module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php" code = "mixed-assignment" message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 +count = 1 [[issues]] file = "module/Core/src/Config/PostProcessor/ShortUrlMethodsProcessor.php" @@ -1452,12 +1392,6 @@ code = "possibly-null-argument" message = 'Argument #2 of method `Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryTrait::applySpecification` is possibly `null`, but parameter type `Happyr\DoctrineSpecification\Filter\Filter|Happyr\DoctrineSpecification\Query\QueryModifier` does not accept it.' count = 1 -[[issues]] -file = "module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php" -code = "possibly-null-property-access" -message = "Attempting to access a property on a possibly `null` value." -count = 2 - [[issues]] file = "module/Core/src/ShortUrl/Repository/ShortUrlRepository.php" code = "mixed-assignment" @@ -1764,12 +1698,6 @@ code = "mixed-method-access" message = "Attempting to access a method on a non-object type (`mixed`)." count = 1 -[[issues]] -file = "module/Core/src/Visit/Repository/VisitIterationRepository.php" -code = "possibly-null-property-access" -message = "Attempting to access a property on a possibly `null` value." -count = 2 - [[issues]] file = "module/Core/src/Visit/Repository/VisitRepository.php" code = "array-to-string-conversion" @@ -1806,12 +1734,6 @@ code = "less-specific-nested-return-statement" message = '''Returned type `array` is less specific than the declared return type `array` for function `Shlinkio\Shlink\Core\Visit\Repository\VisitRepository::findVisitsByTag` due to nested 'mixed'.''' count = 1 -[[issues]] -file = "module/Core/src/Visit/Repository/VisitRepository.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `Doctrine\DBAL\Connection::quote`: expected `string`, but found `mixed`.' -count = 2 - [[issues]] file = "module/Core/src/Visit/Repository/VisitRepository.php" code = "mixed-return-statement" @@ -1824,24 +1746,12 @@ code = "mixed-return-statement" message = 'Could not infer a precise return type for function `Shlinkio\Shlink\Core\Visit\Repository\VisitRepository::resolveVisitsWithNativeQuery`. Saw type `mixed`.' count = 1 -[[issues]] -file = "module/Core/src/Visit/Repository/VisitRepository.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 2 - [[issues]] file = "module/Core/src/Visit/Repository/VisitRepository.php" code = "possibly-null-argument" message = 'Argument #2 of method `Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryTrait::applySpecification` is possibly `null`, but parameter type `Happyr\DoctrineSpecification\Filter\Filter|Happyr\DoctrineSpecification\Query\QueryModifier` does not accept it.' count = 3 -[[issues]] -file = "module/Core/src/Visit/Repository/VisitRepository.php" -code = "possibly-null-property-access" -message = "Attempting to access a property on a possibly `null` value." -count = 2 - [[issues]] file = "module/Core/src/Visit/RequestTracker.php" code = "mixed-assignment" @@ -2118,12 +2028,6 @@ code = "less-specific-argument" message = 'Argument type mismatch for argument #1 of `ShlinkioTest\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRuleTest::createRule`: expected `Doctrine\Common\Collections\ArrayCollection`, but provided type `Doctrine\Common\Collections\ArrayCollection` is less specific.' count = 1 -[[issues]] -file = "module/Core/test/ShortUrl/Entity/ShortUrlTest.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PHPUnit\Framework\Assert::assertStringStartsWith`: expected `non-empty-string`, but possibly received `string`.' -count = 1 - [[issues]] file = "module/Core/test/ShortUrl/Helper/ShortCodeUniquenessHelperTest.php" code = "redundant-comparison" diff --git a/module/CLI/src/Util/PhpProcessRunner.php b/module/CLI/src/Util/PhpProcessRunner.php index 47f8161c1..537570492 100644 --- a/module/CLI/src/Util/PhpProcessRunner.php +++ b/module/CLI/src/Util/PhpProcessRunner.php @@ -19,6 +19,9 @@ public function __construct(private ProcessRunnerInterface $wrappedProcessRunner $this->phpBinary = $phpFinder->find(includeArgs: false) ?: 'php'; } + /** + * @inheritDoc + */ public function run(OutputInterface $output, array $cmd): void { $this->wrappedProcessRunner->run($output, [$this->phpBinary, ...$cmd]); diff --git a/module/CLI/src/Util/ProcessRunner.php b/module/CLI/src/Util/ProcessRunner.php index a27efa605..8c35e77bb 100644 --- a/module/CLI/src/Util/ProcessRunner.php +++ b/module/CLI/src/Util/ProcessRunner.php @@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; -use function spl_object_hash; +use function spl_object_id; use function sprintf; use function str_replace; @@ -19,8 +19,12 @@ class ProcessRunner implements ProcessRunnerInterface { private const int TIMEOUT = 1_200; // 20 minutes + /** @var Closure(string[] $cmd): Process */ private Closure $createProcess; + /** + * @param null|(callable(string[] $cmd): Process) $createProcess + */ public function __construct(private readonly ProcessHelper $helper, callable|null $createProcess = null) { $this->createProcess = $createProcess !== null @@ -28,6 +32,9 @@ public function __construct(private readonly ProcessHelper $helper, callable|nul : static fn (array $cmd) => new Process($cmd, timeout: self::TIMEOUT); } + /** + * @inheritDoc + */ public function run(OutputInterface $output, array $cmd): void { if ($output instanceof ConsoleOutputInterface) { @@ -40,7 +47,10 @@ public function run(OutputInterface $output, array $cmd): void if ($output->isVeryVerbose()) { $output->write( - $formatter->start(spl_object_hash($process), str_replace('<', '\\<', $process->getCommandLine())), + $formatter->start( + (string) spl_object_id($process), + str_replace('<', '\\<', $process->getCommandLine()), + ), ); } @@ -54,7 +64,7 @@ public function run(OutputInterface $output, array $cmd): void '%s Command did not run successfully', $process->getExitCode(), ); - $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); + $output->write($formatter->stop((string) spl_object_id($process), $message, $process->isSuccessful())); } } } diff --git a/module/CLI/src/Util/ProcessRunnerInterface.php b/module/CLI/src/Util/ProcessRunnerInterface.php index c00a4691d..d3c5ec916 100644 --- a/module/CLI/src/Util/ProcessRunnerInterface.php +++ b/module/CLI/src/Util/ProcessRunnerInterface.php @@ -8,5 +8,8 @@ interface ProcessRunnerInterface { + /** + * @param string[] $cmd + */ public function run(OutputInterface $output, array $cmd): void; } diff --git a/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php b/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php index 160cd20d0..0dfcaf1ed 100644 --- a/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php +++ b/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php @@ -117,10 +117,12 @@ private function tryToResolveTitle(ResponseInterface $response, string $contentT } return ( - $this->encodeToUtf8WithMbString($titleInOriginalEncoding, $pageCharset) ?? $this->encodeToUtf8WithIconv( + $this->encodeToUtf8WithMbString($titleInOriginalEncoding, $pageCharset) + ?? $this->encodeToUtf8WithIconv( $titleInOriginalEncoding, $pageCharset, - ) ?? $titleInOriginalEncoding + ) + ?? $titleInOriginalEncoding ); } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php index 9386b791b..dad17dc85 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php @@ -34,12 +34,13 @@ public function __construct( public readonly TagsMode $excludeTagsMode = TagsMode::ANY, string|null $apiKeyName = null, ) { - $this->searchIncludesDefaultDomain = !empty($searchTerm) - && !empty($defaultDomain) - && str_contains( - strtolower($defaultDomain), - strtolower($searchTerm), - ); + $this->searchIncludesDefaultDomain = + !empty($searchTerm) + && !empty($defaultDomain) + && str_contains( + strtolower($defaultDomain), + strtolower($searchTerm), + ); // Filtering by API key name is only allowed if the API key used in the request is an admin one, or it matches // the API key name