| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/arni-solutions.de/web/crm/app/ |
Upload File : |
<?php
/**
* Online/Offline-Prüfung für Webseiten. Schreibt Status + History und
* benachrichtigt bei Statuswechsel per Pushover. Holt beim Check zugleich
* das Favicon (falls keins gespeichert oder online aktueller).
*/
class WebsiteChecker
{
const FAVICON_TTL = 86400; // Favicon höchstens 1×/Tag neu prüfen
const FAVICON_MAX = 120000; // max. 120 KB speichern
const SCREENSHOT_TTL = 259200; // Screenshot höchstens alle 3 Tage neu holen
const SCREENSHOT_MAX = 600000; // max. 600 KB speichern
/** Führt einen HTTP-Check aus. @return array status/code/ms/error/body/final_url */
public static function probe($url)
{
$start = microtime(true);
$code = 0; $err = null; $resp = false; $final = $url;
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_USERAGENT => 'ArNi-CRM-Checker/1.0',
CURLOPT_SSL_VERIFYPEER => true,
]);
$resp = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$final = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) ?: $url;
$cerr = curl_error($ch);
curl_close($ch);
if ($resp === false) $err = $cerr ?: 'Verbindungsfehler';
} else {
$ctx = stream_context_create(['http' => ['timeout' => 15, 'ignore_errors' => true, 'user_agent' => 'ArNi-CRM-Checker/1.0']]);
$resp = @file_get_contents($url, false, $ctx);
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) $code = (int)$m[1];
if ($resp === false) $err = 'Verbindungsfehler';
}
$ms = (int)round((microtime(true) - $start) * 1000);
$status = ($code >= 200 && $code < 400) ? 'up' : 'down';
return ['status' => $status, 'code' => $code ?: null, 'ms' => $ms, 'error' => $err,
'body' => is_string($resp) ? $resp : '', 'final_url' => $final];
}
/** Prüft EINE Webseite, speichert Ergebnis + History, benachrichtigt bei Wechsel. */
public static function check($site)
{
$r = self::probe($site['url']);
$prev = $site['last_status'];
$update = [
'last_status' => $r['status'], 'last_code' => $r['code'], 'last_ms' => $r['ms'],
'last_checked' => date('Y-m-d H:i:s'), 'last_error' => $r['error'], 'updated_at' => date('Y-m-d H:i:s'),
];
// Favicon zugleich prüfen/aktualisieren (nur wenn online und fällig)
if ($r['status'] === 'up' && self::faviconDue($site)) {
$fav = self::fetchFavicon($r['final_url'] ?: $site['url'], $r['body']);
$update['favicon_checked'] = date('Y-m-d H:i:s');
if ($fav && $fav['hash'] !== ($site['favicon_hash'] ?? null)) {
$update['favicon_data'] = $fav['data'];
$update['favicon_hash'] = $fav['hash'];
}
}
// Vorschau-Screenshot (nur wenn online und fällig)
if ($r['status'] === 'up' && self::screenshotDue($site)) {
$shot = self::fetchScreenshot($site['url']);
if ($shot) {
$update['screenshot_data'] = $shot;
$update['screenshot_at'] = date('Y-m-d H:i:s');
}
}
Website::update($site['id'], $update);
WebsiteCheck::insert([
'website_id' => $site['id'], 'status' => $r['status'], 'code' => $r['code'],
'ms' => $r['ms'], 'error' => $r['error'], 'created_at' => date('Y-m-d H:i:s'),
]);
if ((int)$site['notify'] === 1 && $prev !== 'unknown' && $prev !== $r['status']) {
$title = 'Webseite ' . ($r['status'] === 'up' ? 'wieder ONLINE' : 'OFFLINE') . ': ' . $site['name'];
$msg = $site['url'] . ' → ' . strtoupper($r['status'])
. ($r['code'] ? ' (HTTP ' . $r['code'] . ')' : '')
. ($r['error'] ? ' – ' . $r['error'] : '');
Pushover::send($title, $msg, $r['status'] === 'down' ? 1 : 0, $site['url'], 'Webseite öffnen');
}
return $r;
}
/** Prüft alle aktiven Webseiten. */
public static function checkAll()
{
$n = 0;
foreach (Website::activeAll() as $s) { self::check($s); $n++; }
CronLog::add('website_check', $n . ' Webseite(n) geprüft');
return $n;
}
// ===================== Screenshot =====================
/** Öffentliche Live-Screenshot-URL (Fallback fürs Frontend, falls noch nichts gespeichert). */
public static function screenshotUrl($url, $w = 1024, $h = 640)
{
return 'https://s.wordpress.com/mshots/v1/' . rawurlencode($url) . '?w=' . (int)$w . '&h=' . (int)$h;
}
private static function screenshotDue($site)
{
if (empty($site['screenshot_data'])) return true;
if (empty($site['screenshot_at'])) return true;
return (time() - strtotime($site['screenshot_at'])) > self::SCREENSHOT_TTL;
}
/** Vorschau-Screenshot via WordPress-mShots holen und als data-URI zurückgeben. */
public static function fetchScreenshot($url)
{
$shotUrl = self::screenshotUrl($url, 1024, 640);
// mShots liefert während der Erstellung einen animierten GIF-Platzhalter;
// der fertige Screenshot ist JPEG/PNG. Daher nur diese akzeptieren und sonst
// nicht speichern (→ nächster Check versucht es erneut).
for ($try = 0; $try < 4; $try++) {
$bin = self::download($shotUrl);
if ($bin) {
$mime = self::imageMime($bin, $shotUrl);
if (($mime === 'image/jpeg' || $mime === 'image/png')
&& strlen($bin) > 4000 && strlen($bin) <= self::SCREENSHOT_MAX) {
return 'data:' . $mime . ';base64,' . base64_encode($bin);
}
}
if ($try < 3) sleep(3);
}
return null;
}
// ===================== Favicon =====================
/** Ist ein (erneuter) Favicon-Abruf fällig? */
private static function faviconDue($site)
{
if (empty($site['favicon_data'])) return true;
if (empty($site['favicon_checked'])) return true;
return (time() - strtotime($site['favicon_checked'])) > self::FAVICON_TTL;
}
/**
* Favicon ermitteln (aus HTML-<link> oder /favicon.ico, sonst Google-Dienst),
* herunterladen und als data-URI + Hash zurückgeben.
* @return array|null ['data'=>dataUri,'hash'=>md5]
*/
public static function fetchFavicon($pageUrl, $html = '')
{
foreach (self::iconCandidates($pageUrl, $html) as $iconUrl) {
$bin = self::download($iconUrl);
if (!$bin) continue;
$mime = self::imageMime($bin, $iconUrl);
if (!$mime) continue;
if (strlen($bin) > self::FAVICON_MAX) continue;
return ['data' => 'data:' . $mime . ';base64,' . base64_encode($bin), 'hash' => md5($bin)];
}
return null;
}
/** Kandidaten-URLs in Präferenz-Reihenfolge. */
private static function iconCandidates($pageUrl, $html)
{
$out = [];
if (is_string($html) && $html !== '' && preg_match_all('/<link\b[^>]*>/i', $html, $links)) {
$cands = [];
foreach ($links[0] as $tag) {
if (!preg_match('/\brel\s*=\s*["\']?([^"\'>]*)/i', $tag, $rm)) continue;
$rel = strtolower($rm[1]);
if (strpos($rel, 'icon') === false) continue;
if (!preg_match('/\bhref\s*=\s*["\']([^"\']+)["\']/i', $tag, $hm)) continue;
$href = html_entity_decode(trim($hm[1]));
$size = preg_match('/\bsizes\s*=\s*["\']?(\d+)/i', $tag, $sm) ? (int)$sm[1] : 0;
// Präferenz: normales "icon" vor apple-touch; größere (aber ≤64) Sizes leicht bevorzugt
$score = 0;
if (strpos($rel, 'apple') !== false) $score -= 5;
if ($size > 0) $score += ($size <= 64 ? $size : 64 - ($size - 64) * 0.1);
$cands[] = ['url' => self::absUrl($href, $pageUrl), 'score' => $score];
}
usort($cands, function ($a, $b) { return $b['score'] <=> $a['score']; });
foreach ($cands as $c) $out[] = $c['url'];
}
// Standard-Fallback
$p = parse_url($pageUrl);
if (!empty($p['scheme']) && !empty($p['host'])) {
$root = $p['scheme'] . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '');
$out[] = $root . '/favicon.ico';
// Letzter Ausweg: Googles Favicon-Dienst (liefert fast immer ein Icon)
$out[] = 'https://www.google.com/s2/favicons?sz=64&domain=' . rawurlencode($p['host']);
}
return array_values(array_unique($out));
}
/** Datei laden (binär), mit Limit. @return string|null */
private static function download($url)
{
if (!function_exists('curl_init')) {
$ctx = stream_context_create(['http' => ['timeout' => 10, 'user_agent' => 'ArNi-CRM-Checker/1.0']]);
$b = @file_get_contents($url, false, $ctx, 0, self::SCREENSHOT_MAX + 1);
return ($b === false || $b === '') ? null : $b;
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_USERAGENT => 'ArNi-CRM-Checker/1.0', CURLOPT_SSL_VERIFYPEER => true,
]);
$b = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($b === false || $b === '' || $code < 200 || $code >= 400) return null;
return $b;
}
/** MIME aus Bytes/Endung bestimmen; null wenn kein Bild. */
private static function imageMime($bin, $url = '')
{
$b = substr($bin, 0, 16);
if (substr($b, 0, 8) === "\x89PNG\x0d\x0a\x1a\x0a") return 'image/png';
if (substr($b, 0, 3) === "GIF") return 'image/gif';
if (substr($b, 0, 2) === "\xff\xd8") return 'image/jpeg';
if (substr($b, 0, 4) === "\x00\x00\x01\x00") return 'image/x-icon';
if (substr($b, 0, 4) === "RIFF" && substr($bin, 8, 4) === "WEBP") return 'image/webp';
$head = ltrim(substr($bin, 0, 256));
if (stripos($head, '<svg') !== false || (stripos($head, '<?xml') !== false && stripos($bin, '<svg') !== false)) return 'image/svg+xml';
// Endungs-Heuristik
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION));
$map = ['ico' => 'image/x-icon', 'png' => 'image/png', 'gif' => 'image/gif',
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'svg' => 'image/svg+xml', 'webp' => 'image/webp'];
return isset($map[$ext]) ? $map[$ext] : null;
}
/** Relative URL gegen Basis auflösen. */
private static function absUrl($href, $base)
{
$href = trim($href);
if ($href === '') return '';
if (preg_match('#^https?://#i', $href)) return $href;
if (strpos($href, 'data:') === 0) return $href;
$p = parse_url($base);
if (empty($p['scheme']) || empty($p['host'])) return $href;
$root = $p['scheme'] . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '');
if (strpos($href, '//') === 0) return $p['scheme'] . ':' . $href;
if ($href[0] === '/') return $root . $href;
$dir = isset($p['path']) ? preg_replace('#/[^/]*$#', '/', $p['path']) : '/';
if ($dir === '' || $dir[0] !== '/') $dir = '/' . $dir;
return $root . $dir . $href;
}
}