Upload via GUI (258 Dateien)

This commit is contained in:
2026-10-16 19:12:18 +00:00
parent 56deade877
commit 6cced3beda
50 changed files with 11148 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
<?php
/**
* includes/pseudo_cron.php
*
* "Eingebauter" Ersatz für echte Betriebssystem-Cronjobs, für Hosting-Umgebungen
* ohne Crontab-Zugriff (z.B. viele Shared-Hosting-Pakete). Funktioniert komplett
* ohne externe Dienste: Bei jeder authentifizierten Anfrage im Mitarbeiterbereich
* (Web-Backend UND mobile App) wird nach dem Ausliefern der Antwort im Hintergrund
* geprüft, ob einer der 6 Cronjobs fällig ist – und falls ja, direkt ausgeführt.
*
* Ablauf, ohne die eigentliche Seite/den API-Request zu verlangsamen:
* 1. pseudo_cron_schedule_background() wird beim Seiten-/API-Aufruf registriert.
* 2. register_shutdown_function() sorgt dafür, dass der Code erst NACH dem Senden
* der Antwort läuft. Wo verfügbar (PHP-FPM) beendet fastcgi_finish_request()
* die Verbindung zum Browser sofort – der Nutzer merkt nichts von der Wartezeit.
* 3. pseudo_cron_dispatch() prüft für jeden Job das letzte Ausführungsdatum
* (Setting "cron_<job>_last_run") gegen sein empfohlenes Intervall und führt
* ihn bei Bedarf aus – über dieselbe run_*()-Funktion wie der "Jetzt ausführen"-
* Button und der echte Cronjob.
*
* Nebenläufigkeitsschutz: Ein Job wird nur ausgeführt, wenn das UPDATE des
* last_run-Settings tatsächlich eine Zeile ändert (atomares "Compare-and-Swap").
* Laufen zwei Anfragen gleichzeitig, "gewinnt" nur eine davon den Job.
*
* Kann in Einstellungen → Cron-Jobs deaktiviert werden (Setting "pseudo_cron_enabled"),
* falls echte Server-Cronjobs eingerichtet sind und der eingebaute Ersatz nicht
* gebraucht wird.
*/
/** Empfohlene Mindestabstände in Sekunden, angelehnt an die Crontab-Beispiele. */
function pseudo_cron_job_definitions(): array
{
return [
'fetch_emails' => ['setting' => 'cron_fetch_emails_last_run', 'interval' => 300, 'file' => 'fetch_emails.php', 'fn' => 'run_fetch_emails'],
'check_sla' => ['setting' => 'cron_check_sla_last_run', 'interval' => 900, 'file' => 'check_sla.php', 'fn' => 'run_check_sla'],
'check_escalations' => ['setting' => 'cron_check_escalations_last_run', 'interval' => 3600, 'file' => 'check_escalations.php', 'fn' => 'run_check_escalations'],
'process_backups' => ['setting' => 'cron_process_backups_last_run', 'interval' => 3600, 'file' => 'process_backups.php', 'fn' => 'run_process_backups'],
'process_recurring' => ['setting' => 'cron_process_recurring_last_run', 'interval' => 86400, 'file' => 'process_recurring.php', 'fn' => 'run_process_recurring'],
'anonymize_data' => ['setting' => 'cron_anonymize_data_last_run', 'interval' => 86400, 'file' => 'anonymize_data.php', 'fn' => 'run_anonymization'],
'auto_close_tickets'=> ['setting' => 'cron_auto_close_tickets_last_run','interval' => 86400, 'file' => 'auto_close_tickets.php','fn' => 'run_auto_close_tickets'],
];
}
/**
* Versucht, die "Ausführungs-Berechtigung" für ein Setting atomar zu übernehmen:
* Nur wenn der gespeicherte Zeitstempel älter als das Intervall ist, wird er auf
* "jetzt" gesetzt und true zurückgegeben. Verhindert doppelte Ausführung bei
* gleichzeitigen Anfragen (kein SELECT-dann-UPDATE, sondern ein atomares UPDATE).
*/
function pseudo_cron_try_claim(PDO $pdo, string $settingKey, int $intervalSeconds): bool
{
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
// Zeile anlegen falls noch nie vorhanden (fernes Datum = sofort fällig).
if ($driver === 'sqlite') {
$pdo->prepare("INSERT OR IGNORE INTO settings (setting_key, setting_value) VALUES (?, '1970-01-01 00:00:00')")
->execute([$settingKey]);
} else {
$pdo->prepare("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, '1970-01-01 00:00:00')")
->execute([$settingKey]);
}
$now = date('Y-m-d H:i:s');
$threshold = date('Y-m-d H:i:s', time() - $intervalSeconds);
$stmt = $pdo->prepare('UPDATE settings SET setting_value = ? WHERE setting_key = ? AND setting_value < ?');
$stmt->execute([$now, $settingKey, $threshold]);
return $stmt->rowCount() > 0;
}
/** Prüft alle Jobs und führt fällige direkt aus. Läuft NACH dem Senden der Antwort. */
function pseudo_cron_dispatch(): void
{
try {
$pdo = get_db();
if (get_setting('pseudo_cron_enabled', '1') !== '1') return;
// Grobes Gate: höchstens alle 20 Sekunden eine echte Prüfung, um bei viel
// Traffic nicht bei jeder einzelnen Anfrage 6 zusätzliche Abfragen zu machen.
if (!pseudo_cron_try_claim($pdo, 'pseudo_cron_gate', 20)) return;
if (!defined('DESKHUB_CRON_RUNNER')) define('DESKHUB_CRON_RUNNER', true);
foreach (pseudo_cron_job_definitions() as $key => $job) {
if (!pseudo_cron_try_claim($pdo, $job['setting'], $job['interval'])) continue;
try {
require_once __DIR__ . '/../cron/' . $job['file'];
$fn = $job['fn'];
$fn($pdo, false);
} catch (Throwable $e) {
error_log("pseudo_cron [$key]: " . $e->getMessage());
}
}
// Plugin- und Theme-Katalog im Hintergrund aktuell halten (alle 30 Minuten)
if (pseudo_cron_try_claim($pdo, 'catalog_refresh_last_run', 1800)) {
catalog_refresh_background();
}
// Plugins: wiederkehrende Aufgaben (höchstens alle 5 Minuten)
if (pseudo_cron_try_claim($pdo, 'plugins_cron_last_run', 300)) {
plugin_hook('cron.run', ['time' => time()]);
}
// Verwaiste Zeiterfassungs-Sitzungen aufräumen (includes/time_tracking.php,
// lebt bereits in bootstrap.php – kein require_once aus cron/ nötig).
if (pseudo_cron_try_claim($pdo, 'time_tracking_sweep_last_run', 300)) {
try {
finalize_stale_work_sessions($pdo, false);
} catch (Throwable $e) {
error_log('pseudo_cron [time_tracking_sweep]: ' . $e->getMessage());
}
}
} catch (Throwable $e) {
error_log('pseudo_cron_dispatch: ' . $e->getMessage());
}
}
/**
* Registriert die Hintergrundprüfung für das Ende des aktuellen Requests.
* Von require_staff_login() (Web) und api_require_staff() (mobile App) aufgerufen –
* läuft also bei jeder authentifizierten Mitarbeiter-Anfrage, ganz ohne dass
* jemand manuell etwas klicken oder ein externer Dienst konfiguriert werden muss.
*/
function pseudo_cron_schedule_background(): void
{
static $scheduled = false;
if ($scheduled) return; // pro Request nur einmal registrieren
$scheduled = true;
if (php_sapi_name() === 'cli') return; // im echten Cron nicht nötig
register_shutdown_function(function () {
// Verbindung zum Client möglichst sofort beenden, damit die Prüfung
// die eigentliche Seite/den API-Aufruf nicht spürbar verlangsamt.
if (function_exists('fastcgi_finish_request')) {
@fastcgi_finish_request();
} else {
if (function_exists('ob_get_level')) {
while (ob_get_level() > 0) { @ob_end_flush(); }
}
@flush();
}
pseudo_cron_dispatch();
});
}