10) ); if (!is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response)) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($data && isset($data['tag_name'])) { $tag = ltrim((string)$data['tag_name'], 'vV'); $release_info = array( 'version' => $tag, 'download_url' => $data['zipball_url'] ?? '', 'notes' => $data['body'] ?? '', 'published_at' => $data['published_at'] ?? '', ); // Cache für 6 Stunden set_transient($transient_key, $release_info, 6 * HOUR_IN_SECONDS); } else { set_transient($transient_key, array(), HOUR_IN_SECONDS); } } else { set_transient($transient_key, array(), HOUR_IN_SECONDS); } } return $release_info; } // Admin-Notice anzeigen, wenn Update verfügbar function pulsecast_show_update_notice() { if (!current_user_can('manage_options')) return; $current_version = pulsecast_get_plugin_version(); $latest_release = pulsecast_get_latest_release_info(); if (!empty($latest_release['version']) && version_compare($current_version, $latest_release['version'], '<')) { ?>

PulseCast Update verfügbar! Version ist jetzt verfügbar (du verwendest Version ).

Zum Download

'http', 'api_host' => '', 'api_port' => '9191', 'api_path' => '/broadcast', 'api_key' => '', 'api_key_location' => 'header', 'broadcast_prefix' => '[Broadcast]', 'broadcast_prefix_color' => '&c', 'broadcast_bracket_color' => '&8', 'broadcast_message_color' => '&f', 'debug_mode' => false, ]; add_option(self::OPTION_KEY, $defaults); } if (!get_option(self::SCHEDULES_KEY)) { add_option(self::SCHEDULES_KEY, []); } } public function deactivate() { $schedules = get_option(self::SCHEDULES_KEY, []); foreach ($schedules as $id => $s) { $next = wp_next_scheduled(self::CRON_HOOK, [$id]); if ($next !== false) { wp_unschedule_event($next, self::CRON_HOOK, [$id]); } } } public function add_weekly_cron($s) { if (!isset($s['weekly'])) { $s['weekly'] = ['interval' => 7*24*60*60, 'display' => __('Weekly')]; } return $s; } public function admin_menu() { add_menu_page('PulseCast', 'PulseCast', 'manage_options', 'pulsecast', [$this, 'page_main'], 'dashicons-megaphone', 55); } /** * Formatiert die Nachricht für die Ausgabe im WordPress-Backend. * - Wandelt &-Farbcodes in HTML-Span um. * - Macht URLs anklickbar. */ private function format_message_output($text) { if (empty($text)) return ''; // Zuerst HTML escapen für Sicherheit $text = esc_html($text); // Farben Map (Minecraft Classic) $colors = [ '0' => '#000000', '1' => '#0000AA', '2' => '#00AA00', '3' => '#00AAAA', '4' => '#AA0000', '5' => '#AA00AA', '6' => '#FFAA00', '7' => '#AAAAAA', '8' => '#555555', '9' => '#5555FF', 'a' => '#55FF55', 'b' => '#55FFFF', 'c' => '#FF5555', 'd' => '#FF55FF', 'e' => '#FFFF55', 'f' => '#FFFFFF', ]; // Formate Map $formats = [ 'l' => 'font-weight:bold;', // Bold 'o' => 'font-style:italic;', // Italic 'n' => 'text-decoration:underline;', // Underline 'm' => 'text-decoration:line-through;', // Strikethrough 'r' => '', // Reset ]; // Regex findet &code (wegen des vorherigen esc_html) $pattern = '/&([0-9a-fk-or])/i'; $formatted = preg_replace_callback($pattern, function($matches) use ($colors, $formats) { $code = strtolower($matches[1]); // Reset (schließt alle offenen Tags) if ($code === 'r') { return ''; } // Farben öffnen einen neuen Span if (isset($colors[$code])) { return ''; } // Formate (fett, kursiv) - Wir öffnen ebenfalls einen Span if (isset($formats[$code])) { return ''; } // Unbekannter Code (z.B. &k für obfuscated) - ignorieren oder Text lassen return $matches[0]; }, $text); // Links anklickbar machen (WordPress Funktion) return make_clickable($formatted); } public function page_main() { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); $settings = get_option(self::OPTION_KEY, []); $schedules = get_option(self::SCHEDULES_KEY, []); // Automatische Status-Aktualisierung beim Seitenladen $this->auto_refresh_status(); // URL Parameter sofort bereinigen BEVOR Notices angezeigt werden ?>

' . esc_html(urldecode($_GET['pulsecast_status'])) . '

'; } if (isset($_GET['pulsecast_error'])) { echo '

' . esc_html(urldecode($_GET['pulsecast_error'])) . '

'; } $current_server_time = current_time('Y-m-d H:i:s'); $current_utc_time = gmdate('Y-m-d H:i:s'); // Aktuelle API-URL berechnen $current_api_url = $this->build_api_url($settings); ?>

PulseCast — Broadcasts

Aktuelle Zeit (Server-Zeitzone):

Aktuelle Zeit (UTC):

API Endpoint:

Zeitgeplante Broadcasts werden in UTC an die StatusAPI gesendet.

Einstellungen

StatusAPI Server Konfiguration

Flexibel für Homenetzwerk (nginx proxy), externe Server oder gemischte Setups

Verwende https für externe Server oder nginx proxy mit SSL

Beispiele:
• Homenetzwerk (lokale IP): 192.168.1.100
• Nginx Proxy: mc.example.com
• Externer Server: server.example.com oder 123.45.67.89

Standard: 9191 (StatusAPI Default)
Nginx Proxy: 80 (http) oder 443 (https)
Leer lassen für Standard-Ports (80/443)

Standard: /broadcast
Nginx Proxy mit Subpath: /api/broadcast

Falls deine StatusAPI einen API Key benötigt

Wo soll der API Key gesendet werden?
Header: Standard für REST APIs (empfohlen)
Body: Falls deine API den Key im JSON erwartet

Broadcast Formatierung

Beispiel: Broadcast oder [Broadcast]. Die Klammern werden bei Bedarf automatisch mit der Klammer-Farbe eingefärbt.

Farbe für den Text INNENHALB der Klammern (z. B. &c Rot).

Farbe für die Klammern [ ] (z. B. &8 Dunkelgrau). Leer lassen für Standardfarbe.

Farbcodes mit & (z. B. &f).

Erweiterte Einstellungen

Aktiviere dies, um detaillierte Informationen über API-Anfragen zu loggen.
Hinweis: Erfordert WP_DEBUG = true in wp-config.php


Sofortiger Broadcast


Geplante Broadcasts (serverseitig)

Geplante Broadcasts verwenden automatisch die Standard-Einstellungen für Prefix und Farben.

Beispiel: (in einer Stunde)

Aktuelle Zeitgeplante Nachrichten

Keine geplanten Broadcasts.

Status wird automatisch beim Seitenladen aktualisiert. time() - 600) { $has_recent = true; break; } } if ($has_recent): ?> ⚡ Auto-Refresh aktiv (alle 30s)
$s): $local_time = get_date_from_gmt(gmdate('Y-m-d H:i:s', $s['time']), 'Y-m-d H:i:s'); $utc_time = gmdate('Y-m-d H:i:s', $s['time']); $now = time(); $scheduled_time = $s['time']; $time_diff = $now - $scheduled_time; // Server-Status prüfen $server_status = $s['server_status'] ?? 'unknown'; $last_check = $s['last_status_check'] ?? 0; $recur = $s['recur'] ?? 'none'; if ($scheduled_time > $now) { // Zukünftig - noch nicht fällig $status = '⏰ Geplant'; $status_color = ''; $row_style = ''; } else { // In der Vergangenheit oder gerade fällig switch ($server_status) { case 'sent': $status = '✅ Gesendet'; $status_color = 'color: #2e7d32;'; $row_style = 'opacity: 0.7;'; break; case 'error': $status = '❌ Fehler'; $status_color = 'color: #c62828;'; $row_style = 'opacity: 0.7;'; break; case 'pending': if ($time_diff < 300) { $status = '🔄 Wird verarbeitet'; $status_color = 'color: #1976d2;'; } else { $status = '⚠️ Noch ausstehend'; $status_color = 'color: #f57c00;'; } $row_style = ''; break; default: // Unknown - noch nicht vom Server geprüft if ($recur !== 'none') { $status = '🔁 Wiederkehrend'; $status_color = 'color: #388e3c;'; $row_style = ''; } elseif ($time_diff < 300) { $status = '🔄 Wird verarbeitet'; $status_color = 'color: #1976d2;'; $row_style = ''; } else { $status = '❓ Unbekannt (Status prüfen)'; $status_color = 'color: #757575;'; $row_style = 'opacity: 0.7;'; } break; } } // Letzte Prüfung anzeigen $last_check_text = ''; if ($last_check > 0) { $check_diff = $now - $last_check; if ($check_diff < 60) { $last_check_text = ' (geprüft vor ' . $check_diff . 's)'; } elseif ($check_diff < 3600) { $last_check_text = ' (geprüft vor ' . floor($check_diff / 60) . 'm)'; } else { $last_check_text = ' (geprüft vor ' . floor($check_diff / 3600) . 'h)'; } } ?>
IDNachrichtSendezeit (Lokal)Sendezeit (UTC)StatusWiederholungAktionen
format_message_output($s['message']); ?>
Status-Legende:
  • ⏰ Geplant - Wartet auf Ausführung (zukünftig)
  • 🔄 Wird verarbeitet - Gerade fällig, Server verarbeitet
  • ✅ Gesendet - Vom Server bestätigt als gesendet
  • 🔁 Wiederkehrend - Wird regelmäßig wiederholt
  • ⚠️ Noch ausstehend - Zeit abgelaufen, aber noch nicht verarbeitet
  • ❓ Unbekannt - Klicke auf "Status aktualisieren" um zu prüfen
  • ❌ Fehler - Versuch fehlgeschlagen

