asdsd
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-11 02:31:23 +02:00
parent dc4abe9563
commit c15c90bf6d
3 changed files with 309 additions and 2 deletions

View File

@@ -50,7 +50,12 @@ $modules = array_values(array_filter(
<p>Module mit Login-Pflicht erscheinen erst nach passender Anmeldung.</p> <p>Module mit Login-Pflicht erscheinen erst nach passender Anmeldung.</p>
</div> </div>
<?php if ($authUser !== null): ?> <?php if ($authUser !== null): ?>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<a class="nav-link" href="/modules">Module verwalten</a> <a class="nav-link" href="/modules">Module verwalten</a>
<?php if (auth_is_admin()): ?>
<a class="nav-link" href="/exports/database.sql">SQL-Export</a>
<?php endif; ?>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -38,7 +38,7 @@ $publicPaths = [
'auth/me', 'auth/me',
'module/pi_control/terminal_info', '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/'); || str_starts_with($uriPath, 'modules/setup/');
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) { if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
$user = auth_user(); $user = auth_user();
@@ -76,6 +76,23 @@ if ($uriPath === 'auth/me') {
exit; 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)) { if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) {
$moduleName = $moduleAuthMatches[1]; $moduleName = $moduleAuthMatches[1];
$moduleMeta = app()->modules()->get($moduleName); $moduleMeta = app()->modules()->get($moduleName);

285
src/App/SqlDataExporter.php Normal file
View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace App;
use PDO;
final class SqlDataExporter
{
public function export(PDO $pdo, string $label = 'nexus'): string
{
$driver = (string)$pdo->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);
}
}