$msg], $code); } function ensure_data_dir(): void { if (!is_dir(DATA_DIR)) { if (!@mkdir(DATA_DIR, 0750, true)) bad('Kunne ikke opprette data-mappe', 500); } if (!file_exists(DATA_FILE)) { @file_put_contents(DATA_FILE, "[]\n"); @chmod(DATA_FILE, 0640); } } function read_tasks(): array { ensure_data_dir(); $fp = @fopen(DATA_FILE, 'r'); if (!$fp) bad('Kunne ikke lese data', 500); try { if (!flock($fp, LOCK_SH)) bad('Kunne ikke låse for lesing', 500); $size = filesize(DATA_FILE); $raw = $size > 0 ? fread($fp, $size) : '[]'; flock($fp, LOCK_UN); } finally { fclose($fp); } $data = json_decode($raw, true); if (!is_array($data)) return []; return $data; } function write_tasks(array $tasks): void { ensure_data_dir(); $fp = @fopen(DATA_FILE, 'c+'); if (!$fp) bad('Kunne ikke åpne data for skriving', 500); try { if (!flock($fp, LOCK_EX)) bad('Kunne ikke låse for skriving', 500); ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode(array_values($tasks), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); fflush($fp); flock($fp, LOCK_UN); } finally { fclose($fp); } } function add_known_user(string $user): void { ensure_data_dir(); $known = @file_exists(USERS_FILE) ? json_decode(@file_get_contents(USERS_FILE), true) : []; if (!is_array($known)) $known = []; if (!in_array($user, $known, true)) { $known[] = $user; sort($known); @file_put_contents(USERS_FILE, json_encode($known, JSON_UNESCAPED_UNICODE)); } } function known_users(): array { if (!file_exists(USERS_FILE)) return []; $list = json_decode(@file_get_contents(USERS_FILE), true); return is_array($list) ? $list : []; } function uid(): string { return 't_' . bin2hex(random_bytes(5)); } function body_json(): array { $raw = file_get_contents('php://input'); if ($raw === '' || $raw === false) return []; $d = json_decode($raw, true); return is_array($d) ? $d : []; } function sanitize_str($v, int $max = 500): string { if (!is_string($v)) return ''; $v = trim($v); // Fjern kontrolltegn (bortsett fra \n \r \t) $v = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $v); if (mb_strlen($v) > $max) $v = mb_substr($v, 0, $max); return $v; } function sanitize_tags($v): array { if (!is_array($v)) return []; $out = []; foreach ($v as $t) { $t = sanitize_str($t, 40); if ($t !== '') $out[] = $t; if (count($out) >= 20) break; } return $out; } function normalize_task($in, string $user, ?array $existing = null): array { global $VALID_STATUS, $VALID_PRIORITIES, $VALID_CATEGORIES; $t = $existing ?: []; $t['id'] = $existing['id'] ?? (isset($in['id']) && is_string($in['id']) ? preg_replace('/[^a-z0-9_]/i', '', $in['id']) : uid()); if (!$t['id']) $t['id'] = uid(); $t['title'] = sanitize_str($in['title'] ?? ($existing['title'] ?? ''), 200); $t['desc'] = sanitize_str($in['desc'] ?? ($existing['desc'] ?? ''), 4000); $cat = $in['category'] ?? ($existing['category'] ?? 'L3'); $t['category'] = in_array($cat, $VALID_CATEGORIES, true) ? $cat : 'L3'; $prio = $in['priority'] ?? ($existing['priority'] ?? 'P3'); $t['priority'] = in_array($prio, $VALID_PRIORITIES, true) ? $prio : 'P3'; $stat = $in['status'] ?? ($existing['status'] ?? 'todo'); $t['status'] = in_array($stat, $VALID_STATUS, true) ? $stat : 'todo'; $t['estHours'] = isset($in['estHours']) ? max(0, min(999, (float)$in['estHours'])) : ($existing['estHours'] ?? 0); $t['location'] = sanitize_str($in['location'] ?? ($existing['location'] ?? ''), 120); $dl = $in['deadline'] ?? ($existing['deadline'] ?? ''); if (is_string($dl) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dl)) $t['deadline'] = $dl; else $t['deadline'] = ''; $t['tags'] = sanitize_tags($in['tags'] ?? ($existing['tags'] ?? [])); // Owner $owner = isset($in['owner']) ? sanitize_str($in['owner'], 64) : ($existing['owner'] ?? ''); $t['owner'] = $owner !== '' ? $owner : ($existing['owner'] ?? $user); // Audit $t['created'] = $existing['created'] ?? gmdate('c'); $t['createdBy'] = $existing['createdBy'] ?? $user; $t['updated'] = gmdate('c'); $t['updatedBy'] = $user; // Completed timestamp logic $wasDone = isset($existing['status']) && $existing['status'] === 'done'; if ($t['status'] === 'done' && !$wasDone) { $t['completed'] = gmdate('Y-m-d'); $t['completedBy'] = $user; } elseif ($t['status'] !== 'done') { $t['completed'] = null; $t['completedBy'] = null; } else { // still done, keep existing $t['completed'] = $existing['completed'] ?? gmdate('Y-m-d'); $t['completedBy'] = $existing['completedBy'] ?? $user; } return $t; } // ---------- Dispatch ---------- $user = current_user(); add_known_user($user); $action = $_GET['action'] ?? ''; $method = $_SERVER['REQUEST_METHOD']; try { switch ($action) { case 'list': { $tasks = read_tasks(); // Samle eiere som også er kjent fra oppgavene $owners = known_users(); foreach ($tasks as $t) { foreach (['owner','createdBy','updatedBy','completedBy'] as $k) { if (!empty($t[$k]) && !in_array($t[$k], $owners, true)) $owners[] = $t[$k]; } } sort($owners); json_out([ 'user' => $user, 'tasks' => $tasks, 'known_users' => array_values(array_unique($owners)), ]); break; } case 'create': { if ($method !== 'POST') bad('POST required', 405); $in = body_json(); if (empty($in['title'])) bad('title er påkrevd'); $tasks = read_tasks(); $new = normalize_task($in, $user, null); $tasks[] = $new; write_tasks($tasks); json_out(['task' => $new]); break; } case 'update': { if ($method !== 'POST') bad('POST required', 405); $in = body_json(); if (empty($in['id'])) bad('id er påkrevd'); $tasks = read_tasks(); $updated = null; foreach ($tasks as $i => $t) { if (($t['id'] ?? '') === $in['id']) { $tasks[$i] = normalize_task($in, $user, $t); $updated = $tasks[$i]; break; } } if (!$updated) bad('Fant ikke oppgaven', 404); write_tasks($tasks); json_out(['task' => $updated]); break; } case 'delete': { if ($method !== 'POST') bad('POST required', 405); $in = body_json(); if (empty($in['id'])) bad('id er påkrevd'); $tasks = read_tasks(); $before = count($tasks); $tasks = array_values(array_filter($tasks, fn($t) => ($t['id'] ?? '') !== $in['id'])); if (count($tasks) === $before) bad('Fant ikke oppgaven', 404); write_tasks($tasks); json_out(['ok' => true]); break; } case 'replace': { if ($method !== 'POST') bad('POST required', 405); $in = body_json(); if (!is_array($in) || !isset($in[0]) && !empty($in)) bad('Forventet array'); $out = []; foreach ($in as $raw) { if (!is_array($raw) || empty($raw['title'])) continue; $out[] = normalize_task($raw, $user, null); } write_tasks($out); json_out(['ok' => true, 'count' => count($out)]); break; } case 'seed': { // Last inn eksempel-data. Nekter hvis allerede data finnes. if ($method !== 'POST') bad('POST required', 405); $existing = read_tasks(); if (count($existing) > 0) bad('Data finnes allerede', 409); $seed = json_decode(@file_get_contents(__DIR__ . '/seed.json'), true); if (!is_array($seed)) bad('Mangler seed.json', 500); $out = []; foreach ($seed as $raw) { if (!is_array($raw) || empty($raw['title'])) continue; $out[] = normalize_task($raw, $user, null); } write_tasks($out); json_out(['ok' => true, 'count' => count($out)]); break; } default: bad('Ukjent action. Bruk list/create/update/delete/replace/seed'); } } catch (Throwable $e) { bad('Serverfeil: ' . $e->getMessage(), 500); }