Dateien nach "/" hochladen
This commit is contained in:
@@ -1,18 +1,57 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 olli1986
|
||||
Copyright (c) 2026 Oliver Braasch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
Hinweis zu verwendeten Bibliotheken Dritter
|
||||
================================================================================
|
||||
|
||||
Diese Software bindet die folgenden quelloffenen Bibliotheken ein. Sie stehen
|
||||
unter den Lizenzen ihrer jeweiligen Urheber und sind nicht von der obigen
|
||||
MIT-Lizenz erfasst. Bei einer Weitergabe müssen deren Lizenztexte und
|
||||
Copyright-Vermerke mitgeliefert werden.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
vanilla-jsoneditor -- Lizenz: ISC
|
||||
Copyright (c) Jos de Jong
|
||||
https://github.com/josdejong/svelte-jsoneditor
|
||||
--------------------------------------------------------------------------------
|
||||
PapaParse -- Lizenz: MIT
|
||||
Copyright (c) Matthew Holt
|
||||
https://github.com/mholt/PapaParse
|
||||
--------------------------------------------------------------------------------
|
||||
js-yaml -- Lizenz: MIT
|
||||
Copyright (c) Vladimir Zapparov, Vitaly Puzrin und Mitwirkende
|
||||
https://github.com/nodeca/js-yaml
|
||||
--------------------------------------------------------------------------------
|
||||
SheetJS Community Edition (xlsx) -- Lizenz: Apache License 2.0
|
||||
Copyright (c) SheetJS LLC
|
||||
https://github.com/SheetJS/sheetjs
|
||||
|
||||
Die Apache-Lizenz 2.0 verlangt bei Weitergabe zusätzlich, dass der
|
||||
Lizenztext beiliegt und Änderungen an den Dateien kenntlich gemacht werden.
|
||||
Lege dazu die Datei LICENSE aus dem SheetJS-Paket im Ordner lib/ mit ab.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Wenn du die Bibliotheken lokal im Ordner lib/ ablegst (empfohlen), lege bitte
|
||||
auch die jeweils mitgelieferten LICENSE-Dateien dorthin. Damit sind die
|
||||
Weitergabebedingungen aller vier Lizenzen erfüllt.
|
||||
|
||||
@@ -1,3 +1,159 @@
|
||||
# JSON-Studio
|
||||
# JSON Studio & MariaDB Workbench
|
||||
|
||||
Zwei JSON-Editoren nebeneinander: importieren aus CSV, Excel, YAML, XML und SQL, vergleichen, verschachtelte Daten auflösen und als MariaDB-Skript oder JSON-Schema wieder ausgeben.
|
||||
Zwei JSON-Editoren nebeneinander, die komplett im Browser laufen. Gedacht für
|
||||
den Alltag mit Daten aus Shops, Schnittstellen und Tabellen: importieren,
|
||||
vergleichen, umformen, wieder ausgeben.
|
||||
|
||||
**Live nutzen:** https://software.obraasch.de/
|
||||
|
||||
---
|
||||
|
||||
## Funktionen
|
||||
|
||||
**Import**
|
||||
JSON · CSV · Excel (xlsx/xls) · YAML (auch mehrere Dokumente) · XML ·
|
||||
SQL-Dump (liest `INSERT`-Anweisungen zurück) · direkt von einer URL ·
|
||||
aus einer Datenbank über eine HTTP-Brücke
|
||||
|
||||
**Export**
|
||||
JSON · CSV · Excel · YAML · XML · MariaDB-Skript (`CREATE TABLE` + `INSERT`) ·
|
||||
JSON-Schema (Draft 2020-12) · an einen Webhook senden · in eine Datenbank schreiben
|
||||
|
||||
**Arbeiten mit zwei Dokumenten**
|
||||
Inhalt hin- und herkopieren · Verschachtelungen auflösen („Flatten“, nötig für
|
||||
CSV/Excel/Datenbank) · Feld-für-Feld-Vergleich mit Bericht als Markdown oder CSV
|
||||
|
||||
**Sonstiges**
|
||||
Hell- und Dunkelmodus · Snippet-Bibliothek · Tastaturkürzel · integrierte
|
||||
Anleitung · Sicherung aller Einstellungen als JSON-Datei für den Umzug auf
|
||||
einen anderen Rechner
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Es genügt, `json-workbench.html` auf einen Webserver zu legen. Die Datei ist
|
||||
in sich abgeschlossen und braucht keinen Build-Schritt.
|
||||
|
||||
### Bibliotheken lokal ablegen (empfohlen)
|
||||
|
||||
Ohne lokale Kopien lädt die Anwendung ihre Bibliotheken von einem öffentlichen
|
||||
CDN. Das funktioniert, überträgt aber die IP-Adressen deiner Nutzerinnen und
|
||||
Nutzer an einen Dritten — in Deutschland ein datenschutzrechtliches Thema, das
|
||||
du dir sparen kannst. Lege stattdessen an:
|
||||
|
||||
```
|
||||
json-workbench.html
|
||||
lib/
|
||||
papaparse.min.js
|
||||
xlsx.full.min.js
|
||||
js-yaml.min.js
|
||||
jsoneditor/
|
||||
standalone.js
|
||||
```
|
||||
|
||||
Die Quelladressen (mit fest angegebener Version, um unangekündigte Änderungen
|
||||
zu vermeiden) stehen als Kommentar im Kopf der HTML-Datei. Der JSON-Editor
|
||||
bringt seine Stile seit Version 3 direkt im Skript mit — eine separate
|
||||
CSS-Datei ist dafür nicht mehr nötig. Lege die `LICENSE`-Dateien der
|
||||
Bibliotheken gleich mit in `lib/` ab.
|
||||
|
||||
Die Anwendung sucht immer zuerst lokal und geht nur ins Netz, wenn sie dort
|
||||
nichts findet. Welche Quelle benutzt wurde, zeigt die Anleitung unter
|
||||
„Offline-Betrieb“ an.
|
||||
|
||||
**Wichtig:** Der JSON-Editor ist ein ES-Modul. Browser verbieten das Laden
|
||||
solcher Module über `file://`. Per Doppelklick geöffnet, holt die Seite den
|
||||
Editor deshalb weiterhin aus dem Netz. Für echten Offline-Betrieb muss der
|
||||
Ordner über einen Webserver ausgeliefert werden — `python3 -m http.server`
|
||||
im Ordner genügt zum Ausprobieren.
|
||||
|
||||
---
|
||||
|
||||
## Datenbankanbindung (optional)
|
||||
|
||||
Ein Browser kann sich nicht direkt mit MariaDB oder MySQL verbinden. Dafür
|
||||
liegt `db-bridge.php` bei: ein kleines Serverskript, das die Anfragen
|
||||
entgegennimmt und an die Datenbank weiterreicht.
|
||||
|
||||
1. `db-bridge.php` auf den Server legen und im Kopf der Datei Zugangsdaten,
|
||||
API-Schlüssel und erlaubte Herkunft eintragen
|
||||
2. In der Anwendung oben rechts auf das Datenbank-Symbol, Profil anlegen
|
||||
3. Auf „Testen“ klicken — erscheint die Tabellenliste, steht die Verbindung
|
||||
|
||||
### Bitte beachten
|
||||
|
||||
- Lege einen **eigenen Datenbankbenutzer** mit genau den nötigen Rechten an.
|
||||
Niemals `root` verwenden.
|
||||
- Betreibe die Brücke **ausschließlich über HTTPS**.
|
||||
- Die Aktion `query` lässt bewusst nur lesende Abfragen zu. Alle Werte gehen
|
||||
über vorbereitete Statements; Tabellennamen werden gegen ein striktes Muster
|
||||
geprüft, weil sie sich nicht als Parameter binden lassen.
|
||||
- Schreibzugriffe lassen sich über `WRITES_ALLOWED` komplett abschalten.
|
||||
- Der API-Schlüssel wird im Browser-Speicher der Nutzer abgelegt. Behandle ihn
|
||||
wie ein Passwort und tausche ihn aus, wenn ein Gerät abhandenkommt.
|
||||
- Der Schreibmodus „Ersetzen“ führt ein `TRUNCATE` aus und löscht damit alle
|
||||
bestehenden Zeilen der Zieltabelle.
|
||||
|
||||
---
|
||||
|
||||
## Die Übersichtsseite
|
||||
|
||||
`index.php` gehört ins Wurzelverzeichnis der Domain und listet automatisch
|
||||
alle Unterordner auf, die eine Datei `tool.json` enthalten. Ein Ordner ohne
|
||||
diese Datei taucht nicht auf.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "JSON Studio & MariaDB Workbench",
|
||||
"beschreibung": "Kurzbeschreibung in einem Satz.",
|
||||
"einstieg": "json-workbench.html",
|
||||
"repository": "https://gitea.obraasch.de/oliver/json-studio",
|
||||
"repositoryName": "Quellcode (Gitea)",
|
||||
"version": "3.0",
|
||||
"lizenz": "MIT",
|
||||
"stand": "2026-09-16",
|
||||
"tags": ["JSON", "CSV", "Excel"],
|
||||
"sichtbar": true
|
||||
}
|
||||
```
|
||||
|
||||
Nur `name` und `einstieg` sind wirklich nötig, alles andere ist optional.
|
||||
`sichtbar: false` blendet einen Ordner aus, ohne die Datei zu löschen.
|
||||
|
||||
Die Seite verarbeitet bewusst **keinerlei Nutzereingaben** — weder `$_GET`
|
||||
noch `$_POST` noch Cookies. Damit entfällt die übliche Angriffsfläche. Inhalte
|
||||
aus `tool.json` werden trotzdem geprüft und beim Ausgeben vollständig maskiert;
|
||||
Pfade dürfen den Tool-Ordner nicht verlassen, externe Links nur `http`/`https`
|
||||
sein. Die Seite kommt ohne JavaScript aus, entsprechend verbietet die
|
||||
Content-Security-Policy Skripte komplett.
|
||||
|
||||
---
|
||||
|
||||
## Rechtliches beim öffentlichen Betrieb
|
||||
|
||||
In Deutschland brauchst du für ein öffentlich erreichbares Angebot ein
|
||||
Impressum (§ 5 DDG) und eine Datenschutzerklärung. Vorlagen liegen als
|
||||
`impressum.html` und `datenschutz.html` bei — mit Platzhaltern, die vor der
|
||||
Veröffentlichung ausgefüllt werden müssen. Die Vorlagen sind nach bestem
|
||||
Wissen erstellt, ersetzen aber keine Rechtsberatung.
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt steht unter der MIT-Lizenz, siehe [LICENSE](LICENSE).
|
||||
|
||||
Eingebundene Bibliotheken und ihre Lizenzen:
|
||||
|
||||
| Bibliothek | Lizenz | Projekt |
|
||||
|---|---|---|
|
||||
| vanilla-jsoneditor | ISC | https://github.com/josdejong/svelte-jsoneditor |
|
||||
| PapaParse | MIT | https://github.com/mholt/PapaParse |
|
||||
| js-yaml | MIT | https://github.com/nodeca/js-yaml |
|
||||
| SheetJS (xlsx) | Apache-2.0 | https://github.com/SheetJS/sheetjs |
|
||||
|
||||
SheetJS steht unter Apache-2.0. Bei Weitergabe muss der Lizenztext beiliegen
|
||||
und es müssen Änderungen an den Dateien kenntlich gemacht werden — am
|
||||
einfachsten, indem du die unveränderte Datei samt ihrer `LICENSE` in `lib/`
|
||||
ablegst.
|
||||
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
/**
|
||||
* ============================================================
|
||||
* db-bridge.php - HTTP-Brücke für JSON Studio & MariaDB Workbench
|
||||
* ============================================================
|
||||
*
|
||||
* Ein Browser kann sich nicht direkt mit MariaDB/MySQL verbinden.
|
||||
* Dieses Skript nimmt die Anfragen der Weboberfläche entgegen und
|
||||
* reicht sie an die Datenbank weiter.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* VORAUSSETZUNG
|
||||
* ------------------------------------------------------------
|
||||
* PHP 8.0 oder neuer (verwendet str_contains und Arrow-Funktionen)
|
||||
* sowie die PDO-MySQL-Erweiterung.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* EINRICHTUNG
|
||||
* ------------------------------------------------------------
|
||||
* 1. Unten im Abschnitt KONFIGURATION die Zugangsdaten eintragen.
|
||||
* 2. Einen eigenen API-Schlüssel setzen (beliebige lange Zeichenfolge).
|
||||
* 3. ALLOWED_ORIGIN auf die Adresse setzen, unter der die
|
||||
* HTML-Datei ausgeliefert wird.
|
||||
* 4. Datei auf den Server legen, Adresse im Profil der
|
||||
* Weboberfläche eintragen.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* SICHERHEIT - bitte lesen
|
||||
* ------------------------------------------------------------
|
||||
* - Lege einen eigenen Datenbankbenutzer an, der nur auf die
|
||||
* benötigte Datenbank zugreifen darf. Niemals 'root' verwenden.
|
||||
* - Betreibe das Skript ausschließlich über HTTPS, sonst gehen
|
||||
* API-Schlüssel und Daten im Klartext über die Leitung.
|
||||
* - Der API-Schlüssel liegt im Browser-Speicher des Nutzers.
|
||||
* Behandle ihn wie ein Passwort und tausche ihn aus, wenn ein
|
||||
* Rechner abhandenkommt.
|
||||
* - Schreibende Aktionen sind über WRITES_ALLOWED abschaltbar.
|
||||
* Wenn du nur lesen willst, setze es auf false.
|
||||
* - 'query' erlaubt bewusst nur SELECT-Abfragen.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* PROTOKOLL
|
||||
* ------------------------------------------------------------
|
||||
* POST application/json, Header X-Api-Key
|
||||
*
|
||||
* {"action":"tables"}
|
||||
* -> {"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);
|
||||
}
|
||||
+2590
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user