Website-fabianschieder/adminer/index.php

554 lines
25 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
require_once __DIR__ . '/user_auth.php';
require_once __DIR__ . '/views.php';
adminer_app_session_start();
// Bootstrap users table (+ optional seed)
try {
adminer_app_bootstrap();
} catch (Throwable $e) {
admin_layout('DB-Verwaltung', '<div class="notice notice-err">' . h($e->getMessage()) . '</div>', 'Fehler beim Start');
exit;
}
// App-Logout
if ((string)($_GET['auth'] ?? '') === 'logout') {
adminer_app_logout();
header('Location: /adminer', true, 302);
exit;
}
$appPage = (string)($_GET['page'] ?? 'login');
$appError = null;
$appRegError = null;
$appRegOk = null;
// Login POST
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_login') {
$res = adminer_app_try_login((string)($_POST['username'] ?? ''), (string)($_POST['password'] ?? ''));
if (!empty($res['ok'])) {
header('Location: /adminer', true, 302);
exit;
}
$appError = (string)($res['error'] ?? 'Login fehlgeschlagen.');
$appPage = 'login';
}
// Register POST
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_register') {
$res = adminer_app_try_register(
(string)($_POST['username'] ?? ''),
(string)($_POST['password'] ?? ''),
(string)($_POST['password2'] ?? '')
);
if (!empty($res['ok'])) {
$appRegOk = 'Konto erstellt. Bitte lasse dich von einem Server-Administrator verifizieren, bevor du dich anmelden kannst.';
$appPage = 'login';
} else {
$appRegError = (string)($res['error'] ?? 'Registrierung fehlgeschlagen.');
$appPage = 'register';
}
}
// ── LOGIN / REGISTER SEITE ────────────────────────────────────────────────
if (!adminer_app_is_logged_in()) {
$canReg = adminer_app_allow_register();
$isReg = ($appPage === 'register');
$body = '<div class="login-wrap">';
// Tabs
$body .= '<div class="tabs">';
$body .= '<a class="tab ' . (!$isReg ? 'active' : '') . '" href="/adminer?page=login">Login</a>';
if ($canReg) $body .= '<a class="tab ' . ($isReg ? 'active' : '') . '" href="/adminer?page=register">Registrieren</a>';
$body .= '</div>';
$body .= '<div class="card">';
if ($appRegOk) $body .= '<div class="notice notice-ok">' . h($appRegOk) . '</div>';
if ($appError) $body .= '<div class="notice notice-err">' . h($appError) . '</div>';
if ($appRegError)$body .= '<div class="notice notice-err">' . h($appRegError) . '</div>';
if ($isReg && $canReg) {
// ── Registrierungsformular ──────────────────────────────────────
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="app_register">'
. '<div class="field"><label>Benutzername</label><input name="username" autocomplete="username" placeholder="z.B. fabian"></div>'
. '<div class="field"><label>Passwort</label><input name="password" type="password" autocomplete="new-password" placeholder="mind. 8 Zeichen"></div>'
. '<div class="field"><label>Passwort wiederholen</label><input name="password2" type="password" autocomplete="new-password" placeholder="Wiederholung"></div>'
. '<button class="btn" type="submit" style="width:100%">Konto erstellen</button>'
. '</form>';
$body .= '<p class="muted" style="margin-top:.9rem;text-align:center">Bereits ein Konto? <a href="/adminer?page=login">Login</a></p>';
} else {
// ── Login-Formular ──────────────────────────────────────────────
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="app_login">'
. '<div class="field"><label>Benutzername</label><input name="username" autocomplete="username" placeholder="Dein Benutzername"></div>'
. '<div class="field"><label>Passwort</label><input name="password" type="password" autocomplete="current-password" placeholder="Dein Passwort"></div>'
. '<button class="btn" type="submit" style="width:100%">Anmelden</button>'
. '</form>';
if ($canReg) $body .= '<p class="muted" style="margin-top:.9rem;text-align:center">Noch kein Konto? <a href="/adminer?page=register">Registrieren</a></p>';
}
$body .= '</div>';
$body .= '</div>';
admin_layout('DB-Verwaltung', $body, $isReg ? 'Neues Konto erstellen' : 'Bitte einloggen');
exit;
}
// ── DB-VERBINDUNGS-LOGIN ──────────────────────────────────────────────────
require_once __DIR__ . '/auth.php';
admin_session_start();
if ((string)($_GET['a'] ?? '') === 'logout') {
admin_logout();
header('Location: /adminer', true, 302);
exit;
}
if (!isset($_SESSION['db_admin_select'])) $_SESSION['db_admin_select'] = [];
$selectError = null;
$selectMsg = null;
// Probe: Datenbanken laden
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'probe') {
$host = trim((string)($_POST['host'] ?? ''));
$port = (int)($_POST['port'] ?? 3306);
$user = trim((string)($_POST['user'] ?? ''));
$pass = (string)($_POST['pass'] ?? '');
if ($host === '' || $port <= 0 || $user === '') {
$selectError = 'Bitte Host, Port und Benutzer angeben.';
} else {
try {
$pdo = new PDO(sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $host, $port), $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$dbs = $pdo->query('SHOW DATABASES')->fetchAll(PDO::FETCH_COLUMN, 0);
$_SESSION['db_admin_select'] = compact('host', 'port', 'user', 'pass', 'dbs');
$selectMsg = 'Datenbanken geladen bitte unten eine auswählen.';
} catch (Throwable $e) {
$selectError = 'Fehler: ' . $e->getMessage();
}
}
}
// DB-Login
$dbError = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'login') {
$res = admin_try_login(
trim((string)($_POST['host'] ?? '')),
(int)($_POST['port'] ?? 3306),
trim((string)($_POST['user'] ?? '')),
(string)($_POST['pass'] ?? ''),
trim((string)($_POST['db'] ?? ''))
);
if ($res['ok']) {
header('Location: /adminer', true, 302);
exit;
}
$dbError = (string)($res['error'] ?? 'Login fehlgeschlagen.');
}
$defaults = admin_default_creds();
$selectState = is_array($_SESSION['db_admin_select']) ? $_SESSION['db_admin_select'] : [];
$prefHost = isset($selectState['host']) ? (string)$selectState['host'] : (string)$defaults['host'];
$prefPort = isset($selectState['port']) ? (int)$selectState['port'] : (int)$defaults['port'];
$prefUser = isset($selectState['user']) ? (string)$selectState['user'] : (string)$defaults['user'];
$prefPass = isset($selectState['pass']) ? (string)$selectState['pass'] : '';
$dbList = isset($selectState['dbs']) && is_array($selectState['dbs']) ? $selectState['dbs'] : [];
if (!admin_is_logged_in()) {
$body = '<div class="card" style="max-width:520px;margin:0 auto">';
if ($selectMsg) $body .= '<div class="notice notice-ok">' . h($selectMsg) . '</div>';
if ($selectError)$body .= '<div class="notice notice-err">' . h($selectError) . '</div>';
if ($dbError) $body .= '<div class="notice notice-err">' . h($dbError) . '</div>';
// Step 1
$body .= '<h2>1 · Server verbinden</h2>';
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="probe">'
. '<div class="field"><label>Host</label><input name="host" value="' . h($prefHost) . '" placeholder="localhost"></div>'
. '<div class="field"><label>Port</label><div class="field-port"><input id="port-input" name="port" type="number" value="' . h((string)$prefPort) . '" min="1" max="65535"><div class="field-port__spinners"><button type="button" onclick="var i=document.getElementById(\'port-input\');i.value=Math.min(65535,+(i.value||3306)+1)">▲</button><button type="button" onclick="var i=document.getElementById(\'port-input\');i.value=Math.max(1,+(i.value||3306)-1)">▼</button></div></div></div>'
. '<div class="field"><label>Benutzer</label><input name="user" value="' . h($prefUser) . '"></div>'
. '<div class="field"><label>Passwort</label><input name="pass" type="password" value="' . h($prefPass) . '"></div>'
. '<button class="btn btn-ghost btn-sm" type="submit">Datenbanken laden</button>'
. '</form>';
$body .= '<hr>';
// Step 2
$body .= '<h2>2 · Datenbank auswählen & einloggen</h2>';
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="login">'
. '<input type="hidden" name="host" value="' . h($prefHost) . '">'
. '<input type="hidden" name="port" value="' . h((string)$prefPort) . '">'
. '<input type="hidden" name="user" value="' . h($prefUser) . '">'
. '<input type="hidden" name="pass" value="' . h($prefPass) . '">'
. '<div class="field"><label>Datenbank</label>';
if (!empty($dbList)) {
$body .= '<select name="db">';
$sel = (string)$defaults['db'];
foreach ($dbList as $dbName) {
$dbName = (string)$dbName;
$body .= '<option value="' . h($dbName) . '"' . ($dbName === $sel ? ' selected' : '') . '>' . h($dbName) . '</option>';
}
$body .= '</select>';
} else {
$body .= '<input name="db" value="' . h((string)$defaults['db']) . '" placeholder="Datenbank-Name">';
}
$body .= '</div>'
. '<button class="btn" type="submit" style="width:100%">Einloggen</button>'
. '</form>';
$body .= '</div>';
admin_layout('DB-Verwaltung', $body, 'Datenbankverbindung');
exit;
}
// ── DB-VERWALTUNG (eingeloggt) ────────────────────────────────────────────
try {
$pdo = admin_pdo();
$table = (string)($_GET['t'] ?? '');
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$msg = null;
$queryResultHtml = '';
// SQL Query ausführen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'query') {
$sql = trim((string)($_POST['sql'] ?? ''));
if ($sql !== '') {
if (preg_match('/;\s*\S/', $sql)) {
$msg = ['ok' => false, 'text' => 'Nur ein Statement ausführen (kein zweites Semikolon).'];
} else {
try {
$stmt = $pdo->query($sql);
if ($stmt instanceof PDOStatement) {
$rows = $stmt->fetchAll();
$queryResultHtml = '<h3 style="margin-top:1.2rem">Ergebnis</h3>' . admin_render_table($rows);
$msg = ['ok' => true, 'text' => 'Query ausgeführt (' . count($rows) . ' Zeilen).'];
} else {
$msg = ['ok' => true, 'text' => 'Statement ausgeführt.'];
}
} catch (Throwable $e) {
$msg = ['ok' => false, 'text' => $e->getMessage()];
}
}
}
}
// Datensatz löschen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'delete_row') {
$delTable = (string)($_POST['table'] ?? '');
$pkCol = (string)($_POST['pk_col'] ?? '');
$pkVal = (string)($_POST['pk_val'] ?? '');
if (!preg_match('/^[A-Za-z0-9_]+$/', $delTable) || !preg_match('/^[A-Za-z0-9_]+$/', $pkCol)) {
$msg = ['ok' => false, 'text' => 'Ungültige Parameter für Löschen.'];
} else {
try {
// PK-Spalte gegen echte PK-Spalte validieren
$realPk = admin_get_primary_key_column($pdo, $delTable);
if ($realPk === null || $realPk !== $pkCol) {
$msg = ['ok' => false, 'text' => 'Löschen ist nur über eine echte Primary-Key-Spalte möglich.'];
} else {
$stmt = $pdo->prepare('DELETE FROM `' . $delTable . '` WHERE `' . $pkCol . '` = :v LIMIT 1');
$stmt->execute([':v' => $pkVal]);
$msg = ['ok' => true, 'text' => 'Datensatz gelöscht.'];
// Wenn wir gerade diese Tabelle anzeigen: auf Seite 1 zurück, damit man nicht auf leerer Seite landet
if ($table === $delTable) {
$page = 1;
$offset = 0;
}
}
} catch (Throwable $e) {
$msg = ['ok' => false, 'text' => $e->getMessage()];
}
}
}
// Datensatz hinzufügen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'insert_row') {
$insTable = (string)($_POST['table'] ?? '');
if (!preg_match('/^[A-Za-z0-9_]+$/', $insTable)) {
$msg = ['ok' => false, 'text' => 'Ungültiger Tabellenname für Insert.'];
} else {
try {
$colsMeta = admin_get_table_columns($pdo, $insTable);
if (empty($colsMeta)) {
$msg = ['ok' => false, 'text' => 'Keine Spalten gefunden.'];
} else {
$fields = [];
foreach ($colsMeta as $c) {
$name = (string)$c['Field'];
// Nur Spalten zulassen, die wirklich existieren
if (!array_key_exists('col_' . $name, $_POST)) continue;
$raw = (string)($_POST['col_' . $name] ?? '');
$isNull = ((string)($_POST['null_' . $name] ?? '') === '1');
$fields[] = [
'name' => $name,
'value' => $isNull ? null : ($raw === '' ? null : $raw),
'forceNull' => $isNull,
];
}
// Leere Submits verhindern
if (empty($fields)) {
$msg = ['ok' => false, 'text' => 'Keine Felder zum Einfügen übergeben.'];
} else {
$colNames = [];
$placeholders = [];
$params = [];
foreach ($fields as $f) {
$colNames[] = '`' . $f['name'] . '`';
$ph = ':c_' . $f['name'];
$placeholders[] = $ph;
$params[$ph] = $f['value'];
}
$sql = 'INSERT INTO `' . $insTable . '` (' . implode(',', $colNames) . ') VALUES (' . implode(',', $placeholders) . ')';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$msg = ['ok' => true, 'text' => 'Datensatz hinzugefügt.'];
// Nach Insert wieder zur Tabelle springen
$table = $insTable;
$page = 1;
$offset = 0;
}
}
} catch (Throwable $e) {
$msg = ['ok' => false, 'text' => $e->getMessage()];
}
}
}
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM);
// ── TOP BAR ──────────────────────────────────────────────────────────
$dbName = (string)($_SESSION['db_admin']['db'] ?? '');
$uname = (string)($_SESSION['adminer_app']['username'] ?? '');
$body = '<div class="top-bar">'
. '<div class="top-bar-left">'
. '<span style="font-weight:700;font-size:1rem">' . h($dbName) . '</span>'
. ($uname ? '<span class="pill">' . h($uname) . '</span>' : '')
. '</div>'
. '<div class="top-bar-actions">'
. '<a class="btn btn-ghost btn-sm" href="/adminer?a=logout">DB-Logout</a>'
. '<a class="btn btn-ghost btn-sm" href="/adminer?auth=logout">Account-Logout</a>'
. '</div>'
. '</div>';
// ── GRID: TABELLENLISTE + CONTENT ─────────────────────────────────────
$body .= '<div class="admin-grid">';
// Linke Spalte: Tabellenliste
$body .= '<div class="card">';
$body .= '<h2>Tabellen</h2>';
if (empty($tables)) {
$body .= '<p class="muted">Keine Tabellen gefunden.</p>';
} else {
$body .= '<ul class="nav-list">';
foreach ($tables as $row) {
$tn = (string)$row[0];
$cls = ($tn === $table) ? 'active' : '';
$body .= '<li><a class="' . $cls . '" href="/adminer?t=' . rawurlencode($tn) . '">' . h($tn) . '</a></li>';
}
$body .= '</ul>';
}
$body .= '</div>';
// Rechte Spalte: Browse + Query
$body .= '<div>';
// Notices
if ($msg) {
$cls = $msg['ok'] ? 'notice-ok' : 'notice-err';
$body .= '<div class="notice ' . $cls . '">' . h($msg['text']) . '</div>';
}
// Browse
if ($table !== '') {
if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
$body .= '<div class="notice notice-err">Ungültiger Tabellenname.</div>';
} else {
$pkCol = admin_get_primary_key_column($pdo, $table);
$stmt = $pdo->query('SELECT * FROM `' . $table . '` LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset);
$rows = $stmt->fetchAll();
$body .= '<div class="card" style="margin-bottom:16px">';
$body .= '<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">'
. '<h2 style="margin:0">' . h($table) . '</h2>'
. '<a class="btn btn-ghost btn-sm" href="/adminer?t=' . rawurlencode($table) . '&add=1">+ Datensatz</a>'
. '</div>';
$body .= admin_render_table($rows, $table, $pkCol);
$body .= '<div class="pagination">';
if ($page > 1) $body .= '<a class="btn btn-ghost btn-sm" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page - 1) . '">← Zurück</a>';
if (count($rows) === $limit) $body .= '<a class="btn btn-ghost btn-sm" href="/adminer?t=' . rawurlencode($table) . '&p=' . ($page + 1) . '">Weiter →</a>';
if ($page > 1 || count($rows) === $limit) $body .= '<span class="muted">Seite ' . $page . '</span>';
$body .= '</div>';
$body .= '</div>';
// Add form
if ((string)($_GET['add'] ?? '') === '1') {
$colsMeta = admin_get_table_columns($pdo, $table);
$body .= '<div class="card" style="margin-bottom:16px">';
$body .= '<h2>Datensatz hinzufügen</h2>';
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="insert_row">'
. '<input type="hidden" name="table" value="' . h($table) . '">';
foreach ($colsMeta as $c) {
$col = (string)$c['Field'];
$extra = (string)($c['Extra'] ?? '');
$nullOk = ((string)($c['Null'] ?? 'NO') === 'YES');
// auto_increment standardmäßig nicht anfassen
if (stripos($extra, 'auto_increment') !== false) {
$body .= '<div class="field">'
. '<label>' . h($col) . ' <span class="pill">auto</span></label>'
. '<input disabled value="(auto)" />'
. '</div>';
continue;
}
$body .= '<div class="field">'
. '<label>' . h($col) . ($nullOk ? ' <span class="pill">NULL ok</span>' : '') . '</label>'
. '<input name="col_' . h($col) . '" placeholder="Wert…">';
if ($nullOk) {
$body .= '<div style="margin-top:8px;display:flex;align-items:center;gap:8px">'
. '<input style="width:auto" type="checkbox" name="null_' . h($col) . '" value="1" id="null_' . h($col) . '">'
. '<label for="null_' . h($col) . '" style="margin:0;text-transform:none;letter-spacing:0;font-size:.85rem;color:var(--text-muted)">NULL setzen</label>'
. '</div>';
}
$body .= '</div>';
}
$body .= '<div style="display:flex;gap:10px;flex-wrap:wrap">'
. '<button class="btn" type="submit">Speichern</button>'
. '<a class="btn btn-ghost" href="/adminer?t=' . rawurlencode($table) . '">Abbrechen</a>'
. '</div>'
. '</form>';
$body .= '</div>';
}
}
}
// SQL Query Box
$body .= '<div class="card">';
$body .= '<h2>SQL Query</h2>';
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="query">'
. '<div class="field"><textarea name="sql" rows="6" placeholder="SELECT * FROM tabelle LIMIT 10"></textarea></div>'
. '<button class="btn btn-sm" type="submit">Ausführen</button>'
. '</form>';
$body .= $queryResultHtml;
$body .= '</div>';
$body .= '</div>'; // right col
$body .= '</div>'; // admin-grid
admin_layout('DB-Verwaltung', $body, h($dbName), 'wrap--full');
} catch (Throwable $e) {
admin_logout();
admin_layout('DB-Verwaltung',
'<div class="notice notice-err">' . h($e->getMessage()) . '</div>'
. '<p style="margin-top:1rem;text-align:center"><a class="btn btn-ghost btn-sm" href="/adminer">Zurück zum Login</a></p>',
'Fehler',
'wrap--wide'
);
}
function admin_render_table(array $rows, string $table = '', ?string $pkCol = null): string
{
if (empty($rows)) return '<p class="muted">(keine Zeilen)</p>';
$cols = array_keys((array)$rows[0]);
$hasActions = ($table !== '' && $pkCol !== null && in_array($pkCol, $cols, true));
$html = '<div class="table-scroll"><table class="db-table"><thead><tr>';
foreach ($cols as $c) $html .= '<th>' . h((string)$c) . '</th>';
if ($hasActions) $html .= '<th style="width:1%;white-space:nowrap">Aktionen</th>';
$html .= '</tr></thead><tbody>';
foreach ($rows as $r) {
$html .= '<tr>';
foreach ($cols as $c) {
$v = $r[$c] ?? null;
$cell = $v === null
? '<span class="null-val">NULL</span>'
: (strlen((string)$v) > 300 ? h(substr((string)$v, 0, 300)) . '…' : h((string)$v));
$html .= '<td>' . $cell . '</td>';
}
if ($hasActions) {
$pkVal = $r[$pkCol] ?? null;
$safePkVal = $pkVal === null ? '' : (string)$pkVal;
$html .= '<td>'
. '<form method="post" style="display:inline" onsubmit="return confirm(\'Datensatz wirklich löschen?\');">'
. '<input type="hidden" name="action" value="delete_row">'
. '<input type="hidden" name="table" value="' . h($table) . '">'
. '<input type="hidden" name="pk_col" value="' . h((string)$pkCol) . '">'
. '<input type="hidden" name="pk_val" value="' . h($safePkVal) . '">'
. '<button class="btn btn-ghost btn-sm" type="submit">Löschen</button>'
. '</form>'
. '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table></div>';
return $html;
}
function admin_get_primary_key_column(PDO $pdo, string $table): ?string
{
if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) return null;
$stmt = $pdo->prepare(
"SELECT COLUMN_NAME\n"
. "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE\n"
. "WHERE TABLE_SCHEMA = DATABASE()\n"
. " AND TABLE_NAME = :t\n"
. " AND CONSTRAINT_NAME = 'PRIMARY'\n"
. "ORDER BY ORDINAL_POSITION\n"
. "LIMIT 1"
);
$stmt->execute([':t' => $table]);
$pk = $stmt->fetchColumn();
return $pk !== false ? (string)$pk : null;
}
function admin_get_table_columns(PDO $pdo, string $table): array
{
if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) return [];
$stmt = $pdo->query('SHOW COLUMNS FROM `' . $table . '`');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
return is_array($rows) ? $rows : [];
}