/docs/intersection/intersection.php // — climb two levels to find `config/`. The list file sits next to the // script (same directory), so anyone cloning the repo can edit it in // place without touching the project root. $projectRoot = dirname(__DIR__, 2); $configDir = $projectRoot . '/config'; $listPath = __DIR__ . '/cidr4.txt'; if (!is_dir($configDir)) { fwrite(STDERR, "config/ directory not found at {$configDir}\n"); exit(1); } if (!is_file($listPath)) { fwrite(STDERR, "cidr4.txt not found at {$listPath}\n"); exit(1); } $intersections = []; foreach (file($listPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') { continue; } // Flip-map so membership checks below are O(1). $intersections[$line] = true; } if (!$intersections) { fwrite(STDERR, "cidr4.txt is empty — nothing to do\n"); exit(0); } fwrite(STDOUT, sprintf("Loaded %d CIDR(s) from cidr4.txt\n", count($intersections))); $jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; // Key order that mirrors Site::getConfig(), so the file stays consistent // with what the server writes after a reload. Any unknown keys are appended // at the tail to preserve forward-compatibility with future config fields. $orderedKeys = ['domains', 'dns', 'timeout', 'ip4', 'ip6', 'cidr4', 'cidr6', 'external', 'replace']; $touched = 0; $keysAdded = 0; foreach (new DirectoryIterator($configDir) as $groupDir) { if ($groupDir->isDot() || !$groupDir->isDir()) { continue; } foreach (new DirectoryIterator($groupDir->getPathname()) as $file) { if (!$file->isFile() || $file->getExtension() !== 'json') { continue; } $path = $file->getPathname(); $raw = file_get_contents($path); $config = json_decode($raw); if (!$config instanceof stdClass) { fwrite(STDERR, "skip {$path}: top-level JSON is not an object\n"); continue; } $cidr4 = $config->cidr4 ?? null; if (!is_array($cidr4) || !$cidr4) { continue; } // Which of this portal's cidr4 entries are also in cidr4.txt? $hits = []; foreach ($cidr4 as $cidr) { if (is_string($cidr) && isset($intersections[$cidr])) { $hits[] = $cidr; } } if (!$hits) { continue; } // Pull or create the replace block. Guard against admins having put // something non-object in there manually — log and skip rather than // overwrite silently. $replace = $config->replace ?? new stdClass(); if (!$replace instanceof stdClass) { fwrite(STDERR, "skip {$path}: `replace` is present but not an object\n"); continue; } $cidr4Map = $replace->cidr4 ?? new stdClass(); if (!$cidr4Map instanceof stdClass) { fwrite(STDERR, "skip {$path}: `replace.cidr4` is present but not an object\n"); continue; } $cidr6Map = $replace->cidr6 ?? new stdClass(); if (!$cidr6Map instanceof stdClass) { fwrite(STDERR, "skip {$path}: `replace.cidr6` is present but not an object\n"); continue; } $added = 0; foreach ($hits as $cidr) { if (!isset($cidr4Map->{$cidr})) { // Empty array — escalation at next reload will fill it with // /32 entries pulled from $site->ip4 via IP4Helper::growReplace. $cidr4Map->{$cidr} = []; $added++; } } if ($added === 0) { continue; } $replace->cidr4 = $cidr4Map; $replace->cidr6 = $cidr6Map; $config->replace = $replace; // Rebuild the config with the canonical key order. Any extra keys // we didn't enumerate are appended at the tail. $ordered = new stdClass(); foreach ($orderedKeys as $key) { if (property_exists($config, $key)) { $ordered->{$key} = $config->{$key}; } } foreach ($config as $key => $value) { if (!property_exists($ordered, $key)) { $ordered->{$key} = $value; } } file_put_contents($path, json_encode($ordered, $jsonFlags)); $touched++; $keysAdded += $added; fwrite(STDOUT, sprintf(" %s: +%d key(s)\n", $path, $added)); } } fwrite(STDOUT, sprintf("Done. Touched %d portal(s), added %d replace.cidr4 key(s) in total.\n", $touched, $keysAdded));