'danger', 'msg' => 'Token inválido.']; header('Location: ' . $selfUrl); exit; } if (($_POST['action'] ?? 'save') === 'refresh') { // Corre o mesmo script que o cron corre. Não se duplica a lógica de // validação aqui: é ela que impede um download mau de substituir uma // lista boa, e duas cópias divergiriam. $countries = geo_allowed_countries(); if (!$countries) { $_SESSION['flash'] = ['type' => 'warning', 'msg' => 'Defina primeiro os países permitidos.']; } else { $args = ' ' . implode(' ', array_map('escapeshellarg', $countries)) . ' 2>&1'; $out = []; $code = 1; exec(escapeshellcmd(__DIR__ . '/bin/geo-update.sh') . $args, $out, $code); // O Relay corre a seguir e em separado: são duas fontes // independentes, e a da Apple estar em baixo não pode impedir a // actualização das listas de país (nem o contrário). $relayOut = []; $relayCode = 1; exec(escapeshellcmd(__DIR__ . '/bin/geo-relay-update.sh') . $args, $relayOut, $relayCode); $msg = implode(' · ', array_slice(array_merge($out, $relayOut), 0, 8)); $_SESSION['flash'] = ($code === 0 && $relayCode === 0) ? ['type' => 'success', 'msg' => 'Listas actualizadas. ' . $msg] : ['type' => ($code === 0 ? 'warning' : 'danger'), 'msg' => ($code === 0 ? 'Países actualizados; o iCloud Private Relay falhou (lista anterior mantida). ' : 'A actualização falhou (as listas anteriores mantêm-se). ') . $msg]; } header('Location: ' . $selfUrl); exit; } // O botão do "acrescentar o meu endereço" grava só a lista de endereços e // devolve a página. Parte do texto que veio no formulário, e não do que está // gravado, para não deitar fora o que a pessoa tenha escrito antes de // carregar -- que é a maneira mais rápida de alguém perder confiança numa // página de configuração. if (($_POST['action'] ?? 'save') === 'add_my_ip') { $ip = platform_client_ip() ?? ''; $lista = preg_split('/[\s,;]+/', (string) ($_POST['geo_allow_ips'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: []; if ($ip !== '' && !in_array($ip, $lista, true)) { $lista[] = $ip; } $saveSetting('geo_allow_ips', implode(',', $lista)); $_SESSION['flash'] = ['type' => 'success', 'msg' => 'O endereço ' . $ip . ' passa sempre a partir de agora.']; header('Location: ' . $selfUrl); exit; } $mode = strtolower(trim((string) ($_POST['geo_mode'] ?? 'off'))); if (!in_array($mode, ['off', 'forms', 'site'], true)) { $mode = 'off'; } // As caixas mais o campo de escape, tratados da mesma maneira. Um código que // não seja ISO-3166 alfa-2 é recusado com aviso, e não engolido: um país mal // escrito na lista é indistinguível de "ninguém passa". // // O campo de escape existe porque a lista de caixas tem 72 países e não 249: // ver o cabeçalho de data/countries-pt.php. $rejected = []; $valid = []; $submetidos = array_merge( array_map('strval', (array) ($_POST['geo_countries'] ?? [])), preg_split('/[\s,;]+/', (string) ($_POST['geo_countries_extra'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [] ); foreach ($submetidos as $cc) { $up = strtoupper($cc); if (preg_match('/^[A-Z]{2}$/', $up)) { $valid[$up] = true; } else { $rejected[] = $cc; } } $countries = implode(',', array_keys($valid)); $failClosed = !empty($_POST['geo_fail_closed']) ? 'true' : 'false'; // Guarda-rede: ligar o modo `site` sem dados é tirar a plataforma do ar // para toda a gente. Com fail-open não chega a acontecer, mas a combinação // `site` + fail-closed + sem listas é irrecuperável pelo browser. $haveData = false; foreach (array_keys($valid) as $cc) { if (geo_ranges($cc, false) || geo_ranges($cc, true)) { $haveData = true; break; } } if ($mode !== 'off' && $failClosed === 'true' && !$haveData) { $_SESSION['flash'] = ['type' => 'danger', 'msg' => 'Recusado: com "falhar fechado" e sem listas descarregadas, ninguém ' . 'conseguiria entrar — incluindo o utilizador. Actualize as listas primeiro.']; header('Location: ' . $selfUrl); exit; } // Endereços que passam sempre. Validados aqui pela mesma função que o // módulo usa para os ler, para que uma entrada inválida seja recusada com // aviso em vez de ficar gravada a não fazer nada -- que é indistinguível de // "guardei e continua bloqueado". $badIps = []; $validIps = []; foreach (preg_split('/[\s,;]+/', (string) ($_POST['geo_allow_ips'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [] as $entry) { if (geo_cidr_to_range($entry) !== null) { $validIps[$entry] = true; } else { $badIps[] = $entry; } } $allowIps = implode(',', array_keys($validIps)); $trustRelay = !empty($_POST['geo_trust_relay']) ? 'true' : 'false'; $saveSetting('geo_mode', $mode); $saveSetting('geo_allow_countries', $countries); $saveSetting('geo_fail_closed', $failClosed); $saveSetting('geo_allow_ips', $allowIps); $saveSetting('geo_trust_relay', $trustRelay); if (function_exists('platform_audit')) { platform_audit($pdo, 'settings.geo', ['target_type' => 'settings', 'summary' => 'Bloqueio por país guardado', 'changes' => ['geo_mode' => $mode, 'geo_allow_countries' => $countries, 'geo_fail_closed' => $failClosed, 'geo_allow_ips' => $allowIps, 'geo_trust_relay' => $trustRelay]]); } $ignorado = array_merge( $rejected ? ['códigos de país: ' . implode(', ', $rejected)] : [], $badIps ? ['endereços: ' . implode(', ', $badIps)] : [] ); $_SESSION['flash'] = $ignorado ? ['type' => 'warning', 'msg' => 'Guardado, mas isto não é válido e foi ignorado — ' . implode(' · ', $ignorado)] : ['type' => 'success', 'msg' => 'Configuração guardada.']; header('Location: ' . $selfUrl); exit; } // ─── Estado actual ─────────────────────────────────────────────────────────── $mode = geo_mode(); $countries = geo_allowed_countries(); $failClosed = geo_fail_closed(); $allowIps = preg_split('/[\s,;]+/', (string) geo_env('GEO_ALLOW_IPS', ''), -1, PREG_SPLIT_NO_EMPTY) ?: []; $trustRelay = geo_trust_relay(); // A lista de caixas, e os códigos já guardados que não constam dela -- esses // aparecem à mesma, num grupo à parte: uma página que perde silenciosamente um // valor gravado é pior do que uma página feia. $grupos = require __DIR__ . '/data/countries-pt.php'; $conhecidos = array_merge(...array_map('array_keys', array_values($grupos))); $foraDaLista = array_values(array_diff($countries, $conhecidos)); /** Há alguma lista de endereços para este país? (não abre os ficheiros) */ $temDados = function (string $cc): bool { $base = GEO_DATA_DIR . '/' . strtolower($cc); return is_file($base . '.v4') || is_file($base . '.v6'); }; $relayOk = geo_relay_available($countries); // De onde veio cada valor — é o que explica um "guardei e não mudou nada". $origin = function (string $key) use ($pdo): string { $db = trim((string) platform_settings_get($pdo, strtolower($key), '')); if ($db !== '') return 'base de dados'; return (function_exists('env') && (string) env($key, '') !== '') ? '.env' : 'omissão'; }; $dataFiles = []; foreach ($countries as $cc) { // Duas fontes por país: a ipdeny (alocações nacionais) e a Apple (saídas do // Private Relay ao serviço desse país). Lado a lado na mesma tabela porque // a pergunta do operador é uma só -- "tenho dados para PT?". foreach ([['', 'ipdeny'], ['relay-', 'iCloud Relay']] as [$prefix, $fonte]) { foreach (['v4', 'v6'] as $fam) { $key = $prefix . strtolower($cc); $f = GEO_DATA_DIR . '/' . $key . '.' . $fam; $dataFiles[] = [ 'cc' => $cc, 'fonte' => $fonte, 'fam' => $fam, 'exists' => is_file($f), 'ranges' => is_file($f) ? count(geo_ranges($key, $fam === 'v6')) : 0, 'mtime' => is_file($f) ? date('Y-m-d H:i', filemtime($f)) : '—', ]; } } } if ($probe !== '') { $probeResult = filter_var($probe, FILTER_VALIDATE_IP) ? ['ip' => $probe, 'reason' => geo_block_reason($probe), 'internal' => geo_ip_is_internal($probe)] : ['ip' => $probe, 'invalid' => true]; } $myIp = platform_client_ip() ?? ''; // O stub pode dizer onde fica o hub e a página de submissões bloqueadas; nem // todos os sites os têm no mesmo caminho (o design usa views/admin/). $hubUrl = $platform_dashboard_url ?? (function_exists('platform_admin_url') ? platform_admin_url('index.php') : 'index.php'); $blockedUrl = $platform_blocked_url ?? (function_exists('base_url') ? base_url('views/admin/blocked_submissions.php') : null); $pageTitle = 'Bloqueio por país'; // O layout do site, como nas outras páginas partilhadas (o router passa-o a // partir do platform_connect()). Até 2026-09-23 esta página fixava // includes/header.php e ignorava-o: num site com layout noutro sítio saía sem // cabeçalho nenhum. $layout_header = $platform_layout_header ?? (dirname(__DIR__, 2) . '/includes/header.php'); $layout_navbar = $platform_layout_navbar ?? (dirname(__DIR__, 2) . '/includes/navbar.php'); $layout_footer = $platform_layout_footer ?? (dirname(__DIR__, 2) . '/includes/footer.php'); if (is_file($layout_header)) { include $layout_header; } if (is_file($layout_navbar)) { include $layout_navbar; } $csrfField = function_exists('csrf_field') ? csrf_field() : (function_exists('platform_csrf_field') ? platform_csrf_field() : ''); $h = fn($s) => htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
Recusa submissões — ou o site inteiro — a quem chega de fora da lista.
Sem países definidos.
| País | Origem | Família | Gamas | Actualizado |
|---|---|---|---|---|
| = $h($d['cc']) ?> | = $h($d['fonte']) ?> | = $h($d['fam']) ?> |
= (int) $d['ranges'] ?> em falta | = $h($d['mtime']) ?> |
Alocações nacionais da ipdeny.com (mensal chega — movem-se devagar) e saídas do iCloud Private Relay publicadas pela Apple (semanal, que é o ritmo a que aparecem gamas novas). As duas são independentes: uma em baixo não impede a outra de actualizar, e um download curto ou inválido é recusado com a lista anterior a manter-se.
= $h($probeResult['ip']) ?> não é um endereço IP válido.= $h($probeResult['ip']) ?> passa
= !empty($probeResult['internal']) ? ' — é um endereço interno, e esses passam sempre (cron e health checks do próprio contentor).' : '.' ?>
= $h($probeResult['ip']) ?> seria recusado: = $h($probeResult['reason']) ?>
O seu endereço agora é = $h($myIp) ?>
— = $mine === null ? 'passa' : 'seria recusado: ' . $h($mine) . '' ?>.
As recusas ficam registadas com o motivo e o IP em Submissões bloqueadas.