Files
deskhub/DeskHub-Webseite/tests/plugin_cron_job.php
T
2026-10-24 20:14:43 +00:00

94 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft, dass der echte Server-Cronjob (cron/run_plugins.php) die Hintergrundaufgaben der Plugins auslöst (Hook "cron.run"),
* nicht doppelt läuft, im Crontab-Block steht und im eingebauten Hintergrund-Betrieb als Job geführt wird.
*/
$pluginDir = sys_get_temp_dir() . '/deskhub-cj-' . bin2hex(random_bytes(4));
$db = sys_get_temp_dir() . '/deskhub-cj-' . bin2hex(random_bytes(4)) . '.sqlite';
$marker = sys_get_temp_dir() . '/deskhub-cj-marker-' . bin2hex(random_bytes(4)) . '.txt';
mkdir($pluginDir, 0755, true);
foreach (['DESKHUB_PLUGINS_DIR' => $pluginDir, 'DESKHUB_TESTING' => '1', 'DESKHUB_CATALOG_DEFAULTS' => __DIR__ . '/fixtures/no-catalog.php', 'DESKHUB_SQLITE_PATH' => $db, 'DESKHUB_CJ_MARKER' => $marker] as $k => $v) putenv("{$k}={$v}");
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void {
if (!$ok) $failures[] = $what;
};
$pdo = get_db();
// Kleines Plugin, das bei jedem Lauf eine Zeile in eine Datei schreibt
$zipPath = sys_get_temp_dir() . '/cj-' . bin2hex(random_bytes(3)) . '.zip';
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE);
$zip->addFromString('cronprobe/plugin.json', json_encode(['slug' => 'cronprobe', 'name' => 'Cron-Probe', 'version' => '1.0.0', 'permissions' => ['hooks.cron']]));
$zip->addFromString('cronprobe/plugin.php', "<?php\nplugin_add_hook('cron.run', function () { file_put_contents(getenv('DESKHUB_CJ_MARKER'), 'lauf' . PHP_EOL, FILE_APPEND); });\nplugin_add_hook('cron.minute', function () { file_put_contents(getenv('DESKHUB_CJ_MARKER'), 'minute' . PHP_EOL, FILE_APPEND); });\n");
$zip->close();
$check(plugin_install_zip($pdo, $zipPath)['ok'], 'Test-Plugin lässt sich installieren');
$check(plugin_set_active($pdo, 'cronprobe', true, ['hooks.cron'])['ok'], 'Test-Plugin lässt sich aktivieren');
$php = escapeshellarg(PHP_BINARY);
$script = escapeshellarg(dirname(__DIR__) . '/cron/run_plugins.php');
exec("{$php} {$script} 2>&1", $out, $code);
$check($code === 0 && str_contains(implode("\n", $out), '1 aktive'), 'Cronjob löst die Plugin-Aufgaben aus: ' . implode(' | ', $out));
$n = fn(string $w) => substr_count((string) @file_get_contents($marker), $w);
$check(is_file($marker) && $n('lauf') === 1 && $n('minute') === 1, 'beide Hooks des Plugins wurden genau einmal ausgeführt (Minute und 5-Minuten-Lauf)');
unset($out);
exec("{$php} {$script} 2>&1", $out, $code);
$check($code === 0 && str_contains(implode("\n", $out), 'Übersprungen') && $n('lauf') === 1 && $n('minute') === 1, 'direkt danach läuft er nicht doppelt');
$last = (string) $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'cron_run_plugins_last_run'")->fetchColumn();
$check($last !== '' && strtotime($last) > time() - 60, 'letzter Lauf ist vermerkt (Anzeige unter Einstellungen → Cron-Jobs)');
$srcRow = $pdo->query("SELECT setting_key, setting_value FROM settings WHERE setting_key IN ('cron_run_plugins_source','cron_run_plugins_last_cli')")->fetchAll(PDO::FETCH_KEY_PAIR);
$check(($srcRow['cron_run_plugins_source'] ?? '') === 'cron' && strtotime((string) ($srcRow['cron_run_plugins_last_cli'] ?? '')) > time() - 60, 'der Lauf wird als „echter Cronjob“ vermerkt (Quelle und Zeit)');
// Eine Minute später: das Minuten-Ereignis kommt wieder, der 5-Minuten-Lauf noch nicht
$setAgo = fn(string $key, int $sec) => $pdo->prepare('UPDATE settings SET setting_value = ? WHERE setting_key = ?')->execute([date('Y-m-d H:i:s', time() - $sec), $key]);
$setAgo('cron_run_plugins_last_run', 70);
unset($out);
exec("{$php} {$script} 2>&1", $out, $code);
$check($n('minute') === 2 && $n('lauf') === 1, 'nach einer Minute: Minuten-Ereignis ja, 5-Minuten-Ereignis noch nicht');
// Fünf Minuten später: beide
$setAgo('cron_run_plugins_last_run', 70);
$setAgo('cron_plugins_run_last', 310);
unset($out);
exec("{$php} {$script} 2>&1", $out, $code);
$check($n('minute') === 3 && $n('lauf') === 2, 'nach fünf Minuten: beide Ereignisse');
// Der Webzugriff bleibt gesperrt
$src = (string) file_get_contents(dirname(__DIR__) . '/cron/run_plugins.php');
$check(str_contains($src, "php_sapi_name() !== 'cli' && !defined('DESKHUB_CRON_RUNNER')") && str_contains($src, 'Kein Web-Zugriff'), 'Webzugriff auf das Skript ist gesperrt');
// Eintrag im Crontab-Block und im eingebauten Hintergrund-Betrieb
require_once dirname(__DIR__) . '/includes/crontab_installer.php';
$block = crontab_marker_block();
$check(preg_match('#^\* \* \* \* \* .*/cron/run_plugins\.php#m', $block) === 1, 'Crontab-Block enthält den Plugin-Job jede Minute');
$defs = pseudo_cron_job_definitions();
$check(isset($defs['run_plugins']) && $defs['run_plugins']['fn'] === 'run_plugins_cron' && $defs['run_plugins']['interval'] === 60 && is_file(dirname(__DIR__) . '/cron/' . $defs['run_plugins']['file']), 'eingebauter Hintergrund-Betrieb führt den Job (jede Minute)');
$settingsSrc = (string) file_get_contents(dirname(__DIR__) . '/staff/settings.php');
// Jeder Job der Statusliste braucht eine Anzeige „letzter Lauf“ (sonst bleibt die Zeit leer)
preg_match('/\$cronJobs = \[(.*?)\n\];/s', $settingsSrc, $mj);
preg_match('/\$cronLastRun = \[(.*?)\n \];/s', $settingsSrc, $ml);
preg_match_all("/^\s*'([a-z_]+)'\s*=>/m", $mj[1] ?? '', $jobKeys);
preg_match_all("/^\s*'([a-z_]+)'\s*=>/m", $ml[1] ?? '', $lastKeys);
$check(count($jobKeys[1]) >= 10 && array_diff($jobKeys[1], $lastKeys[1]) === [], 'jeder Job der Statusliste hat eine Anzeige des letzten Laufs (fehlt: ' . implode(', ', array_diff($jobKeys[1], $lastKeys[1])) . ')');
$check(str_contains($settingsSrc, "get_setting('cron_run_plugins_last_run')"), 'der Plugin-Job liest seinen letzten Lauf aus der Einstellung, die das Skript schreibt');
$check(str_contains($settingsSrc, "'run_plugins'") && str_contains($settingsSrc, 'run_plugins_cron'), 'Job steht in der Statusliste unter Einstellungen → Cron-Jobs');
$check(!str_contains((string) file_get_contents(dirname(__DIR__) . '/includes/pseudo_cron.php'), "plugin_hook('cron.run'"), 'Plugin-Aufgaben werden nicht noch ein zweites Mal im Hintergrund-Betrieb angestoßen');
$check(plugin_hook_permission('cron.minute') === 'hooks.cron' && plugin_hook_permission('cron.run') === 'hooks.cron', 'das Minuten-Ereignis braucht dieselbe Berechtigung wie cron.run');
foreach (scandir($pluginDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') continue;
is_dir($pluginDir . '/' . $entry) ? plugin_rmdir($pluginDir . '/' . $entry) : @unlink($pluginDir . '/' . $entry);
}
@rmdir($pluginDir);
@unlink($zipPath);
@unlink($db);
@unlink($marker);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo 'Plugin cron job checks passed.' . "\n";