diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f75b8b..f33116af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* [#2431](https://github.com/shlinkio/shlink/issues/2431) Add new date-based condition for the dynamic rules redirections system. +* [#2431](https://github.com/shlinkio/shlink/issues/2431) Add new date-based conditions for the dynamic rules redirections system, that allow to perform redirections based on an ISO-8601 date value. - * `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`: matches when current date and time is earlier than the defined threshold. + * `after-date`: matches when current date and time is later 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 71fc47da..0cbe7b37 100644 --- a/docs/swagger/definitions/SetShortUrlRedirectRule.json +++ b/docs/swagger/definitions/SetShortUrlRedirectRule.json @@ -24,7 +24,8 @@ "ip-address", "geolocation-country-code", "geolocation-city-name", - "before-date" + "before-date", + "after-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 0992670d..635bb48f 100644 --- a/module/CLI/src/RedirectRule/RedirectRuleHandler.php +++ b/module/CLI/src/RedirectRule/RedirectRuleHandler.php @@ -127,6 +127,9 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface RedirectConditionType::BEFORE_DATE => RedirectCondition::forBeforeDate( normalizeDate($this->askMandatory('Date to match?', $io)), ), + RedirectConditionType::AFTER_DATE => RedirectCondition::forAfterDate( + normalizeDate($this->askMandatory('Date to match?', $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 12af8d40..fce94954 100644 --- a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php +++ b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php @@ -192,6 +192,10 @@ class RedirectRuleHandlerTest extends TestCase RedirectConditionType::BEFORE_DATE, [RedirectCondition::forBeforeDate(normalizeDate('2016-05-01T20:34:16+02:00'))], ]; + yield 'After date' => [ + RedirectConditionType::AFTER_DATE, + [RedirectCondition::forAfterDate(normalizeDate('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 45e41c1e..2ed2a235 100644 --- a/module/Core/src/RedirectRule/Entity/RedirectCondition.php +++ b/module/Core/src/RedirectRule/Entity/RedirectCondition.php @@ -82,6 +82,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return new self(RedirectConditionType::BEFORE_DATE, $date->toAtomString()); } + public static function forAfterDate(Chronos $date): self + { + return new self(RedirectConditionType::AFTER_DATE, $date->toAtomString()); + } + public static function fromRawData(array $rawData): self { $type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]); @@ -108,6 +113,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable RedirectConditionType::GEOLOCATION_COUNTRY_CODE => self::forGeolocationCountryCode($cond->matchValue), RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue), RedirectConditionType::BEFORE_DATE => self::forBeforeDate(normalizeDate($cond->matchValue)), + RedirectConditionType::AFTER_DATE => self::forAfterDate(normalizeDate($cond->matchValue)), }; } @@ -126,6 +132,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable RedirectConditionType::GEOLOCATION_COUNTRY_CODE => $this->matchesGeolocationCountryCode($request), RedirectConditionType::GEOLOCATION_CITY_NAME => $this->matchesGeolocationCityName($request), RedirectConditionType::BEFORE_DATE => $this->matchesBeforeDate(), + RedirectConditionType::AFTER_DATE => $this->matchesAfterDate(), }; } @@ -214,6 +221,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return Chronos::now()->lessThan(Chronos::parse($this->matchValue)); } + private function matchesAfterDate(): bool + { + return Chronos::now()->greaterThan(Chronos::parse($this->matchValue)); + } + public function jsonSerialize(): array { return [ @@ -244,7 +256,8 @@ 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), + RedirectConditionType::BEFORE_DATE => sprintf('date is before %s', $this->matchValue), + RedirectConditionType::AFTER_DATE => sprintf('date is after %s', $this->matchValue), }; } } diff --git a/module/Core/src/RedirectRule/Model/RedirectConditionType.php b/module/Core/src/RedirectRule/Model/RedirectConditionType.php index 49f4536a..70347106 100644 --- a/module/Core/src/RedirectRule/Model/RedirectConditionType.php +++ b/module/Core/src/RedirectRule/Model/RedirectConditionType.php @@ -21,6 +21,7 @@ enum RedirectConditionType: string case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code'; case GEOLOCATION_CITY_NAME = 'geolocation-city-name'; case BEFORE_DATE = 'before-date'; + case AFTER_DATE = 'after-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 b35679d9..f6a528d9 100644 --- a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php +++ b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php @@ -211,4 +211,19 @@ class RedirectConditionTest extends TestCase yield 'date later than current' => [Chronos::now()->addHours(1), true]; yield 'date earlier than current' => [Chronos::now()->subHours(1), false]; } + + #[Test, DataProvider('provideVisitsWithAfterDateCondition')] + public function matchesAfterDate(Chronos $date, bool $expectedResult): void + { + $request = ServerRequestFactory::fromGlobals(); + $result = RedirectCondition::forAfterDate($date)->matchesRequest($request); + + self::assertEquals($expectedResult, $result); + } + + public static function provideVisitsWithAfterDateCondition(): iterable + { + yield 'date later than current' => [Chronos::now()->addHours(1), false]; + yield 'date earlier than current' => [Chronos::now()->subHours(1), true]; + } }