0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } function set_json_headers() { header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); } function read_json_safe($filepath) { if (!file_exists($filepath)) { return null; } $fp = fopen($filepath, 'r'); if (!$fp) return null; if (flock($fp, LOCK_SH)) { $content = stream_get_contents($fp); flock($fp, LOCK_UN); fclose($fp); return json_decode($content, true); } fclose($fp); return null; } // Write JSON atomically so a crash during write does not leave a 0-byte file. function write_json_safe($filepath, $data) { $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) return false; $tmp = $filepath . '.tmp.' . uniqid('', true); if (file_put_contents($tmp, $json, LOCK_EX) === false) { @unlink($tmp); return false; } if (!rename($tmp, $filepath)) { @unlink($tmp); return false; } return true; } function require_auth() { start_secure_session(); if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; } if (isset($_SESSION['login_time']) && (time() - $_SESSION['login_time']) > SESSION_LIFETIME) { session_destroy(); http_response_code(401); echo json_encode(['error' => 'Session expired']); exit; } } function ensure_csrf_token() { start_secure_session(); if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function require_csrf() { start_secure_session(); $sessionToken = $_SESSION['csrf_token'] ?? ''; $requestToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (!is_string($sessionToken) || $sessionToken === '' || !is_string($requestToken) || !hash_equals($sessionToken, $requestToken)) { json_response(['error' => 'Invalid CSRF token'], 403); } } function get_json_input() { $input = file_get_contents('php://input'); return json_decode($input, true); } function is_safe_public_url($url) { if (!is_string($url)) return false; $url = trim($url); if ($url === '') return true; if (preg_match('#^uploads/[A-Za-z0-9가-힣._~!$&\'()*+,;=:@%/-]+$#u', $url)) { return !str_contains($url, '..') && !preg_match('#(^|/)\.#', $url); } $parts = parse_url($url); if ($parts === false || empty($parts['scheme'])) return false; return in_array(strtolower($parts['scheme']), ['http', 'https'], true); } function clean_public_url($url) { $url = is_string($url) ? trim($url) : ''; return is_safe_public_url($url) ? $url : ''; } function clean_public_urls($urls) { if (!is_array($urls)) return []; $clean = []; foreach ($urls as $url) { $url = clean_public_url($url); if ($url !== '') $clean[] = $url; } return array_values(array_unique($clean)); } function clean_css_color($value, $fallback = '#00f2ff') { $value = is_string($value) ? trim($value) : ''; if (preg_match('/^#[0-9a-fA-F]{6}$/', $value) || preg_match('/^#[0-9a-fA-F]{3}$/', $value)) { return $value; } return $fallback; } function json_response($data, $status = 200) { http_response_code($status); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; }