{"tables":["kunden","artikel"]} * * {"action":"columns","table":"kunden"} * -> {"columns":[{"name":"id","type":"int(11)"}]} * * {"action":"select","table":"kunden","limit":500} * -> {"rows":[{...}]} * * {"action":"query","sql":"SELECT ...","limit":500} * -> {"rows":[{...}]} * * {"action":"insert","table":"kunden","rows":[{...}], * "mode":"append|replace|upsert","createIfMissing":true, * "createSql":"CREATE TABLE ..."} * -> {"inserted":42} * * Fehler: {"error":"Beschreibung"} * ============================================================ */ /* ===================== KONFIGURATION ===================== */ const DB_HOST = '127.0.0.1'; const DB_PORT = 3306; const DB_NAME = 'meine_datenbank'; // Vorgabe, per "database" im Request überschreibbar const DB_USER = 'workbench'; const DB_PASS = 'HIER_PASSWORT_EINTRAGEN'; const API_KEY = 'HIER_EINEN_LANGEN_ZUFAELLIGEN_SCHLUESSEL_EINTRAGEN'; // Adresse, unter der die HTML-Datei läuft. '*' nur zum Ausprobieren verwenden. const ALLOWED_ORIGIN = 'https://tools.example.de'; // Schreibende Aktionen (insert) erlauben? const WRITES_ALLOWED = true; // Nur diese Datenbanken dürfen angesprochen werden. Leer = nur DB_NAME. const ALLOWED_DATABASES = []; // Obergrenzen const MAX_LIMIT = 10000; const MAX_ROWS_PER_INSERT = 20000; /* =================== AB HIER NICHTS MEHR ================= */ header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: ' . ALLOWED_ORIGIN); header('Access-Control-Allow-Headers: Content-Type, X-Api-Key'); header('Access-Control-Allow-Methods: POST, OPTIONS'); header('X-Content-Type-Options: nosniff'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } function fail(string $message, int $code = 400): void { http_response_code($code); echo json_encode(['error' => $message], JSON_UNESCAPED_UNICODE); exit; } function ok(array $payload): void { echo json_encode($payload, JSON_UNESCAPED_UNICODE); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { fail('Nur POST wird unterstützt.', 405); } // Schlüsselprüfung in konstanter Zeit $sentKey = $_SERVER['HTTP_X_API_KEY'] ?? ''; if (!hash_equals(API_KEY, $sentKey)) { fail('Ungültiger oder fehlender API-Schlüssel.', 401); } $raw = file_get_contents('php://input'); $req = json_decode($raw, true); if (!is_array($req)) { fail('Anfrage ist kein gültiges JSON.'); } $action = $req['action'] ?? ''; // Zieldatenbank bestimmen $database = trim((string)($req['database'] ?? '')) ?: DB_NAME; $allowed = ALLOWED_DATABASES ?: [DB_NAME]; if (!in_array($database, $allowed, true)) { fail('Diese Datenbank ist nicht freigegeben: ' . $database, 403); } try { $pdo = new PDO( sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', DB_HOST, DB_PORT, $database), DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ] ); } catch (PDOException $e) { fail('Verbindung fehlgeschlagen: ' . $e->getMessage(), 500); } /** Tabellen-/Spaltennamen dürfen nicht als Parameter gebunden werden - daher streng prüfen. */ function safeIdentifier(string $name): string { if (!preg_match('/^[A-Za-z_][A-Za-z0-9_$]*$/', $name)) { fail('Unzulässiger Bezeichner: ' . $name); } return '`' . $name . '`'; } function clampLimit($value): int { $limit = (int)($value ?? 500); if ($limit < 1) $limit = 1; if ($limit > MAX_LIMIT) $limit = MAX_LIMIT; return $limit; } /** Werte für die Datenbank aufbereiten: Arrays/Objekte werden zu JSON-Text. */ function normalizeValue($value) { if (is_array($value) || is_object($value)) { return json_encode($value, JSON_UNESCAPED_UNICODE); } if (is_bool($value)) return $value ? 1 : 0; return $value; } try { switch ($action) { case 'tables': { $rows = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM); ok(['tables' => array_map(fn($r) => $r[0], $rows)]); } case 'columns': { $table = safeIdentifier((string)($req['table'] ?? '')); $rows = $pdo->query("SHOW COLUMNS FROM $table")->fetchAll(); ok(['columns' => array_map( fn($r) => ['name' => $r['Field'], 'type' => $r['Type'], 'null' => $r['Null'], 'key' => $r['Key']], $rows )]); } case 'select': { $table = safeIdentifier((string)($req['table'] ?? '')); $limit = clampLimit($req['limit'] ?? null); $stmt = $pdo->query("SELECT * FROM $table LIMIT $limit"); ok(['rows' => $stmt->fetchAll()]); } case 'query': { $sql = trim((string)($req['sql'] ?? '')); if ($sql === '') fail('Keine Abfrage übergeben.'); // Bewusst nur lesende Abfragen zulassen if (!preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)\b/i', $sql)) { fail('Über "query" sind nur lesende Abfragen erlaubt (SELECT, SHOW, DESCRIBE, EXPLAIN).'); } if (str_contains($sql, ';') && !preg_match('/;\s*$/', $sql)) { fail('Mehrere Anweisungen in einer Abfrage sind nicht erlaubt.'); } $sql = rtrim(rtrim($sql), ';'); $limit = clampLimit($req['limit'] ?? null); if (!preg_match('/\bLIMIT\s+\d+/i', $sql)) { $sql .= " LIMIT $limit"; } ok(['rows' => $pdo->query($sql)->fetchAll()]); } case 'insert': { if (!WRITES_ALLOWED) fail('Schreibende Zugriffe sind auf diesem Server abgeschaltet.', 403); $tableName = (string)($req['table'] ?? ''); $table = safeIdentifier($tableName); $rows = $req['rows'] ?? []; $mode = $req['mode'] ?? 'append'; if (!is_array($rows) || count($rows) === 0) fail('Keine Zeilen übergeben.'); if (count($rows) > MAX_ROWS_PER_INSERT) { fail('Zu viele Zeilen auf einmal (Grenze: ' . MAX_ROWS_PER_INSERT . ').'); } if (!in_array($mode, ['append', 'replace', 'upsert'], true)) fail('Unbekannter Modus: ' . $mode); // Tabelle bei Bedarf anlegen $exists = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ?'); $exists->execute([$database, $tableName]); if ((int)$exists->fetchColumn() === 0) { if (empty($req['createIfMissing'])) { fail('Tabelle existiert nicht: ' . $tableName); } $createSql = (string)($req['createSql'] ?? ''); if (!preg_match('/^\s*CREATE\s+TABLE\b/i', $createSql)) { fail('Kein gültiges CREATE TABLE übermittelt.'); } $pdo->exec($createSql); } // Spalten aus der ersten Zeile ableiten und gegen die echte Tabelle prüfen $firstRow = $rows[0]; if (!is_array($firstRow)) fail('Zeilen müssen Objekte mit Spaltennamen sein.'); $tableCols = array_column($pdo->query("SHOW COLUMNS FROM $table")->fetchAll(), 'Field'); $columns = array_values(array_filter(array_keys($firstRow), fn($c) => in_array($c, $tableCols, true))); if (count($columns) === 0) fail('Keine der übergebenen Spalten existiert in der Tabelle.'); $quoted = array_map('safeIdentifier', $columns); $placeholder = '(' . implode(', ', array_fill(0, count($columns), '?')) . ')'; $pdo->beginTransaction(); try { if ($mode === 'replace') { $pdo->exec("TRUNCATE TABLE $table"); } $sql = "INSERT INTO $table (" . implode(', ', $quoted) . ") VALUES $placeholder"; if ($mode === 'upsert') { $updates = array_map(fn($c) => "$c = VALUES($c)", $quoted); $sql .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates); } $stmt = $pdo->prepare($sql); $inserted = 0; foreach ($rows as $row) { if (!is_array($row)) continue; $values = []; foreach ($columns as $col) { $values[] = normalizeValue($row[$col] ?? null); } $stmt->execute($values); $inserted++; } $pdo->commit(); ok(['inserted' => $inserted, 'columns' => $columns, 'mode' => $mode]); } catch (Throwable $e) { $pdo->rollBack(); throw $e; } } default: fail('Unbekannte Aktion: ' . $action); } } catch (PDOException $e) { fail('Datenbankfehler: ' . $e->getMessage(), 500); } catch (Throwable $e) { fail('Fehler: ' . $e->getMessage(), 500); }