mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-21 00:24:27 -06:00
Add support for city name dynamic redirects
This commit is contained in:
parent
dbef32ffcb
commit
a6e0916272
@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
* [#1774](https://github.com/shlinkio/shlink/issues/1774) Add new geolocation redirect rules for the dynamic redirects system.
|
||||
|
||||
* `geolocation-country-code`: Allows to perform redirections based on the ISO 3166-1 alpha-2 two-letter country code resolved while geolocating the visitor.
|
||||
* `geolocation-city-name`: Allows to perform redirections based on the city name resolved while geolocating the visitor.
|
||||
|
||||
### Changed
|
||||
* [#2193](https://github.com/shlinkio/shlink/issues/2193) API keys are now hashed using SHA256, instead of being saved in plain text.
|
||||
|
@ -15,7 +15,14 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["device", "language", "query-param", "ip-address", "geolocation-country-code"],
|
||||
"enum": [
|
||||
"device",
|
||||
"language",
|
||||
"query-param",
|
||||
"ip-address",
|
||||
"geolocation-country-code",
|
||||
"geolocation-city-name"
|
||||
],
|
||||
"description": "The type of the condition, which will determine the logic used to match it"
|
||||
},
|
||||
"matchKey": {
|
||||
|
@ -113,6 +113,9 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
||||
),
|
||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => RedirectCondition::forGeolocationCountryCode(
|
||||
$this->askMandatory('Country code to match?', $io),
|
||||
),
|
||||
RedirectConditionType::GEOLOCATION_CITY_NAME => RedirectCondition::forGeolocationCityName(
|
||||
$this->askMandatory('City name to match?', $io),
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -118,6 +118,7 @@ class RedirectRuleHandlerTest extends TestCase
|
||||
'Query param value?' => 'bar',
|
||||
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
|
||||
'Country code to match?' => 'FR',
|
||||
'City name to match?' => 'Los angeles',
|
||||
default => '',
|
||||
},
|
||||
);
|
||||
@ -170,6 +171,10 @@ class RedirectRuleHandlerTest extends TestCase
|
||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE,
|
||||
[RedirectCondition::forGeolocationCountryCode('FR')],
|
||||
];
|
||||
yield 'Geolocation city name' => [
|
||||
RedirectConditionType::GEOLOCATION_CITY_NAME,
|
||||
[RedirectCondition::forGeolocationCityName('Los angeles')],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
@ -59,6 +59,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
||||
return new self(RedirectConditionType::GEOLOCATION_COUNTRY_CODE, $countryCode);
|
||||
}
|
||||
|
||||
public static function forGeolocationCityName(string $cityName): self
|
||||
{
|
||||
return new self(RedirectConditionType::GEOLOCATION_CITY_NAME, $cityName);
|
||||
}
|
||||
|
||||
public static function fromRawData(array $rawData): self
|
||||
{
|
||||
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]);
|
||||
@ -79,6 +84,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
||||
RedirectConditionType::DEVICE => $this->matchesDevice($request),
|
||||
RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request),
|
||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => $this->matchesGeolocationCountryCode($request),
|
||||
RedirectConditionType::GEOLOCATION_CITY_NAME => $this->matchesGeolocationCityName($request),
|
||||
};
|
||||
}
|
||||
|
||||
@ -137,6 +143,18 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
||||
return strcasecmp($geolocation->countryCode, $this->matchValue) === 0;
|
||||
}
|
||||
|
||||
private function matchesGeolocationCityName(ServerRequestInterface $request): bool
|
||||
{
|
||||
$geolocation = $request->getAttribute(Location::class);
|
||||
// TODO We should eventually rely on `Location` type only
|
||||
if (! ($geolocation instanceof Location) && ! ($geolocation instanceof VisitLocation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cityName = $geolocation instanceof Location ? $geolocation->city : $geolocation->cityName;
|
||||
return strcasecmp($cityName, $this->matchValue) === 0;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
@ -158,6 +176,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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ enum RedirectConditionType: string
|
||||
case QUERY_PARAM = 'query-param';
|
||||
case IP_ADDRESS = 'ip-address';
|
||||
case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code';
|
||||
case GEOLOCATION_CITY_NAME = 'geolocation-city-name';
|
||||
|
||||
/**
|
||||
* Tells if a value is valid for the condition type
|
||||
|
@ -97,7 +97,7 @@ class RedirectConditionTest extends TestCase
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideVisits')]
|
||||
#[Test, DataProvider('provideVisitsWithCountry')]
|
||||
public function matchesGeolocationCountryCode(
|
||||
Location|VisitLocation|null $location,
|
||||
string $countryCodeToMatch,
|
||||
@ -108,7 +108,7 @@ class RedirectConditionTest extends TestCase
|
||||
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
public static function provideVisits(): iterable
|
||||
public static function provideVisitsWithCountry(): iterable
|
||||
{
|
||||
yield 'no location' => [null, 'US', false];
|
||||
yield 'non-matching location' => [new Location(countryCode: 'ES'), 'US', false];
|
||||
@ -125,4 +125,33 @@ class RedirectConditionTest extends TestCase
|
||||
true,
|
||||
];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideVisitsWithCity')]
|
||||
public function matchesGeolocationCityName(
|
||||
Location|VisitLocation|null $location,
|
||||
string $cityNameToMatch,
|
||||
bool $expected,
|
||||
): void {
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute(Location::class, $location);
|
||||
$result = RedirectCondition::forGeolocationCityName($cityNameToMatch)->matchesRequest($request);
|
||||
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
public static function provideVisitsWithCity(): iterable
|
||||
{
|
||||
yield 'no location' => [null, 'New York', false];
|
||||
yield 'non-matching location' => [new Location(city: 'Los Angeles'), 'New York', false];
|
||||
yield 'matching location' => [new Location(city: 'Madrid'), 'Madrid', true];
|
||||
yield 'matching case-insensitive' => [new Location(city: 'Los Angeles'), 'los angeles', true];
|
||||
yield 'matching visit location' => [
|
||||
VisitLocation::fromGeolocation(new Location(city: 'New York')),
|
||||
'New York',
|
||||
true,
|
||||
];
|
||||
yield 'matching visit case-insensitive' => [
|
||||
VisitLocation::fromGeolocation(new Location(city: 'barcelona')),
|
||||
'Barcelona',
|
||||
true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user