From 3f4f2f2d43adabc479e133641fb1d6fb65bc4f3d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 25 Jun 2026 12:46:35 -0500 Subject: [PATCH] feat: auto-enrich IPs with country flags across all admin pages DB-IP plugin onAfterRender scans admin HTML for IP addresses in 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. --- .../src/Extension/DBIP.php | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/source/packages/plg_system_mokosuiteclient_dbip/src/Extension/DBIP.php b/source/packages/plg_system_mokosuiteclient_dbip/src/Extension/DBIP.php index bacfe289..c5d3f6c7 100644 --- a/source/packages/plg_system_mokosuiteclient_dbip/src/Extension/DBIP.php +++ b/source/packages/plg_system_mokosuiteclient_dbip/src/Extension/DBIP.php @@ -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 = '#]*)?>(' . $ipv4 . '|' . $ipv6 . ')#'; + + $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 . ' ' . $escaped . ''; + }, $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; + } }