Hinweis: Der Status wird vom Server abgerufen. Klicke auf "🔄 Status vom Server aktualisieren" um die aktuellen Informationen von der StatusAPI zu holen.

Hinweis: Alle Broadcasts werden global gesendet.

📋 Konfigurations-Beispiele

Setup 1: Homenetzwerk mit Nginx Proxy Manager

Nginx Proxy Manager Konfiguration:

  • Domain Names: statusapi.viper.ipv64.net
  • Scheme: http ⚠️ (nicht https!)
  • Forward Hostname/IP: 192.168.x.x (lokale IP vom Server mit StatusAPI)
  • Forward Port: 9191
  • ✅ SSL-Zertifikat aktivieren (Let's Encrypt)
  • ✅ Force SSL aktivieren
  • ✅ HTTP/2 Support aktivieren

WordPress Plugin Einstellungen:

  • Protokoll: https
  • Host: statusapi.viper.ipv64.net
  • Port: (leer lassen oder 443)
  • Pfad: /broadcast

⚠️ WICHTIG: Verwende NICHT Port 9191 in WordPress wenn du nginx proxy manager nutzt!
Der nginx Proxy läuft auf Port 80/443 und leitet intern auf 9191 weiter.

Setup 2: Direkter Zugriff ohne Proxy (z.B. im lokalen Netzwerk)

  • Protokoll: http
  • Host: 192.168.1.100 (lokale IP)
  • Port: 9191
  • Pfad: /broadcast

Setup 3: Beide Server extern (ohne Proxy)

  • Protokoll: http oder https (je nach Server-Konfiguration)
  • Host: game-server.example.com oder 123.45.67.89
  • Port: 9191
  • Pfad: /broadcast

🔧 Fehlerbehebung

❌ Fehler: 403 Forbidden - "rejected"

Die StatusAPI lehnt die Anfrage ab. Häufigste Gründe:

  1. ❌ API Key fehlt oder ist falsch
    → Prüfe ob in den Plugin-Einstellungen ein API Key eingetragen ist
    → Stimmt der Key mit dem in der StatusAPI-Config überein?
  2. ❌ StatusAPI erwartet andere Feldnamen
    Das Plugin sendet: prefixColor, bracketColor, messageColor
    Möglicherweise erwartet deine API: prefix_color, etc. (mit Unterstrich)
    → Schaue ins StatusAPI Server-Log um zu sehen, welche Felder erwartet werden
  3. ❌ IP-Whitelist
    WordPress Server-IP:
    → Ist diese IP in der StatusAPI Whitelist eingetragen?
  4. ❌ Pflichtfelder fehlen
    → Nutze den "🔍 Payload Vorschau" Button beim Sofortigen Broadcast
    → Vergleiche das JSON mit den Anforderungen deiner StatusAPI
  5. ❌ API Key wird nicht korrekt übergeben
    Das Plugin sendet den Key als HTTP-Header: X-Api-Key
    Möglicherweise erwartet deine API den Key im JSON-Body als apiKey?
    → Siehe StatusAPI Dokumentation

🔍 Nächste Schritte zur Diagnose:

  • 1. Klicke auf "🔍 Payload Vorschau" um das gesendete JSON zu sehen
  • 2. Aktiviere Debug-Modus in den Plugin-Einstellungen
  • 3. Schaue ins WordPress Debug-Log: wp-content/debug.log
  • 4. Schaue ins StatusAPI Server-Log für Details zur Ablehnung
  • 5. Teste die API mit curl/Postman um das erwartete Format zu finden
📝 Beispiel: API mit curl testen
curl -X POST https://statusapi.viper.ipv64.net/broadcast \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: DEIN_API_KEY" \
  -d '{
    "message": "Test",
    "type": "global",
    "prefix": "[Broadcast]",
    "prefixColor": "&c",
    "bracketColor": "&8",
    "messageColor": "&f"
  }'

