query("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('reports','objects','assets')"); $names = $stmt ? array_column($stmt->fetchAll(PDO::FETCH_ASSOC), 'name') : []; return in_array('reports', $names, true) && in_array('objects', $names, true) && in_array('assets', $names, true); } function choose_store(array $config): array { $sqlitePath = (string)$config['db_path']; if (sqlite_available() && is_file($sqlitePath)) return ['type' => 'sqlite', 'path' => $sqlitePath]; $jsonPath = filedb_path($config); if (is_file($jsonPath)) return ['type' => 'filedb', 'path' => $jsonPath]; if (sqlite_available()) return ['type' => 'sqlite_missing', 'path' => $sqlitePath]; return ['type' => 'filedb_missing', 'path' => $jsonPath]; } function read_filedb(string $path): array { if (!is_file($path)) return []; $raw = file_get_contents($path); $db = $raw ? json_decode($raw, true) : null; return is_array($db) ? $db : []; } function write_filedb(string $path, callable $mutator): void { $dir = dirname($path); if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) throw new RuntimeException('data directory unavailable'); $fp = fopen($path, 'c+'); if (!$fp) throw new RuntimeException('file db unavailable'); try { if (!flock($fp, LOCK_EX)) throw new RuntimeException('file db lock failed'); $raw = stream_get_contents($fp); $db = $raw ? json_decode($raw, true) : null; if (!is_array($db)) $db = ['schema'=>1,'accounts'=>[],'assets'=>[],'objects'=>[],'votes'=>[],'reports'=>[],'events'=>[],'commands'=>[],'nextEventId'=>1]; $mutator($db); ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode($db, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); fflush($fp); flock($fp, LOCK_UN); } finally { fclose($fp); } } function asset_name_from_json($json, string $fallback = 'Untitled'): string { if (is_array($json)) return (string)($json['name'] ?? $fallback); $d = json_decode((string)$json, true); return is_array($d) ? (string)($d['name'] ?? $fallback) : $fallback; } function list_sqlite_reports(string $path): array { $db = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); $db->exec('PRAGMA busy_timeout = 5000'); if (!sqlite_tables_ready($db)) return []; $rows = $db->query("SELECT r.*, o.moderation_status, o.json AS object_json, a.json AS asset_json FROM reports r LEFT JOIN objects o ON o.id = r.object_id LEFT JOIN assets a ON a.id = r.asset_id WHERE r.status = 'open' ORDER BY r.created_at DESC LIMIT 200")->fetchAll(); return array_map(function($r) { return [ 'id' => $r['id'] ?? '', 'object_id' => $r['object_id'] ?? '', 'asset_id' => $r['asset_id'] ?? '', 'reason' => $r['reason'] ?? '', 'reporter' => $r['reporter_account_id'] ?? '', 'created_at' => (int)($r['created_at'] ?? 0), 'moderation_status' => $r['moderation_status'] ?? 'missing', 'asset_name' => asset_name_from_json($r['asset_json'] ?? '', 'Untitled'), ]; }, $rows ?: []); } function list_filedb_reports(string $path): array { $db = read_filedb($path); $reports = is_array($db['reports'] ?? null) ? $db['reports'] : []; $objects = is_array($db['objects'] ?? null) ? $db['objects'] : []; $assets = is_array($db['assets'] ?? null) ? $db['assets'] : []; $out = []; foreach ($reports as $r) { if (($r['status'] ?? 'open') !== 'open') continue; $objectId = (string)($r['object_id'] ?? ''); $assetId = (string)($r['asset_id'] ?? ''); $object = $objects[$objectId] ?? []; $asset = $assets[$assetId] ?? []; $out[] = [ 'id' => $r['id'] ?? '', 'object_id' => $objectId, 'asset_id' => $assetId, 'reason' => $r['reason'] ?? '', 'reporter' => $r['reporter_account_id'] ?? '', 'created_at' => (int)($r['created_at'] ?? 0), 'moderation_status' => $object['moderation_status'] ?? 'missing', 'asset_name' => asset_name_from_json($asset['json'] ?? [], 'Untitled'), ]; } usort($out, fn($a, $b) => ((int)$b['created_at']) <=> ((int)$a['created_at'])); return array_slice($out, 0, 200); } function apply_sqlite_action(string $path, string $objectId, string $action): void { $db = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); $db->exec('PRAGMA busy_timeout = 5000'); if (!sqlite_tables_ready($db)) return; $now = (int)floor(microtime(true) * 1000); if ($action === 'hide') { $db->prepare("UPDATE objects SET moderation_status = 'violation_hidden', updated_at = ? WHERE id = ?")->execute([$now, $objectId]); } elseif ($action === 'restore') { $db->prepare("UPDATE objects SET moderation_status = 'active', updated_at = ? WHERE id = ?")->execute([$now, $objectId]); } elseif ($action === 'close_reports') { $db->prepare("UPDATE reports SET status = 'closed' WHERE object_id = ?")->execute([$objectId]); } } function apply_filedb_action(string $path, string $objectId, string $action): void { write_filedb($path, function(array &$db) use ($objectId, $action) { $now = (int)floor(microtime(true) * 1000); if (($action === 'hide' || $action === 'restore') && isset($db['objects'][$objectId])) { $db['objects'][$objectId]['moderation_status'] = $action === 'hide' ? 'violation_hidden' : 'active'; $db['objects'][$objectId]['updated_at'] = $now; } elseif ($action === 'close_reports' && is_array($db['reports'] ?? null)) { foreach ($db['reports'] as &$report) { if (($report['object_id'] ?? '') === $objectId) $report['status'] = 'closed'; } unset($report); } }); } if ($token === '') { http_response_code(403); echo '
Set admin_token in api/config.php to enable this page.
Invalid admin token.
'; exit; } $store = choose_store($config); $error = ''; try { if ($_SERVER['REQUEST_METHOD'] === 'POST') { $objectId = clean_object_id($_POST['object_id'] ?? ''); $action = (string)($_POST['moderation_action'] ?? ''); if ($objectId !== '') { if ($store['type'] === 'sqlite') apply_sqlite_action($store['path'], $objectId, $action); elseif ($store['type'] === 'filedb') apply_filedb_action($store['path'], $objectId, $action); } header('Location: ./?token=' . rawurlencode($given)); exit; } if ($store['type'] === 'sqlite') $reports = list_sqlite_reports($store['path']); elseif ($store['type'] === 'filedb') $reports = list_filedb_reports($store['path']); else $reports = []; } catch (Throwable $e) { $reports = []; $error = $e->getMessage(); } $storeLabel = $store['type'] === 'sqlite' ? 'SQLite' : ($store['type'] === 'filedb' ? 'JSON file DB' : 'DB not initialized yet'); ?>Store: =h($storeLabel)?> / Open reports: =count($reports)?>. Hiding an object removes it from public snapshots; restore makes it visible again.
Admin error: =h($error)?>
No DB has been initialized yet. Open ../api/index.php?action=health and use the app once, then return here.
Reason: =h($r['reason'] ?? '')?> / Reporter: =h($r['reporter'] ?? '')?>
Object: =h($r['object_id'] ?? '')?> / Asset: =h($r['asset_id'] ?? '')?> / Status: =h($r['moderation_status'] ?? 'missing')?>
No open reports.