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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
180 lines
10 KiB
PHP
180 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Vorlage für config/config.php — kopieren und Werte eintragen:
|
|
* cp config/config.example.php config/config.php
|
|
*
|
|
* config.php ist gitignored und liegt außerhalb des Webroots (zusätzlich per .htaccess gesperrt).
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* TAKT DER SYNC-SKRIPTE — über public/cron.php, aufgerufen vom KAS-Cron
|
|
* ---------------------------------------------------------------------------
|
|
* 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.
|
|
*
|
|
* URLs (Schlüssel = cron.key unten):
|
|
* https://www.tsv08kulmbach.de/cron.php?job=matchcenter&key=<cron.key>
|
|
* https://www.tsv08kulmbach.de/cron.php?job=instagram&key=<cron.key>
|
|
* https://www.tsv08kulmbach.de/cron.php?job=logrotate&key=<cron.key>
|
|
*
|
|
* 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.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* DIE DREI CRONJOBS IM KAS (Zeitraster des KAS, nicht Crontab-Syntax)
|
|
* ---------------------------------------------------------------------------
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*
|
|
* Die Minuten sind versetzt, damit sich zwei Läufe nicht überlappen (die Syncs sperren
|
|
* sich per Lock, sonst fiele einer aus).
|
|
*
|
|
* 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
|
|
* ---------------------------------------------------------------------------
|
|
* php bin/preflight.php Config, Secrets, Schreibrechte, vendor/, PHP-Version prüfen
|
|
* php bin/smtp-test.php SMTP-Login testen (ohne Mailversand)
|
|
* danach je eine Testmail über /kontakt, /mitglied-werden und /sportheimbuchung
|
|
*
|
|
* IM BETRIEB
|
|
* php bin/logs.php Läuft alles? Fehler der letzten 7 Tage + Stand aller Kanäle
|
|
*
|
|
* Für die Dateiablage /dateien zusätzlich:
|
|
* storage/dateien/uploads und storage/dateien/system müssen beschreibbar sein
|
|
* php bin/filesgallery-update.php --check (Patch + self-hosted Assets prüfen)
|
|
* php bin/dateien-init.php Ordnerstruktur Jahr → Abteilung anlegen
|
|
*/
|
|
return [
|
|
// Kanonische Basis-URL ohne trailing slash (für Canonical, OG, Sitemap).
|
|
'base_url' => 'https://www.tsv08kulmbach.de',
|
|
|
|
// 'production' oder 'development' (steuert Fehleranzeige).
|
|
'env' => 'production',
|
|
|
|
// Geheimer Schlüssel für die HMAC-Time-Trap der Formulare und den IP-Hash im Spam-Log.
|
|
// Generieren mit: php -r "echo bin2hex(random_bytes(32));"
|
|
// Für Produktion einen EIGENEN Wert erzeugen — nie den aus der lokalen Dev-Config
|
|
// übernehmen. Solange hier CHANGE_ME steht, weisen die Formular-Actions jeden
|
|
// Versand ab (config_problems()), damit die Time-Trap nie mit einem bekannten
|
|
// Schlüssel läuft.
|
|
'app_secret' => 'CHANGE_ME',
|
|
|
|
// All-Inkl SMTP (Postfach im KAS anlegen; Host = <KAS-Login>.kasserver.com).
|
|
// Verbindung testen ohne Mailversand: php bin/smtp-test.php
|
|
//
|
|
// „SMTP Error: Could not authenticate" auf dem Server, obwohl dieselben Daten vom
|
|
// Entwicklungsrechner angenommen werden? Am 31.07.2026 war das VORÜBERGEHEND:
|
|
// derselbe Weg lief Minuten später fehlerfrei, ohne Änderung an der Config
|
|
// (vermutlich der Brute-Force-Schutz des Mailservers). Erst `php bin/smtp-test.php --probe`
|
|
// laufen lassen, bevor irgendetwas hier geändert wird — und nicht in Serie
|
|
// wiederholen, jeder Fehlversuch ist ein fehlgeschlagener Login.
|
|
//
|
|
// Bekannter RÜCKFALLWEG auf diesem Webspace, falls die Anmeldung öfter kippt:
|
|
// host 'localhost', port 25, ohne Verschlüsselung und OHNE Anmeldung — der lokale
|
|
// Relay des Hosters nimmt uns an (am 31.07.2026 nachgewiesen). Dann kann eine
|
|
// Anmeldung gar nicht mehr fehlschlagen. Bewusst NICHT der Standard: mit
|
|
// 'localhost' liefen Dev und Produktion auseinander (lokal gibt es keinen
|
|
// Mailserver), und ein Codepfad ist leichter zu pflegen als zwei.
|
|
'smtp' => [
|
|
'host' => 'w01ca75d.kasserver.com',
|
|
'port' => 587, // STARTTLS
|
|
'username' => 'noreply@tsv08kulmbach.de', // volle Postfach-Adresse
|
|
'password' => 'POSTFACH_PASSWORT',
|
|
// Absender: das authentifizierte Postfach (gleiche Domain, sonst lehnt All-Inkl ab).
|
|
'from' => 'noreply@tsv08kulmbach.de',
|
|
'from_name' => 'TSV 08 Kulmbach',
|
|
// Empfänger der Formular-Mails.
|
|
'to' => 'info@tsv08kulmbach.de',
|
|
],
|
|
|
|
// 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: 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
|
|
// geleaktem Link davor, dass BFV oder Instagram unsere Server-IP sperren.
|
|
// Muss kleiner sein als der engste gewünschte Takt (½-stündlich = 1800 s).
|
|
'min_interval' => 240,
|
|
],
|
|
|
|
// Instagram-Scraper (bin/instagram-sync.php).
|
|
'instagram' => [
|
|
'username' => 'tsv08kulmbach',
|
|
'max_posts' => 9,
|
|
],
|
|
|
|
// Zentrale News-Seite (app/pages/news.php) — mischt data/news.json,
|
|
// Instagram-Cache und Matchcenter-Ergebnisse; hier nur die Obergrenze für
|
|
// die Gesamtliste (News allein könnte sonst unbegrenzt wachsen).
|
|
'news' => [
|
|
'max_items' => 20,
|
|
],
|
|
|
|
// Dateiablage /dateien (Files Gallery, vendored unter public/dateien/) — der Verein
|
|
// lädt dort Fotos und Videos hoch, eingearbeitet werden sie danach von Hand.
|
|
// Konfiguriert wird die App in public/dateien/_filesconfig.php; hier stehen nur die
|
|
// Geheimnisse. Passwort-Hash erzeugen mit:
|
|
// php -r "echo password_hash('DAS-PASSWORT', PASSWORD_DEFAULT), PHP_EOL;"
|
|
// Klartext-Passwörter würden auch funktionieren (index.php:507) — aber nicht in einer Datei.
|
|
// Solange hier CHANGE_ME steht, ist der Zugang gesperrt (kein Passwort = kein Login).
|
|
'dateien' => [
|
|
'license_key' => 'CHANGE_ME', // Vollversion-Schlüssel von files.gallery
|
|
'users' => [
|
|
// Gemeinsames Konto zum Hochladen — Passwort im Verein weitergeben.
|
|
'verein' => ['password_hash' => 'CHANGE_ME'],
|
|
// Verwaltung (Aufräumen, Sortieren, Löschen).
|
|
'admin' => ['password_hash' => 'CHANGE_ME'],
|
|
],
|
|
],
|
|
|
|
// Matchcenter-Scraper (bin/matchcenter-sync.php) — Spielplan/Ergebnisse/Tabelle
|
|
// aus der öffentlichen BFV-Widget-JSON-API. Die IDs sind keine Geheimnisse
|
|
// (sie steckten schon im alten BFV-Widget-Embed) und dürfen hier stehen.
|
|
'matchcenter' => [
|
|
// Referer für die BFV-Requests (gegen HTTP 418).
|
|
'referer' => 'https://widget-prod.bfv.de/',
|
|
'max_upcoming' => 5, // anstehende Spiele pro Team + im globalen Slider
|
|
'max_results' => 5, // letzte Ergebnisse pro Team
|
|
// key = Schlüssel in data/teams.json (für kuratierten Namen/Liga/Slug),
|
|
// team_id = BFV permanentId (saison-stabil). compound_id = Wettbewerbs-ID
|
|
// der Tabelle — nur Fallback: der Scraper leitet die aktuelle compoundId
|
|
// automatisch aus der Matches-API ab, sodass eine neue Saison von selbst
|
|
// übernommen wird. compound_id nur pflegen, falls die Auto-Erkennung mal fehlt.
|
|
'teams' => [
|
|
['key' => 'erste-mannschaft', 'team_id' => '016PKHIPIO000000VV0AG811VUDIC8D7', 'compound_id' => '02TBKEVREG000006VS5489BTVVQ0O654-G'],
|
|
// Zweite = Spielgemeinschaft „(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II".
|
|
// team_id ist die Saison-26/27-ID (reaktiviert 29.07.2026 — die alte ID
|
|
// 016PCRVRJC… lieferte ab 26/27 nur noch den TSV Melkendorf ohne uns; bei
|
|
// SG-Umbauten zur neuen Saison die ID von der BFV-Mannschaftsseite prüfen).
|
|
['key' => 'zweite-mannschaft', 'team_id' => '030RNM1V6K000000VS5489BRVVPSQ1LR'],
|
|
['key' => 'damen', 'team_id' => '01HLQ7QVES000000VV0AG80NVVKQLCU9', 'compound_id' => '02TK5AGFE0000004VS5489BUVVPGA9RV-G'],
|
|
],
|
|
],
|
|
];
|