From 40aa3b27a6f58212a9a24aca420d2a0223db2c59 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 13 Feb 2026 20:20:03 +0000 Subject: [PATCH] pulsecast.php aktualisiert --- pulsecast.php | 938 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 834 insertions(+), 104 deletions(-) diff --git a/pulsecast.php b/pulsecast.php index fab697b..c2d7011 100644 --- a/pulsecast.php +++ b/pulsecast.php @@ -2,8 +2,8 @@ /** * Plugin Name: PulseCast * Plugin URI: https://git.viper.ipv64.net/M_Viper/PulseCast/ - * Description: Sende Broadcasts (sofort oder geplant) an deine StatusAPI direkt aus dem WordPress-Backend. Scheduler serverseitig (StatusAPI) bevorzugt. - * Version: 1.0.1 + * Description: Sende Broadcasts (sofort oder geplant) an deine StatusAPI direkt aus dem WordPress-Backend. Flexibel für Homenetzwerk, externe Server und gemischte Setups. + * Version: 1.0.2 * Author: M_Viper * Author URI: https://m-viper.de */ @@ -24,17 +24,6 @@ function pulsecast_get_plugin_version() { return $plugin_data['Version'] ?? '0.0.0'; } -// Cache manuell leeren (Button "Jetzt neu prüfen") -function pulsecast_clear_update_cache() { - if (isset($_GET['pulsecast_clear_cache']) && current_user_can('manage_options')) { - check_admin_referer('pulsecast_clear_cache_action'); - delete_transient('pulsecast_latest_release'); - wp_redirect(admin_url('plugins.php')); - exit; - } -} -add_action('admin_init', 'pulsecast_clear_update_cache'); - // Neueste Release-Infos von Gitea holen function pulsecast_get_latest_release_info($force_refresh = false) { $transient_key = 'pulsecast_latest_release'; @@ -86,23 +75,16 @@ function pulsecast_show_update_notice() { $latest_release = pulsecast_get_latest_release_info(); if (!empty($latest_release['version']) && version_compare($current_version, $latest_release['version'], '<')) { - $refresh_url = wp_nonce_url(admin_url('plugins.php?pulsecast_clear_cache=1'), 'pulsecast_clear_cache_action'); ?>
-

PulseCast – Update verfügbar

- Installiert:
- Neueste Version: + PulseCast Update verfügbar! + Version ist jetzt verfügbar + (du verwendest Version ).

- - Update herunterladen - - - Release Notes - - - Jetzt neu prüfen + + Zum Download

