diff --git a/LICENSE b/LICENSE index 753e191..ea3ef2f 100644 --- a/LICENSE +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index d236565..8c9f730 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +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. diff --git a/db-bridge.php b/db-bridge.php new file mode 100644 index 0000000..769ef25 --- /dev/null +++ b/db-bridge.php @@ -0,0 +1,297 @@ + {"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); +} diff --git a/json-workbench.html b/json-workbench.html new file mode 100644 index 0000000..74580ba --- /dev/null +++ b/json-workbench.html @@ -0,0 +1,2590 @@ + + +
+ + +