Deploy Ladill Link / deploy (push) Successful in 39s
Resolve countries from CDN headers first, then MaxMind GeoLite2 via visitor IP, with an artisan command to download and refresh the database. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
2.8 KiB
PHP
112 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Support\Link;
|
|
|
|
use GeoIp2\Database\Reader;
|
|
use GeoIp2\Exception\AddressNotFoundException;
|
|
use Illuminate\Http\Request;
|
|
|
|
final class LinkGeoResolver
|
|
{
|
|
/** @var list<string> */
|
|
private const HEADER_KEYS = ['CF-IPCountry', 'X-Country-Code', 'CloudFront-Viewer-Country'];
|
|
|
|
private static ?Reader $reader = null;
|
|
|
|
public static function countryCode(Request $request): ?string
|
|
{
|
|
if ($fromHeader = self::countryFromHeaders($request)) {
|
|
return $fromHeader;
|
|
}
|
|
|
|
return self::countryFromIp($request->ip());
|
|
}
|
|
|
|
public static function countryFromIp(?string $ip): ?string
|
|
{
|
|
if (! self::isPublicIp($ip)) {
|
|
return null;
|
|
}
|
|
|
|
$reader = self::reader();
|
|
if ($reader === null) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$code = strtoupper((string) ($reader->country((string) $ip)->country->isoCode ?? ''));
|
|
|
|
return strlen($code) === 2 && ! in_array($code, ['XX', 'T1'], true) ? $code : null;
|
|
} catch (AddressNotFoundException) {
|
|
return null;
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static function countryLabel(?string $code): string
|
|
{
|
|
if ($code === null || $code === '') {
|
|
return 'Unknown';
|
|
}
|
|
|
|
if (function_exists('locale_get_display_region')) {
|
|
$name = locale_get_display_region('_'.$code, 'en');
|
|
|
|
return is_string($name) && $name !== '' ? $name : $code;
|
|
}
|
|
|
|
return $code;
|
|
}
|
|
|
|
/** Visible for tests. */
|
|
public static function resetReader(): void
|
|
{
|
|
self::$reader = null;
|
|
}
|
|
|
|
private static function countryFromHeaders(Request $request): ?string
|
|
{
|
|
foreach (self::HEADER_KEYS as $header) {
|
|
$value = $request->header($header);
|
|
if (! is_string($value) || strlen($value) !== 2) {
|
|
continue;
|
|
}
|
|
|
|
$code = strtoupper($value);
|
|
if (! in_array($code, ['XX', 'T1'], true)) {
|
|
return $code;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static function isPublicIp(?string $ip): bool
|
|
{
|
|
return is_string($ip)
|
|
&& $ip !== ''
|
|
&& filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
|
|
}
|
|
|
|
private static function reader(): ?Reader
|
|
{
|
|
if (self::$reader !== null) {
|
|
return self::$reader;
|
|
}
|
|
|
|
$path = (string) config('link.geoip_database', '');
|
|
if ($path === '' || ! is_readable($path)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
self::$reader = new Reader($path);
|
|
|
|
return self::$reader;
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|