Dateien nach "/" hochladen

This commit is contained in:
2026-09-16 10:59:24 +02:00
parent e49a69af76
commit 1aa39ba63b
4 changed files with 3097 additions and 15 deletions
+52 -13
View File
@@ -1,18 +1,57 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
associated documentation files (the "Software"), to deal in the Software without restriction, including of this software and associated documentation files (the "Software"), to deal
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell in the Software without restriction, including without limitation the rights
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
following conditions: 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 The above copyright notice and this permission notice shall be included in all
portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
USE OR OTHER DEALINGS IN THE SOFTWARE. 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.
+158 -2
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff