mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-24 09:50:17 -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.
|
* [#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-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
|
### Changed
|
||||||
* [#2193](https://github.com/shlinkio/shlink/issues/2193) API keys are now hashed using SHA256, instead of being saved in plain text.
|
* [#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": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"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"
|
"description": "The type of the condition, which will determine the logic used to match it"
|
||||||
},
|
},
|
||||||
"matchKey": {
|
"matchKey": {
|
||||||
|
@ -113,6 +113,9 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
|||||||
),
|
),
|
||||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => RedirectCondition::forGeolocationCountryCode(
|
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => RedirectCondition::forGeolocationCountryCode(
|
||||||
$this->askMandatory('Country code to match?', $io),
|
$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',
|
'Query param value?' => 'bar',
|
||||||
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
|
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
|
||||||
'Country code to match?' => 'FR',
|
'Country code to match?' => 'FR',
|
||||||
|
'City name to match?' => 'Los angeles',
|
||||||
default => '',
|
default => '',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -170,6 +171,10 @@ class RedirectRuleHandlerTest extends TestCase
|
|||||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE,
|
RedirectConditionType::GEOLOCATION_COUNTRY_CODE,
|
||||||
[RedirectCondition::forGeolocationCountryCode('FR')],
|
[RedirectCondition::forGeolocationCountryCode('FR')],
|
||||||
];
|
];
|
||||||
|
yield 'Geolocation city name' => [
|
||||||
|
RedirectConditionType::GEOLOCATION_CITY_NAME,
|
||||||
|
[RedirectCondition::forGeolocationCityName('Los angeles')],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
@ -59,6 +59,11 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
|||||||
return new self(RedirectConditionType::GEOLOCATION_COUNTRY_CODE, $countryCode);
|
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
|
public static function fromRawData(array $rawData): self
|
||||||
{
|
{
|
||||||
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]);
|
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]);
|
||||||
@ -79,6 +84,7 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
|
|||||||
RedirectConditionType::DEVICE => $this->matchesDevice($request),
|
RedirectConditionType::DEVICE => $this->matchesDevice($request),
|
||||||
RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request),
|
RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request),
|
||||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => $this->matchesGeolocationCountryCode($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;
|
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
|
public function jsonSerialize(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -158,6 +176,7 @@ 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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ enum RedirectConditionType: string
|
|||||||
case QUERY_PARAM = 'query-param';
|
case QUERY_PARAM = 'query-param';
|
||||||
case IP_ADDRESS = 'ip-address';
|
case IP_ADDRESS = 'ip-address';
|
||||||
case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code';
|
case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code';
|
||||||
|
case GEOLOCATION_CITY_NAME = 'geolocation-city-name';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if a value is valid for the condition type
|
* Tells if a value is valid for the condition type
|
||||||
|
@ -97,7 +97,7 @@ class RedirectConditionTest extends TestCase
|
|||||||
self::assertEquals($expected, $result);
|
self::assertEquals($expected, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test, DataProvider('provideVisits')]
|
#[Test, DataProvider('provideVisitsWithCountry')]
|
||||||
public function matchesGeolocationCountryCode(
|
public function matchesGeolocationCountryCode(
|
||||||
Location|VisitLocation|null $location,
|
Location|VisitLocation|null $location,
|
||||||
string $countryCodeToMatch,
|
string $countryCodeToMatch,
|
||||||
@ -108,7 +108,7 @@ class RedirectConditionTest extends TestCase
|
|||||||
|
|
||||||
self::assertEquals($expected, $result);
|
self::assertEquals($expected, $result);
|
||||||
}
|
}
|
||||||
public static function provideVisits(): iterable
|
public static function provideVisitsWithCountry(): iterable
|
||||||
{
|
{
|
||||||
yield 'no location' => [null, 'US', false];
|
yield 'no location' => [null, 'US', false];
|
||||||
yield 'non-matching location' => [new Location(countryCode: 'ES'), 'US', false];
|
yield 'non-matching location' => [new Location(countryCode: 'ES'), 'US', false];
|
||||||
@ -125,4 +125,33 @@ class RedirectConditionTest extends TestCase
|
|||||||
true,
|
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