Support dynamic redirects based on an after-date condition

This commit is contained in:
Alejandro Celaya
2025-12-18 09:41:07 +01:00
parent 39585ed87d
commit 9ae2dce261
7 changed files with 42 additions and 4 deletions

View File

@@ -6,9 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
## [Unreleased] ## [Unreleased]
### Added ### 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 ### 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. * [#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.

View File

@@ -24,7 +24,8 @@
"ip-address", "ip-address",
"geolocation-country-code", "geolocation-country-code",
"geolocation-city-name", "geolocation-city-name",
"before-date" "before-date",
"after-date"
], ],
"description": "The type of the condition, which will determine the logic used to match it" "description": "The type of the condition, which will determine the logic used to match it"
}, },

View File

@@ -127,6 +127,9 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
RedirectConditionType::BEFORE_DATE => RedirectCondition::forBeforeDate( RedirectConditionType::BEFORE_DATE => RedirectCondition::forBeforeDate(
normalizeDate($this->askMandatory('Date to match?', $io)), 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?'); $continue = $io->confirm('Do you want to add another condition?');

View File

@@ -192,6 +192,10 @@ class RedirectRuleHandlerTest extends TestCase
RedirectConditionType::BEFORE_DATE, RedirectConditionType::BEFORE_DATE,
[RedirectCondition::forBeforeDate(normalizeDate('2016-05-01T20:34:16+02:00'))], [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] #[Test]

View File

@@ -82,6 +82,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
return new self(RedirectConditionType::BEFORE_DATE, $date->toAtomString()); 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 public static function fromRawData(array $rawData): self
{ {
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]); $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_COUNTRY_CODE => self::forGeolocationCountryCode($cond->matchValue),
RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue), RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue),
RedirectConditionType::BEFORE_DATE => self::forBeforeDate(normalizeDate($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_COUNTRY_CODE => $this->matchesGeolocationCountryCode($request),
RedirectConditionType::GEOLOCATION_CITY_NAME => $this->matchesGeolocationCityName($request), RedirectConditionType::GEOLOCATION_CITY_NAME => $this->matchesGeolocationCityName($request),
RedirectConditionType::BEFORE_DATE => $this->matchesBeforeDate(), 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)); return Chronos::now()->lessThan(Chronos::parse($this->matchValue));
} }
private function matchesAfterDate(): bool
{
return Chronos::now()->greaterThan(Chronos::parse($this->matchValue));
}
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
return [ return [
@@ -244,7 +256,8 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
RedirectConditionType::IP_ADDRESS => sprintf('IP address matches %s', $this->matchValue), RedirectConditionType::IP_ADDRESS => sprintf('IP address matches %s', $this->matchValue),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => sprintf('country code is %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::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),
}; };
} }
} }

View File

@@ -21,6 +21,7 @@ enum RedirectConditionType: string
case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code'; case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code';
case GEOLOCATION_CITY_NAME = 'geolocation-city-name'; case GEOLOCATION_CITY_NAME = 'geolocation-city-name';
case BEFORE_DATE = 'before-date'; case BEFORE_DATE = 'before-date';
case AFTER_DATE = 'after-date';
/** /**
* Tells if a value is valid for the condition type * Tells if a value is valid for the condition type

View File

@@ -211,4 +211,19 @@ class RedirectConditionTest extends TestCase
yield 'date later than current' => [Chronos::now()->addHours(1), true]; yield 'date later than current' => [Chronos::now()->addHours(1), true];
yield 'date earlier than current' => [Chronos::now()->subHours(1), false]; 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];
}
} }