Compare commits

...

19 Commits

Author SHA1 Message Date
Alejandro Celaya
16f64f6247 Merge pull request #2064 from shlinkio/develop
Release 4.0.3
2024-03-15 23:07:25 +01:00
Alejandro Celaya
98992c656f Merge pull request #2063 from acelaya-forks/feature/non-http-url
Fix error when redirecting to a non-http URL
2024-03-15 23:05:02 +01:00
Alejandro Celaya
053e026982 Fix error when redirecting to a non-http URL 2024-03-15 23:00:40 +01:00
Alejandro Celaya
74180a4381 Merge pull request #2062 from acelaya-forks/feature/string-db-credentials
Feature/string db credentials
2024-03-15 18:33:29 +01:00
Alejandro Celaya
293725f933 Update changelog 2024-03-15 18:22:27 +01:00
Alejandro Celaya
c33f8d0ea2 Cast database credentials to string when read from the env 2024-03-15 14:32:53 +01:00
Alejandro Celaya
0f2cd3cb7f Remove useless return annotation 2024-03-12 09:09:59 +01:00
Alejandro Celaya
2441ac5e77 Update Bug template 2024-03-12 09:00:49 +01:00
Alejandro Celaya
f248001460 Merge pull request #2053 from shlinkio/develop
Release 4.0.2
2024-03-09 09:49:51 +01:00
Alejandro Celaya
1fe2c93946 Merge pull request #2051 from acelaya-forks/feature/fix-geolite-update
Fix infinite GeoLite2 downloads
2024-03-09 09:39:31 +01:00
Alejandro Celaya
a3d50605c1 Update changelog 2024-03-09 09:32:05 +01:00
Alejandro Celaya
5427152f15 Make sure GeoLite2 db file is always read from the filesystem befor etrying to operate on it 2024-03-09 09:30:05 +01:00
Alejandro Celaya
a4e9c2fdde Merge pull request #2046 from shlinkio/develop
Release 4.0.1
2024-03-08 08:57:40 +01:00
Alejandro Celaya
e244b2dc51 Add v4.0.1 to changelog 2024-03-08 08:56:55 +01:00
Alejandro Celaya
31dea8fa99 Merge pull request #2045 from acelaya-forks/feature/match-languages-fix
Ensure language redirect conditions do not match for too low quality accepted languages
2024-03-07 23:23:58 +01:00
Alejandro Celaya
be8cf56240 Ensure language redirect conditions do not match for too low quality accepted languages 2024-03-07 23:21:31 +01:00
Alejandro Celaya
0bc7412430 Fix incorrect redirect condition type definiition 2024-03-05 15:09:44 +01:00
Alejandro Celaya
6d56e92306 Merge pull request #2042 from acelaya-forks/feature/qr-code-color-args
Document color and bgColor QR code query params
2024-03-05 11:16:49 +01:00
Alejandro Celaya
97c94f8fcc Document color and bgColor QR code query params 2024-03-05 11:14:07 +01:00
19 changed files with 223 additions and 63 deletions

View File

@@ -58,5 +58,10 @@ body:
validations:
required: true
attributes:
label: How to reproduce
value: '<!-- Provide steps to reproduce the bug. -->'
label: Minimum steps to reproduce
value: |
<!--
Emphasis in MINIMUM: What is the simplest way to reproduce the bug?
Avoid things like "Create a kubernetes cluster", or anything related with cloud providers, as that is rarely the root cause and the bug may be closed as "not reproducible".
If you can provide a simple docker compose config, that's even better.
-->

View File

