mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 12:13:13 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2649395f8 | ||
|
|
b7d9ba8258 | ||
|
|
6526cf8c44 | ||
|
|
a85afb2bee | ||
|
|
8b4067efbe | ||
|
|
c7c2272fab | ||
|
|
bc77750713 | ||
|
|
1ceb38f50b | ||
|
|
d273b56144 | ||
|
|
5cd7305666 | ||
|
|
3040a22c02 | ||
|
|
6991138812 | ||
|
|
5eb1808217 | ||
|
|
5eb14c5315 | ||
|
|
a18360a4d6 | ||
|
|
104b1e7d04 | ||
|
|
af2d67695b | ||
|
|
449a588796 | ||
|
|
7bbc938743 | ||
|
|
766758ff9b |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
- upload-coverage
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: |
|
||||
coverage-*
|
||||
|
||||
2
.github/workflows/publish-release.yml
vendored
2
.github/workflows/publish-release.yml
vendored
@@ -45,6 +45,6 @@ jobs:
|
||||
needs: ['publish']
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: dist-files-*
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -4,6 +4,61 @@ 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).
|
||||
|
||||
## [4.4.6] - 2025-03-20
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2391](https://github.com/shlinkio/shlink/issues/2391) When sending visits to Matomo, send the country code, not the country name.
|
||||
* Fix error with new option introduced by `endroid/qr-code` 6.0.4.
|
||||
|
||||
|
||||
## [4.4.5] - 2025-03-01
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2373](https://github.com/shlinkio/shlink/issues/2373) Ensure deprecation warnings do not end up escalated to `ErrorException`s by `ProblemDetailsMiddleware`.
|
||||
|
||||
In order to do this, Shlink will entirely ignore deprecation warnings when running in production, as those do not mean something is not working, but only that something will break in future versions.
|
||||
|
||||
|
||||
## [4.4.4] - 2025-02-19
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2366](https://github.com/shlinkio/shlink/issues/2366) Fix error "Cannot use 'SCRIPT' with redis-cluster" thrown when creating a lock while using a redis cluster.
|
||||
* [#2368](https://github.com/shlinkio/shlink/issues/2368) Fix error when listing non-orphan visits using API key with `AUTHORED_SHORT_URLS` role.
|
||||
|
||||
|
||||
## [4.4.3] - 2025-02-15
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"doctrine/migrations": "^3.8",
|
||||
"doctrine/orm": "^3.3",
|
||||
"donatj/phpuseragentparser": "^1.10",
|
||||
"endroid/qr-code": "^6.0",
|
||||
"endroid/qr-code": "^6.0.5",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^3.1",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
@@ -56,7 +56,7 @@
|
||||
"spiral/roadrunner-jobs": "^4.6",
|
||||
"symfony/console": "^7.2",
|
||||
"symfony/filesystem": "^7.2",
|
||||
"symfony/lock": "7.2.0",
|
||||
"symfony/lock": "7.1.6",
|
||||
"symfony/process": "^7.2",
|
||||
"symfony/string": "^7.2"
|
||||
},
|
||||
@@ -77,7 +77,8 @@
|
||||
"veewee/composer-run-parallel": "^1.4"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/var-exporter": ">=6.3.9,<=6.4.0"
|
||||
"symfony/var-exporter": ">=6.3.9,<=6.4.0",
|
||||
"phpunit/phpunit": "12.0.9"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
@@ -11,6 +11,7 @@ use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
use const Shlinkio\Shlink\LOCAL_LOCK_FACTORY;
|
||||
|
||||
// Set current directory to the project's root directory
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
@@ -21,7 +22,11 @@ loadEnvVarsFromConfig(
|
||||
enumValues(EnvVars::class),
|
||||
);
|
||||
|
||||
// This is one of the first files loaded. Configure the timezone and memory limit here
|
||||
// This is one of the first files loaded. Set global configuration here
|
||||
error_reporting(
|
||||
// Set a less strict error reporting for prod, where deprecation warnings should be ignored
|
||||
EnvVars::isProdEnv() ? E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED : E_ALL,
|
||||
);
|
||||
ini_set('memory_limit', EnvVars::MEMORY_LIMIT->loadFromEnv());
|
||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv());
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
display_errors=On
|
||||
error_reporting=-1
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
|
||||
@@ -38,6 +38,7 @@ final readonly class QrCodeParams
|
||||
public int $size,
|
||||
public int $margin,
|
||||
public WriterInterface $writer,
|
||||
public array $writerOptions,
|
||||
public ErrorCorrectionLevel $errorCorrectionLevel,
|
||||
public RoundBlockSizeMode $roundBlockSizeMode,
|
||||
public ColorInterface $color,
|
||||
@@ -49,11 +50,13 @@ final readonly class QrCodeParams
|
||||
public static function fromRequest(ServerRequestInterface $request, QrCodeOptions $defaults): self
|
||||
{
|
||||
$query = $request->getQueryParams();
|
||||
[$writer, $writerOptions] = self::resolveWriterAndWriterOptions($query, $defaults);
|
||||
|
||||
return new self(
|
||||
size: self::resolveSize($query, $defaults),
|
||||
margin: self::resolveMargin($query, $defaults),
|
||||
writer: self::resolveWriter($query, $defaults),
|
||||
writer: $writer,
|
||||
writerOptions: $writerOptions,
|
||||
errorCorrectionLevel: self::resolveErrorCorrection($query, $defaults),
|
||||
roundBlockSizeMode: self::resolveRoundBlockSize($query, $defaults),
|
||||
color: self::resolveColor($query, $defaults),
|
||||
@@ -83,14 +86,17 @@ final readonly class QrCodeParams
|
||||
return max($intMargin, 0);
|
||||
}
|
||||
|
||||
private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface
|
||||
/**
|
||||
* @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(),
|
||||
'svg' => [new SvgWriter(), []],
|
||||
default => [new PngWriter(), [PngWriter::WRITER_OPTION_NUMBER_OF_COLORS => null]],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ readonly class QrCodeAction implements MiddlewareInterface
|
||||
$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,
|
||||
|
||||
@@ -11,6 +11,8 @@ use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\VisitIterationRepositoryInterface;
|
||||
use Throwable;
|
||||
|
||||
use function strtolower;
|
||||
|
||||
readonly class MatomoVisitSender implements MatomoVisitSenderInterface
|
||||
{
|
||||
public function __construct(
|
||||
@@ -60,7 +62,7 @@ readonly class MatomoVisitSender implements MatomoVisitSenderInterface
|
||||
if ($location !== null) {
|
||||
$tracker
|
||||
->setCity($location->cityName)
|
||||
->setCountry($location->countryName)
|
||||
->setCountry(strtolower($location->countryCode))
|
||||
->setLatitude($location->latitude)
|
||||
->setLongitude($location->longitude);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
|
||||
use Shlinkio\Shlink\Core\Visit\Spec\CountOfNonOrphanVisits;
|
||||
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
|
||||
@@ -177,7 +178,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
||||
$qb = $this->createAllVisitsQueryBuilder($filtering);
|
||||
$qb->andWhere($qb->expr()->isNotNull('v.shortUrl'));
|
||||
|
||||
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec());
|
||||
$apiKey = $filtering->apiKey;
|
||||
if (ApiKey::isShortUrlRestricted($apiKey)) {
|
||||
$qb->join('v.shortUrl', 's');
|
||||
}
|
||||
|
||||
$this->applySpecification($qb, $apiKey?->inlinedSpec(), 'v');
|
||||
|
||||
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
|
||||
}
|
||||
|
||||
@@ -470,22 +470,18 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
#[Test]
|
||||
public function findNonOrphanVisitsReturnsExpectedResult(): void
|
||||
{
|
||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://1']));
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
$this->createVisitsForShortUrl($shortUrl, 7);
|
||||
$authoredApiKey = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
|
||||
$this->getEntityManager()->persist($authoredApiKey);
|
||||
|
||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://2']));
|
||||
$this->getEntityManager()->persist($shortUrl2);
|
||||
$this->createVisitsForShortUrl($shortUrl2, 4);
|
||||
|
||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://3']));
|
||||
$this->getEntityManager()->persist($shortUrl3);
|
||||
$this->createVisitsForShortUrl($shortUrl3, 10);
|
||||
$this->createShortUrlsAndVisits(withDomain: false, visitsAmount: 7);
|
||||
$this->createShortUrlsAndVisits(withDomain: false, apiKey: $authoredApiKey, visitsAmount: 4);
|
||||
$this->createShortUrlsAndVisits(withDomain: false, visitsAmount: 10);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertCount(21, $this->repo->findNonOrphanVisits(new VisitsListFiltering()));
|
||||
self::assertCount(21, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::allTime())));
|
||||
self::assertCount(4, $this->repo->findNonOrphanVisits(new VisitsListFiltering(apiKey: $authoredApiKey)));
|
||||
self::assertCount(7, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::since(
|
||||
Chronos::parse('2016-01-05')->endOfDay(),
|
||||
))));
|
||||
@@ -503,11 +499,11 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
self::assertCount(3, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::between(
|
||||
Chronos::parse('2016-01-03')->startOfDay(),
|
||||
Chronos::parse('2016-01-08')->endOfDay(),
|
||||
), false, null, 10, 10)));
|
||||
self::assertCount(15, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, true)));
|
||||
self::assertCount(10, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 10)));
|
||||
self::assertCount(1, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 10, 20)));
|
||||
self::assertCount(5, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 5, 5)));
|
||||
), limit: 10, offset: 10)));
|
||||
self::assertCount(15, $this->repo->findNonOrphanVisits(new VisitsListFiltering(excludeBots: true)));
|
||||
self::assertCount(10, $this->repo->findNonOrphanVisits(new VisitsListFiltering(limit: 10)));
|
||||
self::assertCount(1, $this->repo->findNonOrphanVisits(new VisitsListFiltering(limit: 10, offset: 20)));
|
||||
self::assertCount(5, $this->repo->findNonOrphanVisits(new VisitsListFiltering(limit: 5, offset: 5)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -535,6 +531,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
bool|string $withDomain = true,
|
||||
array $tags = [],
|
||||
ApiKey|null $apiKey = null,
|
||||
int $visitsAmount = 6,
|
||||
): array {
|
||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://longUrl',
|
||||
@@ -545,7 +542,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
$shortCode = $shortUrl->getShortCode();
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
|
||||
$this->createVisitsForShortUrl($shortUrl);
|
||||
$this->createVisitsForShortUrl($shortUrl, $visitsAmount);
|
||||
|
||||
if ($withDomain !== false) {
|
||||
$shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
|
||||
@@ -43,6 +43,9 @@ class MatomoVisitSenderTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideTrackerMethods')]
|
||||
/**
|
||||
* @param array<string, string[]> $invokedMethods
|
||||
*/
|
||||
public function visitIsSentToMatomo(Visit $visit, string|null $originalIpAddress, array $invokedMethods): void
|
||||
{
|
||||
$tracker = $this->createMock(MatomoTracker::class);
|
||||
@@ -66,8 +69,8 @@ class MatomoVisitSenderTest extends TestCase
|
||||
)->willReturn($tracker);
|
||||
}
|
||||
|
||||
foreach ($invokedMethods as $invokedMethod) {
|
||||
$tracker->expects($this->once())->method($invokedMethod)->willReturn($tracker);
|
||||
foreach ($invokedMethods as $invokedMethod => $args) {
|
||||
$tracker->expects($this->once())->method($invokedMethod)->with(...$args)->willReturn($tracker);
|
||||
}
|
||||
|
||||
$this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker);
|
||||
@@ -81,18 +84,28 @@ class MatomoVisitSenderTest extends TestCase
|
||||
yield 'located regular visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://shlink.io'), Visitor::empty())
|
||||
->locate(VisitLocation::fromGeolocation(new Location(
|
||||
countryCode: 'countryCode',
|
||||
countryCode: 'US',
|
||||
countryName: 'countryName',
|
||||
regionName: 'regionName',
|
||||
city: 'city',
|
||||
latitude: 123,
|
||||
longitude: 123,
|
||||
longitude: 456,
|
||||
timeZone: 'timeZone',
|
||||
))),
|
||||
'1.2.3.4',
|
||||
['setCity', 'setCountry', 'setLatitude', 'setLongitude', 'setIp'],
|
||||
[
|
||||
'setCity' => ['city'],
|
||||
'setCountry' => ['us'],
|
||||
'setLatitude' => [123],
|
||||
'setLongitude' => [456],
|
||||
'setIp' => ['1.2.3.4'],
|
||||
],
|
||||
];
|
||||
yield 'fallback IP' => [
|
||||
Visit::forBasePath(Visitor::fromParams(remoteAddress: '5.6.7.8')),
|
||||
null,
|
||||
['setIp' => ['5.6.7.0']],
|
||||
];
|
||||
yield 'fallback IP' => [Visit::forBasePath(Visitor::fromParams(remoteAddress: '1.2.3.4')), null, ['setIp']];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideUrlsToTrack')]
|
||||
|
||||
Reference in New Issue
Block a user