CMS 2.0 - Theme engine, logging, admin improvements

Major changes:
- New ThemeManager with Twig templating and SCSS compilation
- Dynamic themes system (themes/default, themes/demo)
- LogManager with SQLite storage and syslog forwarding
- RequestLogger with static helper methods
- Admin UI overhaul (Bootstrap 5, dark mode)
- Admin config page with logging and theme settings
- Admin logs page with filters and search
- Removed legacy Mustache templates
- Removed test plugin and theme
- Composer dependencies: Twig, scssphp, CommonMark, MaxMind GeoIP
This commit is contained in:
2026-08-08 18:02:14 +02:00
parent d453b8073f
commit 6333bc410f
833 changed files with 108974 additions and 1386 deletions
+73
View File
@@ -44,6 +44,79 @@ class RequestLogger
return $ip;
}
/**
* Check whether an IP matches any entry in a list of IPs/CIDR ranges.
*
* Supports exact IPv4/IPv6 addresses and CIDR notation (e.g. 192.168.0.0/16).
*
* @param string $ip The client IP to test
* @param array $list List of IPs and/or CIDR ranges
* @return bool True if the IP matches any entry
*/
public static function ipMatchesList(string $ip, array $list): bool
{
$ip = trim($ip);
if ($ip === '') {
return false;
}
$isV6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
$packed = $isV6 ? inet_pton($ip) : inet_pton($ip);
foreach ($list as $entry) {
$entry = trim((string)$entry);
if ($entry === '') {
continue;
}
// Exact match
if ($entry === $ip) {
return true;
}
// CIDR notation
if (strpos($entry, '/') !== false) {
[$subnet, $bits] = array_pad(explode('/', $entry, 2), 2, null);
$subnet = trim($subnet);
$subnetPacked = inet_pton($subnet);
if ($subnetPacked === false || $packed === false) {
continue;
}
// Ensure both are the same address family
if (strlen($subnetPacked) !== strlen($packed)) {
continue;
}
$maxBits = strlen($packed) * 8;
$bits = (int)$bits;
if ($bits < 0 || $bits > $maxBits) {
continue;
}
if ($bits === 0) {
return true;
}
$fullBytes = intdiv($bits, 8);
$remainingBits = $bits % 8;
$match = true;
for ($i = 0; $i < $fullBytes; $i++) {
if ($subnetPacked[$i] !== $packed[$i]) {
$match = false;
break;
}
}
if ($match && $remainingBits > 0) {
$mask = 0xFF << (8 - $remainingBits);
if ((ord($subnetPacked[$fullBytes]) & $mask) !== (ord($packed[$fullBytes]) & $mask)) {
$match = false;
}
}
if ($match) {
return true;
}
}
}
return false;
}
public static function getClientIp(): string
{
$headerKeys = [