@@ -4,6 +4,59 @@ 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.0.3] - 2024-03-15
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#2058](https://github.com/shlinkio/shlink/issues/2058) Fix DB credentials provided as env vars being casted to `int` if they include only numbers.
* [#2060](https://github.com/shlinkio/shlink/issues/2060) Fix error when trying to redirect to a non-http long URL.
## [4.0.2] - 2024-03-09
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#2021](https://github.com/shlinkio/shlink/issues/2021) Fix infinite GeoLite2 downloads.
## [4.0.1] - 2024-03-08
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#2041](https://github.com/shlinkio/shlink/issues/2041) Document missing `color` and `bgColor` params for the QR code route in the OAS docs.
* [#2043](https://github.com/shlinkio/shlink/issues/2043) Fix language redirect conditions matching too low quality accepted languages.
## [4.0.0] - 2024-03-03
### Added
* [#1914](https://github.com/shlinkio/shlink/issues/1914) Add new dynamic redirects engine based on rules. Rules are conditions checked against the visitor's request, and when matching, they can result in a redirect to a different long URL.
@@ -56,7 +109,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
### Changed
* [#1968](https://github.com/shlinkio/shlink/issues/1968) Move migrations from `data` to `module/Core`.
* *Nothing*
### Deprecated
* *Nothing*

View File

@@ -48,7 +48,7 @@
"shlinkio/shlink-event-dispatcher": "^4.0",
"shlinkio/shlink-importer": "^5.3",
"shlinkio/shlink-installer": "^9.0",
"shlinkio/shlink-ip-geolocation": "^3.5",
"shlinkio/shlink-ip-geolocation": "^4.0",
"shlinkio/shlink-json": "^1.1",
"spiral/roadrunner": "^2023.3",
"spiral/roadrunner-cli": "^2.6",

View File

@@ -16,6 +16,10 @@ return (static function (): array {
'mssql' => 'pdo_sqlsrv',
default => 'pdo_mysql',
};
$readCredentialAsString = static function (EnvVars $envVar): string|null {
$value = $envVar->loadFromEnv();
return $value === null ? null : (string) $value;
};
$resolveDefaultPort = static fn () => match ($driver) {
'postgres' => '5432',
'mssql' => '1433',
@@ -28,6 +32,7 @@ return (static function (): array {
'postgres' => 'utf8',
default => null,
};
$resolveConnection = static fn () => match ($driver) {
null, 'sqlite' => [
'driver' => 'pdo_sqlite',
@@ -36,8 +41,8 @@ return (static function (): array {
default => [
'driver' => $resolveDriver(),
'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'),
'user' => EnvVars::DB_USER->loadFromEnv(),
'password' => EnvVars::DB_PASSWORD->loadFromEnv(),
'user' => $readCredentialAsString(EnvVars::DB_USER),
'password' => $readCredentialAsString(EnvVars::DB_PASSWORD),
'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()),
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,

View File

@@ -15,7 +15,7 @@
"properties": {
"type": {
"type": "string",
"enum": ["device", "language", "query"],
"enum": ["device", "language", "query-param"],
"description": "The type of the condition, which will condition the logic used to match it"
},
"matchKey": {

View File

@@ -65,6 +65,26 @@
"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"
}
}
],
"responses": {

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use GeoIp2\Database\Reader;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
@@ -18,6 +17,7 @@ use Shlinkio\Shlink\Core\Tag\TagService;
use Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2ReaderFactory;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console as SymfonyCli;
use Symfony\Component\Lock\LockFactory;
@@ -76,7 +76,7 @@ return [
ConfigAbstractFactory::class => [
GeoLite\GeolocationDbUpdater::class => [
DbUpdater::class,
Reader::class,
GeoLite2ReaderFactory::class,
LOCAL_LOCK_FACTORY,
TrackingOptions::class,
],

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\GeoLite;
use Cake\Chronos\Chronos;
use Closure;
use GeoIp2\Database\Reader;
use MaxMind\Db\Reader\Metadata;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
@@ -21,12 +22,19 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
{
private const LOCK_NAME = 'geolocation-db-update';
/** @var Closure(): Reader */
private readonly Closure $geoLiteDbReaderFactory;
/**
* @param callable(): Reader $geoLiteDbReaderFactory
*/
public function __construct(
private readonly DbUpdaterInterface $dbUpdater,
private readonly Reader $geoLiteDbReader,
callable $geoLiteDbReaderFactory,
private readonly LockFactory $locker,
private readonly TrackingOptions $trackingOptions,
) {
$this->geoLiteDbReaderFactory = $geoLiteDbReaderFactory(...);
}
/**
@@ -57,7 +65,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
return $this->downloadNewDb(false, $beforeDownload, $handleProgress);
}
$meta = $this->geoLiteDbReader->metadata();
$meta = ($this->geoLiteDbReaderFactory)()->metadata();
if ($this->buildIsTooOld($meta)) {
return $this->downloadNewDb(true, $beforeDownload, $handleProgress);
}

View File

@@ -187,7 +187,7 @@ class GeolocationDbUpdaterTest extends TestCase
return new GeolocationDbUpdater(
$this->dbUpdater,
$this->geoLiteDbReader,
fn () => $this->geoLiteDbReader,
$locker,
$options ?? new TrackingOptions(),
);

View File

@@ -17,7 +17,6 @@ function contains(mixed $value, array $array): bool
/**
* @param array[] $multiArray
* @return array
*/
function flatten(array $multiArray): array
{

View File

@@ -8,6 +8,7 @@ use BackedEnum;
use Cake\Chronos\Chronos;
use DateTimeInterface;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use GuzzleHttp\Psr7\Query;
use Jaybizzle\CrawlerDetect\CrawlerDetect;
use Laminas\Filter\Word\CamelCaseToSeparator;
use Laminas\Filter\Word\CamelCaseToUnderscore;
@@ -16,7 +17,6 @@ use PUGX\Shortid\Factory as ShortIdFactory;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
use function array_filter;
use function array_keys;
use function array_map;
use function array_pad;
@@ -27,6 +27,7 @@ use function implode;
use function is_array;
use function print_r;
use function Shlinkio\Shlink\Common\buildDateRange;
use function Shlinkio\Shlink\Core\ArrayUtils\map;
use function sprintf;
use function str_repeat;
use function str_replace;
@@ -85,16 +86,30 @@ function normalizeLocale(string $locale): string
}
/**
* Parse an accept-language-like pattern into a list of locales, optionally filtering out those which do not match a
* minimum quality
*
* @param non-empty-string $acceptLanguage
* @return string[];
* @param float<0, 1> $minQuality
* @return iterable<string>;
*/
function acceptLanguageToLocales(string $acceptLanguage): array
function acceptLanguageToLocales(string $acceptLanguage, float $minQuality = 0): iterable
{
$acceptLanguagesList = array_map(function (string $lang): string {
[$lang] = explode(';', $lang); // Discard everything after the semicolon (en-US;q=0.7)
return normalizeLocale($lang);
}, explode(',', $acceptLanguage));
return array_filter($acceptLanguagesList, static fn (string $lang) => $lang !== '*');
/** @var array{string, float|null}[] $acceptLanguagesList */
$acceptLanguagesList = map(explode(',', $acceptLanguage), static function (string $lang): array {
// Split locale/language and quality (en-US;q=0.7) -> [en-US, q=0.7]
[$lang, $qualityString] = array_pad(explode(';', $lang), length: 2, value: '');
$normalizedLang = normalizeLocale($lang);
$quality = Query::parse(trim($qualityString))['q'] ?? 1;
return [$normalizedLang, (float) $quality];
});
foreach ($acceptLanguagesList as [$lang, $quality]) {
if ($lang !== '*' && $quality >= $minQuality) {
yield $lang;
}
}
}
/**
@@ -108,7 +123,7 @@ function acceptLanguageToLocales(string $acceptLanguage): array
*/
function splitLocale(string $locale): array
{
return array_pad(explode('-', $locale), 2, null);
return array_pad(explode('-', $locale), length: 2, value: null);
}
function getOptionalIntFromInputFilter(InputFilter $inputFilter, string $fieldName): ?int

View File

@@ -77,7 +77,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
return false;
}
$acceptedLanguages = acceptLanguageToLocales($acceptLanguage);
$acceptedLanguages = acceptLanguageToLocales($acceptLanguage, minQuality: 0.9);
[$matchLanguage, $matchCountryCode] = splitLocale(normalizeLocale($this->matchValue));
return some(

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
use GuzzleHttp\Psr7\Query;
use Laminas\Diactoros\Uri;
use GuzzleHttp\Psr7\Uri;
use Laminas\Stdlib\ArrayUtils;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Options\TrackingOptions;
@@ -30,16 +30,18 @@ readonly class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderI
$uri = new Uri($this->redirectionResolver->resolveLongUrl($shortUrl, $request));
$currentQuery = $request->getQueryParams();
$shouldForwardQuery = $shortUrl->forwardQuery();
$baseQueryString = $uri->getQuery();
$basePath = $uri->getPath();
return $uri
->withQuery($shouldForwardQuery ? $this->resolveQuery($uri, $currentQuery) : $uri->getQuery())
->withPath($this->resolvePath($uri, $extraPath))
->withQuery($shouldForwardQuery ? $this->resolveQuery($baseQueryString, $currentQuery) : $baseQueryString)
->withPath($this->resolvePath($basePath, $extraPath))
->__toString();
}
private function resolveQuery(Uri $uri, array $currentQuery): string
private function resolveQuery(string $baseQueryString, array $currentQuery): string
{
$hardcodedQuery = Query::parse($uri->getQuery());
$hardcodedQuery = Query::parse($baseQueryString);
$disableTrackParam = $this->trackingOptions->disableTrackParam;
if ($disableTrackParam !== null) {
@@ -47,14 +49,13 @@ readonly class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderI
}
// We want to merge preserving numeric keys, as some params might be numbers
$mergedQuery = ArrayUtils::merge($hardcodedQuery, $currentQuery, true);
$mergedQuery = ArrayUtils::merge($hardcodedQuery, $currentQuery, preserveNumericKeys: true);
return Query::build($mergedQuery);
}
private function resolvePath(Uri $uri, ?string $extraPath): string
private function resolvePath(string $basePath, ?string $extraPath): string
{
$hardcodedPath = $uri->getPath();
return $extraPath === null ? $hardcodedPath : sprintf('%s%s', $hardcodedPath, $extraPath);
return $extraPath === null ? $basePath : sprintf('%s%s', $basePath, $extraPath);
}
}

View File

@@ -87,7 +87,7 @@ class ExtraPathRedirectMiddleware implements MiddlewareInterface
}
/**
* @return array{0: string, 1: string|null}
* @return array{string, string|null}
*/
private function resolvePotentialShortCodeAndExtraPath(UriInterface $uri, int $shortCodeSegments): array
{

View File

@@ -7,6 +7,7 @@ namespace ShlinkioApiTest\Shlink\Core\Action;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
@@ -15,8 +16,8 @@ use const ShlinkioTest\Shlink\IOS_USER_AGENT;
class RedirectTest extends ApiTestCase
{
#[Test, DataProvider('provideUserAgents')]
public function properRedirectHappensBasedOnUserAgent(array $options, string $expectedRedirect): void
#[Test, DataProvider('provideRequestOptions')]
public function properRedirectHappensBasedOnRedirectRules(array $options, string $expectedRedirect): void
{
$response = $this->callShortUrl('def456', $options);
@@ -24,19 +25,19 @@ class RedirectTest extends ApiTestCase
self::assertEquals($expectedRedirect, $response->getHeaderLine('Location'));
}
public static function provideUserAgents(): iterable
public static function provideRequestOptions(): iterable
{
yield 'android' => [
[
RequestOptions::HEADERS => ['User-Agent' => ANDROID_USER_AGENT],
],
'https://blog.alejandrocelaya.com/android',
'android://foo/bar',
];
yield 'ios' => [
[
RequestOptions::HEADERS => ['User-Agent' => IOS_USER_AGENT],
],
'https://blog.alejandrocelaya.com/ios',
'fb://profile/33138223345',
];
yield 'desktop' => [
[
@@ -75,9 +76,38 @@ class RedirectTest extends ApiTestCase
];
yield 'rule: complex matching accept language' => [
[
RequestOptions::HEADERS => ['Accept-Language' => 'fr-FR, es;q=08, en;q=0.5, *;q=0.2'],
RequestOptions::HEADERS => ['Accept-Language' => 'fr-FR, es;q=0.9, en;q=0.9, *;q=0.2'],
],
'https://example.com/only-english',
];
yield 'rule: too low quality accept language' => [
[
RequestOptions::HEADERS => ['Accept-Language' => 'fr-FR, es;q=0.8, en;q=0.5, *;q=0.2'],
],
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
];
}
/**
* @param non-empty-string $longUrl
*/
#[Test]
#[TestWith(['android://foo/bar'])]
#[TestWith(['fb://profile/33138223345'])]
#[TestWith(['viber://pa?chatURI=1234'])]
public function properRedirectHappensForNonHttpLongUrls(string $longUrl): void
{
$slug = 'non-http-schema';
$this->callApiWithKey('POST', '/short-urls', [
RequestOptions::JSON => [
'longUrl' => $longUrl,
'customSlug' => $slug,
],
]);
$response = $this->callShortUrl($slug);
self::assertEquals(302, $response->getStatusCode());
self::assertEquals($longUrl, $response->getHeaderLine('Location'));
}
}

View File

@@ -35,6 +35,8 @@ class RedirectConditionTest extends TestCase
#[TestWith(['es, en,fr', 'en', true])] // multiple languages match
#[TestWith(['es, en-US,fr', 'EN', true])] // multiple locales match
#[TestWith(['es_ES', 'es-ES', true])] // single locale match
#[TestWith(['en-US,es-ES;q=0.6', 'es-ES', false])] // too low quality
#[TestWith(['en-US,es-ES;q=0.9', 'es-ES', true])] // quality high enough
#[TestWith(['en-UK', 'en-uk', true])] // different casing match
#[TestWith(['en-UK', 'en', true])] // only lang
#[TestWith(['es-AR', 'en', false])] // different only lang

View File

@@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper;
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;
@@ -37,7 +38,7 @@ class ShortUrlRedirectionBuilderTest extends TestCase
?bool $forwardQuery,
): void {
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
'longUrl' => 'https://domain.com/foo/bar?some=thing',
'longUrl' => 'https://example.com/foo/bar?some=thing',
'forwardQuery' => $forwardQuery,
]));
$this->redirectionResolver->expects($this->once())->method('resolveLongUrl')->with(
@@ -54,59 +55,81 @@ class ShortUrlRedirectionBuilderTest extends TestCase
{
$request = static fn (array $query = []) => ServerRequestFactory::fromGlobals()->withQueryParams($query);
yield ['https://domain.com/foo/bar?some=thing', $request(), null, true];
yield ['https://domain.com/foo/bar?some=thing', $request(), null, null];
yield ['https://domain.com/foo/bar?some=thing', $request(), null, false];
yield ['https://domain.com/foo/bar?some=thing&else', $request(['else' => null]), null, true];
yield ['https://domain.com/foo/bar?some=thing&foo=bar', $request(['foo' => 'bar']), null, true];
yield ['https://domain.com/foo/bar?some=thing&foo=bar', $request(['foo' => 'bar']), null, null];
yield ['https://domain.com/foo/bar?some=thing', $request(['foo' => 'bar']), null, false];
yield ['https://domain.com/foo/bar?some=thing&123=foo', $request(['123' => 'foo']), null, true];
yield ['https://domain.com/foo/bar?some=thing&456=foo', $request([456 => 'foo']), null, true];
yield ['https://domain.com/foo/bar?some=thing&456=foo', $request([456 => 'foo']), null, null];
yield ['https://domain.com/foo/bar?some=thing', $request([456 => 'foo']), null, false];
yield ['https://example.com/foo/bar?some=thing', $request(), null, true];
yield ['https://example.com/foo/bar?some=thing', $request(), null, null];
yield ['https://example.com/foo/bar?some=thing', $request(), null, false];
yield ['https://example.com/foo/bar?some=thing&else', $request(['else' => null]), null, true];
yield ['https://example.com/foo/bar?some=thing&foo=bar', $request(['foo' => 'bar']), null, true];
yield ['https://example.com/foo/bar?some=thing&foo=bar', $request(['foo' => 'bar']), null, null];
yield ['https://example.com/foo/bar?some=thing', $request(['foo' => 'bar']), null, false];
yield ['https://example.com/foo/bar?some=thing&123=foo', $request(['123' => 'foo']), null, true];
yield ['https://example.com/foo/bar?some=thing&456=foo', $request([456 => 'foo']), null, true];
yield ['https://example.com/foo/bar?some=thing&456=foo', $request([456 => 'foo']), null, null];
yield ['https://example.com/foo/bar?some=thing', $request([456 => 'foo']), null, false];
yield [
'https://domain.com/foo/bar?some=overwritten&foo=bar',
'https://example.com/foo/bar?some=overwritten&foo=bar',
$request(['foo' => 'bar', 'some' => 'overwritten']),
null,
true,
];
yield [
'https://domain.com/foo/bar?some=overwritten',
'https://example.com/foo/bar?some=overwritten',
$request(['foobar' => 'notrack', 'some' => 'overwritten']),
null,
true,
];
yield [
'https://domain.com/foo/bar?some=overwritten',
'https://example.com/foo/bar?some=overwritten',
$request(['foobar' => 'notrack', 'some' => 'overwritten']),
null,
null,
];
yield [
'https://domain.com/foo/bar?some=thing',
'https://example.com/foo/bar?some=thing',
$request(['foobar' => 'notrack', 'some' => 'overwritten']),
null,
false,
];
yield ['https://domain.com/foo/bar/something/else-baz?some=thing', $request(), '/something/else-baz', true];
yield ['https://example.com/foo/bar/something/else-baz?some=thing', $request(), '/something/else-baz', true];
yield [
'https://domain.com/foo/bar/something/else-baz?some=thing&hello=world',
'https://example.com/foo/bar/something/else-baz?some=thing&hello=world',
$request(['hello' => 'world']),
'/something/else-baz',
true,
];
yield [
'https://domain.com/foo/bar/something/else-baz?some=thing&hello=world',
'https://example.com/foo/bar/something/else-baz?some=thing&hello=world',
$request(['hello' => 'world']),
'/something/else-baz',
null,
];
yield [
'https://domain.com/foo/bar/something/else-baz?some=thing',
'https://example.com/foo/bar/something/else-baz?some=thing',
$request(['hello' => 'world']),
'/something/else-baz',
false,
];
}
/**
* @param non-empty-string $longUrl
*/
#[Test]
#[TestWith(['android://foo/bar'])]
#[TestWith(['fb://profile/33138223345'])]
#[TestWith(['viber://pa?chatURI=1234'])]
public function buildShortUrlRedirectBuildsNonHttpUrls(string $longUrl): void
{
$shortUrl = ShortUrl::withLongUrl($longUrl);
$request = ServerRequestFactory::fromGlobals();
$this->redirectionResolver->expects($this->once())->method('resolveLongUrl')->with(
$shortUrl,
$request,
)->willReturn($shortUrl->getLongUrl());
$result = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $request);
self::assertEquals($longUrl, $result);
}
}

View File

@@ -66,7 +66,7 @@ class ListRedirectRulesTest extends ApiTestCase
'conditions' => [self::LANGUAGE_EN_CONDITION],
],
[
'longUrl' => 'https://blog.alejandrocelaya.com/android',
'longUrl' => 'android://foo/bar',
'priority' => 4,
'conditions' => [
[
@@ -77,7 +77,7 @@ class ListRedirectRulesTest extends ApiTestCase
],
],
[
'longUrl' => 'https://blog.alejandrocelaya.com/ios',
'longUrl' => 'fb://profile/33138223345',
'priority' => 5,
'conditions' => [
[

View File

@@ -49,7 +49,7 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
$androidRule = new ShortUrlRedirectRule(
shortUrl: $defShortUrl,
priority: 4,
longUrl: 'https://blog.alejandrocelaya.com/android',
longUrl: 'android://foo/bar',
conditions: new ArrayCollection([RedirectCondition::forDevice(DeviceType::ANDROID)]),
);
$manager->persist($androidRule);
@@ -65,7 +65,7 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
$iosRule = new ShortUrlRedirectRule(
shortUrl: $defShortUrl,
priority: 5,
longUrl: 'https://blog.alejandrocelaya.com/ios',
longUrl: 'fb://profile/33138223345',
conditions: new ArrayCollection([RedirectCondition::forDevice(DeviceType::IOS)]),
);
$manager->persist($iosRule);