true, 'cookie_samesite' => 'Lax']); } $here = '/' . ltrim((string)strtok($_SERVER['REQUEST_URI'], '?'), '/'); function go($query = '') { global $here; header('Location: ' . $here . $query); exit; } // ?adminpswd=... : check it, then redirect to the same page without it. if (isset($_GET['adminpswd'])) { $pw = $_GET['adminpswd']; if (is_string($pw) && ADMIN_PASSWORD_HASH !== '' && password_verify($pw, ADMIN_PASSWORD_HASH)) { session_regenerate_id(true); $_SESSION['admin'] = true; $_SESSION['csrf'] = bin2hex(random_bytes(16)); } else { $_SESSION['flash'] = ['danger', 'Incorrect password.']; } $rest = $_GET; unset($rest['adminpswd']); go($rest ? '?' . http_build_query($rest) : ''); } if (session_status() === PHP_SESSION_ACTIVE) { $isAdmin = !empty($_SESSION['admin']); $csrf = (string)($_SESSION['csrf'] ?? ''); if (isset($_SESSION['flash'])) { $flash = $_SESSION['flash']; unset($_SESSION['flash']); } } // Read a trimmed single-line text field. Returns null if invalid (empty when required, too long, control characters). function field($name, $max, $required = true) { $v = trim((string)($_POST[$name] ?? '')); if ($v === '') return $required ? null : ''; return (strlen($v) > $max || !preg_match('/^[^\x00-\x1F\x7F]*$/u', $v)) ? null : $v; } // Lock, re-read the current JSON, apply one change, write atomically. Returns an error message or null. function portal_update($change) { $lock = @fopen(CONFIG_FILE . '.lock', 'c'); if (!$lock || !flock($lock, LOCK_EX)) return 'Could not lock the configuration.'; $cfg = json_decode((string)@file_get_contents(CONFIG_FILE), true); if (!is_array($cfg)) return 'config.json is missing or malformed. Nothing was saved; fix the file first.'; $err = $change($cfg); if ($err) return $err; $json = json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; $tmp = tempnam(dirname(CONFIG_FILE), 'cfg'); if (!$tmp || file_put_contents($tmp, $json) !== strlen($json) || !@rename($tmp, CONFIG_FILE)) { if ($tmp) @unlink($tmp); return 'Could not write config.json. Check the directory permissions.'; } return null; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { if (!$isAdmin || !hash_equals($csrf, (string)($_POST['csrf'] ?? ''))) { http_response_code(403); exit('Not allowed.'); } $act = (string)($_POST['action'] ?? ''); if ($act === 'exit') { session_destroy(); go(); } $id = (string)($_POST['id'] ?? ''); $err = portal_update(function (array &$cfg) use ($act, $id) { $cfg['links'] = $cfg['links'] ?? []; $i = null; foreach ($cfg['links'] as $k => $l) { if (($l['id'] ?? '') === $id) $i = $k; } if ($act === 'save_site') { $t = field('title', 80); $w = field('welcome', 300, false); if ($t === null || $w === null) return 'Title is required (max 80) and welcome is max 300 characters, one line.'; $cfg['title'] = $t; $cfg['welcome'] = $w; } elseif ($act === 'save_appearance') { $m = $_POST['mode'] ?? ''; $a = $_POST['accent'] ?? ''; if (!in_array($m, MODES, true) || !in_array($a, ACCENTS, true)) return 'Invalid appearance choice.'; $cfg['appearance'] = ['mode' => $m, 'accent' => $a] + ($cfg['appearance'] ?? []); } elseif ($act === 'save_link') { $t = field('title', 60); $d = field('description', 200, false); $u = field('url', 300); $ic = field('icon', 16, false); if ($t === null || $d === null || $u === null || $ic === null) return 'Title and URL are required; check the lengths.'; if (!preg_match('~^(https?://[^\s/]+|/(?!/))~i', $u)) return 'URL must start with http://, https:// or a single /.'; $row = ['title' => $t, 'description' => $d, 'url' => $u, 'icon' => $ic, 'enabled' => isset($_POST['enabled'])]; if ($id === 'new') $cfg['links'][] = ['id' => 'l' . bin2hex(random_bytes(4))] + $row; elseif ($i === null) return 'That link no longer exists.'; else $cfg['links'][$i] = $row + $cfg['links'][$i]; } elseif ($i === null) { return 'That link no longer exists.'; } elseif ($act === 'toggle') { $cfg['links'][$i]['enabled'] = empty($cfg['links'][$i]['enabled']); } elseif ($act === 'delete_link') { array_splice($cfg['links'], $i, 1); } elseif ($act === 'up' || $act === 'down') { $j = $act === 'up' ? $i - 1 : $i + 1; if (isset($cfg['links'][$j])) { $tmp = $cfg['links'][$i]; $cfg['links'][$i] = $cfg['links'][$j]; $cfg['links'][$j] = $tmp; } } else { return 'Unknown action.'; } return null; }); $_SESSION['flash'] = $err ? ['danger', $err] : ['success', 'Saved.']; $back = ['save_site' => 'site', 'save_appearance' => 'appearance', 'save_link' => $id][$act] ?? ''; go($err && $back !== '' ? '?edit=' . urlencode($back) : ''); }