From dae52fedf4609fdb6ff2b0aa1bff19728a452b1b Mon Sep 17 00:00:00 2001 From: Andrei Vasilev Date: Wed, 7 May 2025 23:04:05 +0700 Subject: [PATCH 1/3] Support for redirects with a condition before date --- CHANGELOG.md | 4 +++- .../definitions/SetShortUrlRedirectRule.json | 3 ++- .../CLI/src/RedirectRule/RedirectRuleHandler.php | 3 +++ .../RedirectRule/RedirectRuleHandlerTest.php | 5 +++++ .../RedirectRule/Entity/RedirectCondition.php | 14 ++++++++++++++ .../RedirectRule/Model/RedirectConditionType.php | 1 + .../Entity/RedirectConditionTest.php | 16 ++++++++++++++++ 7 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beffec7c..9585bc34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* *Nothing* +* [#2431](https://github.com/shlinkio/shlink/issues/2431) Add new date-based condition for the dynamic rules redirections system. + + * `before-date`: Allows to perform redirections based on an ISO 8601 date value, when the current date and time is earlier than the defined threshold. ### Changed * [#2522](https://github.com/shlinkio/shlink/issues/2522) Shlink no longer tries to detect trusted proxies automatically, when resolving the visitor's IP address, as this is a potential security issue. diff --git a/docs/swagger/definitions/SetShortUrlRedirectRule.json b/docs/swagger/definitions/SetShortUrlRedirectRule.json index 15380faa..71fc47da 100644 --- a/docs/swagger/definitions/SetShortUrlRedirectRule.json +++ b/docs/swagger/definitions/SetShortUrlRedirectRule.json @@ -23,7 +23,8 @@ "valueless-query-param", "ip-address", "geolocation-country-code", - "geolocation-city-name" + "geolocation-city-name", + "before-date" ], "description": "The type of the condition, which will determine the logic used to match it" }, diff --git a/module/CLI/src/RedirectRule/RedirectRuleHandler.php b/module/CLI/src/RedirectRule/RedirectRuleHandler.php index baab9c9e..c1251b1d 100644 --- a/module/CLI/src/RedirectRule/RedirectRuleHandler.php +++ b/module/CLI/src/RedirectRule/RedirectRuleHandler.php @@ -122,6 +122,9 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface ), RedirectConditionType::GEOLOCATION_CITY_NAME => RedirectCondition::forGeolocationCityName( $this->askMandatory('City name to match?', $io), + ), + RedirectConditionType::BEFORE_DATE => RedirectCondition::forBeforeDate( + $this->askMandatory('Date to match? (ISO 8601)', $io), ) }; diff --git a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php index 76e48dc8..c93b5b71 100644 --- a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php +++ b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php @@ -122,6 +122,7 @@ class RedirectRuleHandlerTest extends TestCase 'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4', 'Country code to match?' => 'FR', 'City name to match?' => 'Los angeles', + 'Date to match? (ISO 8601)' => '2016-05-01T20:34:16+02:00', default => '', }, ); @@ -186,6 +187,10 @@ class RedirectRuleHandlerTest extends TestCase RedirectConditionType::GEOLOCATION_CITY_NAME, [RedirectCondition::forGeolocationCityName('Los angeles')], ]; + yield 'Before date' => [ + RedirectConditionType::BEFORE_DATE, + [RedirectCondition::forBeforeDate('2016-05-01T20:34:16+02:00')], + ]; } #[Test] diff --git a/module/Core/src/RedirectRule/Entity/RedirectCondition.php b/module/Core/src/RedirectRule/Entity/RedirectCondition.php index 2413400d..167b723a 100644 --- a/module/Core/src/RedirectRule/Entity/RedirectCondition.php +++ b/module/Core/src/RedirectRule/Entity/RedirectCondition.php @@ -2,6 +2,7 @@ namespace Shlinkio\Shlink\Core\RedirectRule\Entity; +use Cake\Chronos\Chronos; use JsonSerializable; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Common\Entity\AbstractEntity; @@ -75,6 +76,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return new self(RedirectConditionType::GEOLOCATION_CITY_NAME, $cityName); } + public static function forBeforeDate(string $date): self + { + return new self(RedirectConditionType::BEFORE_DATE, $date); + } + public static function fromRawData(array $rawData): self { $type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]); @@ -100,6 +106,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable RedirectConditionType::IP_ADDRESS => self::forIpAddress($cond->matchValue), RedirectConditionType::GEOLOCATION_COUNTRY_CODE => self::forGeolocationCountryCode($cond->matchValue), RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue), + RedirectConditionType::BEFORE_DATE => self::forBeforeDate($cond->matchValue), }; } @@ -117,6 +124,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request), RedirectConditionType::GEOLOCATION_COUNTRY_CODE => $this->matchesGeolocationCountryCode($request), RedirectConditionType::GEOLOCATION_CITY_NAME => $this->matchesGeolocationCityName($request), + RedirectConditionType::BEFORE_DATE => $this->matchesBeforeDate(), }; } @@ -200,6 +208,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return strcasecmp($geolocation->city, $this->matchValue) === 0; } + private function matchesBeforeDate(): bool + { + return Chronos::now()->lessThan(Chronos::parse($this->matchValue)); + } + public function jsonSerialize(): array { return [ @@ -230,6 +243,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable RedirectConditionType::IP_ADDRESS => sprintf('IP address matches %s', $this->matchValue), RedirectConditionType::GEOLOCATION_COUNTRY_CODE => sprintf('country code is %s', $this->matchValue), RedirectConditionType::GEOLOCATION_CITY_NAME => sprintf('city name is %s', $this->matchValue), + RedirectConditionType::BEFORE_DATE => sprintf('date before %s', $this->matchValue), }; } } diff --git a/module/Core/src/RedirectRule/Model/RedirectConditionType.php b/module/Core/src/RedirectRule/Model/RedirectConditionType.php index 0461d968..49f4536a 100644 --- a/module/Core/src/RedirectRule/Model/RedirectConditionType.php +++ b/module/Core/src/RedirectRule/Model/RedirectConditionType.php @@ -20,6 +20,7 @@ enum RedirectConditionType: string case IP_ADDRESS = 'ip-address'; case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code'; case GEOLOCATION_CITY_NAME = 'geolocation-city-name'; + case BEFORE_DATE = 'before-date'; /** * Tells if a value is valid for the condition type diff --git a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php index cc544353..5edda6af 100644 --- a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php +++ b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php @@ -2,6 +2,7 @@ namespace ShlinkioTest\Shlink\Core\RedirectRule\Entity; +use Cake\Chronos\Chronos; use Laminas\Diactoros\ServerRequestFactory; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; @@ -195,4 +196,19 @@ class RedirectConditionTest extends TestCase ); self::assertEquals($expectedType, $condition?->type); } + + #[Test, DataProvider('provideVisitsWithBeforeDateCondition')] + public function matchesBeforeDate(string $date, bool $expectedResult): void + { + $request = ServerRequestFactory::fromGlobals(); + $result = RedirectCondition::forBeforeDate($date)->matchesRequest($request); + + self::assertEquals($expectedResult, $result); + } + + public static function provideVisitsWithBeforeDateCondition(): iterable + { + yield 'date later than current' => [Chronos::now()->addHours(1)->toIso8601String(), true]; + yield 'date earlier than current' => [Chronos::now()->subHours(1)->toIso8601String(), false]; + } } From 54dc82cb902d22826d0753b6e086e6e1d13ae7f6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 18 Dec 2025 09:11:54 +0100 Subject: [PATCH 2/3] Type date in RedirectCondition::forBeforeDate as Chronos --- module/CLI/src/RedirectRule/RedirectRuleHandler.php | 5 +++-- module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php | 3 ++- module/Core/src/RedirectRule/Entity/RedirectCondition.php | 7 ++++--- .../test/RedirectRule/Entity/RedirectConditionTest.php | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/module/CLI/src/RedirectRule/RedirectRuleHandler.php b/module/CLI/src/RedirectRule/RedirectRuleHandler.php index c1251b1d..689bbb20 100644 --- a/module/CLI/src/RedirectRule/RedirectRuleHandler.php +++ b/module/CLI/src/RedirectRule/RedirectRuleHandler.php @@ -24,6 +24,7 @@ use function max; use function min; use function Shlinkio\Shlink\Core\ArrayUtils\map; use function Shlinkio\Shlink\Core\enumValues; +use function Shlinkio\Shlink\Core\normalizeDate; use function sprintf; use function str_pad; use function strlen; @@ -124,8 +125,8 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface $this->askMandatory('City name to match?', $io), ), RedirectConditionType::BEFORE_DATE => RedirectCondition::forBeforeDate( - $this->askMandatory('Date to match? (ISO 8601)', $io), - ) + normalizeDate($this->askMandatory('Date to match? (ISO 8601)', $io)), + ), }; $continue = $io->confirm('Do you want to add another condition?'); diff --git a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php index c93b5b71..60422949 100644 --- a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php +++ b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php @@ -19,6 +19,7 @@ use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Symfony\Component\Console\Style\StyleInterface; +use function Shlinkio\Shlink\Core\normalizeDate; use function sprintf; #[AllowMockObjectsWithoutExpectations] @@ -189,7 +190,7 @@ class RedirectRuleHandlerTest extends TestCase ]; yield 'Before date' => [ RedirectConditionType::BEFORE_DATE, - [RedirectCondition::forBeforeDate('2016-05-01T20:34:16+02:00')], + [RedirectCondition::forBeforeDate(normalizeDate('2016-05-01T20:34:16+02:00'))], ]; } diff --git a/module/Core/src/RedirectRule/Entity/RedirectCondition.php b/module/Core/src/RedirectRule/Entity/RedirectCondition.php index 167b723a..45e41c1e 100644 --- a/module/Core/src/RedirectRule/Entity/RedirectCondition.php +++ b/module/Core/src/RedirectRule/Entity/RedirectCondition.php @@ -17,6 +17,7 @@ use function Shlinkio\Shlink\Core\acceptLanguageToLocales; use function Shlinkio\Shlink\Core\ArrayUtils\some; use function Shlinkio\Shlink\Core\geolocationFromRequest; use function Shlinkio\Shlink\Core\ipAddressFromRequest; +use function Shlinkio\Shlink\Core\normalizeDate; use function Shlinkio\Shlink\Core\normalizeLocale; use function Shlinkio\Shlink\Core\splitLocale; use function sprintf; @@ -76,9 +77,9 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return new self(RedirectConditionType::GEOLOCATION_CITY_NAME, $cityName); } - public static function forBeforeDate(string $date): self + public static function forBeforeDate(Chronos $date): self { - return new self(RedirectConditionType::BEFORE_DATE, $date); + return new self(RedirectConditionType::BEFORE_DATE, $date->toAtomString()); } public static function fromRawData(array $rawData): self @@ -106,7 +107,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable RedirectConditionType::IP_ADDRESS => self::forIpAddress($cond->matchValue), RedirectConditionType::GEOLOCATION_COUNTRY_CODE => self::forGeolocationCountryCode($cond->matchValue), RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue), - RedirectConditionType::BEFORE_DATE => self::forBeforeDate($cond->matchValue), + RedirectConditionType::BEFORE_DATE => self::forBeforeDate(normalizeDate($cond->matchValue)), }; } diff --git a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php index 5edda6af..b35679d9 100644 --- a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php +++ b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php @@ -198,7 +198,7 @@ class RedirectConditionTest extends TestCase } #[Test, DataProvider('provideVisitsWithBeforeDateCondition')] - public function matchesBeforeDate(string $date, bool $expectedResult): void + public function matchesBeforeDate(Chronos $date, bool $expectedResult): void { $request = ServerRequestFactory::fromGlobals(); $result = RedirectCondition::forBeforeDate($date)->matchesRequest($request); @@ -208,7 +208,7 @@ class RedirectConditionTest extends TestCase public static function provideVisitsWithBeforeDateCondition(): iterable { - yield 'date later than current' => [Chronos::now()->addHours(1)->toIso8601String(), true]; - yield 'date earlier than current' => [Chronos::now()->subHours(1)->toIso8601String(), false]; + yield 'date later than current' => [Chronos::now()->addHours(1), true]; + yield 'date earlier than current' => [Chronos::now()->subHours(1), false]; } } From ca183d6e213192ce43fa152e4be12b3040692dd9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 18 Dec 2025 09:27:11 +0100 Subject: [PATCH 3/3] Some changes in before-date rule wording --- CHANGELOG.md | 2 +- module/CLI/src/RedirectRule/RedirectRuleHandler.php | 2 +- module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9585bc34..83f75b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Added * [#2431](https://github.com/shlinkio/shlink/issues/2431) Add new date-based condition for the dynamic rules redirections system. - * `before-date`: Allows to perform redirections based on an ISO 8601 date value, when the current date and time is earlier than the defined threshold. + * `before-date`: Allows to perform redirections based on an ISO-8601 date value, when the current date and time is earlier than the defined threshold. ### Changed * [#2522](https://github.com/shlinkio/shlink/issues/2522) Shlink no longer tries to detect trusted proxies automatically, when resolving the visitor's IP address, as this is a potential security issue. diff --git a/module/CLI/src/RedirectRule/RedirectRuleHandler.php b/module/CLI/src/RedirectRule/RedirectRuleHandler.php index 689bbb20..0992670d 100644 --- a/module/CLI/src/RedirectRule/RedirectRuleHandler.php +++ b/module/CLI/src/RedirectRule/RedirectRuleHandler.php @@ -125,7 +125,7 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface $this->askMandatory('City name to match?', $io), ), RedirectConditionType::BEFORE_DATE => RedirectCondition::forBeforeDate( - normalizeDate($this->askMandatory('Date to match? (ISO 8601)', $io)), + normalizeDate($this->askMandatory('Date to match?', $io)), ), }; diff --git a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php index 60422949..12af8d40 100644 --- a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php +++ b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php @@ -123,7 +123,7 @@ class RedirectRuleHandlerTest extends TestCase 'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4', 'Country code to match?' => 'FR', 'City name to match?' => 'Los angeles', - 'Date to match? (ISO 8601)' => '2016-05-01T20:34:16+02:00', + 'Date to match?' => '2016-05-01T20:34:16+02:00', default => '', }, );