From c15c90bf6de1e7699af2bbfbb57c6cb1064b3c5f Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 11 Apr 2026 02:31:23 +0200 Subject: [PATCH] asdsd --- partials/landingpages/index.php | 7 +- public/index.php | 19 ++- src/App/SqlDataExporter.php | 285 ++++++++++++++++++++++++++++++++ 3 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 src/App/SqlDataExporter.php diff --git a/partials/landingpages/index.php b/partials/landingpages/index.php index 13313b7..0f119ae 100755 --- a/partials/landingpages/index.php +++ b/partials/landingpages/index.php @@ -50,7 +50,12 @@ $modules = array_values(array_filter(

Module mit Login-Pflicht erscheinen erst nach passender Anmeldung.

- Module verwalten +
+ Module verwalten + + SQL-Export + +
diff --git a/public/index.php b/public/index.php index 33715c0..21983c7 100755 --- a/public/index.php +++ b/public/index.php @@ -38,7 +38,7 @@ $publicPaths = [ 'auth/me', 'module/pi_control/terminal_info', ]; -$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'debug'], true) +$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'debug', 'exports/database.sql'], true) || str_starts_with($uriPath, 'modules/setup/'); if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) { $user = auth_user(); @@ -76,6 +76,23 @@ if ($uriPath === 'auth/me') { exit; } +if ($uriPath === 'exports/database.sql') { + require_admin(); + $pdo = app()->basePdo() ?: app()->pdo(); + if (!$pdo instanceof PDO) { + http_response_code(500); + exit('Keine Datenbankverbindung fuer den Export verfuegbar.'); + } + + $filename = 'nexus-export-' . gmdate('Ymd-His') . '.sql'; + $sql = (new \App\SqlDataExporter())->export($pdo, 'nexus'); + header('Content-Type: application/sql; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('X-Content-Type-Options: nosniff'); + echo $sql; + exit; +} + if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) { $moduleName = $moduleAuthMatches[1]; $moduleMeta = app()->modules()->get($moduleName); diff --git a/src/App/SqlDataExporter.php b/src/App/SqlDataExporter.php new file mode 100644 index 0000000..909729c --- /dev/null +++ b/src/App/SqlDataExporter.php @@ -0,0 +1,285 @@ +getAttribute(PDO::ATTR_DRIVER_NAME); + $tables = $this->orderedTables($pdo, $driver); + $lines = [ + '-- Nexus SQL data export', + '-- Generated at: ' . gmdate('c'), + '-- Driver: ' . $driver, + '-- Label: ' . $label, + '-- Restore target: empty database with existing schema/migrations.', + '', + 'BEGIN;', + '', + ]; + + foreach ($tables as $table) { + $columns = $this->columns($pdo, $driver, $table); + if ($columns === []) { + continue; + } + + $lines[] = '-- Table: ' . $table; + $quotedTable = $this->quoteIdentifier($table, $driver); + $quotedColumns = array_map(fn(string $column): string => $this->quoteIdentifier($column, $driver), $columns); + $select = 'SELECT ' . implode(', ', $quotedColumns) . ' FROM ' . $quotedTable; + + $count = 0; + $stmt = $pdo->query($select); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $values = []; + foreach ($columns as $column) { + $values[] = $this->quoteValue($pdo, $row[$column] ?? null); + } + + $lines[] = 'INSERT INTO ' . $quotedTable . ' (' . implode(', ', $quotedColumns) . ') VALUES (' . implode(', ', $values) . ');'; + $count++; + } + + $lines[] = '-- Rows: ' . $count; + foreach ($this->sequenceSetvalLines($pdo, $driver, $table, $columns) as $sequenceLine) { + $lines[] = $sequenceLine; + } + $lines[] = ''; + } + + $lines[] = 'COMMIT;'; + $lines[] = ''; + + return implode("\n", $lines); + } + + private function orderedTables(PDO $pdo, string $driver): array + { + $tables = $this->tables($pdo, $driver); + $dependencies = $this->tableDependencies($pdo, $driver, $tables); + $ordered = []; + $remaining = array_fill_keys($tables, true); + + while ($remaining !== []) { + $progress = false; + foreach (array_keys($remaining) as $table) { + $parents = array_filter( + $dependencies[$table] ?? [], + static fn(string $parent): bool => isset($remaining[$parent]) + ); + if ($parents !== []) { + continue; + } + + $ordered[] = $table; + unset($remaining[$table]); + $progress = true; + } + + if (!$progress) { + foreach (array_keys($remaining) as $table) { + $ordered[] = $table; + unset($remaining[$table]); + } + } + } + + return $ordered; + } + + private function tables(PDO $pdo, string $driver): array + { + if ($driver === 'pgsql') { + $stmt = $pdo->query( + "SELECT table_name + FROM information_schema.tables + WHERE table_schema = current_schema() + AND table_type = 'BASE TABLE' + ORDER BY table_name" + ); + return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))); + } + + if ($driver === 'mysql') { + $stmt = $pdo->query( + "SELECT table_name + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_type = 'BASE TABLE' + ORDER BY table_name" + ); + return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))); + } + + if ($driver === 'sqlite') { + $stmt = $pdo->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"); + return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))); + } + + throw new \RuntimeException('SQL export supports pgsql, mysql and sqlite only.'); + } + + private function columns(PDO $pdo, string $driver, string $table): array + { + if ($driver === 'pgsql') { + $stmt = $pdo->prepare( + "SELECT column_name + FROM information_schema.columns + WHERE table_schema = current_schema() + AND table_name = :table + ORDER BY ordinal_position" + ); + $stmt->execute(['table' => $table]); + return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))); + } + + if ($driver === 'mysql') { + $stmt = $pdo->prepare( + "SELECT column_name + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = :table + ORDER BY ordinal_position" + ); + $stmt->execute(['table' => $table]); + return array_values(array_map('strval', $stmt->fetchAll(PDO::FETCH_COLUMN))); + } + + if ($driver === 'sqlite') { + $stmt = $pdo->query('PRAGMA table_info(' . $this->quoteIdentifier($table, $driver) . ')'); + $columns = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $columns[] = (string)$row['name']; + } + return $columns; + } + + return []; + } + + private function tableDependencies(PDO $pdo, string $driver, array $tables): array + { + $known = array_fill_keys($tables, true); + $dependencies = []; + foreach ($tables as $table) { + $dependencies[$table] = []; + } + + if ($driver === 'pgsql') { + $stmt = $pdo->query( + "SELECT tc.table_name AS child_table, ccu.table_name AS parent_table + FROM information_schema.table_constraints tc + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.constraint_schema = tc.constraint_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = current_schema() + AND ccu.table_schema = current_schema()" + ); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $child = (string)$row['child_table']; + $parent = (string)$row['parent_table']; + if (isset($known[$child], $known[$parent]) && $child !== $parent) { + $dependencies[$child][] = $parent; + } + } + } + + if ($driver === 'mysql') { + $stmt = $pdo->query( + "SELECT table_name AS child_table, referenced_table_name AS parent_table + FROM information_schema.key_column_usage + WHERE table_schema = DATABASE() + AND referenced_table_name IS NOT NULL" + ); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $child = (string)$row['child_table']; + $parent = (string)$row['parent_table']; + if (isset($known[$child], $known[$parent]) && $child !== $parent) { + $dependencies[$child][] = $parent; + } + } + } + + if ($driver === 'sqlite') { + foreach ($tables as $table) { + $stmt = $pdo->query('PRAGMA foreign_key_list(' . $this->quoteIdentifier($table, $driver) . ')'); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $parent = (string)($row['table'] ?? ''); + if (isset($known[$parent]) && $parent !== $table) { + $dependencies[$table][] = $parent; + } + } + } + } + + foreach ($dependencies as $table => $parents) { + $dependencies[$table] = array_values(array_unique($parents)); + } + + return $dependencies; + } + + private function quoteIdentifier(string $identifier, string $driver): string + { + $parts = explode('.', $identifier); + $quote = $driver === 'mysql' ? '`' : '"'; + return implode('.', array_map( + static fn(string $part): string => $quote . str_replace($quote, $quote . $quote, $part) . $quote, + $parts + )); + } + + private function sequenceSetvalLines(PDO $pdo, string $driver, string $table, array $columns): array + { + if ($driver !== 'pgsql') { + return []; + } + + $lines = []; + foreach ($columns as $column) { + $sequenceStmt = $pdo->prepare('SELECT pg_get_serial_sequence(:table, :column)'); + $sequenceStmt->execute(['table' => $table, 'column' => $column]); + $sequence = (string)($sequenceStmt->fetchColumn() ?: ''); + if ($sequence === '') { + continue; + } + + $maxStmt = $pdo->query( + 'SELECT MAX(' . $this->quoteIdentifier($column, $driver) . ') FROM ' . $this->quoteIdentifier($table, $driver) + ); + $max = $maxStmt->fetchColumn(); + $maxValue = is_numeric($max) ? (int)$max : 0; + if ($maxValue > 0) { + $lines[] = 'SELECT setval(' . $this->quoteValue($pdo, $sequence) . ', ' . $maxValue . ', true);'; + } else { + $lines[] = 'SELECT setval(' . $this->quoteValue($pdo, $sequence) . ', 1, false);'; + } + } + + return $lines; + } + + private function quoteValue(PDO $pdo, mixed $value): string + { + if ($value === null) { + return 'NULL'; + } + if (is_bool($value)) { + return $value ? 'TRUE' : 'FALSE'; + } + if (is_int($value) || is_float($value)) { + return is_finite((float)$value) ? (string)$value : 'NULL'; + } + if (is_resource($value)) { + $value = stream_get_contents($value); + } + + return $pdo->quote((string)$value); + } +}