Upload via GUI (267 Dateien)
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ende-zu-Ende: Adminportal mit aktivem Plugin "Synology NAS Backup" gegen einen Test-WebDAV-Server (Menüpunkt, Einstellungen speichern,
|
||||
* Backup der Seite "Backup" wird kopiert, Sicherungen nur auf der NAS holen/löschen, automatische Prüfung in der Backup-Liste).
|
||||
*/
|
||||
|
||||
if (!extension_loaded('pdo_sqlite') || !function_exists('curl_init') || !class_exists('ZipArchive')) { echo "Backup-Seiten-Test übersprungen: pdo_sqlite, curl oder zip fehlt.\n"; exit(0); }
|
||||
require_once __DIR__ . '/fixtures/paths.php';
|
||||
$root = dirname(__DIR__);
|
||||
$pluginSrc = plugin_source_dir_or_skip('nas-backup', 'backup_page_nas');
|
||||
|
||||
$tmp = sys_get_temp_dir() . '/deskhub-bp-' . bin2hex(random_bytes(4));
|
||||
$dav = sys_get_temp_dir() . '/deskhub-bpdav-' . bin2hex(random_bytes(4));
|
||||
mkdir($tmp, 0755, true);
|
||||
mkdir($dav . '/share', 0755, true);
|
||||
$skip = '#^(config/config\.php|database/[^/]+\.sqlite[^/]*|backups|uploads|tests|plugins/[^/]+|\.git)(/|$)#';
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST) as $f) {
|
||||
$rel = str_replace(chr(92), '/', substr($f->getPathname(), strlen($root) + 1));
|
||||
if (preg_match($skip, $rel) && !preg_match('#^(backups|uploads|plugins)/(\.htaccess|README\.md)$#', $rel)) continue;
|
||||
if ($f->isDir()) { @mkdir($tmp . '/' . $rel, 0755, true); continue; }
|
||||
@mkdir(dirname($tmp . '/' . $rel), 0755, true);
|
||||
copy($f->getPathname(), $tmp . '/' . $rel);
|
||||
}
|
||||
@mkdir($tmp . '/uploads');
|
||||
@mkdir($tmp . '/config');
|
||||
@mkdir($tmp . '/backups');
|
||||
file_put_contents($tmp . '/config/config.php', '<?php return ' . var_export([
|
||||
'app_key' => bin2hex(random_bytes(32)),
|
||||
'db' => ['driver' => 'sqlite', 'sqlite_path' => $tmp . '/database/deskhub.sqlite'],
|
||||
'site' => ['name' => 'Test', 'url' => 'http://127.0.0.1', 'admin_email' => 'a@test.de', 'timezone' => 'Europe/Berlin'],
|
||||
'uploads' => ['max_size_bytes' => 10485760, 'allowed_extensions' => ['png'], 'path' => $tmp . '/uploads'],
|
||||
], true) . ';');
|
||||
// Plugin in den Plugin-Ordner der Testinstanz kopieren
|
||||
$copyDir = function (string $from, string $to) use (&$copyDir): void {
|
||||
@mkdir($to, 0755, true);
|
||||
foreach (scandir($from) ?: [] as $e) {
|
||||
if ($e === '.' || $e === '..') continue;
|
||||
is_dir("$from/$e") ? $copyDir("$from/$e", "$to/$e") : copy("$from/$e", "$to/$e");
|
||||
}
|
||||
};
|
||||
$copyDir($pluginSrc, $tmp . '/plugins/nas-backup');
|
||||
|
||||
require_once $root . '/includes/db.php';
|
||||
require_once $root . '/database/migrate.php';
|
||||
$pdo = new PDO('sqlite:' . $tmp . '/database/deskhub.sqlite');
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
$pdo->exec(convert_schema_to_sqlite((string) file_get_contents($root . '/database/schema.sql')));
|
||||
run_migrations($pdo, true);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$pdo->exec('DELETE FROM staff');
|
||||
$deptA = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
|
||||
$pdo->prepare("INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES ('Admin','admin@test.de',?,'admin',?,1,?)")->execute([password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), $deptA, $now]);
|
||||
$pdo->prepare("INSERT INTO plugins (slug, name, version, is_active, installed_at, updated_at, signature_ok, source, approved_permissions) VALUES ('nas-backup','NAS-Backup','1.1.0',1,?,?,0,'manual','hooks.cron,ui.menu')")->execute([$now, $now]);
|
||||
|
||||
$failures = [];
|
||||
$total = 0;
|
||||
$check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; };
|
||||
|
||||
$davPort = random_int(30000, 30499);
|
||||
$webPort = random_int(30500, 30999);
|
||||
$davPass = 'pass#wort';
|
||||
putenv('DAV_ROOT=' . $dav . '/share');
|
||||
putenv('DAV_PASS=' . $davPass);
|
||||
putenv('DESKHUB_TESTING=1'); // erlaubt dem Test, ein unsigniertes Plugin zu laden
|
||||
$logA = tempnam(sys_get_temp_dir(), 'bpd');
|
||||
$logB = tempnam(sys_get_temp_dir(), 'bpw');
|
||||
$davProc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$davPort", __DIR__ . '/fixtures/webdav_server.php'], [['pipe', 'r'], ['file', $logA, 'w'], ['file', $logA, 'a']], $p1);
|
||||
$webProc = proc_open([PHP_BINARY, '-d', 'max_execution_time=120', '-S', "127.0.0.1:$webPort", '-t', $tmp], [['pipe', 'r'], ['file', $logB, 'w'], ['file', $logB, 'a']], $p2, $tmp);
|
||||
|
||||
$set = function (string $k, string $v) use ($pdo): void {
|
||||
$pdo->prepare('DELETE FROM settings WHERE setting_key = ?')->execute(['plugin.nas-backup.' . $k]);
|
||||
$pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)')->execute(['plugin.nas-backup.' . $k, $v]);
|
||||
};
|
||||
$req = function (string $method, string $path, array $data, string $jar) use ($webPort): array {
|
||||
$ch = curl_init("http://127.0.0.1:$webPort$path");
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 120, CURLOPT_COOKIEJAR => $jar, CURLOPT_COOKIEFILE => $jar]);
|
||||
if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); }
|
||||
$b = (string) curl_exec($ch);
|
||||
$s = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($ch);
|
||||
return [$s, $b];
|
||||
};
|
||||
$csrf = fn(string $h) => preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $h, $m) ? $m[1] : '';
|
||||
$remoteFiles = fn() => array_values(array_filter(is_dir($dav . '/share/DeskHub-Backups') ? (scandir($dav . '/share/DeskHub-Backups') ?: []) : [], fn($f) => $f[0] !== '.'));
|
||||
$localZips = fn() => array_map('basename', glob($tmp . '/backups/backup_*.zip') ?: []);
|
||||
|
||||
try {
|
||||
for ($i = 0; $i < 50; $i++) { if (@fsockopen('127.0.0.1', $davPort) && @fsockopen('127.0.0.1', $webPort)) break; usleep(100000); }
|
||||
$jar = tempnam(sys_get_temp_dir(), 'bpj');
|
||||
[, $l] = $req('GET', '/staff/login.php', [], $jar);
|
||||
$req('POST', '/staff/login.php', ['csrf_token' => $csrf($l), 'email' => 'admin@test.de', 'password' => 'Sehr-Sicher-1234'], $jar);
|
||||
|
||||
$pluginUrl = '/staff/plugin.php?p=nas-backup&page=einstellungen';
|
||||
|
||||
// 1) Menüpunkt und Seite, noch nicht eingerichtet
|
||||
[$s, $side] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
$check($s === 200 && str_contains($side, 'class="plugin-tabs"') && str_contains($side, 'plugin.php?p=nas-backup&page=einstellungen'), 'Auf der Seite „Backup“ gibt es den Reiter „Synology NAS Backup“');
|
||||
$sidebar = preg_match('#<aside.*?</aside>|<nav.*?</nav>#s', $side, $sb) ? $sb[0] : '';
|
||||
$check($sidebar === '' || !str_contains($sidebar, 'Synology NAS Backup'), 'Kein eigener Eintrag in der Seitenleiste');
|
||||
[$s, $page] = $req('GET', $pluginUrl, [], $jar);
|
||||
$check($s === 200 && str_contains($page, 'class="plugin-tabs"') && str_contains($page, 'href="/staff/backup.php"') && str_contains($page, '<h1>Synology NAS Backup</h1>') && str_contains($page, 'name="do" value="save"') && str_contains($page, 'Zielverzeichnis auf der NAS'), 'Plugin-Seite lädt mit den Einstellungen (HTTP ' . $s . ')');
|
||||
$check(str_contains($page, 'Das NAS-Backup ist ausgeschaltet'), 'Ohne Einrichtung steht dort „ausgeschaltet“');
|
||||
|
||||
// 2) Einrichten über das Formular
|
||||
[$s, $b] = $req('POST', $pluginUrl, ['csrf_token' => $csrf($page), 'do' => 'save', 'enabled' => '1', 'host' => '127.0.0.1', 'port' => (string) $davPort, 'https' => '', 'user' => 'nas', 'password' => $davPass,
|
||||
'target' => '/dav/DeskHub-Backups/', 'mirror' => '1', 'db' => '1', 'files' => '1', 'scope' => 'important', 'interval' => 'daily', 'time' => '02:00', 'keep_days' => '0', 'keep' => '10', 'verify_tls' => '1'], $jar);
|
||||
$check($s === 200 && str_contains($b, 'Einstellungen gespeichert'), 'Einstellungen speichern meldet Erfolg (HTTP ' . $s . ')');
|
||||
$check(str_contains($b, 'NAS erreichbar'), 'Die Seite zeigt „NAS erreichbar“ nach dem Einrichten');
|
||||
$check(!str_contains($b, 'Fatal error') && !str_contains($b, 'Warning:'), 'Die Seite erzeugt keine PHP-Fehler');
|
||||
|
||||
// 3) Backup auf der Seite „Backup“ erstellen: automatisch geprüft, danach manuell kopiert (der Takt läuft in der Testinstanz nur alle 5 Minuten)
|
||||
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
[$s] = $req('POST', '/staff/backup.php', ['csrf_token' => $csrf($bp), 'action' => 'create_backup'], $jar);
|
||||
$zips = $localZips();
|
||||
$name = $zips[0] ?? '';
|
||||
$check($s === 302 && count($zips) === 1, 'Backup erstellen funktioniert und leitet zurück (HTTP ' . $s . ')');
|
||||
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
$check(str_contains($bp, '✔ geprüft') && !str_contains($bp, 'nicht geprüft'), 'Ohne Zutun: das neue Backup ist automatisch geprüft (✔ geprüft in der Liste)');
|
||||
$check(str_contains($bp, 'class="bk-row"') && str_contains($bp, $name), 'Neue kompakte Listenansicht auf der Backup-Seite');
|
||||
$check(str_contains($bp, '☁ auf NAS') || (str_contains($bp, 'nur hier') && str_contains($bp, '☁ Auf NAS')), 'Die Backup-Liste zeigt bei dem Backup die NAS-Marke (schon kopiert oder „nur hier“ mit Knopf „Auf NAS“; der Plugin-Takt kann bereits gelaufen sein)');
|
||||
[, $pp] = $req('GET', $pluginUrl, [], $jar);
|
||||
[$s, $b] = $req('POST', $pluginUrl, ['csrf_token' => $csrf($pp), 'do' => 'sync'], $jar);
|
||||
$check($name !== '' && in_array($name, $remoteFiles(), true), 'Das Backup der Backup-Seite wird auf die NAS kopiert (Aktion „Fehlende kopieren“)');
|
||||
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
$check(str_contains($bp, '☁ auf NAS'), 'Danach trägt es in der Backup-Liste die Marke „auf NAS“');
|
||||
$check(str_contains($b, '☁ auf der NAS') && str_contains($b, $name), 'Die Plugin-Seite zeigt es als „auf der NAS“');
|
||||
|
||||
// 4) Lokal löschen: bleibt als „nur auf der NAS“ sichtbar und lässt sich zurückholen
|
||||
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
$req('POST', '/staff/backup.php', ['csrf_token' => $csrf($bp), 'action' => 'delete_backup', 'filename' => $name], $jar);
|
||||
$check(count($localZips()) === 0, 'Lokale Sicherung gelöscht');
|
||||
[, $b] = $req('GET', $pluginUrl, [], $jar);
|
||||
$check(str_contains($b, '☁ nur auf der NAS') && str_contains($b, 'Auf Server holen'), 'Die Plugin-Seite zeigt die Sicherung als „nur auf der NAS“ mit „Auf Server holen“');
|
||||
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
$check(str_contains($bp, '☁ nur auf NAS') && str_contains($bp, 'Auf Server holen') && str_contains($bp, $name) && str_contains($bp, 'nicht auf diesem Server'), 'Auch die Backup-Seite listet die Sicherung, die nur auf der NAS liegt, mit „Auf Server holen“');
|
||||
$statsText = preg_replace('/\s+/', ' ', strip_tags(substr($bp, (int) strpos($bp, 'class="stat-grid"'), 1800)));
|
||||
$check(str_contains($statsText, '1Backups vorhanden') && str_contains($statsText, '0 auf dem Server') && str_contains($statsText, '1 nur extern (NAS)'), 'Die Kacheln oben zählen auch die Sicherung auf der NAS (1 vorhanden, 0 auf dem Server, 1 nur extern): ' . substr($statsText, 0, 220));
|
||||
$req('POST', $pluginUrl, ['csrf_token' => $csrf($b), 'do' => 'fetch', 'file' => $name], $jar);
|
||||
$check(in_array($name, $localZips(), true), '„Auf Server holen“ legt die Sicherung im Backup-Ordner ab');
|
||||
[, $bp] = $req('GET', '/staff/backup.php', [], $jar);
|
||||
$check(str_contains($bp, 'Wiederherstellen') && str_contains($bp, $name), 'Auf der Backup-Seite ist sie danach wieder normal wiederherstellbar');
|
||||
|
||||
// 5) Schutz: ungültiger Dateiname, fehlendes Token
|
||||
[, $b] = $req('GET', $pluginUrl, [], $jar);
|
||||
$beforeFiles = $remoteFiles();
|
||||
$req('POST', $pluginUrl, ['csrf_token' => $csrf($b), 'do' => 'upload', 'file' => '../config/config.php'], $jar);
|
||||
$check($remoteFiles() === $beforeFiles, 'Aktion mit ungültigem Dateinamen verändert nichts');
|
||||
[$s] = $req('POST', $pluginUrl, ['do' => 'sync'], $jar);
|
||||
$check($s !== 200 || $remoteFiles() === $beforeFiles, 'Aktionen ohne gültiges Token werden nicht ausgeführt (HTTP ' . $s . ')');
|
||||
|
||||
// 6) Normale Mitarbeiter haben keinen Zugriff
|
||||
$pdo2 = new PDO('sqlite:' . $GLOBALS['tmp'] . '/database/deskhub.sqlite');
|
||||
$deptId = (int) $pdo2->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
|
||||
$pdo2->prepare("INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES ('Agent','agent@test.de',?,'agent',?,1,?)")->execute([password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), $deptId, date('Y-m-d H:i:s')]);
|
||||
$ajar = tempnam(sys_get_temp_dir(), 'bpa');
|
||||
[, $l2] = $req('GET', '/staff/login.php', [], $ajar);
|
||||
$req('POST', '/staff/login.php', ['csrf_token' => $csrf($l2), 'email' => 'agent@test.de', 'password' => 'Sehr-Sicher-1234'], $ajar);
|
||||
[$s, $b2] = $req('GET', $pluginUrl, [], $ajar);
|
||||
$check($s === 403 || !str_contains($b2, 'Zielverzeichnis auf der NAS'), 'Ein normaler Mitarbeiter sieht die Einstellungen nicht (HTTP ' . $s . ')');
|
||||
} finally {
|
||||
proc_terminate($davProc);
|
||||
proc_terminate($webProc);
|
||||
proc_close($davProc);
|
||||
proc_close($webProc);
|
||||
}
|
||||
|
||||
usleep(300000);
|
||||
$rm = function (string $d) use (&$rm) {
|
||||
foreach (scandir($d) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") && !is_link("$d/$e") ? $rm("$d/$e") : @unlink("$d/$e"); }
|
||||
@rmdir($d);
|
||||
};
|
||||
foreach ([$tmp, $dav] as $d) if (str_starts_with($d, sys_get_temp_dir() . '/deskhub-bp')) $rm($d);
|
||||
@unlink($logA);
|
||||
@unlink($logB);
|
||||
|
||||
if ($failures) {
|
||||
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "Backup page + NAS plugin checks passed ({$total} Prüfungen).\n";
|
||||
Reference in New Issue
Block a user