From 18c4c39fee48fa6b8b8e06a48648323b99860c69 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 17 Jul 2025 08:26:52 +0200 Subject: [PATCH 1/4] Add support for any-value and valueless query param redirect rules --- CHANGELOG.md | 5 +++ .../definitions/SetShortUrlRedirectRule.json | 2 ++ .../src/RedirectRule/RedirectRuleHandler.php | 6 ++++ .../RedirectRule/Entity/RedirectCondition.php | 35 +++++++++++++++++++ .../Model/RedirectConditionType.php | 5 +++ 5 files changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a22b5be9..ac74a67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#2387](https://github.com/shlinkio/shlink/issues/2387) Add `REAL_TIME_UPDATES_TOPICS` env var and corresponding config option, to granularly decide which real-time updates topics should be enabled. * [#2418](https://github.com/shlinkio/shlink/issues/2418) Add more granular control over how Shlink handles CORS. It is now possible to customize the `Access-Control-Allow-Origin`, `Access-Control-Max-Age` and `Access-Control-Allow-Credentials` headers via env vars or config options. +* [#2386](https://github.com/shlinkio/shlink/issues/2386) Add new `any-value-query-param` and `valueless-query-param` redirect rule conditions. + + These new rules expand the existing `query-param`, which requires both a specific non-empty value in order to match the condition. + + The new conditions match as soon as a query param exists with any or no value (in the case of `any-value-query-param`), or if a query param exists with no value at all (in the case of `valueless-query-param`). ### Changed * [#2406](https://github.com/shlinkio/shlink/issues/2406) Remove references to bootstrap from error templates, and instead inline the very minimum required styles. diff --git a/docs/swagger/definitions/SetShortUrlRedirectRule.json b/docs/swagger/definitions/SetShortUrlRedirectRule.json index 00f0a27b..b6c66f5f 100644 --- a/docs/swagger/definitions/SetShortUrlRedirectRule.json +++ b/docs/swagger/definitions/SetShortUrlRedirectRule.json @@ -19,6 +19,8 @@ "device", "language", "query-param", + "any-value-query-param", + "valueless-query-param", "ip-address", "geolocation-country-code", "geolocation-city-name" diff --git a/module/CLI/src/RedirectRule/RedirectRuleHandler.php b/module/CLI/src/RedirectRule/RedirectRuleHandler.php index 89f93833..baab9c9e 100644 --- a/module/CLI/src/RedirectRule/RedirectRuleHandler.php +++ b/module/CLI/src/RedirectRule/RedirectRuleHandler.php @@ -108,6 +108,12 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface $this->askMandatory('Query param name?', $io), $this->askOptional('Query param value?', $io), ), + RedirectConditionType::ANY_VALUE_QUERY_PARAM => RedirectCondition::forAnyValueQueryParam( + $this->askMandatory('Query param name?', $io), + ), + RedirectConditionType::VALUELESS_QUERY_PARAM => RedirectCondition::forValuelessQueryParam( + $this->askMandatory('Query param name?', $io), + ), RedirectConditionType::IP_ADDRESS => RedirectCondition::forIpAddress( $this->askMandatory('IP address, CIDR block or wildcard-pattern (1.2.*.*)', $io), ), diff --git a/module/Core/src/RedirectRule/Entity/RedirectCondition.php b/module/Core/src/RedirectRule/Entity/RedirectCondition.php index 255cb19e..dd24ca6d 100644 --- a/module/Core/src/RedirectRule/Entity/RedirectCondition.php +++ b/module/Core/src/RedirectRule/Entity/RedirectCondition.php @@ -11,6 +11,7 @@ use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter; use Shlinkio\Shlink\Core\Util\IpAddressUtils; use Shlinkio\Shlink\Importer\Model\ImportedShlinkRedirectCondition; +use function array_key_exists; use function Shlinkio\Shlink\Core\acceptLanguageToLocales; use function Shlinkio\Shlink\Core\ArrayUtils\some; use function Shlinkio\Shlink\Core\geolocationFromRequest; @@ -35,6 +36,16 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return new self(RedirectConditionType::QUERY_PARAM, $value, $param); } + public static function forAnyValueQueryParam(string $param): self + { + return new self(RedirectConditionType::ANY_VALUE_QUERY_PARAM, $param); + } + + public static function forValuelessQueryParam(string $param): self + { + return new self(RedirectConditionType::VALUELESS_QUERY_PARAM, $param); + } + public static function forLanguage(string $language): self { return new self(RedirectConditionType::LANGUAGE, $language); @@ -82,6 +93,8 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return match ($type) { RedirectConditionType::QUERY_PARAM => self::forQueryParam($cond->matchKey ?? '', $cond->matchValue), + RedirectConditionType::ANY_VALUE_QUERY_PARAM => self::forAnyValueQueryParam($cond->matchValue), + RedirectConditionType::VALUELESS_QUERY_PARAM => self::forValuelessQueryParam($cond->matchValue), RedirectConditionType::LANGUAGE => self::forLanguage($cond->matchValue), RedirectConditionType::DEVICE => self::forDevice(DeviceType::from($cond->matchValue)), RedirectConditionType::IP_ADDRESS => self::forIpAddress($cond->matchValue), @@ -97,6 +110,8 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable { return match ($this->type) { RedirectConditionType::QUERY_PARAM => $this->matchesQueryParam($request), + RedirectConditionType::ANY_VALUE_QUERY_PARAM => $this->matchesAnyValueQueryParam($request), + RedirectConditionType::VALUELESS_QUERY_PARAM => $this->matchesValuelessQueryParam($request), RedirectConditionType::LANGUAGE => $this->matchesLanguage($request), RedirectConditionType::DEVICE => $this->matchesDevice($request), RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request), @@ -113,6 +128,18 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable return $queryValue === $this->matchValue; } + private function matchesValuelessQueryParam(ServerRequestInterface $request): bool + { + $query = $request->getQueryParams(); + return array_key_exists($this->matchValue, $query) && empty($query[$this->matchValue]); + } + + private function matchesAnyValueQueryParam(ServerRequestInterface $request): bool + { + $query = $request->getQueryParams(); + return array_key_exists($this->matchValue, $query); + } + private function matchesLanguage(ServerRequestInterface $request): bool { $acceptLanguage = trim($request->getHeaderLine('Accept-Language')); @@ -188,6 +215,14 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable $this->matchKey, $this->matchValue, ), + RedirectConditionType::ANY_VALUE_QUERY_PARAM => sprintf( + 'query string contains %s param', + $this->matchValue, + ), + RedirectConditionType::VALUELESS_QUERY_PARAM => sprintf( + 'query string contains %s param without a value (https://example.com?foo)', + $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_CITY_NAME => sprintf('city name is %s', $this->matchValue), diff --git a/module/Core/src/RedirectRule/Model/RedirectConditionType.php b/module/Core/src/RedirectRule/Model/RedirectConditionType.php index efc314f9..bcd482e7 100644 --- a/module/Core/src/RedirectRule/Model/RedirectConditionType.php +++ b/module/Core/src/RedirectRule/Model/RedirectConditionType.php @@ -13,6 +13,8 @@ enum RedirectConditionType: string case DEVICE = 'device'; case LANGUAGE = 'language'; case QUERY_PARAM = 'query-param'; + case ANY_VALUE_QUERY_PARAM = 'any-value-query-param'; + case VALUELESS_QUERY_PARAM = 'valueless-query-param'; case IP_ADDRESS = 'ip-address'; case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code'; case GEOLOCATION_CITY_NAME = 'geolocation-city-name'; @@ -45,6 +47,9 @@ enum RedirectConditionType: string 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW', ]), + RedirectConditionType::ANY_VALUE_QUERY_PARAM, RedirectConditionType::VALUELESS_QUERY_PARAM => $value !== '', + // FIXME We should at least validate the value is not empty + // default => $value !== '', default => true, }; } From 47293be85c1ca67179d41b3930d1e4347a6b2da7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 17 Jul 2025 08:37:03 +0200 Subject: [PATCH 2/4] Enhance RedirectConditionTest with new query-param-related conditions --- CHANGELOG.md | 2 +- .../Entity/RedirectConditionTest.php | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac74a67b..383f89a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Added * [#2438](https://github.com/shlinkio/shlink/issues/2438) Add `MERCURE_ENABLED` env var and corresponding config option, to more easily allow the mercure integration to be toggled. - For BC, if this env vars is not present, we'll still consider the integration enabled if the `MERCURE_PUBLIC_HUB_URL` env var has a value. This is considered deprecated though, and next major version will rely only on `MERCURE_ENABLED`, so if you are using Mercure, make sure to set `MERCURE_ENABLED=true` to be ready. + For BC, if this env var is not present, we'll still consider the integration enabled if the `MERCURE_PUBLIC_HUB_URL` env var has a value. This is considered deprecated though, and next major version will rely only on `MERCURE_ENABLED`, so if you are using Mercure, make sure to set `MERCURE_ENABLED=true` to be ready. * [#2387](https://github.com/shlinkio/shlink/issues/2387) Add `REAL_TIME_UPDATES_TOPICS` env var and corresponding config option, to granularly decide which real-time updates topics should be enabled. * [#2418](https://github.com/shlinkio/shlink/issues/2418) Add more granular control over how Shlink handles CORS. It is now possible to customize the `Access-Control-Allow-Origin`, `Access-Control-Max-Age` and `Access-Control-Allow-Credentials` headers via env vars or config options. diff --git a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php index bec6d263..28e3f6b1 100644 --- a/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php +++ b/module/Core/test/RedirectRule/Entity/RedirectConditionTest.php @@ -32,6 +32,33 @@ class RedirectConditionTest extends TestCase self::assertEquals($expectedResult, $result); } + #[Test] + #[TestWith(['nop', '', false])] // param not present + #[TestWith(['foo', '', true])] + #[TestWith(['foo', 'something', true])] + #[TestWith(['foo', 'something else', true])] + public function matchesAnyValueQueryParams(string $param, string $value, bool $expectedResult): void + { + $request = ServerRequestFactory::fromGlobals()->withQueryParams(['foo' => $value]); + $result = RedirectCondition::forAnyValueQueryParam($param)->matchesRequest($request); + + self::assertEquals($expectedResult, $result); + } + + #[Test] + #[TestWith(['nop', '', false])] // param not present + #[TestWith(['foo', '', true])] + #[TestWith(['foo', null, true])] + #[TestWith(['foo', 'something', false])] + #[TestWith(['foo', 'something else', false])] + public function matchesValuelessQueryParams(string $param, string|null $value, bool $expectedResult): void + { + $request = ServerRequestFactory::fromGlobals()->withQueryParams(['foo' => $value]); + $result = RedirectCondition::forValuelessQueryParam($param)->matchesRequest($request); + + self::assertEquals($expectedResult, $result); + } + #[Test] #[TestWith([null, '', false], 'no accept language')] #[TestWith(['', '', false], 'empty accept language')] @@ -141,6 +168,8 @@ class RedirectConditionTest extends TestCase #[TestWith([RedirectConditionType::DEVICE->value, RedirectConditionType::DEVICE])] #[TestWith([RedirectConditionType::LANGUAGE->value, RedirectConditionType::LANGUAGE])] #[TestWith([RedirectConditionType::QUERY_PARAM->value, RedirectConditionType::QUERY_PARAM])] + #[TestWith([RedirectConditionType::ANY_VALUE_QUERY_PARAM->value, RedirectConditionType::ANY_VALUE_QUERY_PARAM])] + #[TestWith([RedirectConditionType::VALUELESS_QUERY_PARAM->value, RedirectConditionType::VALUELESS_QUERY_PARAM])] #[TestWith([RedirectConditionType::IP_ADDRESS->value, RedirectConditionType::IP_ADDRESS])] #[TestWith( [RedirectConditionType::GEOLOCATION_COUNTRY_CODE->value, RedirectConditionType::GEOLOCATION_COUNTRY_CODE], From 223901324f74fb1dcd8189279df55515791f4f76 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 17 Jul 2025 08:44:19 +0200 Subject: [PATCH 3/4] Enhance RedirectRuleHandlerTest with new query-param-related conditions --- module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php index aa6e0ac7..aed8f3c5 100644 --- a/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php +++ b/module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php @@ -162,6 +162,14 @@ class RedirectRuleHandlerTest extends TestCase yield 'device' => [RedirectConditionType::DEVICE, [RedirectCondition::forDevice(DeviceType::ANDROID)]]; yield 'language' => [RedirectConditionType::LANGUAGE, [RedirectCondition::forLanguage('en-US')]]; yield 'query param' => [RedirectConditionType::QUERY_PARAM, [RedirectCondition::forQueryParam('foo', 'bar')]]; + yield 'any value query param' => [ + RedirectConditionType::ANY_VALUE_QUERY_PARAM, + [RedirectCondition::forAnyValueQueryParam('foo')], + ]; + yield 'valueless query param' => [ + RedirectConditionType::VALUELESS_QUERY_PARAM, + [RedirectCondition::forValuelessQueryParam('foo')], + ]; yield 'multiple query params' => [ RedirectConditionType::QUERY_PARAM, [RedirectCondition::forQueryParam('foo', 'bar'), RedirectCondition::forQueryParam('foo', 'bar')], From c3d3cc62880a75602598bf30a6095dda89f1f993 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 17 Jul 2025 08:51:59 +0200 Subject: [PATCH 4/4] Test RedirectConditionType::isValid() in isolation --- config/constants.php | 22 +++++++++++++++ .../Model/RedirectConditionType.php | 26 ++++------------- .../Model/RedirectConditionTypeTest.php | 28 +++++++++++++++++++ 3 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 module/Core/test/RedirectRule/Model/RedirectConditionTypeTest.php diff --git a/config/constants.php b/config/constants.php index f5dff4d5..28440653 100644 --- a/config/constants.php +++ b/config/constants.php @@ -16,6 +16,28 @@ const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a s const IP_ADDRESS_REQUEST_ATTRIBUTE = 'remote_address'; const REDIRECT_URL_REQUEST_ATTRIBUTE = 'redirect_url'; +/** + * List of ISO 3166-1 alpha-2 two-letter country codes https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + */ +const ISO_COUNTRY_CODES = [ + 'AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', 'AU', 'AT', 'AZ', + 'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR', + 'IO', 'BN', 'BG', 'BF', 'BI', 'CV', 'KH', 'CM', 'CA', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC', + 'CO', 'KM', 'CG', 'CD', 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO', + 'EC', 'EG', 'SV', 'GQ', 'ER', 'EE', 'SZ', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', 'TF', + 'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY', + 'HT', 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM', + 'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', 'KP', 'KR', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', + 'LI', 'LT', 'LU', 'MO', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', 'MX', + 'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI', + 'NE', 'NG', 'NU', 'NF', 'MK', 'MP', 'NO', 'OM', 'PK', 'PW', 'PS', 'PA', 'PG', 'PY', 'PE', 'PH', + 'PN', 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC', + 'WS', 'SM', 'ST', 'SA', 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', + 'SS', 'ES', 'LK', 'SD', 'SR', 'SJ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', 'TL', 'TG', 'TK', + 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', + 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW', +]; + /** @deprecated */ const DEFAULT_QR_CODE_SIZE = 300; /** @deprecated */ diff --git a/module/Core/src/RedirectRule/Model/RedirectConditionType.php b/module/Core/src/RedirectRule/Model/RedirectConditionType.php index bcd482e7..8f7657e3 100644 --- a/module/Core/src/RedirectRule/Model/RedirectConditionType.php +++ b/module/Core/src/RedirectRule/Model/RedirectConditionType.php @@ -8,6 +8,8 @@ use Shlinkio\Shlink\Core\Util\IpAddressUtils; use function Shlinkio\Shlink\Core\ArrayUtils\contains; use function Shlinkio\Shlink\Core\enumValues; +use const Shlinkio\Shlink\ISO_COUNTRY_CODES; + enum RedirectConditionType: string { case DEVICE = 'device'; @@ -28,26 +30,10 @@ enum RedirectConditionType: string RedirectConditionType::DEVICE => contains($value, enumValues(DeviceType::class)), // RedirectConditionType::LANGUAGE => TODO Validate at least format, RedirectConditionType::IP_ADDRESS => IpAddressUtils::isStaticIpCidrOrWildcard($value), - RedirectConditionType::GEOLOCATION_COUNTRY_CODE => contains($value, [ - // List of ISO 3166-1 alpha-2 two-letter country codes https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - 'AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', 'AU', 'AT', 'AZ', - 'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR', - 'IO', 'BN', 'BG', 'BF', 'BI', 'CV', 'KH', 'CM', 'CA', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC', - 'CO', 'KM', 'CG', 'CD', 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO', - 'EC', 'EG', 'SV', 'GQ', 'ER', 'EE', 'SZ', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', 'TF', - 'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY', - 'HT', 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM', - 'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', 'KP', 'KR', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', - 'LI', 'LT', 'LU', 'MO', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', 'MX', - 'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI', - 'NE', 'NG', 'NU', 'NF', 'MK', 'MP', 'NO', 'OM', 'PK', 'PW', 'PS', 'PA', 'PG', 'PY', 'PE', 'PH', - 'PN', 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC', - 'WS', 'SM', 'ST', 'SA', 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', - 'SS', 'ES', 'LK', 'SD', 'SR', 'SJ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', 'TL', 'TG', 'TK', - 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', - 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW', - ]), - RedirectConditionType::ANY_VALUE_QUERY_PARAM, RedirectConditionType::VALUELESS_QUERY_PARAM => $value !== '', + RedirectConditionType::GEOLOCATION_COUNTRY_CODE => contains($value, ISO_COUNTRY_CODES), + RedirectConditionType::QUERY_PARAM, + RedirectConditionType::ANY_VALUE_QUERY_PARAM, + RedirectConditionType::VALUELESS_QUERY_PARAM => $value !== '', // FIXME We should at least validate the value is not empty // default => $value !== '', default => true, diff --git a/module/Core/test/RedirectRule/Model/RedirectConditionTypeTest.php b/module/Core/test/RedirectRule/Model/RedirectConditionTypeTest.php new file mode 100644 index 00000000..1c68a0f4 --- /dev/null +++ b/module/Core/test/RedirectRule/Model/RedirectConditionTypeTest.php @@ -0,0 +1,28 @@ +isValid($value)); + } +}