@@ -124,6 +106,9 @@ class PulseCast { add_action('admin_post_pulsecast_schedule', [$this, 'handle_schedule_post']); add_action('admin_post_pulsecast_delete_schedule', [$this, 'handle_delete_schedule']); add_action('admin_post_pulsecast_resync', [$this, 'handle_resync_post']); + add_action('admin_post_pulsecast_test_connection', [$this, 'handle_test_connection']); + add_action('admin_post_pulsecast_refresh_status', [$this, 'handle_refresh_status']); + add_action('wp_ajax_pulsecast_ajax_refresh_status', [$this, 'ajax_refresh_status']); add_action(self::CRON_HOOK, [$this, 'cron_send_broadcast'], 10, 1); add_filter('cron_schedules', [$this, 'add_weekly_cron']); register_activation_hook(__FILE__, [$this, 'activate']); @@ -133,12 +118,17 @@ class PulseCast { public function activate() { if (!get_option(self::OPTION_KEY)) { $defaults = [ - 'api_url' => '', + 'api_protocol' => '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', // Neu: Standard Dunkelgrau + 'broadcast_bracket_color' => '&8', 'broadcast_message_color' => '&f', + 'debug_mode' => false, ]; add_option(self::OPTION_KEY, $defaults); } @@ -174,16 +164,39 @@ class PulseCast { $settings = get_option(self::OPTION_KEY, []); $schedules = get_option(self::SCHEDULES_KEY, []); - // Feedback Nachrichten + // Automatische Status-Aktualisierung beim Seitenladen + $this->auto_refresh_status(); + + // URL Parameter sofort bereinigen BEVOR Notices angezeigt werden + ?> + +

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

'; + echo '

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

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

' . esc_html($_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

@@ -191,6 +204,9 @@ class PulseCast {

Aktuelle Zeit (Server-Zeitzone):

Aktuelle Zeit (UTC):

+ +

API Endpoint:

+

Zeitgeplante Broadcasts werden in UTC an die StatusAPI gesendet.

@@ -198,21 +214,100 @@ class PulseCast {
+ - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + +
+

StatusAPI Server Konfiguration

+

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

+
- -

Gib die Serveradresse an — Port (9191) und Pfad (/broadcast) werden automatisch ergänzt.

+ +

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

+
@@ -245,8 +340,38 @@ class PulseCast {

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 +

+
-

+ +

+ +

+
+ +
+ + +

@@ -261,8 +386,53 @@ class PulseCast { -

+

+ + +

+ + + +
@@ -314,6 +484,28 @@ class PulseCast {

Keine geplanten Broadcasts.

+
+
+ + + +
+ + Status wird automatisch beim Seitenladen aktualisiert. + time() - 600) { + $has_recent = true; + break; + } + } + if ($has_recent): ?> + ⚡ Auto-Refresh aktiv (alle 30s) + + +
+ @@ -321,16 +513,84 @@ class PulseCast { $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(); - $is_past = $s['time'] <= $now; - $status = $is_past ? '⚠️ Verpasst/Verarbeitet' : '⏰ Geplant'; + $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
+ + +
@@ -343,13 +603,314 @@ class PulseCast {
+ +
+ 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. + +
  3. + ❌ 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 +
  4. + +
  5. + ❌ IP-Whitelist
    + WordPress Server-IP:
    + → Ist diese IP in der StatusAPI Whitelist eingetragen? +
  6. + +
  7. + ❌ Pflichtfelder fehlen
    + → Nutze den "🔍 Payload Vorschau" Button beim Sofortigen Broadcast
    + → Vergleiche das JSON mit den Anforderungen deiner StatusAPI +
  8. + +
  9. + ❌ 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 +
  10. +
+ +

🔍 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. +
  3. Im Nginx Proxy Manager: +
      +
    • Scheme: http (intern zum Server)
    • +
    • Forward Port: 9191
    • +
    • SSL-Zertifikat: ✅ aktiviert (für externe Verbindung)
    • +
    +
  4. +
+

+ 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'); @@ -360,17 +921,22 @@ class PulseCast { $type = 'global'; $prefix = $settings['broadcast_prefix'] ?? '[Broadcast]'; $prefix_color = $settings['broadcast_prefix_color'] ?? '&c'; - $bracket_color = $settings['broadcast_bracket_color'] ?? '&8'; // Neu + $bracket_color = $settings['broadcast_bracket_color'] ?? '&8'; $message_color = $settings['broadcast_message_color'] ?? '&f'; if (empty($message)) { - wp_redirect(add_query_arg('pulsecast_error', 'empty_message', wp_get_referer())); + 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); - $code = is_wp_error($res) ? 'error' : 'ok'; - wp_redirect(add_query_arg('pulsecast_status', $code, wp_get_referer())); + + 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; } @@ -380,14 +946,19 @@ class PulseCast { 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_url'] = esc_url_raw($_POST['api_url'] ?? ''); + $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'); // Neu + $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', 'settings_saved', wp_get_referer())); + wp_redirect(add_query_arg('pulsecast_status', urlencode('✅ Einstellungen gespeichert'), wp_get_referer())); exit; } @@ -402,23 +973,23 @@ class PulseCast { $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'] ?? ''); // Neu + $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'; // Neu + 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', 'empty_fields', wp_get_referer())); + wp_redirect(add_query_arg('pulsecast_error', urlencode('Nachricht oder Zeit fehlt'), wp_get_referer())); exit; } $timeRaw = str_replace('T', ' ', $timeRaw); $local_timestamp = strtotime($timeRaw); if ($local_timestamp === false || $local_timestamp <= 0) { - wp_redirect(add_query_arg('pulsecast_error', 'bad_time', wp_get_referer())); + wp_redirect(add_query_arg('pulsecast_error', urlencode('Ungültiges Zeitformat'), wp_get_referer())); exit; } @@ -434,16 +1005,17 @@ class PulseCast { 'type' => $type, 'prefix' => $prefix, 'prefix_color' => $prefix_color, - 'bracket_color' => $bracket_color, // Neu + '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)) { - wp_redirect(add_query_arg('pulsecast_error', 'register_failed', wp_get_referer())); + $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', 'scheduled_server', wp_get_referer())); + wp_redirect(add_query_arg('pulsecast_status', urlencode('✅ Broadcast geplant und am Server registriert'), wp_get_referer())); } exit; } @@ -465,12 +1037,12 @@ class PulseCast { } } - $msg = "$count Broadcasts synchronisiert."; + $msg = "✅ $count Broadcasts synchronisiert."; if ($errors > 0) { - $msg .= " $errors Fehler sind aufgetreten."; + $msg .= " ❌ $errors Fehler aufgetreten."; } - wp_redirect(add_query_arg('pulsecast_status', $msg, wp_get_referer())); + wp_redirect(add_query_arg('pulsecast_status', urlencode($msg), wp_get_referer())); exit; } @@ -486,10 +1058,89 @@ class PulseCast { update_option(self::SCHEDULES_KEY, $schedules); } - wp_redirect(remove_query_arg(['pulsecast_error','pulsecast_status'], wp_get_referer())); + 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; @@ -501,10 +1152,14 @@ class PulseCast { $s['type'] ?? 'global', $s['prefix'] ?? '', $s['prefix_color'] ?? '', - $s['bracket_color'] ?? '', // Neu + $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(), @@ -536,48 +1191,76 @@ class PulseCast { } } - private function build_final_api_url($raw_url) { - $raw = trim($raw_url); - if ($raw === '') return ''; - - if (!preg_match('#^https?://#i', $raw)) { - $raw = 'http://' . $raw; + /** + * 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 ''; } - $parts = parse_url($raw); - if ($parts === false || empty($parts['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 ''; + } + } - $scheme = $parts['scheme'] ?? 'http'; - $host = $parts['host']; - $port = $parts['port'] ?? 9191; - $path = $parts['path'] ?? '/broadcast'; - if (substr($path, -1) === '/') $path = rtrim($path, '/'); - if ($path === '') $path = '/broadcast'; - $url = $scheme . '://' . $host . ($port ? ':' . $port : '') . $path; + // 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, []); - $raw = rtrim($settings['api_url'] ?? '', '/'); $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'; // Neu + $default_bracket_color = $settings['broadcast_bracket_color'] ?? '&8'; $default_message_color = $settings['broadcast_message_color'] ?? '&f'; - if (empty($raw)) { - return new WP_Error('no_url', 'StatusAPI URL ist nicht konfiguriert.'); - } - - $final_url = $this->build_final_api_url($raw); + $final_url = $this->build_api_url($settings); + if (empty($final_url)) { - return new WP_Error('bad_url', 'StatusAPI URL ist ungültig.'); + 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); // Neu + $payload_bracket_color = ($bracket_color !== '' ? $bracket_color : $default_bracket_color); $payload_message_color = ($message_color !== '' ? $message_color : $default_message_color); $payload = [ @@ -585,7 +1268,7 @@ class PulseCast { 'type' => $type, 'prefix' => $payload_prefix, 'prefixColor' => $payload_prefix_color, - 'bracketColor' => $payload_bracket_color, // Neu + 'bracketColor' => $payload_bracket_color, 'messageColor' => $payload_message_color, 'meta' => [ 'source' => 'PulseCast-WordPress', @@ -593,27 +1276,77 @@ class PulseCast { ], ]; + // 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 ]; - if (!empty($api_key)) { + // 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) { - return new WP_Error('http_error', 'HTTP ' . $code . ': ' . wp_remote_retrieve_response_message($response)); + // 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; @@ -621,15 +1354,12 @@ class PulseCast { private function send_schedule_to_api($localId, $schedule) { $settings = get_option(self::OPTION_KEY, []); - $raw = rtrim($settings['api_url'] ?? '', '/'); $api_key = $settings['api_key'] ?? ''; - if (empty($raw)) { - return new WP_Error('no_url', 'StatusAPI URL ist nicht konfiguriert.'); - } - $final_url = $this->build_final_api_url($raw); + $final_url = $this->build_api_url($settings); + if (empty($final_url)) { - return new WP_Error('bad_url', 'StatusAPI URL ist ungültig.'); + return new WP_Error('no_url', 'StatusAPI URL ist nicht konfiguriert.'); } $timeSec = intval($schedule['time'] ?? 0); @@ -640,7 +1370,7 @@ class PulseCast { 'type' => 'global', 'prefix' => $schedule['prefix'] ?? '', 'prefixColor' => $schedule['prefix_color'] ?? '', - 'bracketColor' => $schedule['bracket_color'] ?? '', // Neu + 'bracketColor' => $schedule['bracket_color'] ?? '', 'messageColor' => $schedule['message_color'] ?? '', 'scheduleTime' => $timeMs, 'recur' => $schedule['recur'] ?? 'none', @@ -669,7 +1399,8 @@ class PulseCast { $code = wp_remote_retrieve_response_code($response); if ($code < 200 || $code >= 300) { - return new WP_Error('http_error', 'HTTP ' . $code . ': ' . wp_remote_retrieve_response_message($response)); + $body = wp_remote_retrieve_body($response); + return new WP_Error('http_error', 'HTTP ' . $code . ': ' . wp_remote_retrieve_response_message($response) . ' - ' . $body); } return true; @@ -677,14 +1408,13 @@ class PulseCast { private function send_cancel_schedule_to_api($localId) { $settings = get_option(self::OPTION_KEY, []); - $raw = rtrim($settings['api_url'] ?? '', '/'); $api_key = $settings['api_key'] ?? ''; - if (empty($raw)) return new WP_Error('no_url', 'no api'); - - $base = $this->build_final_api_url($raw); - if (empty($base)) return new WP_Error('bad_url', 'bad'); - $cancelUrl = rtrim($base, '/') . '/cancel'; + $cancelUrl = $this->build_api_url($settings, '/cancel'); + + if (empty($cancelUrl)) { + return new WP_Error('no_url', 'no api'); + } $payload = [ 'clientScheduleId' => $localId,