'กฎหมายใกล้ตัว', 2 => 'กฎหมายสิทธิพื้นฐาน', 3 => 'กฎหมายจราจร', 4 => 'คดีเกี่ยวกับทรัพย์', 5 => 'คดีหมิ่นประมาท', 6 => 'คดีทางเพศ', 7 => 'คดีอาชญากรรม', 8 => 'อาชญากรรมออนไลน์', 9 => 'กระบวนการยุติธรรม', 10 => 'งานบริการกระทรวงยุติธรรม', ]; } function qb_default_modes(): array { $titles = qb_default_titles(); $modes = []; for ($i = 1; $i <= QB_ROOM_COUNT; $i++) { $modes[(string)$i] = [ 'title' => $titles[$i] ?? ('ห้อง ' . $i), 'enabled' => true, 'openUntil' => '', 'maxPlayers' => QB_MAX_PLAYERS_CAP, ]; } return $modes; } /** ชื่อหัวข้อห้อง — แก้ได้ [2026-07-23] · ตัดช่องว่าง/อักขระควบคุม จำกัด 60 ตัว · ว่าง → คืน default */ function qb_clean_title($raw, string $default): string { if (!is_string($raw)) { return $default; } $s = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $raw); // ตัดอักขระควบคุม/ขึ้นบรรทัด $s = trim(preg_replace('/\s+/u', ' ', $s ?? '')); if ($s === '') { return $default; } if (function_exists('mb_substr')) { $s = mb_substr($s, 0, 60, 'UTF-8'); } else { $s = substr($s, 0, 180); } return $s; } /** 'YYYY-MM-DD HH:MM' เท่านั้น (เวลาไทย) — ค่าอื่นถือว่าไม่ได้ตั้ง */ function qb_clean_until($raw): string { if (!is_string($raw)) { return ''; } $s = trim($raw); if ($s === '') { return ''; } $s = str_replace('T', ' ', $s); if (strlen($s) === 16 && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $s) === 1) { [$d, $t] = explode(' ', $s, 2); [$y, $m, $dd] = array_map('intval', explode('-', $d)); if (checkdate($m, $dd, $y)) { [$hh, $mm] = array_map('intval', explode(':', $t)); if ($hh >= 0 && $hh <= 23 && $mm >= 0 && $mm <= 59) { return $s; } } } return ''; } function qb_read_modes(): array { $out = ['version' => 1, 'updatedAt' => '', 'modes' => qb_default_modes()]; $raw = @file_get_contents(QB_MODES_FILE); if ($raw === false || $raw === '') { return $out; } $j = json_decode($raw, true); if (!is_array($j) || !isset($j['modes']) || !is_array($j['modes'])) { return $out; } $titles = qb_default_titles(); foreach ($out['modes'] as $k => $def) { $src = $j['modes'][$k] ?? null; if (!is_array($src)) { continue; } $out['modes'][$k] = [ /* [2026-07-23] หัวข้อแก้ได้แล้ว — ใช้ค่าที่เก็บไว้ ถ้าว่าง fallback เป็น default ของห้องนั้น */ 'title' => qb_clean_title($src['title'] ?? '', $titles[(int)$k] ?? $def['title']), 'enabled' => !isset($src['enabled']) || (bool)$src['enabled'], 'openUntil' => qb_clean_until($src['openUntil'] ?? ''), 'maxPlayers' => qb_clamp_max($src['maxPlayers'] ?? QB_MAX_PLAYERS_CAP), ]; } $out['updatedAt'] = is_string($j['updatedAt'] ?? null) ? $j['updatedAt'] : ''; return $out; } function qb_write_modes(array $data): bool { $dir = dirname(QB_MODES_FILE); if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) { return false; } $tmp = QB_MODES_FILE . '.tmp'; $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false || @file_put_contents($tmp, $json, LOCK_EX) === false) { return false; } /* เขียน tmp แล้ว rename — กันไฟล์พังครึ่งทางถ้า Node อ่านพอดีจังหวะ */ if (!@rename($tmp, QB_MODES_FILE)) { @unlink($tmp); return false; } @chmod(QB_MODES_FILE, 0664); return true; } /** สถานะที่คำนวณแล้ว ณ เวลาไทยตอนนี้ — ให้หน้า admin โชว์ว่าห้องไหน "ปิดไปแล้ว" */ function qb_state(array $mode): array { $tz = new DateTimeZone(QB_TZ); $now = new DateTimeImmutable('now', $tz); $until = $mode['openUntil'] ?? ''; $expired = false; if ($until !== '') { $end = DateTimeImmutable::createFromFormat('Y-m-d H:i', $until, $tz); $expired = ($end !== false) && ($now > $end); } return [ 'open' => (bool)($mode['enabled'] ?? true) && !$expired, 'expired' => $expired, ]; } $method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { $data = qb_read_modes(); $tz = new DateTimeZone(QB_TZ); $now = new DateTimeImmutable('now', $tz); foreach ($data['modes'] as $k => $m) { $data['modes'][$k] = array_merge($m, qb_state($m)); } $data['nowThai'] = $now->format('Y-m-d H:i'); $data['tz'] = QB_TZ; json_response(['ok' => true] + $data); } if ($method === 'PUT' || $method === 'POST') { $body = require_json_body(); $src = $body['modes'] ?? null; if (!is_array($src)) { json_response(['ok' => false, 'error' => 'ต้องส่ง modes มาด้วย'], 400); } $cur = qb_read_modes(); $next = $cur['modes']; foreach ($next as $k => $def) { $m = $src[$k] ?? null; if (!is_array($m)) { continue; } $next[$k] = [ /* [2026-07-23] หัวข้อแก้ได้ — sanitize + ถ้าไม่ส่ง/ว่าง คงค่าเดิม (ไม่ reset เป็น default) */ 'title' => qb_clean_title($m['title'] ?? ($def['title'] ?? ''), $def['title'] ?? ('ห้อง ' . $k)), 'enabled' => !isset($m['enabled']) || (bool)$m['enabled'], 'openUntil' => qb_clean_until($m['openUntil'] ?? ''), /* [2026-07-23] จำนวนคนต่อห้อง — ถ้าไม่ส่งมา คงค่าเดิมไว้ (ไม่ reset เป็น 50) */ 'maxPlayers' => qb_clamp_max($m['maxPlayers'] ?? ($def['maxPlayers'] ?? QB_MAX_PLAYERS_CAP)), ]; } $me = current_admin(); $payload = [ 'version' => 1, 'updatedAt' => (new DateTimeImmutable('now', new DateTimeZone(QB_TZ)))->format('Y-m-d H:i'), 'updatedBy' => $me['username'] ?? '', 'tz' => QB_TZ, 'modes' => $next, ]; if (!qb_write_modes($payload)) { json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ (สิทธิ์เขียนไฟล์?)'], 500); } foreach ($payload['modes'] as $k => $m) { $payload['modes'][$k] = array_merge($m, qb_state($m)); } json_response(['ok' => true, 'message' => 'บันทึกแล้ว'] + $payload); } json_response(['ok' => false, 'error' => 'Use GET / PUT'], 405);