mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 04:03:12 +08:00
Add support for any-value and valueless query param redirect rules
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"device",
|
||||
"language",
|
||||
"query-param",
|
||||
"any-value-query-param",
|
||||
"valueless-query-param",
|
||||
"ip-address",
|
||||
"geolocation-country-code",
|
||||
"geolocation-city-name"
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user