307 lines
10 KiB
PHP
307 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* NetOps To-Do — REST API
|
|
*
|
|
* Lagrer oppgaver i data/tasks.json. Alle skrive-operasjoner bruker
|
|
* fil-låsing (flock) så parallelle requests ikke ødelegger fila.
|
|
*
|
|
* Endepunkt (alle via POST unntatt list som også godtar GET):
|
|
* ?action=list → { user, tasks: [...], known_users: [...] }
|
|
* ?action=create body: task JSON → { task }
|
|
* ?action=update body: task JSON → { task }
|
|
* ?action=delete body: {id} → { ok: true }
|
|
* ?action=replace body: [task,...] → { ok: true, count }
|
|
*
|
|
* Brukernavn hentes fra (i denne rekkefølgen):
|
|
* 1. $_SERVER['REMOTE_USER'] (satt av Apache Basic Auth / mod_auth_*)
|
|
* 2. $_SERVER['PHP_AUTH_USER'] (Basic Auth håndtert av PHP)
|
|
* 3. $_SERVER['HTTP_X_REMOTE_USER'](en reverse-proxy kan sette dette)
|
|
* 4. 'anon'
|
|
*/
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
// ---------- Config ----------
|
|
define('DATA_DIR', __DIR__ . '/data');
|
|
define('DATA_FILE', DATA_DIR . '/tasks.json');
|
|
define('USERS_FILE', DATA_DIR . '/known_users.json');
|
|
|
|
$VALID_STATUS = ['todo', 'progress', 'blocked', 'done'];
|
|
$VALID_PRIORITIES = ['P1', 'P2', 'P3', 'P4'];
|
|
$VALID_CATEGORIES = ['L1','L2','L3','L4','BGP','PEER','SEC','NMS','AUTO','DOC','INC','PROJ'];
|
|
|
|
// ---------- Helpers ----------
|
|
function current_user(): string {
|
|
foreach (['REMOTE_USER', 'PHP_AUTH_USER', 'HTTP_X_REMOTE_USER'] as $k) {
|
|
if (!empty($_SERVER[$k])) {
|
|
$u = preg_replace('/[^A-Za-z0-9._@\-]/', '', $_SERVER[$k]);
|
|
if ($u !== '') return substr($u, 0, 64);
|
|
}
|
|
}
|
|
return 'anon';
|
|
}
|
|
|
|
function json_out($data, int $code = 200): void {
|
|
http_response_code($code);
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function bad(string $msg, int $code = 400): void {
|
|
json_out(['error' => $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);
|
|
}
|