From 05c64c8f1296aa621d288c2b7efef06644bc92f1 Mon Sep 17 00:00:00 2001 From: fs Date: Fri, 31 Jul 2026 17:16:12 +0200 Subject: [PATCH] =?UTF-8?q?Takt=20=C3=BCber=20public/cron.php:=20KAS-Cronj?= =?UTF-8?q?obs=20k=C3=B6nnen=20nur=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Korrektur meiner eigenen Annahme von heute früh. Der Tarif hat SSH und Cronjobs, aber das KAS-Formular kennt nur ein Feld "Protokoll / Pfad" mit https:// — ein `php bin/…` ist dort nicht eintragbar. Der Endpoint, den ich vorhin stillgelegt hatte, ist damit wieder der einzige Takt-Geber, und zwar in einer besseren Form als geplant: Aufrufer ist der Hoster selbst, der Schlüssel verlässt den Webspace nie, kein Dritter ist beteiligt. Dritter Job 'logrotate' in der Whitelist, denn ohne ihn liefe die Logpflege nie automatisch und die 2-MB-Grenze samt 90-Tage-Frist wäre toter Code. Er passt in denselben Rahmen wie die Syncs: kein Parameter aus der URL, keine Verbindung nach außen, schreibt nur in storage/logs. bin/log-rotate.php trägt dafür denselben Riegel wie die Syncs (CRON_HTTP) und require_once. Eine vierte Ausnahme braucht wieder denselben Aufwand an Begründung. Geprüft: falscher Schlüssel 403, unbekannter Job 403, gültiger Aufruf 204 mit leerem Body, sofortige Wiederholung 204 plus WARN, Logdateien unversehrt. Zeitraster angepasst, weil das KAS Wochentage nicht mit Halbstunden kombiniert: Matchcenter stündlich statt 2x/Tag plus Wochenende, Instagram als zwei Tageseinträge, Logpflege täglich nachts. Die Abwägung steht in config.example.php und docs/deploy.md. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7 --- CLAUDE.md | 43 ++++++++++++-------- bin/log-rotate.php | 9 ++++- config/config.example.php | 84 ++++++++++++++++++--------------------- docs/deploy.md | 62 +++++++++++++++++++---------- public/cron.php | 29 ++++++++++---- 5 files changed, 133 insertions(+), 94 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e26acb5..016b164 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,9 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern. - **Frontend:** Natives HTML, Vanilla CSS, Vanilla JS. Kein Framework, keine Build-Pipeline, kein Node. - **Backend:** Vanilla PHP ≥ 8.1, Composer nur für `phpmailer/phpmailer`. Keine Template-Engine. -- **Hosting-Ziel:** All-Inkl Shared Hosting, PHP 8.x, `.htaccess`, SSH **und** Shell-Cronjobs - (seit 31.07.2026 im Tarif). Deploy-Anleitung Schritt für Schritt: **`docs/deploy.md`**. +- **Hosting-Ziel:** All-Inkl Shared Hosting, PHP 8.x, `.htaccess`, SSH (seit 31.07.2026) und + Cronjobs, die aber **nur URLs aufrufen können** — deshalb läuft der Takt über + `public/cron.php`, siehe dort. Deploy-Anleitung Schritt für Schritt: **`docs/deploy.md`**. - **Lokales Dev:** `php -d upload_max_filesize=250M -d post_max_size=260M -S localhost:8000 -t public public/index.php` (index.php fungiert als Router-Script, weil `php -S` kein .htaccess kennt). Die beiden `-d`-Schalter brauchst du für die Dateiablage `/dateien`: der CLI-Server liest keine `.user.ini`, und mit den @@ -227,7 +228,9 @@ Funktion, sie ist die einzige Stelle, an der aus JSON HTML entsteht. Web wie CLI. Kein `error_log()`, kein `file_put_contents()` auf eine Logdatei, keine eigene Log-Closure in einem neuen Skript. - Kanal = Dateiname unter `storage/logs/`: `mail` · `spam` · `instagram` · `matchcenter` · - `stadionzeitung` · `veranstaltungen` + `stadionzeitung` · `veranstaltungen` · `chronik` · `filesgallery` · `logrotate` + (letzterer nur für den Übersprungen-Hinweis des Cron-Endpoints, der seinen Kanal aus dem + Job-Namen bildet) (`php-errors.log` schreibt PHP selbst, eigenes Format). - Level mit klarer Bedeutung: **INFO** Normalbetrieb (auch ein geblockter Bot — der Schutz arbeitet, das ist kein Fehler) · **WARN** Auffälligkeit, Betrieb läuft weiter (Einzelbild @@ -341,23 +344,29 @@ Keine rohen Werte in `base/layout/components/utilities.css` — es gibt für all - [ ] Keine Duplikate: Inhalte/Werte aus den Single-Source-Dateien beziehen - [ ] `php -l` sauber; Seite lokal geprüft -## Cron-Endpoint `public/cron.php` (Ausnahme 3b) — **stillgelegt** +## Cron-Endpoint `public/cron.php` (Ausnahme 3b) — **der Takt-Geber** -**Seit 31.07.2026 hat der Tarif SSH und echte Shell-Cronjobs.** Damit ist der Endpoint überflüssig: -er war ausschließlich der Ersatz für den fehlenden Cron. `cron.key` steht in der Produktiv-Config auf -`''`, der Endpoint antwortet also auf jeden Aufruf mit 403 — ein Geheimnis weniger, das bei einem -externen Dienst liegt und leaken kann. Der Takt läuft über die Crontab-Zeilen aus -`config/config.example.php` (Weg A), der Ablauf steht in **`docs/deploy.md`**, Teil D. +**Die KAS-Cronjobs können ausschließlich URLs aufrufen, keine Shell-Befehle** (geprüft 31.07.2026: +das Anlege-Formular hat nur ein Feld „Protokoll / Pfad" mit `https://`). Der Tarif hat inzwischen SSH +und Cronjobs, aber ein `php bin/…` ist dort nicht eintragbar. Deshalb ruft der **KAS-Cron diesen +Endpoint** auf, und er startet das Skript serverseitig. Der Ablauf steht in **`docs/deploy.md`**, Teil D, +die drei Zeitpunkte in `config/config.example.php`. -**Der Code bleibt.** Er ist geprüft, kostet nichts (403 in drei Zeilen) und ist der Rückweg, falls der -Shell-Cron ausfällt: neuen Schlüssel in `cron.key`, fertig. Deshalb sind die Grenzen unten weiter -beschrieben, statt sie mit dem Code zu löschen — wer ihn je wieder aufschließt, braucht sie. -`bin/preflight.php` warnt bei leerem Schlüssel und weist damit auf genau diese Entscheidung hin; -die Warnung ist erwartet, kein Mangel. +Das ist besser als der ursprünglich geplante externe Cron-Dienst: **der Aufrufer ist der Hoster +selbst**, der Schlüssel verlässt den Webspace nie, es ist überhaupt kein Dritter beteiligt. +`cron.key` muss also gesetzt sein — steht er leer, läuft **kein** Sync mehr. -Die Begründung von damals, weiterhin gültig für den Fall der Reaktivierung: ein externer Cron-Dienst ruft -`https://…/cron.php?job=matchcenter` bzw. `?job=instagram` auf, der Endpoint startet das Skript -serverseitig. Schlüssel aus `config('cron.key')`, per Header `X-Cron-Key` **oder** als `key`-Parameter. +Ein externer Cron-Dienst ruft, falls je nötig, dieselben URLs auf: +`https://…/cron.php?job=matchcenter` · `?job=instagram` · `?job=logrotate`. Schlüssel aus +`config('cron.key')`, per Header `X-Cron-Key` **oder** als `key`-Parameter (im KAS notgedrungen als +Parameter, das Formular kennt keine Header). + +**Warum es drei Jobs sind:** `logrotate` kam am 31.07.2026 dazu, weil dieser Endpoint der einzige Weg +ist, überhaupt etwas getaktet laufen zu lassen. Ohne ihn liefe die Logpflege nie automatisch und die +2-MB-Grenze samt 90-Tage-Frist wäre toter Code. Der Job passt in denselben Rahmen wie die Syncs: kein +Parameter aus der URL, keine Verbindung nach außen, er schreibt nur in `storage/logs`. Eine **vierte** +Ausnahme braucht wieder denselben Aufwand an Begründung. `bin/log-rotate.php` trägt dafür denselben +Riegel wie die Syncs (`PHP_SAPI !== 'cli' && !defined('CRON_HTTP')`) und `require_once`. **Warum dieser Weg und nicht GitHub Actions mit FTP-Upload:** dort müsste ein Dritter Schreibzugang zum ganzen Webspace kennen. Hier kennt er einen Schlüssel, der ausschließlich „starte einen der beiden diff --git a/bin/log-rotate.php b/bin/log-rotate.php index 63dbed5..8287025 100644 --- a/bin/log-rotate.php +++ b/bin/log-rotate.php @@ -16,11 +16,16 @@ declare(strict_types=1); * * Aufruf: php bin/log-rotate.php [--max-mb=2] [--keep-days=90] [-v] */ -if (PHP_SAPI !== 'cli') { +// Nur CLI — oder der authentifizierte Cron-Endpoint public/cron.php, der CRON_HTTP +// definiert. Nötig, weil die KAS-Cronjobs ausschließlich URLs aufrufen können und +// dieses Skript sonst nie automatisch liefe (Stand 31.07.2026). +if (PHP_SAPI !== 'cli' && !defined('CRON_HTTP')) { exit(1); } -require dirname(__DIR__) . '/app/bootstrap.php'; +// require_once, weil cron.php den Bootstrap schon geladen hat: app/bootstrap.php +// definiert Konstanten, ein zweiter Durchlauf wäre ein Fatal. +require_once dirname(__DIR__) . '/app/bootstrap.php'; $maxMb = 2.0; $keepDays = 90; diff --git a/config/config.example.php b/config/config.example.php index 42a9257..e14c3e1 100644 --- a/config/config.example.php +++ b/config/config.example.php @@ -7,57 +7,49 @@ * config.php ist gitignored und liegt außerhalb des Webroots (zusätzlich per .htaccess gesperrt). * * --------------------------------------------------------------------------- - * TAKT DER SYNC-SKRIPTE — zwei Wege, je nach Tarif + * TAKT DER SYNC-SKRIPTE — über public/cron.php, aufgerufen vom KAS-Cron * --------------------------------------------------------------------------- - * A) Der Tarif hat Cronjobs → Shell-Cronjobs unten verwenden. Sauberster Weg, - * kein Endpoint, kein Dritter im Spiel. - * ►►► DAS IST SEIT 31.07.2026 UNSER WEG: der Tarif hat SSH und Shell-Cronjobs. - * Deshalb steht `cron.key` unten leer und public/cron.php ist stillgelegt. - * B) Der Tarif hat KEINE Cronjobs (so war es bis 30.07.2026, das KAS meldete - * „in deinem Tarif nicht verfügbar“) → public/cron.php + externer Cron-Dienst. - * Nur noch als Rückweg dokumentiert, falls der Shell-Cron ausfällt. - * Dann `cron.key` unten setzen und beim Dienst diese URLs im gewünschten Takt - * aufrufen lassen (Schlüssel besser als Header X-Cron-Key als in der URL): + * Geprüft im KAS am 31.07.2026: der Tarif hat SSH und Cronjobs, aber die Cronjobs + * können ausschließlich **URLs** aufrufen. Ein `php bin/…` ist dort nicht eintragbar + * (das Formular hat nur ein Feld „Protokoll / Pfad" mit https://). Also läuft der Takt + * über public/cron.php, und der Aufrufer ist der Hoster selbst — kein Dritter, und der + * Schlüssel verlässt den Webspace nie. * - * https://www.tsv08kulmbach.de/cron.php?job=matchcenter - * https://www.tsv08kulmbach.de/cron.php?job=instagram + * URLs (Schlüssel = cron.key unten): + * https://www.tsv08kulmbach.de/cron.php?job=matchcenter&key= + * https://www.tsv08kulmbach.de/cron.php?job=instagram&key= + * https://www.tsv08kulmbach.de/cron.php?job=logrotate&key= * - * Takt wie unten bei den Cronjobs. Der Dienst muss keine Antwort auswerten: - * 204 = angenommen oder wegen Mindestabstand übersprungen, 403 = Schlüssel oder - * Job falsch (dann ist der Dienst falsch eingerichtet und es syncte nichts). - * Erwarteter Fehlerfall: Instagram sperrt Rechenzentrums-IPs. Einmal von Hand - * prüfen, ob der Scraper auf dem Server überhaupt durchkommt. + * Antwort ist immer leer: 204 = gelaufen oder wegen Mindestabstand übersprungen, + * 403 = Schlüssel oder Job falsch. Der KAS-Cron muss nichts auswerten. Das + * E-Mail-Feld im KAS kann leer bleiben; ob die Syncs laufen, sagt `php bin/logs.php`, + * und `php bin/preflight.php` warnt, wenn ein Datenstand zu alt wird. * * --------------------------------------------------------------------------- - * CRONJOBS (im KAS anlegen, Typ „eigener Cronjob“ / Shell) — nur Weg A + * DIE DREI CRONJOBS IM KAS (Zeitraster des KAS, nicht Crontab-Syntax) * --------------------------------------------------------------------------- - * SITE=/www/htdocs/w01ca75d/tsv08kulmbach-website (echter Pfad, ermittelt 31.07.2026) - * PHP=/usr/bin/php (8.3.29; mit `which php` gegenprüfen) + * 1. Matchcenter — „stündlich", Minute 37 + * Das KAS kann keine Wochentage mit Halbstunden-Takt kombinieren, wie es die + * ursprüngliche Crontab vorsah (Sa+So 13-21 Uhr halbstündlich). Stündlich rund um + * die Uhr ist der brauchbare Ersatz: am Spieltag stehen Ergebnis und Tabelle + * spätestens eine Stunde später. Kostet 24 Läufe/Tag statt 2 plus Wochenende. + * Wer den Verkehr zum BFV klein halten will, nimmt stattdessen zwei „täglich"-Jobs + * (07:37 und 19:37) und lebt damit, dass Samstagsergebnisse erst abends stehen. * - * Die Fehlerausgabe geht bewusst nach php-errors.log und NICHT in die Kanal-Logs: - * die Skripte schreiben ihren Verlauf selbst über log_write() im Format - * `[Zeit] LEVEL Meldung`, und bin/logs.php wertet Kanäle nach Level aus. Eine rohe - * PHP-Warnung dazwischen bricht das Format. php-errors.log ist die Datei für PHPs - * eigene Ausgabe, und bin/logs.php behandelt sie schon als "eigenes Format". - * Nach /dev/null darf nur log-rotate: ein Fatal dort ist harmlos, ein Fatal in einem - * Sync (etwa fehlendes vendor/ nach einem halben Deploy) soll auffallen. + * 2. Instagram — ZWEI Jobs „täglich", 06:17 und 18:17 + * Zwei Einträge, weil „täglich" nur einen Zeitpunkt kennt. Bewusst nicht stündlich: + * Instagram sperrt auffällige Zugriffsmuster deutlich schneller als der BFV. * - * # Instagram-Feed: 2×/Tag (versetzte Minuten, damit die Läufe sich nicht überlappen) - * 17 6,18 * * * cd $SITE && $PHP bin/instagram-sync.php >> storage/logs/php-errors.log 2>&1 + * 3. Logpflege — „täglich", 03:23 (`?job=logrotate`) + * Rotiert erst über 2 MB und löscht Einträge älter als 90 Tage, ein täglicher Lauf + * ist deshalb meistens ein No-Op. Wöchentlich genügt auch. * - * # Matchcenter: 2×/Tag als Grundtakt - * 37 7,19 * * * cd $SITE && $PHP bin/matchcenter-sync.php >> storage/logs/php-errors.log 2>&1 + * Die Minuten sind versetzt, damit sich zwei Läufe nicht überlappen (die Syncs sperren + * sich per Lock, sonst fiele einer aus). * - * # Matchcenter am Spieltag-Nachmittag/Abend ½-stündlich (Sa+So 13–21 Uhr), - * # damit Ergebnisse und Tabelle zeitnah stehen. - * 7,37 13-21 * * 6,0 cd $SITE && $PHP bin/matchcenter-sync.php >> storage/logs/php-errors.log 2>&1 - * - * # Logpflege: sonntags nachts. Rotiert Dateien über 2 MB und löscht Einträge älter als 90 Tage. - * 23 3 * * 0 cd $SITE && $PHP bin/log-rotate.php > /dev/null 2>&1 - * - * Alle Skripte sind CLI-only; die Syncs sperren sich per Lock gegen Parallelläufe und lassen - * bei jedem Fehler den letzten guten Cache stehen — ein fehlgeschlagener Lauf schadet nie. - * Erstlauf nach dem Deploy einmal von Hand anstoßen (füllt Cache + Bilder/Wappen). + * Alle Skripte sind CLI-only bzw. laufen über den authentifizierten Endpoint; die Syncs + * lassen bei jedem Fehler den letzten guten Cache stehen — ein fehlgeschlagener Lauf + * schadet nie. Erstlauf nach dem Deploy einmal per SSH anstoßen (füllt Cache + Bilder). * * --------------------------------------------------------------------------- * NACH DEM DEPLOY @@ -117,12 +109,12 @@ return [ 'to' => 'info@tsv08kulmbach.de', ], - // Cron-Endpoint public/cron.php — nur nötig, wenn der Tarif keine Cronjobs hat - // (Weg B oben). Leer oder 'CHANGE_ME' lassen heißt: der Endpoint ist zu und - // antwortet auf jeden Aufruf mit 403. + // Cron-Endpoint public/cron.php — der Takt-Geber (siehe oben). Leer oder + // 'CHANGE_ME' heißt: der Endpoint ist zu und antwortet auf jeden Aufruf mit 403, + // dann läuft KEIN Sync mehr. 'cron' => [ - // Langes Zufallsgeheimnis, NICHT app_secret wiederverwenden — der Schlüssel - // reist bei einem externen Dienst mit und darf dort nichts anderes aufschließen. + // Langes Zufallsgeheimnis, NICHT app_secret wiederverwenden: dieser Schlüssel + // steht im KAS in einer Cronjob-URL und darf nichts anderes aufschließen. // Erzeugen: php -r 'echo bin2hex(random_bytes(24)), "\n";' 'key' => 'CHANGE_ME', // Mindestabstand zwischen zwei Läufen desselben Jobs in Sekunden. Schützt bei diff --git a/docs/deploy.md b/docs/deploy.md index 4cd9503..0268c8c 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -255,35 +255,53 @@ führen (macOS NFD gegen Linux NFC). ## Teil D — Cronjobs -Erst jetzt, damit ab dem Umschalten schon frische Daten liegen. `SITE` und der -PHP-Pfad kommen aus B1/B2. +**Die KAS-Cronjobs können nur URLs aufrufen, keine Shell-Befehle** (geprüft 31.07.2026: +das Formular hat ein Feld „Protokoll / Pfad" mit `https://`, kein Kommandofeld). Der +Takt läuft deshalb über `public/cron.php`. Aufrufer ist der Hoster selbst — der +Schlüssel verlässt den Webspace nie, kein Dritter ist beteiligt. -[KAS] → **Cronjobs** → eigener Cronjob (Shell), vier Einträge: +Voraussetzung: `cron.key` muss in `config/config.php` gesetzt sein (48 Zeichen, +nicht `app_secret`). Steht er leer, antwortet der Endpoint mit 403 und **nichts** synct. +`bin/preflight.php` prüft Länge und Verwechslung. -``` -# Instagram 2x/Tag -17 6,18 * * * cd SITE && PHP bin/instagram-sync.php >> storage/logs/instagram.log 2>&1 +[KAS] → **Cronjobs** → Anlegen, drei bis vier Einträge. `KEY` durch den Wert aus +`config.php` ersetzen: -# Matchcenter Grundtakt 2x/Tag -37 7,19 * * * cd SITE && PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1 +| # | Pfad | Zeitpunkt | +|---|---|---| +| 1 | `www.tsv08kulmbach.de/cron.php?job=matchcenter&key=KEY` | stündlich, Minute 37 | +| 2 | `www.tsv08kulmbach.de/cron.php?job=instagram&key=KEY` | täglich, 06:17 | +| 3 | `www.tsv08kulmbach.de/cron.php?job=instagram&key=KEY` | täglich, 18:17 | +| 4 | `www.tsv08kulmbach.de/cron.php?job=logrotate&key=KEY` | täglich, 03:23 | -# Matchcenter am Spieltag halbstuendlich (Sa+So 13-21 Uhr) -7,37 13-21 * * 6,0 cd SITE && PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1 +Vier Dinge dahinter sind Absicht: -# Logpflege sonntags nachts -23 3 * * 0 cd SITE && PHP bin/log-rotate.php > /dev/null 2>&1 +- **Matchcenter stündlich statt halbstündlich am Spieltag.** Das KAS kann Wochentage + nicht mit einem Halbstunden-Takt kombinieren, wie es die ursprüngliche Crontab vorsah. + Stündlich rund um die Uhr ist der brauchbare Ersatz: am Spieltag steht das Ergebnis + spätestens eine Stunde später. Kostet 24 Läufe/Tag. Wer den Verkehr zum BFV klein + halten will, nimmt zwei „täglich"-Jobs (07:37 und 19:37) und lebt damit, dass + Samstagsergebnisse erst abends stehen. +- **Instagram zwei Einträge**, weil „täglich" nur einen Zeitpunkt kennt. Bewusst nicht + stündlich: Instagram sperrt auffällige Zugriffsmuster schneller als der BFV. +- **Versetzte Minuten**, damit sich zwei Läufe nie überlappen (die Syncs sperren sich + per Lock, sonst fällt einer aus). +- **Das E-Mail-Feld im KAS bleibt leer.** Die Antwort ist immer leer (204 = gelaufen oder + wegen Mindestabstand übersprungen, 403 = Schlüssel oder Job falsch), es gäbe also + nichts zu mailen. Ob der Takt läuft, sagt `php bin/logs.php`; `php bin/preflight.php` + warnt, wenn ein Datenstand zu alt wird. + +**Am Tag nach dem ersten Lauf** einmal `php bin/logs.php` ansehen. Läuft nichts, ist fast +immer der Schlüssel in der URL falsch oder abgeschnitten. Gegenprobe von Hand: + +```bash +curl -s -o /dev/null -w '%{http_code}\n' \ + 'https://www.tsv08kulmbach.de/cron.php?job=matchcenter&key=KEY' ``` -`SITE` und `PHP` durch die echten absoluten Pfade ersetzen, auf diesem Webspace also -`cd /www/htdocs/w01ca75d/tsv08kulmbach-website && /usr/bin/php bin/…`. Die Minuten sind -versetzt, damit sich zwei Läufe nicht überlappen. Alle Skripte sind CLI-only, -sperren sich per Lock gegen Parallelläufe und lassen bei Fehlern den letzten guten -Cache stehen — ein fehlgeschlagener Lauf schadet nie. - -Am Tag nach dem ersten Cron-Lauf einmal `php bin/logs.php` ansehen. Läuft nichts, -ist fast immer der PHP-Pfad falsch oder relativ. - ---- +204 = angenommen, 403 = Schlüssel oder Job falsch. Direkt danach ein zweites Mal +aufgerufen muss es ebenfalls 204 geben — dann hat der Mindestabstand gegriffen und der +Lauf wurde übersprungen, was im Log als WARN steht. ## Teil E — Umschalten auf die Hauptdomain diff --git a/public/cron.php b/public/cron.php index e875544..8fb3975 100644 --- a/public/cron.php +++ b/public/cron.php @@ -5,14 +5,19 @@ declare(strict_types=1); /** * Cron-Endpoint — die ZWEITE benannte Ausnahme zu Regel 3 (siehe CLAUDE.md). * - * Warum es ihn gibt: der All-Inkl-Tarif kennt keine zeitgesteuerten Aufgaben, weder - * Shell- noch URL-Cronjobs ("in deinem Tarif nicht verfügbar"). Die Sync-Skripte müssen - * aber regelmäßig laufen, also muss der Takt von außen kommen. Ein externer Dienst - * (cron-job.org o. ä.) ruft diese Datei auf, sie startet das Skript serverseitig. + * Warum es ihn gibt (Stand 31.07.2026, geprüft im KAS): der Tarif hat inzwischen + * SSH und Cronjobs, aber die KAS-Cronjobs können ausschließlich **URLs** aufrufen, + * keine Shell-Befehle. Ein `php bin/…` als Cronjob ist dort nicht eintragbar. Die + * Skripte müssen aber getaktet laufen, also ruft der KAS-Cron diese Datei auf und + * sie startet das Skript serverseitig. * - * Warum nicht der Weg über GitHub Actions mit FTP-Upload: dort müsste ein Dritter - * Schreibzugang zum ganzen Webspace kennen. Hier kennt er einen Schlüssel, der - * ausschließlich "starte einen der beiden Syncs" kann — deutlich kleinerer Radius. + * Das ist besser als der ursprünglich geplante Weg mit einem externen Cron-Dienst: + * der Aufrufer ist jetzt der Hoster selbst, der Schlüssel verlässt den Webspace nie + * und es ist überhaupt kein Dritter beteiligt. + * + * Warum nicht GitHub Actions mit FTP-Upload: dort müsste ein Dritter Schreibzugang + * zum ganzen Webspace kennen. Hier kennt der Aufrufer einen Schlüssel, der + * ausschließlich "starte einen der drei Jobs" kann — deutlich kleinerer Radius. * * Aufruf: https://www.tsv08kulmbach.de/cron.php?job=matchcenter&key= * Schlüssel alternativ als Header X-Cron-Key (bevorzugt, landet nicht im Log @@ -37,9 +42,19 @@ 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'] ?? '');