bin/instagram-sync.php lieferte seit dem 03.09.2026 bei jedem Lauf "Keine Posts ermittelbar". Von einem privaten Anschluss gegengeprueft: feed/user und web_profile_info antworten mit 401 require_login, das Profil-HTML traegt keine Beitragsdaten mehr, auch /embed/ liefert nur noch eine App-Huelle. Es lag also nicht an der Server-IP, und der Scraper ist geloescht, damit ihn niemand wiederbelebt. Graph API (Meta-Konto noetig) und kommerzielle Dienste wurden verworfen. Neu: bin/instagram-add.php. --profil liest die Beitragsliste ueber headless Chrome aus dem gerenderten Profil-DOM (im HTML stehen die Codes nicht) und holt zu jedem neuen Beitrag Bild, Caption und Datum ueber Instagrams oEmbed-Endpunkt; das Datum steckt in der media_id. Daneben einzelne URLs, --liste, --entfernen und --bild als Notfall. Der Shortcode ist Primaerschluessel und damit die komplette Dedupe-Logik. Die Struktur von data/instagram.json bleibt exakt gleich, deshalb war an Startseite, News-Feed und Stadionzeitung nichts zu aendern. Kein Cron mehr: der Job faellt aus public/cron.php, auf dem Webspace gibt es kein Chrome. Die Chrome-Suche liegt jetzt gemeinsam in bin/chrome-lib.php, genutzt von bin/logo-build.php und bin/instagram-add.php. data/instagram.json ist ab jetzt versioniert. Solange sie ignoriert war, lief sie gegen die versionierten Bilder auseinander: lokal lagen 10 Bilder zu 9 Eintraegen, drei Beitraege fehlten im Feed, zwei Eintraege hatten kein Bild. Repariert, jetzt 16 Eintraege zu 16 Bildern. bin/preflight.php prueft das in beide Richtungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J9mubC8gtpjtJntbhSJBRS
122 lines
5.5 KiB
PHP
122 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Cron-Endpoint — die ZWEITE benannte Ausnahme zu Regel 3 (siehe CLAUDE.md).
|
|
*
|
|
* 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.
|
|
*
|
|
* 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 beiden Jobs" kann — deutlich kleinerer Radius.
|
|
*
|
|
* Aufruf: https://www.tsv08kulmbach.de/cron.php?job=matchcenter&key=<cron.key>
|
|
* 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 beim BFV 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.
|
|
// 'instagram' stand hier bis zum 17.09.2026 und ist entfallen: Instagram verlangt seit
|
|
// Anfang September Login für den Profilabruf, der Scraper ist gelöscht. Beiträge kommen
|
|
// jetzt einzeln über bin/instagram-add.php herein (Chat-Pflege, kein Cron).
|
|
$jobs = [
|
|
'matchcenter' => 'bin/matchcenter-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];
|