query("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('accounts','assets','objects','reports')"); $names = $stmt ? array_column($stmt->fetchAll(PDO::FETCH_ASSOC), 'name') : []; return in_array('accounts', $names, true) && in_array('assets', $names, true) && in_array('objects', $names, true) && in_array('reports', $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 redirect_admin(string $token, string $owner = ''): void { $url = './?token=' . rawurlencode($token); if ($owner !== '') $url .= '&owner=' . rawurlencode($owner); header('Location: ' . $url); exit; } function decode_json_value($json): array { if (is_array($json)) return $json; $decoded = json_decode((string)$json, true); return is_array($decoded) ? $decoded : []; } function encode_json_value(array $value): string { return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}'; } function asset_name_from_json($json, string $fallback = 'Untitled'): string { $asset = decode_json_value($json); return (string)($asset['name'] ?? $fallback); } function thumbnail_html($json): string { $asset = decode_json_value($json); $pixels = (string)($asset['faces']['right'] ?? $asset['pixels'] ?? ''); if ($pixels === '') return '
'; $rows = str_contains($pixels, '|') ? explode('|', $pixels) : []; $width = max(1, min(64, (int)($asset['width'] ?? $asset['w'] ?? $asset['size'] ?? 16))); $height = max(1, min(64, (int)($asset['height'] ?? $asset['ht'] ?? $asset['size'] ?? $width))); if (!$rows) { for ($y = 0; $y < $height; $y++) $rows[] = substr($pixels, $y * $width, $width); } $codes = str_split('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' . "!#$%&'()*+,-/:;<=>?@[]^_`{|}~"); $colors = [ '#111827', '#374151', '#6b7280', '#d1d5db', '#fff7ed', '#7f1d1d', '#dc2626', '#f97316', '#facc15', '#84cc16', '#16a34a', '#14b8a6', '#06b6d4', '#2563eb', '#4f46e5', '#7c3aed', '#c026d3', '#ec4899', '#f5d0a9', '#7c4a2d', '#fafafa', '#f5f5f4', '#e7e5e4', '#d6d3d1', '#a8a29e', '#78716c', '#57534e', '#292524', '#0c0a09', '#fef3c7', '#fde68a', '#d6a25f', '#9a6a3a', '#5c3b24', '#fee2e2', '#fca5a5', '#ef4444', '#b91c1c', '#7f1d1d', '#ffedd5', '#fdba74', '#f97316', '#c2410c', '#7c2d12', '#fef9c3', '#fde047', '#eab308', '#a16207', '#713f12', '#ecfccb', '#bef264', '#84cc16', '#4d7c0f', '#365314', '#dcfce7', '#86efac', '#15803d', '#ccfbf1', '#5eead4', '#14b8a6', '#0f766e', '#134e4a', '#cffafe', '#67e8f9', '#22d3ee', '#06b6d4', '#0e7490', '#e0f2fe', '#38bdf8', '#dbeafe', '#93c5fd', '#3b82f6', '#2563eb', '#1d4ed8', '#1e3a8a', '#e0e7ff', '#a5b4fc', '#6366f1', '#4f46e5', '#3730a3', '#ede9fe', '#c4b5fd', '#8b5cf6', '#fae8ff', '#e879f9', '#c026d3', '#86198f', '#fce7f3', '#f9a8d4', '#ec4899', '#be185d' ]; $palette = array_combine($codes, array_slice($colors, 0, count($codes))) ?: []; $cells = ''; foreach (array_slice($rows, 0, $height) as $y => $row) { $chars = preg_split('//u', (string)$row, -1, PREG_SPLIT_NO_EMPTY) ?: []; for ($x = 0; $x < $width; $x++) { $ch = $chars[$x] ?? '.'; if ($ch === '.') continue; $color = $palette[$ch] ?? sprintf('#%06x', (hexdec(substr(md5($ch), 0, 6)) & 0x7fffff) | 0x303030); $cells .= ''; } } return '
' . $cells . '
'; } 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, encode_json_value($db)); fflush($fp); flock($fp, LOCK_UN); } finally { fclose($fp); } } function sqlite_db(string $path): PDO { $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'); return $db; } function ensure_sqlite_anonymous_account(PDO $db): void { $now = now_ms(); $hash = password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT); $db->prepare('INSERT OR IGNORE INTO accounts(id, name, password_hash, created_at, updated_at) VALUES(?,?,?,?,?)') ->execute(['anonymous', 'Anonymous', $hash, $now, $now]); } function list_sqlite_accounts(string $path): array { $db = sqlite_db($path); if (!sqlite_tables_ready($db)) return []; return $db->query("SELECT a.id, a.name, a.created_at, a.updated_at, a.disabled_at, (SELECT COUNT(*) FROM assets x WHERE x.owner_account_id = a.id AND x.deleted_at IS NULL) AS asset_count, (SELECT COUNT(*) FROM objects o WHERE o.owner_account_id = a.id AND o.deleted_at IS NULL) AS object_count, (SELECT COUNT(*) FROM reports r WHERE r.reporter_account_id = a.id AND r.status = 'open') AS open_report_count FROM accounts a ORDER BY a.created_at DESC LIMIT 500")->fetchAll() ?: []; } function list_sqlite_assets(string $path, string $owner = ''): array { $db = sqlite_db($path); if (!sqlite_tables_ready($db)) return []; $sql = "SELECT a.*, acc.name AS owner_name, (SELECT COUNT(*) FROM objects o WHERE o.asset_id = a.id AND o.deleted_at IS NULL) AS object_count FROM assets a LEFT JOIN accounts acc ON acc.id = a.owner_account_id WHERE a.deleted_at IS NULL"; $params = []; if ($owner !== '') { $sql .= ' AND a.owner_account_id = ?'; $params[] = $owner; } $sql .= ' ORDER BY a.updated_at DESC LIMIT 500'; $stmt = $db->prepare($sql); $stmt->execute($params); return array_map(function($row) { $asset = decode_json_value($row['json'] ?? ''); return [ 'id' => $row['id'] ?? '', 'owner_account_id' => $row['owner_account_id'] ?? '', 'owner_name' => $row['owner_name'] ?? '', 'name' => $asset['name'] ?? ($row['author_name'] ?? 'Untitled'), 'json' => $row['json'] ?? '', 'category' => $asset['category'] ?? '', 'subtype' => $asset['subtype'] ?? '', 'updated_at' => (int)($row['updated_at'] ?? 0), 'object_count' => (int)($row['object_count'] ?? 0), ]; }, $stmt->fetchAll() ?: []); } function list_sqlite_reports(string $path): array { $db = sqlite_db($path); if (!sqlite_tables_ready($db)) return []; $rows = $db->query("SELECT r.*, o.moderation_status, 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(fn($r) => [ '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'), 'asset_json' => $r['asset_json'] ?? '', ], $rows ?: []); } function apply_sqlite_admin_action(string $path, array $post): void { $db = sqlite_db($path); if (!sqlite_tables_ready($db)) return; $action = (string)($post['admin_action'] ?? $post['moderation_action'] ?? ''); $now = now_ms(); if ($action === 'rename_account') { $accountId = clean_id($post['account_id'] ?? ''); $name = clean_text($post['name'] ?? '', '', 32); if ($accountId === '' || $name === '') return; $db->beginTransaction(); $db->prepare('UPDATE accounts SET name = ?, updated_at = ? WHERE id = ?')->execute([$name, $now, $accountId]); $rows = $db->prepare('SELECT id, json FROM assets WHERE owner_account_id = ? AND deleted_at IS NULL'); $rows->execute([$accountId]); foreach ($rows->fetchAll() ?: [] as $row) { $asset = decode_json_value($row['json']); $asset['author'] = $name; $asset['updatedAt'] = $now; $db->prepare('UPDATE assets SET author_name = ?, json = ?, updated_at = ? WHERE id = ?')->execute([$name, encode_json_value($asset), $now, $row['id']]); } $db->commit(); } elseif ($action === 'disable_account' || $action === 'enable_account') { $accountId = clean_id($post['account_id'] ?? ''); if ($accountId !== '') $db->prepare('UPDATE accounts SET disabled_at = ?, updated_at = ? WHERE id = ?')->execute([$action === 'disable_account' ? $now : null, $now, $accountId]); } elseif ($action === 'delete_account') { $accountId = clean_id($post['account_id'] ?? ''); if ($accountId === '' || $accountId === 'island-team') return; $db->beginTransaction(); ensure_sqlite_anonymous_account($db); $assetRows = $db->prepare('SELECT id, json FROM assets WHERE owner_account_id = ?'); $assetRows->execute([$accountId]); foreach ($assetRows->fetchAll() ?: [] as $row) { $asset = decode_json_value($row['json'] ?? ''); $asset['ownerAccountId'] = 'anonymous'; $asset['author'] = 'Anonymous'; $asset['updatedAt'] = $now; $db->prepare('UPDATE assets SET owner_account_id = ?, author_name = ?, json = ?, updated_at = ? WHERE id = ?') ->execute(['anonymous', 'Anonymous', encode_json_value($asset), $now, $row['id']]); } $objectRows = $db->prepare('SELECT id, json FROM objects WHERE owner_account_id = ?'); $objectRows->execute([$accountId]); foreach ($objectRows->fetchAll() ?: [] as $row) { $object = decode_json_value($row['json'] ?? ''); $object['ownerAccountId'] = 'anonymous'; $db->prepare('UPDATE objects SET owner_account_id = ?, json = ?, updated_at = ? WHERE id = ?') ->execute(['anonymous', encode_json_value($object), $now, $row['id']]); } $db->prepare('DELETE FROM votes WHERE voter_account_id = ?')->execute([$accountId]); $db->prepare("UPDATE reports SET status = 'closed' WHERE reporter_account_id = ?")->execute([$accountId]); $db->prepare('DELETE FROM accounts WHERE id = ?')->execute([$accountId]); $db->commit(); } elseif ($action === 'rename_asset') { $assetId = clean_id($post['asset_id'] ?? ''); $name = clean_text($post['name'] ?? '', '', 32); if ($assetId === '' || $name === '') return; $stmt = $db->prepare('SELECT json FROM assets WHERE id = ? AND deleted_at IS NULL'); $stmt->execute([$assetId]); $row = $stmt->fetch(); if (!$row) return; $asset = decode_json_value($row['json']); $asset['name'] = $name; $asset['updatedAt'] = $now; $db->prepare('UPDATE assets SET json = ?, updated_at = ? WHERE id = ?')->execute([encode_json_value($asset), $now, $assetId]); } elseif ($action === 'delete_asset') { $assetId = clean_id($post['asset_id'] ?? ''); if ($assetId === '') return; $db->beginTransaction(); $db->prepare('UPDATE assets SET deleted_at = ?, deleted_by = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL')->execute([$now, 'admin', $now, $assetId]); $db->prepare('UPDATE objects SET deleted_at = ?, deleted_by = ?, updated_at = ? WHERE asset_id = ? AND deleted_at IS NULL')->execute([$now, 'admin', $now, $assetId]); $db->prepare('DELETE FROM votes WHERE target_type = ? AND target_id = ?')->execute(['asset', $assetId]); $db->commit(); } elseif ($action === 'hide' || $action === 'restore' || $action === 'close_reports') { $objectId = clean_id($post['object_id'] ?? ''); if ($objectId === '') return; 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]); else $db->prepare("UPDATE reports SET status = 'closed' WHERE object_id = ?")->execute([$objectId]); } } function list_filedb_accounts(string $path): array { $db = read_filedb($path); $out = []; foreach (($db['accounts'] ?? []) as $account) { $id = (string)($account['id'] ?? ''); $assets = 0; $objects = 0; $reports = 0; foreach (($db['assets'] ?? []) as $asset) if (($asset['owner_account_id'] ?? '') === $id && empty($asset['deleted_at'])) $assets++; foreach (($db['objects'] ?? []) as $object) if (($object['owner_account_id'] ?? '') === $id && empty($object['deleted_at'])) $objects++; foreach (($db['reports'] ?? []) as $report) if (($report['reporter_account_id'] ?? '') === $id && ($report['status'] ?? 'open') === 'open') $reports++; $out[] = ['id'=>$id, 'name'=>$account['name'] ?? $id, 'created_at'=>(int)($account['created_at'] ?? 0), 'updated_at'=>(int)($account['updated_at'] ?? 0), 'disabled_at'=>$account['disabled_at'] ?? null, 'asset_count'=>$assets, 'object_count'=>$objects, 'open_report_count'=>$reports]; } usort($out, fn($a, $b) => ((int)$b['created_at']) <=> ((int)$a['created_at'])); return array_slice($out, 0, 500); } function list_filedb_assets(string $path, string $owner = ''): array { $db = read_filedb($path); $accounts = $db['accounts'] ?? []; $out = []; foreach (($db['assets'] ?? []) as $asset) { if (!empty($asset['deleted_at'])) continue; if ($owner !== '' && ($asset['owner_account_id'] ?? '') !== $owner) continue; $id = (string)($asset['id'] ?? ''); $json = decode_json_value($asset['json'] ?? []); $objectCount = 0; foreach (($db['objects'] ?? []) as $object) if (($object['asset_id'] ?? '') === $id && empty($object['deleted_at'])) $objectCount++; $ownerId = (string)($asset['owner_account_id'] ?? ''); $out[] = ['id'=>$id, 'owner_account_id'=>$ownerId, 'owner_name'=>$accounts[$ownerId]['name'] ?? '', 'name'=>$json['name'] ?? 'Untitled', 'json'=>$asset['json'] ?? [], 'category'=>$json['category'] ?? '', 'subtype'=>$json['subtype'] ?? '', 'updated_at'=>(int)($asset['updated_at'] ?? 0), 'object_count'=>$objectCount]; } usort($out, fn($a, $b) => ((int)$b['updated_at']) <=> ((int)$a['updated_at'])); return array_slice($out, 0, 500); } function list_filedb_reports(string $path): array { $db = read_filedb($path); $out = []; foreach (($db['reports'] ?? []) as $r) { if (($r['status'] ?? 'open') !== 'open') continue; $asset = ($db['assets'] ?? [])[(string)($r['asset_id'] ?? '')] ?? []; $object = ($db['objects'] ?? [])[(string)($r['object_id'] ?? '')] ?? []; $out[] = ['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'=>$object['moderation_status'] ?? 'missing', 'asset_name'=>asset_name_from_json($asset['json'] ?? [], 'Untitled'), 'asset_json'=>$asset['json'] ?? []]; } usort($out, fn($a, $b) => ((int)$b['created_at']) <=> ((int)$a['created_at'])); return array_slice($out, 0, 200); } function apply_filedb_admin_action(string $path, array $post): void { write_filedb($path, function(array &$db) use ($post) { $action = (string)($post['admin_action'] ?? $post['moderation_action'] ?? ''); $now = now_ms(); $ensureAnonymous = function() use (&$db, $now): void { $db['accounts'] = is_array($db['accounts'] ?? null) ? $db['accounts'] : []; if (!isset($db['accounts']['anonymous'])) { $db['accounts']['anonymous'] = ['id'=>'anonymous','name'=>'Anonymous','password_hash'=>password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT),'created_at'=>$now,'updated_at'=>$now,'disabled_at'=>null]; } }; if ($action === 'rename_account') { $accountId = clean_id($post['account_id'] ?? ''); $name = clean_text($post['name'] ?? '', '', 32); if ($accountId === '' || $name === '' || !isset($db['accounts'][$accountId])) return; $db['accounts'][$accountId]['name'] = $name; $db['accounts'][$accountId]['updated_at'] = $now; foreach ($db['assets'] as &$asset) { if (($asset['owner_account_id'] ?? '') !== $accountId || !empty($asset['deleted_at'])) continue; $asset['author_name'] = $name; $asset['updated_at'] = $now; $json = decode_json_value($asset['json'] ?? []); $json['author'] = $name; $json['updatedAt'] = $now; $asset['json'] = $json; } unset($asset); } elseif ($action === 'disable_account' || $action === 'enable_account') { $accountId = clean_id($post['account_id'] ?? ''); if ($accountId !== '' && isset($db['accounts'][$accountId])) { $db['accounts'][$accountId]['disabled_at'] = $action === 'disable_account' ? $now : null; $db['accounts'][$accountId]['updated_at'] = $now; } } elseif ($action === 'delete_account') { $accountId = clean_id($post['account_id'] ?? ''); if ($accountId === '' || $accountId === 'island-team') return; $ensureAnonymous(); foreach ($db['assets'] as &$asset) if (($asset['owner_account_id'] ?? '') === $accountId) { $asset['owner_account_id'] = 'anonymous'; $asset['author_name'] = 'Anonymous'; $asset['updated_at'] = $now; $json = decode_json_value($asset['json'] ?? []); $json['ownerAccountId'] = 'anonymous'; $json['author'] = 'Anonymous'; $json['updatedAt'] = $now; $asset['json'] = $json; } unset($asset); foreach ($db['objects'] as &$object) if (($object['owner_account_id'] ?? '') === $accountId) { $object['owner_account_id'] = 'anonymous'; $object['updated_at'] = $now; $json = decode_json_value($object['json'] ?? []); $json['ownerAccountId'] = 'anonymous'; $object['json'] = $json; } unset($object); foreach ($db['reports'] as &$report) if (($report['reporter_account_id'] ?? '') === $accountId) $report['status'] = 'closed'; unset($report); unset($db['accounts'][$accountId]); foreach ($db['votes'] as $key => $vote) if (($vote['voter_account_id'] ?? '') === $accountId) unset($db['votes'][$key]); } elseif ($action === 'rename_asset') { $assetId = clean_id($post['asset_id'] ?? ''); $name = clean_text($post['name'] ?? '', '', 32); if ($assetId === '' || $name === '' || !isset($db['assets'][$assetId])) return; $json = decode_json_value($db['assets'][$assetId]['json'] ?? []); $json['name'] = $name; $json['updatedAt'] = $now; $db['assets'][$assetId]['json'] = $json; $db['assets'][$assetId]['updated_at'] = $now; } elseif ($action === 'delete_asset') { $assetId = clean_id($post['asset_id'] ?? ''); if ($assetId === '' || !isset($db['assets'][$assetId])) return; $db['assets'][$assetId]['deleted_at'] = $now; $db['assets'][$assetId]['deleted_by'] = 'admin'; $db['assets'][$assetId]['updated_at'] = $now; foreach ($db['objects'] as &$object) if (($object['asset_id'] ?? '') === $assetId && empty($object['deleted_at'])) { $object['deleted_at'] = $now; $object['deleted_by'] = 'admin'; $object['updated_at'] = $now; } unset($object); foreach ($db['votes'] as $key => $vote) if (($vote['target_type'] ?? '') === 'asset' && ($vote['target_id'] ?? '') === $assetId) unset($db['votes'][$key]); } elseif ($action === 'hide' || $action === 'restore' || $action === 'close_reports') { $objectId = clean_id($post['object_id'] ?? ''); if ($objectId === '') return; 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') { foreach ($db['reports'] as &$report) if (($report['object_id'] ?? '') === $objectId) $report['status'] = 'closed'; unset($report); } } }); } if ($token === '') { http_response_code(403); echo 'Admin disabled

Admin disabled

'; exit; } if (!hash_equals($token, $given)) { http_response_code(403); echo 'Forbidden

Forbidden

Invalid admin token.

'; exit; } $store = choose_store($config); $error = ''; $accounts = []; $assets = []; $reports = []; try { if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($store['type'] === 'sqlite') apply_sqlite_admin_action($store['path'], $_POST); elseif ($store['type'] === 'filedb') apply_filedb_admin_action($store['path'], $_POST); redirect_admin($given, clean_id($_POST['owner'] ?? '')); } if ($store['type'] === 'sqlite') { $accounts = list_sqlite_accounts($store['path']); $assets = list_sqlite_assets($store['path'], $ownerFilter); $reports = list_sqlite_reports($store['path']); } elseif ($store['type'] === 'filedb') { $accounts = list_filedb_accounts($store['path']); $assets = list_filedb_assets($store['path'], $ownerFilter); $reports = list_filedb_reports($store['path']); } } catch (Throwable $e) { $error = $e->getMessage(); } $storeLabel = $store['type'] === 'sqlite' ? 'SQLite' : ($store['type'] === 'filedb' ? 'JSON file DB' : 'DB not initialized yet'); $baseUrl = './?token=' . rawurlencode($given); ?> Pixel Island Admin

Pixel Island Admin

Store: / Users: / Pictures: / Reports:

Admin error:

No DB has been initialized yet. Open ../api/index.php?action=health and use the app once, then return here.

Users

disabled

ID: / Pictures: / Objects: / Reports:

Pictures

No accounts.

Pictures

ID:
User:
Type: / Uses:

No pictures in this view.

Reports

Reason: / By:

Object: / Asset: / Status:

No open reports.