Skip to content

Commit

Permalink
Add new geolocatio-country-code redirect condition type
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Nov 12, 2024
1 parent 15cb3bb commit 7051acb
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 3 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"shlinkio/shlink-event-dispatcher": "^4.1",
"shlinkio/shlink-importer": "^5.3.2",
"shlinkio/shlink-installer": "^9.2",
"shlinkio/shlink-ip-geolocation": "^4.1",
"shlinkio/shlink-ip-geolocation": "dev-main#fadae5d as 4.2",
"shlinkio/shlink-json": "^1.1",
"spiral/roadrunner": "^2024.1",
"spiral/roadrunner-cli": "^2.6",
Expand Down
3 changes: 3 additions & 0 deletions module/CLI/src/RedirectRule/RedirectRuleHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ private function addRule(ShortUrl $shortUrl, StyleInterface $io, array $currentR
RedirectConditionType::IP_ADDRESS => RedirectCondition::forIpAddress(
$this->askMandatory('IP address, CIDR block or wildcard-pattern (1.2.*.*)', $io),
),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => RedirectCondition::forGeolocationCountryCode(
$this->askMandatory('Country code to match?', $io),
)

Check warning on line 116 in module/CLI/src/RedirectRule/RedirectRuleHandler.php

View check run for this annotation

Codecov / codecov/patch

module/CLI/src/RedirectRule/RedirectRuleHandler.php#L114-L116

Added lines #L114 - L116 were not covered by tests
};

$continue = $io->confirm('Do you want to add another condition?');
Expand Down
22 changes: 20 additions & 2 deletions module/Core/src/RedirectRule/Entity/RedirectCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter;
use Shlinkio\Shlink\Core\Util\IpAddressUtils;
use Shlinkio\Shlink\IpGeolocation\Model\Location;

use function Shlinkio\Shlink\Core\acceptLanguageToLocales;
use function Shlinkio\Shlink\Core\ArrayUtils\some;
use function Shlinkio\Shlink\Core\ipAddressFromRequest;
use function Shlinkio\Shlink\Core\normalizeLocale;
use function Shlinkio\Shlink\Core\splitLocale;
use function sprintf;
use function strtolower;
use function strcasecmp;
use function trim;

class RedirectCondition extends AbstractEntity implements JsonSerializable
Expand Down Expand Up @@ -52,6 +53,11 @@ public static function forIpAddress(string $ipAddressPattern): self
return new self(RedirectConditionType::IP_ADDRESS, $ipAddressPattern);
}

public static function forGeolocationCountryCode(string $countryCode): self
{
return new self(RedirectConditionType::GEOLOCATION_COUNTRY_CODE, $countryCode);
}

public static function fromRawData(array $rawData): self
{
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]);
Expand All @@ -71,6 +77,7 @@ public function matchesRequest(ServerRequestInterface $request): bool
RedirectConditionType::LANGUAGE => $this->matchesLanguage($request),
RedirectConditionType::DEVICE => $this->matchesDevice($request),
RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => $this->matchesGeolocationCountryCode($request),
};
}

Expand Down Expand Up @@ -109,7 +116,7 @@ static function (string $lang) use ($matchLanguage, $matchCountryCode): bool {
private function matchesDevice(ServerRequestInterface $request): bool
{
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
return $device !== null && $device->value === strtolower($this->matchValue);
return $device !== null && $device->value === $this->matchValue;
}

private function matchesRemoteIpAddress(ServerRequestInterface $request): bool
Expand All @@ -118,6 +125,16 @@ private function matchesRemoteIpAddress(ServerRequestInterface $request): bool
return $remoteAddress !== null && IpAddressUtils::ipAddressMatchesGroups($remoteAddress, [$this->matchValue]);
}

private function matchesGeolocationCountryCode(ServerRequestInterface $request): bool
{
$geolocation = $request->getAttribute(Location::class);
if (!($geolocation instanceof Location)) {
return false;
}

return strcasecmp($geolocation->countryCode, $this->matchValue) === 0;
}

public function jsonSerialize(): array
{
return [
Expand All @@ -138,6 +155,7 @@ public function toHumanFriendly(): string
$this->matchValue,
),
RedirectConditionType::IP_ADDRESS => sprintf('IP address matches %s', $this->matchValue),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => sprintf('country code is %s', $this->matchValue),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum RedirectConditionType: string
case LANGUAGE = 'language';
case QUERY_PARAM = 'query-param';
case IP_ADDRESS = 'ip-address';
case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code';
}
18 changes: 18 additions & 0 deletions module/Core/test/RedirectRule/Entity/RedirectConditionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
namespace ShlinkioTest\Shlink\Core\RedirectRule\Entity;

use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
use Shlinkio\Shlink\IpGeolocation\Model\Location;

use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
use const ShlinkioTest\Shlink\DESKTOP_USER_AGENT;
Expand Down Expand Up @@ -93,4 +95,20 @@ public function matchesRemoteIpAddress(string|null $remoteIp, string $ipToMatch,

self::assertEquals($expected, $result);
}

#[Test, DataProvider('provideVisits')]
public function matchesGeolocationCountryCode(Location|null $location, string $countryCodeToMatch, bool $expected): void
{
$request = ServerRequestFactory::fromGlobals()->withAttribute(Location::class, $location);
$result = RedirectCondition::forGeolocationCountryCode($countryCodeToMatch)->matchesRequest($request);

self::assertEquals($expected, $result);
}
public static function provideVisits(): iterable
{
yield 'no location' => [null, 'US', false];
yield 'non-matching location' => [new Location(countryCode: 'ES'), 'US', false];
yield 'matching location' => [new Location(countryCode: 'US'), 'US', true];
yield 'matching case-insensitive' => [new Location(countryCode: 'US'), 'us', true];
}
}

0 comments on commit 7051acb

Please sign in to comment.