feat: auto-enrich IPs with country flags across all admin pages
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s

DB-IP plugin onAfterRender scans admin HTML for IP addresses in
<code> tags, looks up geolocation, prepends country flag emoji and
adds city/region/country hover tooltip. Caches lookups per request,
skips private/loopback IPs and already-enriched elements.
This commit is contained in:
2026-06-25 12:46:35 -05:00
parent 7ac776ff28
commit 3f4f2f2d43
@@ -23,6 +23,7 @@ class DBIP extends CMSPlugin implements SubscriberInterface
{
return [
'onAfterInitialise' => 'onAfterInitialise',
'onAfterRender' => 'onAfterRender',
];
}
@@ -80,4 +81,92 @@ class DBIP extends CMSPlugin implements SubscriberInterface
DBIPHelper::downloadCityDb($url);
}
/**
* Scan rendered admin HTML for IP addresses and enrich with geo flags + tooltips.
*/
public function onAfterRender(): void
{
$app = $this->getApplication();
if (!$app->isClient('administrator'))
{
return;
}
if ($app->getDocument()->getType() !== 'html')
{
return;
}
$body = $app->getBody();
if (empty($body))
{
return;
}
$ipv4 = '(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)';
$ipv6 = '(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))?';
$pattern = '#<code(?:\s[^>]*)?>(' . $ipv4 . '|' . $ipv6 . ')</code>#';
$cache = [];
$newBody = preg_replace_callback($pattern, function ($m) use (&$cache) {
$fullMatch = $m[0];
$ip = $m[1];
// Skip private/loopback
if (filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE) === false)
{
return $fullMatch;
}
// Already enriched (has a title attribute)
if (strpos($fullMatch, 'title=') !== false)
{
return $fullMatch;
}
if (!isset($cache[$ip]))
{
$cache[$ip] = DBIPHelper::lookup($ip);
}
$geo = $cache[$ip];
if ($geo === null || empty($geo['country_code']))
{
return $fullMatch;
}
$cc = strtoupper($geo['country_code']);
$flag = self::countryFlag($cc);
$parts = array_filter([$geo['city'], $geo['region'], $geo['country_name']]);
$tooltip = htmlspecialchars(implode(', ', $parts), \ENT_QUOTES, 'UTF-8');
$escaped = htmlspecialchars($ip, \ENT_QUOTES, 'UTF-8');
return $flag . ' <code title="' . $tooltip . '" style="cursor:help;">' . $escaped . '</code>';
}, $body);
if ($newBody !== null && $newBody !== $body)
{
$app->setBody($newBody);
}
}
private static function countryFlag(string $cc): string
{
if (\strlen($cc) !== 2)
{
return '';
}
$cc = strtoupper($cc);
$first = mb_chr(0x1F1E6 + \ord($cc[0]) - \ord('A'));
$second = mb_chr(0x1F1E6 + \ord($cc[1]) - \ord('A'));
return $first . $second;
}
}