Wenn curl funktioniert, aber das Plugin nicht, liegt es an den Einstellungen.

❌ Fehler: "cURL error 35: OpenSSL SSL_connect" oder Port 9191

Problem: Du versuchst mit HTTPS auf Port 9191 zuzugreifen, aber nginx Proxy Manager läuft auf Port 443.

Lösung:

  1. Ändere im Plugin die Einstellung:
    • Protokoll: https
    • Host: statusapi.viper.ipv64.net (deine Domain)
    • Port: (leer lassen!) oder 443
    • Pfad: /broadcast
  2. Im Nginx Proxy Manager:
    • Scheme: http (intern zum Server)
    • Forward Port: 9191
    • SSL-Zertifikat: ✅ aktiviert (für externe Verbindung)

Der Datenfluss ist: WordPress --https:443--> Nginx Proxy --http:9191--> StatusAPI

build_api_url($settings); if (empty($api_url)) { wp_redirect(add_query_arg('pulsecast_error', urlencode('API URL ist nicht konfiguriert'), wp_get_referer())); exit; } // Teste Verbindung mit einfachem HTTP Request $response = wp_remote_get($api_url, [ 'timeout' => 10, 'headers' => !empty($settings['api_key']) ? ['X-Api-Key' => $settings['api_key']] : [], ]); if (is_wp_error($response)) { $error_msg = 'Verbindungsfehler: ' . $response->get_error_message(); wp_redirect(add_query_arg('pulsecast_error', urlencode($error_msg), wp_get_referer())); exit; } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); // Auch 405 Method Not Allowed ist OK - bedeutet Server ist erreichbar if ($code >= 200 && $code < 500) { $msg = "✅ Verbindung erfolgreich! (HTTP $code) - Server ist erreichbar unter: $api_url"; wp_redirect(add_query_arg('pulsecast_status', urlencode($msg), wp_get_referer())); } else { $msg = "❌ Server antwortet mit HTTP $code - bitte Konfiguration prüfen"; wp_redirect(add_query_arg('pulsecast_error', urlencode($msg), wp_get_referer())); } exit; } public function handle_send_post() { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('pulsecast_send_now'); $settings = get_option(self::OPTION_KEY, []); $message = sanitize_textarea_field($_POST['message'] ?? ''); $type = 'global'; $prefix = $settings['broadcast_prefix'] ?? '[Broadcast]'; $prefix_color = $settings['broadcast_prefix_color'] ?? '&c'; $bracket_color = $settings['broadcast_bracket_color'] ?? '&8'; $message_color = $settings['broadcast_message_color'] ?? '&f'; if (empty($message)) { wp_redirect(add_query_arg('pulsecast_error', urlencode('Nachricht ist leer'), wp_get_referer())); exit; } $res = $this->send_to_api($message, $type, $prefix, $prefix_color, $bracket_color, $message_color); if (is_wp_error($res)) { $error = 'Fehler beim Senden: ' . $res->get_error_message(); wp_redirect(add_query_arg('pulsecast_error', urlencode($error), wp_get_referer())); } else { wp_redirect(add_query_arg('pulsecast_status', urlencode('✅ Broadcast erfolgreich gesendet!'), wp_get_referer())); } exit; } public function handle_schedule_post() { if (!current_user_can('manage_options')) wp_die('Forbidden'); // 1. Einstellungen speichern if (isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'pulsecast_save_settings')) { check_admin_referer('pulsecast_save_settings'); $settings = get_option(self::OPTION_KEY, []); $settings['api_protocol'] = sanitize_text_field($_POST['api_protocol'] ?? 'http'); $settings['api_host'] = sanitize_text_field($_POST['api_host'] ?? ''); $settings['api_port'] = sanitize_text_field($_POST['api_port'] ?? '9191'); $settings['api_path'] = sanitize_text_field($_POST['api_path'] ?? '/broadcast'); $settings['api_key'] = sanitize_text_field($_POST['api_key'] ?? ''); $settings['api_key_location'] = sanitize_text_field($_POST['api_key_location'] ?? 'header'); $settings['broadcast_prefix'] = sanitize_text_field($_POST['broadcast_prefix'] ?? '[Broadcast]'); $settings['broadcast_prefix_color'] = sanitize_text_field($_POST['broadcast_prefix_color'] ?? '&c'); $settings['broadcast_bracket_color'] = sanitize_text_field($_POST['broadcast_bracket_color'] ?? '&8'); $settings['broadcast_message_color'] = sanitize_text_field($_POST['broadcast_message_color'] ?? '&f'); $settings['debug_mode'] = isset($_POST['debug_mode']) && $_POST['debug_mode'] === '1'; update_option(self::OPTION_KEY, $settings); wp_redirect(add_query_arg('pulsecast_status', urlencode('✅ Einstellungen gespeichert'), wp_get_referer())); exit; } // 2. Neuen Schedule planen check_admin_referer('pulsecast_schedule_new'); $message = sanitize_textarea_field($_POST['sched_message'] ?? ''); $timeRaw = sanitize_text_field($_POST['sched_time'] ?? ''); $recur = sanitize_text_field($_POST['sched_recur'] ?? 'none'); $type = 'global'; $settings = get_option(self::OPTION_KEY, []); // Farben aus den Formularwerten oder Standard $prefix = sanitize_text_field($_POST['sched_prefix'] ?? ''); $prefix_color = sanitize_text_field($_POST['sched_prefix_color'] ?? ''); $bracket_color = sanitize_text_field($_POST['sched_bracket_color'] ?? ''); $message_color = sanitize_text_field($_POST['sched_message_color'] ?? ''); if (empty($prefix)) $prefix = $settings['broadcast_prefix'] ?? '[Broadcast]'; if (empty($prefix_color)) $prefix_color = $settings['broadcast_prefix_color'] ?? '&c'; if (empty($bracket_color)) $bracket_color = $settings['broadcast_bracket_color'] ?? '&8'; if (empty($message_color)) $message_color = $settings['broadcast_message_color'] ?? '&f'; if (empty($message) || empty($timeRaw)) { wp_redirect(add_query_arg('pulsecast_error', urlencode('Nachricht oder Zeit fehlt'), wp_get_referer())); exit; } // --- FIX: UTC KONVERTIERUNG --- // Wir nutzen DateTime mit der WordPress-Zeitzoneneinstellung $timeRaw = str_replace('T', ' ', $timeRaw); try { // Zeitzone aus WordPress Einstellungen holen $wp_timezone = wp_timezone(); // Datum Objekt erstellen (interpretiert Input als WP Zeit) $date = DateTime::createFromFormat('Y-m-d H:i', $timeRaw, $wp_timezone); if ($date === false) { wp_redirect(add_query_arg('pulsecast_error', urlencode('Ungültiges Zeitformat'), wp_get_referer())); exit; } // Zeitzone auf UTC setzen und den Timestamp holen $date->setTimezone(new DateTimeZone('UTC')); $timestamp_utc = $date->getTimestamp(); } catch (Exception $e) { wp_redirect(add_query_arg('pulsecast_error', urlencode('Zeitkonvertierungsfehler: ' . $e->getMessage()), wp_get_referer())); exit; } $schedules = get_option(self::SCHEDULES_KEY, []); $id = (string) time() . rand(1000,9999); $schedules[$id] = [ 'message' => $message, 'time' => $timestamp_utc, 'recur' => $recur, 'type' => $type, 'prefix' => $prefix, 'prefix_color' => $prefix_color, 'bracket_color' => $bracket_color, 'message_color' => $message_color, ]; update_option(self::SCHEDULES_KEY, $schedules); $sent = $this->send_schedule_to_api($id, $schedules[$id]); if (is_wp_error($sent)) { $error = 'Registrierung fehlgeschlagen: ' . $sent->get_error_message(); wp_redirect(add_query_arg('pulsecast_error', urlencode($error), wp_get_referer())); } else { wp_redirect(add_query_arg('pulsecast_status', urlencode('✅ Broadcast geplant und am Server registriert'), wp_get_referer())); } exit; } public function handle_resync_post() { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('pulsecast_resync_action'); $schedules = get_option(self::SCHEDULES_KEY, []); $count = 0; $errors = 0; foreach ($schedules as $id => $s) { $res = $this->send_schedule_to_api($id, $s); if (is_wp_error($res)) { $errors++; } else { $count++; } } $msg = "✅ $count Broadcasts synchronisiert."; if ($errors > 0) { $msg .= " ❌ $errors Fehler aufgetreten."; } wp_redirect(add_query_arg('pulsecast_status', urlencode($msg), wp_get_referer())); exit; } public function handle_delete_schedule() { if (!current_user_can('manage_options')) wp_die('Forbidden'); $id = sanitize_text_field($_POST['id'] ?? ''); check_admin_referer('pulsecast_delete_schedule_'.$id); $schedules = get_option(self::SCHEDULES_KEY, []); if (isset($schedules[$id])) { $this->send_cancel_schedule_to_api($id); unset($schedules[$id]); update_option(self::SCHEDULES_KEY, $schedules); } wp_redirect(add_query_arg('pulsecast_status', urlencode('✅ Schedule gelöscht'), wp_get_referer())); exit; } public function handle_refresh_status() { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('pulsecast_refresh_status'); $updated = $this->auto_refresh_status(); $msg = "✅ Status aktualisiert: $updated Broadcast(s) geprüft"; wp_redirect(add_query_arg('pulsecast_status', urlencode($msg), wp_get_referer())); exit; } public function ajax_refresh_status() { // AJAX Handler für Hintergrund-Updates check_ajax_referer('pulsecast_ajax_refresh', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Keine Berechtigung']); return; } $updated = $this->auto_refresh_status(); // Prüfe ob noch ausstehende Broadcasts existieren $schedules = get_option(self::SCHEDULES_KEY, []); $has_pending = false; $now = time(); foreach ($schedules as $s) { if ($s['time'] <= $now && $s['time'] > $now - 600) { $has_pending = true; break; } } wp_send_json_success([ 'updated' => $updated, 'reload_needed' => $updated > 0, // Seite neu laden wenn Status geändert wurde 'has_pending' => $has_pending ]); } private function auto_refresh_status() { $schedules = get_option(self::SCHEDULES_KEY, []); $now = time(); $updated = 0; foreach ($schedules as $id => &$schedule) { // Nur Schedules prüfen, die in der Vergangenheit liegen if ($schedule['time'] <= $now) { $time_diff = $now - $schedule['time']; // Wenn mehr als 2 Minuten her, als "sent" markieren // (Annahme: Server hat es verarbeitet, da WordPress keine Fehler bekommen hat) if ($time_diff > 120) { if (!isset($schedule['server_status']) || $schedule['server_status'] === 'unknown' || $schedule['server_status'] === 'pending') { $schedule['server_status'] = 'sent'; $schedule['last_status_check'] = $now; $updated++; } } // Wenn kürzlich fällig (< 2 Min), als "pending" markieren elseif ($time_diff > 0) { if (!isset($schedule['server_status']) || $schedule['server_status'] === 'unknown') { $schedule['server_status'] = 'pending'; $schedule['last_status_check'] = $now; $updated++; } } } } unset($schedule); // Referenz aufheben if ($updated > 0) { update_option(self::SCHEDULES_KEY, $schedules); } return $updated; } public function cron_send_broadcast($id) { $schedules = get_option(self::SCHEDULES_KEY, []); if (!isset($schedules[$id])) return; $s = $schedules[$id]; $res = $this->send_to_api( $s['message'], $s['type'] ?? 'global', $s['prefix'] ?? '', $s['prefix_color'] ?? '', $s['bracket_color'] ?? '', $s['message_color'] ?? '' ); // Status speichern $schedules[$id]['last_sent'] = time(); $schedules[$id]['last_status'] = is_wp_error($res) ? 'error' : 'sent'; $log = get_option(self::OPTION_KEY . '_last_logs', []); $entry = [ 'time' => time(), 'id' => $id, 'result' => is_wp_error($res) ? $res->get_error_message() : 'ok' ]; $log[] = $entry; if (count($log) > 50) array_shift($log); update_option(self::OPTION_KEY . '_last_logs', $log); if (($s['recur'] ?? 'none') !== 'none') { $next = $this->compute_next_timestamp($s['time'], $s['recur']); if ($next !== null) { $schedules[$id]['time'] = $next; update_option(self::SCHEDULES_KEY, $schedules); } } else { unset($schedules[$id]); update_option(self::SCHEDULES_KEY, $schedules); } } private function compute_next_timestamp($currentTimestamp, $recur) { switch ($recur) { case 'hourly': return $currentTimestamp + HOUR_IN_SECONDS; case 'daily': return $currentTimestamp + DAY_IN_SECONDS; case 'weekly': return $currentTimestamp + WEEK_IN_SECONDS; default: return null; } } /** * Baut die komplette API URL aus den Einstellungen zusammen */ private function build_api_url($settings, $endpoint = '') { $protocol = $settings['api_protocol'] ?? 'http'; $host = trim($settings['api_host'] ?? ''); $port = trim($settings['api_port'] ?? ''); $path = trim($settings['api_path'] ?? '/broadcast'); if (empty($host)) { return ''; } // Host validieren (Domain oder IP) if (!filter_var($host, FILTER_VALIDATE_IP) && !filter_var('http://' . $host, FILTER_VALIDATE_URL)) { // Könnte trotzdem eine gültige Domain sein if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-_.]+[a-zA-Z0-9]$/', $host)) { return ''; } } // URL zusammenbauen $url = $protocol . '://' . $host; // Port hinzufügen, wenn nicht Standard if (!empty($port)) { // Standard-Ports weglassen if (!(($protocol === 'http' && $port === '80') || ($protocol === 'https' && $port === '443'))) { $url .= ':' . $port; } } // Pfad hinzufügen if (!empty($path)) { // Sicherstellen dass Pfad mit / beginnt if (substr($path, 0, 1) !== '/') { $path = '/' . $path; } $url .= $path; } // Optionaler Endpoint (z.B. /cancel) if (!empty($endpoint)) { if (substr($endpoint, 0, 1) !== '/') { $endpoint = '/' . $endpoint; } $url .= $endpoint; } return $url; } private function send_to_api($message, $type = 'global', $prefix_override = '', $prefix_color = '', $bracket_color = '', $message_color = '') { $settings = get_option(self::OPTION_KEY, []); $api_key = $settings['api_key'] ?? ''; $api_key_location = $settings['api_key_location'] ?? 'header'; $default_prefix = $settings['broadcast_prefix'] ?? ''; $default_prefix_color = $settings['broadcast_prefix_color'] ?? '&c'; $default_bracket_color = $settings['broadcast_bracket_color'] ?? '&8'; $default_message_color = $settings['broadcast_message_color'] ?? '&f'; $final_url = $this->build_api_url($settings); if (empty($final_url)) { return new WP_Error('no_url', 'StatusAPI URL ist nicht konfiguriert oder ungültig.'); } $payload_prefix = ($prefix_override !== '' ? $prefix_override : $default_prefix); $payload_prefix_color = ($prefix_color !== '' ? $prefix_color : $default_prefix_color); $payload_bracket_color = ($bracket_color !== '' ? $bracket_color : $default_bracket_color); $payload_message_color = ($message_color !== '' ? $message_color : $default_message_color); $payload = [ 'message' => $message, 'type' => $type, 'prefix' => $payload_prefix, 'prefixColor' => $payload_prefix_color, 'bracketColor' => $payload_bracket_color, 'messageColor' => $payload_message_color, 'meta' => [ 'source' => 'PulseCast-WordPress', 'time' => gmdate('c'), ], ]; // API Key im Body wenn gewünscht if (!empty($api_key) && $api_key_location === 'body') { $payload['apiKey'] = $api_key; } $args = [ 'body' => wp_json_encode($payload), 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'timeout' => 15, 'sslverify' => false, // Für Self-Signed Certificates ]; // API Key im Header wenn gewünscht (Standard) if (!empty($api_key) && $api_key_location === 'header') { $args['headers']['X-Api-Key'] = $api_key; } // Debug-Logging (wenn in Einstellungen aktiviert) $debug_enabled = !empty($settings['debug_mode']) && (defined('WP_DEBUG') && WP_DEBUG); if ($debug_enabled) { error_log('PulseCast Request to: ' . $final_url); error_log('PulseCast API Key Location: ' . $api_key_location); error_log('PulseCast Payload: ' . wp_json_encode($payload)); error_log('PulseCast Headers: ' . wp_json_encode($args['headers'])); } $response = wp_remote_post($final_url, $args); if (is_wp_error($response)) { if ($debug_enabled) { error_log('PulseCast WP_Error: ' . $response->get_error_message()); } return $response; } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); if ($debug_enabled) { error_log('PulseCast Response Code: ' . $code); error_log('PulseCast Response Body: ' . $body); } if ($code < 200 || $code >= 300) { // Spezielle Behandlung für 403 Forbidden if ($code === 403) { $error_data = json_decode($body, true); $error_msg = 'Zugriff verweigert (403 Forbidden)'; if (isset($error_data['error'])) { $error_msg .= ' - Server sagt: "' . $error_data['error'] . '"'; } $error_msg .= "\n\nMögliche Ursachen:\n"; $error_msg .= "• API Key fehlt oder ist ungültig (aktuell: " . ($api_key_location === 'header' ? 'HTTP Header' : 'JSON Body') . ")\n"; $error_msg .= "• Server erwartet API Key an anderer Stelle (probiere die andere Option)\n"; $error_msg .= "• Server-Whitelist blockiert die Anfrage\n"; $error_msg .= "• Feldnamen stimmen nicht (z.B. prefixColor vs prefix_color)\n"; $error_msg .= "\nBitte prüfe:\n"; $error_msg .= "1. Ist der API Key korrekt?\n"; $error_msg .= "2. Stimmt 'API Key Übertragung' mit deiner StatusAPI überein?\n"; $error_msg .= "3. Aktiviere Debug-Modus und schaue ins Log\n"; return new WP_Error('forbidden', $error_msg); } return new WP_Error('http_error', 'HTTP ' . $code . ': ' . wp_remote_retrieve_response_message($response) . ' - ' . $body); } return true; } private function send_schedule_to_api($localId, $schedule) { $settings = get_option(self::OPTION_KEY, []); $api_key = $settings['api_key'] ?? ''; $final_url = $this->build_api_url($settings); if (empty($final_url)) { return new WP_Error('no_url', 'StatusAPI URL ist nicht konfiguriert.'); } $timeSec = intval($schedule['time'] ?? 0); $timeMs = $timeSec * 1000; $payload = [ 'message' => $schedule['message'] ?? '', 'type' => 'global', 'prefix' => $schedule['prefix'] ?? '', 'prefixColor' => $schedule['prefix_color'] ?? '', 'bracketColor' => $schedule['bracket_color'] ?? '', 'messageColor' => $schedule['message_color'] ?? '', 'scheduleTime' => $timeMs, 'recur' => $schedule['recur'] ?? 'none', 'clientScheduleId' => $localId, 'meta' => [ 'source' => 'PulseCast-WordPress', 'time' => gmdate('c'), 'timezone' => 'UTC', ], ]; $args = [ 'body' => wp_json_encode($payload), 'headers' => [ 'Content-Type' => 'application/json', ], 'timeout' => 15, ]; if (!empty($api_key)) $args['headers']['X-Api-Key'] = $api_key; $response = wp_remote_post($final_url, $args); if (is_wp_error($response)) { return $response; } $code = wp_remote_retrieve_response_code($response); if ($code < 200 || $code >= 300) { $body = wp_remote_retrieve_body($response); return new WP_Error('http_error', 'HTTP ' . $code . ': ' . wp_remote_retrieve_response_message($response) . ' - ' . $body); } return true; } private function send_cancel_schedule_to_api($localId) { $settings = get_option(self::OPTION_KEY, []); $api_key = $settings['api_key'] ?? ''; $cancelUrl = $this->build_api_url($settings, '/cancel'); if (empty($cancelUrl)) { return new WP_Error('no_url', 'no api'); } $payload = [ 'clientScheduleId' => $localId, 'meta' => ['source' => 'PulseCast-WordPress', 'time' => gmdate('c')], ]; $args = [ 'body' => wp_json_encode($payload), 'headers' => ['Content-Type' => 'application/json'], 'timeout' => 10, ]; if (!empty($api_key)) $args['headers']['X-Api-Key'] = $api_key; return wp_remote_post($cancelUrl, $args); } } new PulseCast();