v1.3.0
AtheraStudio SyncDrop
- <?php
- declare(strict_types=1);
-
- defined('SD_BOOT') || exit;
-
- const SD_BAN_THRESHOLD = 100;
- const SD_BAN_WINDOW = 86400;
- const SD_BAN_STEPS = [86400, 604800, 2592000];
- const SD_BAN_MEMORY = 7776000;
- const SD_BAN_MAX = 5000;
- const SD_ALLOW_MAX = 200;
- const SD_TRUST_TTL = 2592000;
- const SD_BAN_MAIL_LIMIT = 5;
- const SD_BAN_DURATIONS = [3600, 86400, 604800, 2592000, 0];
- const SD_THREATS = [
- 'scanner' => 100,
- 'signature' => 50,
- 'honeypot' => 50,
- 'pow_forged' => 30,
- 'pow_invalid' => 20,
- 'probe' => 20,
- 'method' => 30,
- 'host' => 20,
- 'api_anon' => 25,
- 'lockout' => 30,
- 'fail_setup_code' => 15,
- 'fail_password' => 10,
- 'fail_totp' => 10,
- 'fail_antibot' => 10,
- ];
- const SD_SCANNER_UA = '/(sqlmap|nikto|nmap|masscan|zgrab|wpscan|nuclei|dirbuster|gobuster|feroxbuster|ffuf|wfuzz|acunetix|netsparker|jaeles|whatweb|openvas|nessus|hydra|havij|commix|xsstrike|arachni|skipfish|w3af|zmeu|morfeus|fimap|dirsearch|nmap scripting engine)/i';
- const SD_ATTACK_PATTERNS = [
- '#\.\.(/|\\\\)#',
- '#(/etc/(passwd|shadow|hosts|group)|win\.ini|boot\.ini|/proc/self/)#',
- '#<\s*/?\s*(script|iframe|object|embed|svg|img|body)\b#',
- '#(javascript|vbscript|livescript)\s*:#',
- '#\bon(error|load|mouseover|focus|toggle|animationstart)\s*=#',
- '#\bunion\b[\s/*+()]{1,40}(all\s+)?select\b#',
- '#\b(sleep|benchmark|pg_sleep|extractvalue|updatexml)\s*\(#',
- '#\bwaitfor\s+delay\b#',
- '#[\'"]\s*(or|and|\|\||&&)\s*[\'"]?\s*\d+\s*[\'"]?\s*(=|<|>|like)#',
- '#\binformation_schema\b#',
- '#\$\{\s*(jndi|env|sys|java|lower|upper|::-)#',
- '#\b(base64_decode|shell_exec|passthru|proc_open|popen|assert|eval|system|exec|phpinfo|file_put_contents)\s*\(#',
- '#\b(php|phar|expect|zip|glob|data):/{1,2}#',
- '#/(usr/)?bin/(ba|z|da|k)?sh\b#',
- '#\b(wget|curl)\s+(-\S+\s+)*https?://#',
- '#(wp-(login|admin|content|includes|config|json)|xmlrpc\.php|phpmyadmin|/pma/|adminer|/\.env|\.git/|/\.aws|/\.ssh|cgi-bin|vendor/phpunit|eval-stdin|server-status|/actuator|hnap1|boaform|/owa/|\.ds_store|web\.config|\.htpasswd|/shell\.php|/(backup|dump|db|database|site)\.(zip|sql|tar|gz|rar|7z))#',
- ];
-
- function sd_ban_key(string $ip): string
- {
- if (!str_contains($ip, ':')) return $ip;
- return sd_cidr_parse($ip . '/64')['text'] ?? $ip;
- }
-
- function sd_shield_match(array $list, string $ip): ?array
- {
- $now = time();
- foreach ([$ip, sd_ban_key($ip)] as $key) {
- $entry = $list[$key] ?? null;
- if (is_array($entry) && sd_shield_active($entry, $now)) return [$key, $entry];
- }
- foreach ($list as $key => $entry) {
- $key = (string)$key;
- if (!str_contains($key, '/') || !is_array($entry) || !sd_shield_active($entry, $now)) continue;
- if (sd_cidr_contains($key, $ip)) return [$key, $entry];
- }
- return null;
- }
-
- function sd_shield_active(array $entry, int $now): bool
- {
- $until = (int)($entry['until'] ?? 0);
- return $until === 0 || $until > $now;
- }
-
- function sd_shield_allowed(?array $state = null): bool
- {
- $state ??= sd_read_state('bans');
- return sd_shield_match($state['allow'] ?? [], sd_ip()) !== null;
- }
-
- function sd_shield_trusted(?array $state = null): bool
- {
- $state ??= sd_read_state('bans');
- return (int)($state['trust'][sd_ban_key(sd_ip())] ?? 0) > time();
- }
-
- function sd_shield_exempt(): bool
- {
- if (session_status() === PHP_SESSION_ACTIVE && ($_SESSION['stage'] ?? '') === 'full') return true;
- $state = sd_read_state('bans');
- return sd_shield_allowed($state) || sd_shield_trusted($state);
- }
-
- function sd_shield_deny(int $code, string $message = ''): never
- {
- http_response_code($code);
- header('Content-Type: text/plain; charset=utf-8');
- $texts = [400 => 'Bad Request', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed'];
- echo $message !== '' ? $message : $code . ' ' . ($texts[$code] ?? 'Error');
- exit;
- }
-
- function sd_shield_text(string $raw): string
- {
- $once = rawurldecode($raw);
- $twice = rawurldecode($once);
- return strtolower($raw . "\n" . $once . "\n" . $twice . "\n" . html_entity_decode($twice, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
- }
-
- function sd_shield_signature(string $text): bool
- {
- foreach (SD_ATTACK_PATTERNS as $re) {
- if (preg_match($re, $text)) return true;
- }
- return false;
- }
-
- function sd_shield_prune(array &$s, int $now): void
- {
- foreach (['bans', 'allow', 'trust', 'score', 'level'] as $k) {
- if (!isset($s[$k]) || !is_array($s[$k])) $s[$k] = [];
- }
- foreach ($s['bans'] as $k => $b) {
- if (!is_array($b) || !sd_shield_active($b, $now)) unset($s['bans'][$k]);
- }
- foreach ($s['trust'] as $k => $until) {
- if ((int)$until <= $now) unset($s['trust'][$k]);
- }
- foreach ($s['score'] as $k => $events) {
- $events = array_values(array_filter((array)$events, static fn($e) => is_array($e) && $now - (int)($e[0] ?? 0) < SD_BAN_WINDOW));
- if ($events) $s['score'][$k] = $events;
- else unset($s['score'][$k]);
- }
- foreach ($s['level'] as $k => $l) {
- if (!is_array($l) || $now - (int)($l[1] ?? 0) > SD_BAN_MEMORY) unset($s['level'][$k]);
- }
- foreach (['bans' => 'at', 'level' => 1, 'trust' => null, 'score' => null] as $k => $field) {
- if (count($s[$k]) <= SD_BAN_MAX) continue;
- uasort($s[$k], static function ($a, $b) use ($field): int {
- $ta = is_array($a) ? (int)($field === null ? (end($a)[0] ?? 0) : ($a[$field] ?? 0)) : (int)$a;
- $tb = is_array($b) ? (int)($field === null ? (end($b)[0] ?? 0) : ($b[$field] ?? 0)) : (int)$b;
- return $tb <=> $ta;
- });
- $s[$k] = array_slice($s[$k], 0, SD_BAN_MAX, true);
- }
- }
-
- function sd_threat(string $reason): bool
- {
- if (sd_shield_exempt()) return false;
- $points = SD_THREATS[$reason] ?? 10;
- $key = sd_ban_key(sd_ip());
- $result = sd_state('bans', static function (array &$s) use ($key, $reason, $points): ?array {
- $now = time();
- sd_shield_prune($s, $now);
- if (isset($s['bans'][$key])) return null;
- $events = $s['score'][$key] ?? [];
- $events[] = [$now, $points, $reason];
- $events = array_slice($events, -40);
- $total = array_sum(array_map(static fn($e) => (int)$e[1], $events));
- if ($total < SD_BAN_THRESHOLD) {
- $s['score'][$key] = $events;
- return null;
- }
- unset($s['score'][$key]);
- $level = (int)($s['level'][$key][0] ?? 0) + 1;
- $s['level'][$key] = [$level, $now];
- $until = $now + SD_BAN_STEPS[min($level, count(SD_BAN_STEPS)) - 1];
- $s['bans'][$key] = ['at' => $now, 'until' => $until, 'reason' => $reason, 'auto' => 1, 'note' => '', 'last' => $now, 'level' => $level];
- $mail = array_values(array_filter((array)($s['mail'] ?? []), static fn($t) => $now - (int)$t < 3600));
- $send = count($mail) < SD_BAN_MAIL_LIMIT;
- if ($send) $mail[] = $now;
- $s['mail'] = $mail;
- return ['until' => $until, 'level' => $level, 'mail' => $send];
- });
- if (!str_starts_with($reason, 'fail_')) sd_audit('threat', ['reason' => $reason, 'points' => $points]);
- if ($result === null) return false;
- sd_audit('ip_banned', ['ban' => $key, 'reason' => $reason, 'until' => gmdate('c', $result['until']), 'level' => $result['level']]);
- if ($result['mail']) {
- sd_alert('alert_ban_subject', 'alert_ban_body', ['ban' => $key, 'reason' => t('shield_r_' . $reason), 'until' => gmdate('Y-m-d H:i', $result['until']) . ' UTC']);
- }
- return true;
- }
-
- function sd_shield_trust(): void
- {
- $key = sd_ban_key(sd_ip());
- sd_state('bans', static function (array &$s) use ($key): void {
- $now = time();
- sd_shield_prune($s, $now);
- $s['trust'][$key] = $now + SD_TRUST_TTL;
- unset($s['score'][$key]);
- });
- }
-
- function sd_shield_gate(): void
- {
- $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
- $state = sd_read_state('bans');
- if (!sd_shield_allowed($state)) {
- $ban = sd_shield_match($state['bans'] ?? [], sd_ip());
- if ($ban !== null) {
- $now = time();
- if ($now - (int)($ban[1]['last'] ?? 0) >= 60) {
- sd_state('bans', static function (array &$s) use ($ban, $now): void {
- if (isset($s['bans'][$ban[0]])) $s['bans'][$ban[0]]['last'] = $now;
- });
- }
- sd_shield_deny(403, t('shield_blocked'));
- }
- if (preg_match(SD_SCANNER_UA, sd_ua())) {
- sd_threat('scanner');
- sd_shield_deny(403);
- }
- if (!in_array($method, ['GET', 'POST', 'HEAD'], true)) {
- sd_threat('method');
- sd_shield_deny(405);
- }
- if (sd_host() === '') {
- sd_threat('host');
- sd_shield_deny(400);
- }
- } elseif (sd_host() === '') {
- sd_shield_deny(400);
- }
- $probe = isset($_SERVER['SD_PROBE']) || isset($_SERVER['REDIRECT_SD_PROBE']);
- if (!$probe && sd_request_path_ok()) return;
- $uri = sd_shield_text((string)($_SERVER['REQUEST_URI'] ?? ''));
- $attack = sd_shield_signature($uri);
- if ($probe || $attack) {
- sd_threat($attack ? 'signature' : 'probe');
- sd_shield_deny(404);
- }
- if ($method !== 'GET') sd_shield_deny(404);
- header('Location: ' . sd_url_base(), true, 303);
- exit;
- }
-
- function sd_shield_inspect(): void
- {
- if (($_SESSION['stage'] ?? '') === 'full') return;
- $text = (string)($_SERVER['QUERY_STRING'] ?? '');
- foreach ($_POST as $name => $value) {
- if (in_array($name, ['password', 'password2', 'current', '_pow', '_nonce', '_csrf'], true)) continue;
- $text .= "\n" . (is_string($value) ? $value : (string)json_encode($value));
- }
- if (sd_shield_signature(sd_shield_text($text))) {
- sd_threat('signature');
- sd_shield_deny(403);
- }
- }
-
- function sd_shield_ban(string $address, int $duration, string $note): ?string
- {
- $range = sd_cidr_parse($address);
- if ($range === null) return 'err_ban_invalid';
- if ($range['bits'] < ($range['max'] === 32 ? 16 : 32)) return 'err_ban_range';
- if (sd_cidr_contains($range['text'], sd_ip())) return 'err_ban_self';
- if (!in_array($duration, SD_BAN_DURATIONS, true)) $duration = 86400;
- $note = mb_substr(trim((string)preg_replace('/[\x00-\x1F\x7F]/u', ' ', $note)), 0, 120);
- $key = $range['text'];
- sd_state('bans', static function (array &$s) use ($key, $duration, $note): void {
- $now = time();
- sd_shield_prune($s, $now);
- $s['bans'][$key] = ['at' => $now, 'until' => $duration === 0 ? 0 : $now + $duration, 'reason' => 'manual', 'auto' => 0, 'note' => $note, 'last' => 0, 'level' => 0];
- unset($s['score'][$key]);
- });
- sd_audit('ip_banned', ['ban' => $key, 'reason' => 'manual', 'until' => $duration === 0 ? 'permanent' : gmdate('c', time() + $duration)]);
- return null;
- }
-
- function sd_shield_unban(string $key): bool
- {
- $done = sd_state('bans', static function (array &$s) use ($key): bool {
- sd_shield_prune($s, time());
- $found = isset($s['bans'][$key]);
- unset($s['bans'][$key], $s['score'][$key], $s['level'][$key]);
- return $found;
- });
- if ($done) sd_audit('ip_unbanned', ['ban' => $key]);
- return $done;
- }
-
- function sd_shield_allow(string $address, string $note): ?string
- {
- $range = sd_cidr_parse($address);
- if ($range === null) return 'err_ban_invalid';
- if ($range['bits'] < ($range['max'] === 32 ? 16 : 32)) return 'err_ban_range';
- $key = $range['text'];
- $note = mb_substr(trim((string)preg_replace('/[\x00-\x1F\x7F]/u', ' ', $note)), 0, 120);
- $ok = sd_state('bans', static function (array &$s) use ($key, $note): bool {
- sd_shield_prune($s, time());
- if (!isset($s['allow'][$key]) && count($s['allow']) >= SD_ALLOW_MAX) return false;
- $s['allow'][$key] = ['at' => time(), 'note' => $note];
- return true;
- });
- if (!$ok) return 'err_allow_full';
- sd_audit('allow_added', ['ip' => $key]);
- return null;
- }
-
- function sd_shield_disallow(string $key): void
- {
- sd_state('bans', static function (array &$s) use ($key): void {
- unset($s['allow'][$key]);
- });
- sd_audit('allow_removed', ['ip' => $key]);
- }
-
- function sd_route_shield(): never
- {
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (!sd_csrf_ok()) sd_redirect('shield');
- $error = null;
- $message = '';
- switch ((string)($_POST['action'] ?? '')) {
- case 'ban':
- $error = sd_shield_ban((string)($_POST['address'] ?? ''), (int)($_POST['duration'] ?? 86400), (string)($_POST['note'] ?? ''));
- $message = 'msg_banned';
- break;
- case 'unban':
- sd_shield_unban((string)($_POST['key'] ?? ''));
- $message = 'msg_unbanned';
- break;
- case 'allow':
- $error = sd_shield_allow((string)($_POST['address'] ?? ''), (string)($_POST['note'] ?? ''));
- $message = 'msg_allowed';
- break;
- case 'allow_me':
- $error = sd_shield_allow(sd_ip(), t('shield_me_note'));
- $message = 'msg_allowed';
- break;
- case 'disallow':
- sd_shield_disallow((string)($_POST['key'] ?? ''));
- $message = 'msg_allow_removed';
- break;
- }
- if ($message !== '') sd_flash($error === null ? 'ok' : 'error', t($error ?? $message));
- sd_redirect('shield');
- }
- sd_view_shield();
- exit;
- }
-
- function sd_shield_time(int $ts): string
- {
- return '<time class="sd-time" datetime="' . h(gmdate('c', $ts)) . '">' . h(gmdate('Y-m-d H:i', $ts)) . ' UTC</time>';
- }
-
- function sd_shield_form(string $action, string $key, string $label, string $tone, string $icon, string $confirm = ''): string
- {
- return '<form method="post" action="' . h(sd_url('shield')) . '"' . ($confirm !== '' ? ' data-confirm="' . h($confirm) . '"' : '') . '>' . sd_csrf_field()
- . '<input type="hidden" name="action" value="' . h($action) . '"><input type="hidden" name="key" value="' . h($key) . '">'
- . '<button type="submit" class="orb-btn orb-btn-outline orb-btn-sm orb-tone-' . h($tone) . '">' . sd_icon($icon, 14) . h($label) . '</button></form>';
- }
-
- function sd_view_shield(): void
- {
- sd_open(t('shield_title'), 'app', 'shield');
- $state = sd_read_state('bans');
- $now = time();
- $bans = array_filter((array)($state['bans'] ?? []), static fn($b) => is_array($b) && sd_shield_active($b, $now));
- uasort($bans, static fn($a, $b) => (int)($b['at'] ?? 0) <=> (int)($a['at'] ?? 0));
- $allow = (array)($state['allow'] ?? []);
- $watch = [];
- foreach ((array)($state['score'] ?? []) as $key => $events) {
- $events = array_values(array_filter((array)$events, static fn($e) => is_array($e) && $now - (int)($e[0] ?? 0) < SD_BAN_WINDOW));
- if (!$events || isset($bans[$key])) continue;
- $last = end($events);
- $watch[$key] = ['points' => array_sum(array_map(static fn($e) => (int)$e[1], $events)), 'reason' => (string)$last[2], 't' => (int)$last[0]];
- }
- uasort($watch, static fn($a, $b) => $b['points'] <=> $a['points']);
- $watch = array_slice($watch, 0, 50, true);
- $ip = sd_ip();
- $status = sd_shield_allowed($state) ? ['success', t('shield_allowed')] : (sd_shield_trusted($state) ? ['success', t('shield_trusted')] : ['neutral', t('shield_untrusted')]);
- $auto = count(array_filter($bans, static fn($b) => !empty($b['auto'])));
-
- echo '<p class="orb-lead orb-mb-4 sd-shield-lead">' . h(t('shield_lead')) . '</p>';
- echo '<div class="sd-stats sd-shield-stats orb-mb-4">'
- . '<div class="sd-stat"><span class="sd-stat-n orb-mono sd-shield-ip">' . h($ip) . '</span><span class="sd-stat-l">' . h(t('shield_your_ip')) . ' <span class="orb-pill orb-pill-' . $status[0] . '">' . h($status[1]) . '</span></span></div>'
- . '<div class="sd-stat"><span class="sd-stat-n">' . count($bans) . '</span><span class="sd-stat-l">' . h(t('shield_active')) . '</span><span class="orb-text-xs orb-text-muted">' . h(t('shield_active_split', ['a' => (string)$auto, 'm' => (string)(count($bans) - $auto)])) . '</span></div>'
- . '<div class="sd-stat"><span class="sd-stat-n">' . count($watch) . '</span><span class="sd-stat-l">' . h(t('shield_watched')) . '</span></div>'
- . '<div class="sd-stat"><span class="sd-stat-n">' . count($allow) . '</span><span class="sd-stat-l">' . h(t('shield_allow_title')) . '</span></div></div>';
-
- echo '<section class="orb-card"><div class="orb-card-header"><span class="orb-card-title">' . sd_icon('lock', 16) . ' ' . h(t('shield_bans')) . '</span><span class="orb-badge">' . count($bans) . '</span></div>';
- if (!$bans) {
- echo '<div class="orb-card-body orb-text-muted">' . h(t('shield_no_bans')) . '</div>';
- } else {
- echo '<div class="orb-table-wrap"><table class="orb-table orb-table-responsive"><thead><tr><th>' . h(t('col_address')) . '</th><th>' . h(t('col_reason')) . '</th><th>' . h(t('col_since')) . '</th><th>' . h(t('col_until')) . '</th><th>' . h(t('col_last')) . '</th><th></th></tr></thead><tbody>';
- foreach ($bans as $key => $b) {
- $reason = t('shield_r_' . (string)($b['reason'] ?? 'manual'));
- $note = trim((string)($b['note'] ?? ''));
- $until = (int)($b['until'] ?? 0);
- echo '<tr><td data-label="' . h(t('col_address')) . '" class="orb-mono">' . h((string)$key) . '</td>'
- . '<td data-label="' . h(t('col_reason')) . '"><div class="sd-reason"><span class="orb-pill orb-pill-' . (!empty($b['auto']) ? 'danger' : 'warning') . '">' . h(!empty($b['auto']) ? t('shield_auto') : t('shield_manual')) . '</span><span>' . h($reason) . '</span>' . ($note !== '' ? '<span class="orb-text-xs orb-text-muted">' . h($note) . '</span>' : '') . '</div></td>'
- . '<td data-label="' . h(t('col_since')) . '">' . sd_shield_time((int)($b['at'] ?? 0)) . '</td>'
- . '<td data-label="' . h(t('col_until')) . '">' . ($until === 0 ? h(t('shield_forever')) : sd_shield_time($until)) . '</td>'
- . '<td data-label="' . h(t('col_last')) . '">' . ((int)($b['last'] ?? 0) > 0 ? sd_shield_time((int)$b['last']) : '—') . '</td>'
- . '<td>' . sd_shield_form('unban', (string)$key, t('shield_unban'), 'success', 'undo', t('shield_unban_confirm')) . '</td></tr>';
- }
- echo '</tbody></table></div>';
- }
- echo '</section>';
-
- $durations = '';
- foreach (SD_BAN_DURATIONS as $d) $durations .= '<option value="' . $d . '"' . ($d === 86400 ? ' selected' : '') . '>' . h(t('shield_d_' . $d)) . '</option>';
- echo '<div class="orb-grid orb-grid-2 orb-gap-4 orb-mt-4 sd-settings">';
- echo '<section class="orb-card"><div class="orb-card-header"><span class="orb-card-title">' . sd_icon('plus', 16) . ' ' . h(t('shield_ban_title')) . '</span></div><div class="orb-card-body"><form method="post" action="' . h(sd_url('shield')) . '">' . sd_csrf_field() . '<input type="hidden" name="action" value="ban">'
- . sd_field('address', t('shield_address'), 'text', ['id' => 'f-ban-address', 'required' => true, 'maxlength' => 64, 'class' => 'orb-mono', 'spellcheck' => 'false', 'autocomplete' => 'off', 'placeholder' => '203.0.113.7'], t('shield_address_hint'))
- . '<div class="orb-form-group"><label class="orb-label" for="f-duration">' . h(t('shield_duration')) . '</label><select class="orb-select" id="f-duration" name="duration">' . $durations . '</select></div>'
- . sd_field('note', t('shield_note'), 'text', ['id' => 'f-ban-note', 'maxlength' => 120, 'autocomplete' => 'off'])
- . '<button class="orb-btn orb-btn-primary" type="submit">' . sd_icon('lock') . h(t('shield_ban_btn')) . '</button></form></div></section>';
-
- echo '<section class="orb-card"><div class="orb-card-header"><span class="orb-card-title">' . sd_icon('check', 16) . ' ' . h(t('shield_allow_title')) . '</span></div><div class="orb-card-body">'
- . '<p class="orb-text-sm orb-text-muted orb-mb-4">' . h(t('shield_allow_hint')) . '</p>';
- if ($allow) {
- echo '<ul class="sd-allow">';
- foreach ($allow as $key => $a) {
- $note = trim((string)($a['note'] ?? ''));
- echo '<li><div><span class="orb-mono">' . h((string)$key) . '</span>' . ($note !== '' ? '<div class="orb-text-xs orb-text-muted">' . h($note) . '</div>' : '') . '</div>' . sd_shield_form('disallow', (string)$key, t('shield_remove'), 'danger', 'trash') . '</li>';
- }
- echo '</ul>';
- } else {
- echo '<p class="orb-text-sm orb-text-muted orb-mb-4">' . h(t('shield_allow_empty')) . '</p>';
- }
- echo '<form method="post" action="' . h(sd_url('shield')) . '" class="orb-mt-4">' . sd_csrf_field() . '<input type="hidden" name="action" value="allow">'
- . sd_field('address', t('shield_address'), 'text', ['required' => true, 'maxlength' => 64, 'class' => 'orb-mono', 'spellcheck' => 'false', 'autocomplete' => 'off', 'id' => 'f-allow-address', 'placeholder' => $ip])
- . sd_field('note', t('shield_note'), 'text', ['id' => 'f-allow-note', 'maxlength' => 120, 'autocomplete' => 'off'])
- . '<div class="orb-flex orb-gap-2 orb-flex-wrap"><button class="orb-btn orb-btn-primary" type="submit">' . sd_icon('plus') . h(t('shield_allow_btn')) . '</button>'
- . '<button class="orb-btn orb-btn-secondary" type="submit" name="action" value="allow_me" formnovalidate>' . sd_icon('shield') . h(t('shield_allow_me')) . '</button></div></form>'
- . '</div></section></div>';
-
- echo '<section class="orb-card orb-mt-4"><div class="orb-card-header"><span class="orb-card-title">' . sd_icon('alert', 16) . ' ' . h(t('shield_watch_title')) . '</span><span class="orb-badge">' . count($watch) . '</span></div>';
- if (!$watch) {
- echo '<div class="orb-card-body orb-text-muted">' . h(t('shield_watch_empty')) . '</div>';
- } else {
- echo '<div class="orb-table-wrap"><table class="orb-table orb-table-responsive"><thead><tr><th>' . h(t('col_address')) . '</th><th>' . h(t('col_points')) . '</th><th>' . h(t('col_reason')) . '</th><th>' . h(t('col_last')) . '</th><th></th></tr></thead><tbody>';
- foreach ($watch as $key => $w) {
- $pct = min(100, (int)round($w['points'] * 100 / SD_BAN_THRESHOLD));
- echo '<tr><td data-label="' . h(t('col_address')) . '" class="orb-mono">' . h((string)$key) . '</td>'
- . '<td data-label="' . h(t('col_points')) . '"><progress class="sd-meter" max="100" value="' . $pct . '"></progress><span class="orb-text-xs orb-text-muted">' . h(t('shield_points', ['n' => (string)$w['points']])) . '</span></td>'
- . '<td data-label="' . h(t('col_reason')) . '">' . h(t('shield_r_' . $w['reason'])) . '</td>'
- . '<td data-label="' . h(t('col_last')) . '">' . sd_shield_time($w['t']) . '</td>'
- . '<td><form method="post" action="' . h(sd_url('shield')) . '" data-confirm="' . h(t('shield_ban_confirm')) . '">' . sd_csrf_field() . '<input type="hidden" name="action" value="ban"><input type="hidden" name="address" value="' . h((string)$key) . '"><input type="hidden" name="duration" value="2592000"><button type="submit" class="orb-btn orb-btn-outline orb-btn-sm orb-tone-danger">' . sd_icon('lock', 14) . h(t('shield_ban_btn')) . '</button></form></td></tr>';
- }
- echo '</tbody></table></div>';
- }
- echo '</section>';
- sd_close('app');
- }
-