190 lines
14 KiB
PHP
190 lines
14 KiB
PHP
<?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, 'location.replace') && !str_contains($b, 'Fatal error'), 'Nach dem Speichern ersetzt sich die Seite durch die normale Adresse (kein erneutes Absenden beim Neuladen, HTTP ' . $s . ')');
|
|
[, $b] = $req('GET', $pluginUrl, [], $jar);
|
|
$check(str_contains($b, 'Einstellungen gespeichert'), 'Einstellungen speichern meldet Erfolg');
|
|
$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(str_contains($b, 'location.replace'), 'Auch nach „Fehlende kopieren“ lädt die Seite die normale Adresse: Neuladen (alle 10 s) oder F5 sendet den Klick nicht noch einmal ab');
|
|
$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“');
|
|
// Das Kopieren läuft seit Plugin 2.3 im Hintergrund (nach der Antwort): die Seite nach dem Klick neu laden
|
|
[, $b] = $req('GET', $pluginUrl, [], $jar);
|
|
$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";
|