* Schlüssel alternativ als Header X-Cron-Key (bevorzugt, landet nicht im Log * des aufrufenden Dienstes) — die Datei akzeptiert beides. * * Grenzen, die den Endpoint harmlos halten: * - Job-Whitelist, keine freien Skriptnamen aus der URL * - Schlüssel aus config.php, Vergleich mit hash_equals; ohne echten Schlüssel ist zu * - Mindestabstand zwischen zwei Läufen (cron.min_interval) — ein geleakter Link kann * damit nicht dauerfeuern und uns die Server-IP bei BFV/Instagram sperren lassen * - flock in den Skripten selbst: parallele Aufrufe überspringen sich * - Antwort ist immer leer. 403 nur bei falschem Schlüssel/Job, damit ein falsch * eingerichteter Cron-Dienst überhaupt auffällt (sonst syncte wochenlang nichts, * ohne dass es jemand merkt). Der Schlüssel ist lang genug, dass das kein Orakel ist. * * Erreichbar ohne Rewrite-Anpassung: public/.htaccess leitet nur nicht-existierende * Pfade auf index.php (RewriteCond %{REQUEST_FILENAME} !-f). */ require dirname(__DIR__) . '/app/bootstrap.php'; header('X-Robots-Tag: noindex, nofollow'); header('Cache-Control: no-store'); // Whitelist — nie ein Skriptname aus der URL. // // 'logrotate' kam am 31.07.2026 dazu, und das ist eine bewusste Erweiterung: weil die // KAS-Cronjobs nur URLs aufrufen können, ist dieser Endpoint der EINZIGE Weg, überhaupt // etwas getaktet laufen zu lassen. Ohne den Eintrag liefe die Logpflege nie automatisch, // und die Logs wüchsen unbegrenzt (die 2-MB-Grenze und die 90-Tage-Frist wären toter // Code). Der Job passt in denselben Rahmen wie die zwei Syncs: kein Parameter aus der // URL, keine Netzverbindung nach außen, er schreibt ausschließlich in storage/logs. // Eine VIERTE Ausnahme braucht wieder denselben Aufwand an Begründung. $jobs = [ 'matchcenter' => 'bin/matchcenter-sync.php', 'instagram' => 'bin/instagram-sync.php', 'logrotate' => 'bin/log-rotate.php', ]; $job = (string) ($_GET['job'] ?? ''); $key = (string) config('cron.key', ''); $given = (string) ($_SERVER['HTTP_X_CRON_KEY'] ?? $_GET['key'] ?? ''); $deny = static function (string $reason): never { // Kein Detail in der Antwort, der Grund steht im Log. IP nur als gesalzener Hash. log_spam('cron', $reason); http_response_code(403); exit; }; // Not-Aus wie in den Formular-Actions: ohne echte config.php liefe die Seite auf der // Vorlage, deren Werte öffentlich im Repo stehen — der Endpoint wäre dann offen. if ($key === '' || $key === 'CHANGE_ME' || strlen($key) < 24) { $deny('kein brauchbarer cron.key konfiguriert'); } // Ein gemeinsamer Grund für beide Fälle: verrät nicht, ob Job oder Schlüssel falsch war. if (!isset($jobs[$job]) || !hash_equals($key, $given)) { $deny('job oder key ungueltig'); } // --- Mindestabstand --- // Marker wird bei JEDEM angenommenen Versuch gesetzt, nicht erst bei Erfolg: sonst // dürfte ein dauerhaft fehlschlagender Sync beliebig oft neu angestoßen werden. $minInterval = max(60, (int) config('cron.min_interval', 240)); $marker = STORAGE_PATH . '/cache/cron-' . $job . '.last'; clearstatcache(true, $marker); if (is_file($marker) && filemtime($marker) > time() - $minInterval) { log_write($job, 'WARN', "Cron-Trigger übersprungen — letzter Lauf vor weniger als {$minInterval} s."); http_response_code(204); exit; } touch($marker); // --- Lauf --- // Status steht fest, BEVOR das Skript startet: es beendet sich per exit(), unser Code // nach dem require läuft also nicht mehr zuverlässig. http_response_code(204); // Was die Skripte auf stdout schreiben (Debug-Ausgaben, PHP-Warnungen), darf nie im // HTTP-Body landen. Der Shutdown-Handler greift auch bei exit() mitten im Skript. ob_start(); register_shutdown_function(static function (): void { while (ob_get_level() > 0) { ob_end_clean(); } }); set_time_limit(180); // Steady State ~2,5 s; Luft nur für den Erstlauf mit allen Bildern ignore_user_abort(true); // Timeout beim aufrufenden Dienst darf den Sync nicht zerreißen // Hebt den CLI-Riegel in den Sync-Skripten auf — nur hier, nach der Prüfung oben. define('CRON_HTTP', true); // Die Skripte lesen ihre Schalter aus $argv; im Web-SAPI existiert das nicht. $argv = [$jobs[$job]]; $argc = 1; require ROOT_PATH . '/' . $jobs[$job];