' . esc_html(urldecode($_GET['pulsecast_status'])) . '
';
}
if (isset($_GET['pulsecast_error'])) {
echo '
🔧 Fehlerbehebung
❌ Fehler: 403 Forbidden - "rejected"
Die StatusAPI lehnt die Anfrage ab. Häufigste Gründe:
-
❌ 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?
-
❌ 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
-
❌ IP-Whitelist
WordPress Server-IP:
→ Ist diese IP in der StatusAPI Whitelist eingetragen?
-
❌ Pflichtfelder fehlen
→ Nutze den "🔍 Payload Vorschau" Button beim Sofortigen Broadcast
→ Vergleiche das JSON mit den Anforderungen deiner StatusAPI
-
❌ 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:
- Ändere im Plugin die Einstellung:
- Protokoll:
https
- Host:
statusapi.viper.ipv64.net (deine Domain)
- Port: (leer lassen!) oder
443
- Pfad:
/broadcast
- 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();