From dae52fedf4609fdb6ff2b0aa1bff19728a452b1b Mon Sep 17 00:00:00 2001 From: Andrei Vasilev Date: Wed, 7 May 2025 23:04:05 +0700 Subject: [PATCH] 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]; + } }