Files
PulseCast/pulsecast.php
2026-02-13 21:32:56 +00:00

1509 lines
70 KiB
PHP

<?php
/**
* 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. Flexibel für Homenetzwerk, externe Server und gemischte Setups.
* Version: 1.0.3
* Author: M_Viper
* Author URI: https://m-viper.de
*/
if (!defined('ABSPATH')) exit;
/**
* PulseCast - Update-Notice (Gitea Releases)
* Zeigt eine Admin-Notice, wenn eine neue Version auf Gitea verfügbar ist.
*/
// Plugin-Version aus Header lesen
function pulsecast_get_plugin_version() {
if (!function_exists('get_plugin_data')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_data = get_plugin_data(__FILE__);
return $plugin_data['Version'] ?? '0.0.0';
}
// Neueste Release-Infos von Gitea holen
function pulsecast_get_latest_release_info($force_refresh = false) {
$transient_key = 'pulsecast_latest_release';
if ($force_refresh) {
delete_transient($transient_key);
}
$release_info = get_transient($transient_key);
if (false === $release_info) {
$response = wp_remote_get(
'https://git.viper.ipv64.net/api/v1/repos/M_Viper/PulseCast/releases/latest',
array('timeout' => 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'], '<')) {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong>PulseCast Update verfügbar!</strong>
Version <strong><?php echo esc_html($latest_release['version']); ?></strong> ist jetzt verfügbar
(du verwendest Version <?php echo esc_html($current_version); ?>).
</p>
<p>
<a href="https://git.viper.ipv64.net/M_Viper/PulseCast/releases" class="button button-primary" target="_blank" rel="noreferrer noopener">
Zum Download
</a>
</p>
</div>
<?php
}
}
add_action('admin_notices', 'pulsecast_show_update_notice');
class PulseCast {
const OPTION_KEY = 'pulsecast_settings';
const SCHEDULES_KEY = 'pulsecast_schedules';
const CRON_HOOK = 'pulsecast_send_event';
public function __construct() {
add_action('admin_menu', [$this, 'admin_menu']);
add_action('admin_post_pulsecast_send', [$this, 'handle_send_post']);
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']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
}
public function activate() {
if (!get_option(self::OPTION_KEY)) {
$defaults = [
'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',
'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 &amp;code (wegen des vorherigen esc_html)
$pattern = '/&amp;([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 '</span>';
}
// Farben öffnen einen neuen Span
if (isset($colors[$code])) {
return '<span style="color:' . $colors[$code] . ';">';
}
// Formate (fett, kursiv) - Wir öffnen ebenfalls einen Span
if (isset($formats[$code])) {
return '<span style="' . $formats[$code] . ';">';
}
// 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
?>
<script>
// URL Parameter SOFORT entfernen (läuft vor PHP-Rendering der Notices)
(function() {
if (window.history && window.history.replaceState) {
var url = new URL(window.location);
if (url.searchParams.has('pulsecast_status') || url.searchParams.has('pulsecast_error')) {
url.searchParams.delete('pulsecast_status');
url.searchParams.delete('pulsecast_error');
window.history.replaceState({}, document.title, url.toString());
}
}
})();
</script>
<?php
// Feedback Nachrichten - werden NUR EINMAL angezeigt
if (isset($_GET['pulsecast_status'])) {
echo '<div class="notice notice-success is-dismissible pulsecast-auto-dismiss"><p>' . esc_html(urldecode($_GET['pulsecast_status'])) . '</p></div>';
}
if (isset($_GET['pulsecast_error'])) {
echo '<div class="notice notice-error is-dismissible pulsecast-auto-dismiss"><p>' . esc_html(urldecode($_GET['pulsecast_error'])) . '</p></div>';
}
$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);
?>
<div class="wrap">
<h1>PulseCast — Broadcasts</h1>
<div class="notice notice-info">
<p><strong>Aktuelle Zeit (Server-Zeitzone):</strong> <?php echo esc_html($current_server_time); ?></p>
<p><strong>Aktuelle Zeit (UTC):</strong> <?php echo esc_html($current_utc_time); ?></p>
<?php if (!empty($current_api_url)): ?>
<p><strong>API Endpoint:</strong> <code><?php echo esc_html($current_api_url); ?></code></p>
<?php endif; ?>
<p class="description">Zeitgeplante Broadcasts werden in UTC an die StatusAPI gesendet.</p>
</div>
<h2>Einstellungen</h2>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('pulsecast_save_settings'); ?>
<input type="hidden" name="action" value="pulsecast_schedule">
<table class="form-table">
<tr>
<th scope="row" colspan="2">
<h3 style="margin: 0;">StatusAPI Server Konfiguration</h3>
<p class="description">Flexibel für Homenetzwerk (nginx proxy), externe Server oder gemischte Setups</p>
</th>
</tr>
<tr>
<th scope="row"><label for="api_protocol">Protokoll</label></th>
<td>
<select name="api_protocol" id="api_protocol" class="regular-text">
<option value="http" <?php selected($settings['api_protocol'] ?? 'http', 'http'); ?>>http://</option>
<option value="https" <?php selected($settings['api_protocol'] ?? 'http', 'https'); ?>>https://</option>
</select>
<p class="description">Verwende <code>https</code> für externe Server oder nginx proxy mit SSL</p>
</td>
</tr>
<tr>
<th scope="row"><label for="api_host">Host / Domain / IP</label></th>
<td>
<input name="api_host" id="api_host" type="text" class="regular-text"
value="<?php echo esc_attr($settings['api_host'] ?? ''); ?>"
placeholder="mc.example.com oder 192.168.1.100"
required>
<p class="description">
<strong>Beispiele:</strong><br>
• Homenetzwerk (lokale IP): <code>192.168.1.100</code><br>
• Nginx Proxy: <code>mc.example.com</code><br>
• Externer Server: <code>server.example.com</code> oder <code>123.45.67.89</code>
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="api_port">Port</label></th>
<td>
<input name="api_port" id="api_port" type="text" class="regular-text"
value="<?php echo esc_attr($settings['api_port'] ?? '9191'); ?>"
placeholder="9191">
<p class="description">
<strong>Standard:</strong> <code>9191</code> (StatusAPI Default)<br>
<strong>Nginx Proxy:</strong> <code>80</code> (http) oder <code>443</code> (https)<br>
Leer lassen für Standard-Ports (80/443)
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="api_path">API Pfad</label></th>
<td>
<input name="api_path" id="api_path" type="text" class="regular-text"
value="<?php echo esc_attr($settings['api_path'] ?? '/broadcast'); ?>"
placeholder="/broadcast">
<p class="description">
<strong>Standard:</strong> <code>/broadcast</code><br>
<strong>Nginx Proxy mit Subpath:</strong> <code>/api/broadcast</code>
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="api_key">API Key (optional)</label></th>
<td>
<input name="api_key" id="api_key" type="text" class="regular-text"
value="<?php echo esc_attr($settings['api_key'] ?? ''); ?>"
placeholder="Optional">
<p class="description">Falls deine StatusAPI einen API Key benötigt</p>
</td>
</tr>
<tr>
<th scope="row"><label for="api_key_location">API Key Übertragung</label></th>
<td>
<select name="api_key_location" id="api_key_location">
<option value="header" <?php selected($settings['api_key_location'] ?? 'header', 'header'); ?>>HTTP Header (X-Api-Key)</option>
<option value="body" <?php selected($settings['api_key_location'] ?? 'header', 'body'); ?>>JSON Body (apiKey)</option>
</select>
<p class="description">
Wo soll der API Key gesendet werden?<br>
<strong>Header:</strong> Standard für REST APIs (empfohlen)<br>
<strong>Body:</strong> Falls deine API den Key im JSON erwartet
</p>
</td>
</tr>
<tr>
<th scope="row" colspan="2">
<h3 style="margin: 20px 0 0 0;">Broadcast Formatierung</h3>
</th>
</tr>
<tr>
<th scope="row"><label for="broadcast_prefix">Broadcast Prefix</label></th>
<td>
<input name="broadcast_prefix" id="broadcast_prefix" type="text" class="regular-text"
value="<?php echo esc_attr($settings['broadcast_prefix'] ?? '[Broadcast]'); ?>">
<p class="description">Beispiel: <code>Broadcast</code> oder <code>[Broadcast]</code>. Die Klammern werden bei Bedarf automatisch mit der Klammer-Farbe eingefärbt.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="broadcast_prefix_color">Prefix Text Farbe</label></th>
<td>
<input name="broadcast_prefix_color" id="broadcast_prefix_color" type="text" class="regular-text"
value="<?php echo esc_attr($settings['broadcast_prefix_color'] ?? '&c'); ?>">
<p class="description">Farbe für den Text INNENHALB der Klammern (z. B. <code>&amp;c</code> Rot).</p>
</td>
</tr>
<tr>
<th scope="row"><label for="broadcast_bracket_color">Klammer Farbe (optional)</label></th>
<td>
<input name="broadcast_bracket_color" id="broadcast_bracket_color" type="text" class="regular-text"
value="<?php echo esc_attr($settings['broadcast_bracket_color'] ?? '&8'); ?>">
<p class="description">Farbe für die Klammern [ ] (z. B. <code>&amp;8</code> Dunkelgrau). Leer lassen für Standardfarbe.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="broadcast_message_color">Nachrichten Farbe</label></th>
<td>
<input name="broadcast_message_color" id="broadcast_message_color" type="text" class="regular-text"
value="<?php echo esc_attr($settings['broadcast_message_color'] ?? '&f'); ?>">
<p class="description">Farbcodes mit <code>&amp;</code> (z. B. <code>&amp;f</code>).</p>
</td>
</tr>
<tr>
<th scope="row" colspan="2">
<h3 style="margin: 20px 0 0 0;">Erweiterte Einstellungen</h3>
</th>
</tr>
<tr>
<th scope="row"><label for="debug_mode">Debug-Modus</label></th>
<td>
<label>
<input name="debug_mode" id="debug_mode" type="checkbox" value="1"
<?php checked($settings['debug_mode'] ?? false, true); ?>>
Detaillierte Anfrage-Logs aktivieren (erscheinen im WordPress Debug-Log)
</label>
<p class="description">
Aktiviere dies, um detaillierte Informationen über API-Anfragen zu loggen.<br>
<strong>Hinweis:</strong> Erfordert <code>WP_DEBUG = true</code> in wp-config.php
</p>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary">Einstellungen speichern</button>
</p>
</form>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
<?php wp_nonce_field('pulsecast_test_connection'); ?>
<input type="hidden" name="action" value="pulsecast_test_connection">
<button type="submit" class="button">🔌 Verbindung testen</button>
</form>
<hr>
<h2>Sofortiger Broadcast</h2>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('pulsecast_send_now'); ?>
<input type="hidden" name="action" value="pulsecast_send">
<table class="form-table">
<tr>
<th><label for="message">Nachricht</label></th>
<td><textarea name="message" id="message" rows="3" class="large-text" required placeholder="Hier Nachricht eingeben..."></textarea></td>
</tr>
</table>
<p>
<button type="submit" class="button button-primary">Senden</button>
<button type="button" class="button" id="preview-payload">🔍 Payload Vorschau</button>
</p>
</form>
<div id="payload-preview" style="display:none; background: #f5f5f5; padding: 15px; margin: 15px 0; border: 1px solid #ddd; border-radius: 4px;">
<h3>📋 Request Vorschau</h3>
<p><strong>URL:</strong> <code id="preview-url"></code></p>
<p><strong>Payload (JSON):</strong></p>
<pre id="preview-json" style="background: #fff; padding: 10px; border: 1px solid #ddd; overflow-x: auto; max-height: 400px;"></pre>
<p class="description">Dies ist die exakte JSON-Struktur, die an die StatusAPI gesendet wird.</p>
</div>
<script>
document.getElementById('preview-payload').addEventListener('click', function() {
var message = document.getElementById('message').value;
var settings = <?php echo wp_json_encode($settings); ?>;
var url = settings.api_protocol + '://' + settings.api_host;
if (settings.api_port && settings.api_port !== '80' && settings.api_port !== '443') {
url += ':' + settings.api_port;
}
url += settings.api_path;
var payload = {
message: message,
type: 'global',
prefix: settings.broadcast_prefix,
prefixColor: settings.broadcast_prefix_color,
bracketColor: settings.broadcast_bracket_color,
messageColor: settings.broadcast_message_color,
meta: {
source: 'PulseCast-WordPress',
time: new Date().toISOString()
}
};
if (settings.api_key) {
payload.apiKey = '***' + settings.api_key.substr(-4);
}
document.getElementById('preview-url').textContent = url;
document.getElementById('preview-json').textContent = JSON.stringify(payload, null, 2);
document.getElementById('payload-preview').style.display = 'block';
});
</script>
<hr>
<h2>Geplante Broadcasts (serverseitig)</h2>
<div style="background: #fff; padding: 10px; border: 1px solid #ccc; margin-bottom: 20px; display:flex; justify-content:space-between; align-items:center;">
<div>
<p style="margin:0;">Geplante Broadcasts verwenden automatisch die Standard-Einstellungen für Prefix und Farben.</p>
</div>
<div>
<form style="display:inline;" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('pulsecast_resync_action'); ?>
<input type="hidden" name="action" value="pulsecast_resync">
<button type="submit" class="button" onclick="return confirm('Alle gespeicherten Pläne erneut an den Server senden?');">↻ Synchronisation mit Server erzwingen</button>
</form>
</div>
</div>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('pulsecast_schedule_new'); ?>
<input type="hidden" name="action" value="pulsecast_schedule">
<table class="form-table">
<tr>
<th><label for="sched_message">Nachricht</label></th>
<td><textarea name="sched_message" id="sched_message" rows="3" class="large-text" required></textarea></td>
</tr>
<tr>
<th><label for="sched_time">Zeit (YYYY-MM-DD HH:MM, Server-Zeitzone)</label></th>
<td>
<input name="sched_time" id="sched_time" type="datetime-local" class="regular-text" required>
<p class="description">Beispiel: <?php echo esc_html(date('Y-m-d\TH:i', strtotime('+1 hour'))); ?> (in einer Stunde)</p>
</td>
</tr>
<tr>
<th><label for="sched_recur">Wiederholung</label></th>
<td>
<select name="sched_recur" id="sched_recur">
<option value="none">Keine</option>
<option value="hourly">Stündlich</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
</select>
</td>
</tr>
</table>
<p><button class="button">Planen (serverseitig)</button></p>
</form>
<h3>Aktuelle Zeitgeplante Nachrichten</h3>
<?php if (empty($schedules)): ?>
<p>Keine geplanten Broadcasts.</p>
<?php else: ?>
<div style="margin-bottom: 10px;">
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
<?php wp_nonce_field('pulsecast_refresh_status'); ?>
<input type="hidden" name="action" value="pulsecast_refresh_status">
<button type="submit" class="button">🔄 Status manuell aktualisieren</button>
</form>
<span class="description" style="margin-left: 10px;">
Status wird automatisch beim Seitenladen aktualisiert.
<?php
$has_recent = false;
foreach ($schedules as $s) {
if ($s['time'] <= time() && $s['time'] > time() - 600) {
$has_recent = true;
break;
}
}
if ($has_recent): ?>
<strong style="color: #1976d2;">⚡ Auto-Refresh aktiv (alle 30s)</strong>
<?php endif; ?>
</span>
</div>
<table class="widefat">
<thead><tr><th>ID</th><th>Nachricht</th><th>Sendezeit (Lokal)</th><th>Sendezeit (UTC)</th><th>Status</th><th>Wiederholung</th><th>Aktionen</th></tr></thead>
<tbody>
<?php foreach ($schedules as $id => $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)';
}
}
?>
<tr style="<?php echo $row_style; ?>">
<td><?php echo esc_html($id); ?></td>
<td><?php echo $this->format_message_output($s['message']); ?></td>
<td><?php echo esc_html($local_time); ?></td>
<td><?php echo esc_html($utc_time); ?></td>
<td style="<?php echo $status_color; ?> font-weight: bold;">
<?php echo esc_html($status); ?>
<span style="font-weight: normal; font-size: 11px; color: #666;"><?php echo esc_html($last_check_text); ?></span>
</td>
<td><?php echo esc_html($recur); ?></td>
<td>
<form style="display:inline" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('pulsecast_delete_schedule_'.$id); ?>
<input type="hidden" name="action" value="pulsecast_delete_schedule">
<input type="hidden" name="id" value="<?php echo esc_attr($id); ?>">
<button class="button button-small" onclick="return confirm('Schedule löschen?');">Löschen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div style="margin-top: 10px; padding: 10px; background: #f9f9f9; border-left: 4px solid #2196f3;">
<strong>Status-Legende:</strong>
<ul style="margin: 5px 0; padding-left: 20px;">
<li><strong>⏰ Geplant</strong> - Wartet auf Ausführung (zukünftig)</li>
<li style="color: #1976d2;"><strong>🔄 Wird verarbeitet</strong> - Gerade fällig, Server verarbeitet</li>
<li style="color: #2e7d32;"><strong>✅ Gesendet</strong> - Vom Server bestätigt als gesendet</li>
<li style="color: #388e3c;"><strong>🔁 Wiederkehrend</strong> - Wird regelmäßig wiederholt</li>
<li style="color: #f57c00;"><strong>⚠️ Noch ausstehend</strong> - Zeit abgelaufen, aber noch nicht verarbeitet</li>
<li style="color: #757575;"><strong>❓ Unbekannt</strong> - Klicke auf "Status aktualisieren" um zu prüfen</li>
<li style="color: #c62828;"><strong>❌ Fehler</strong> - Versuch fehlgeschlagen</li>
</ul>
<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;">
<strong>Hinweis:</strong> Der Status wird vom Server abgerufen. Klicke auf "🔄 Status vom Server aktualisieren"
um die aktuellen Informationen von der StatusAPI zu holen.
</p>
</div>
<?php endif; ?>
<p class="description">Hinweis: Alle Broadcasts werden global gesendet.</p>
</div>
<style>
.pulsecast-setup-examples {
background: #f9f9f9;
border: 1px solid #ddd;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.pulsecast-setup-examples h4 {
margin-top: 0;
color: #23282d;
}
.pulsecast-setup-examples code {
background: #fff;
padding: 2px 6px;
border: 1px solid #ddd;
border-radius: 2px;
}
</style>
<script>
// Auto-dismiss Notices nach 5 Sekunden
jQuery(document).ready(function($) {
// Notices automatisch ausblenden
setTimeout(function() {
$('.pulsecast-auto-dismiss').fadeOut(400, function() {
$(this).remove();
});
}, 5000);
// Auto-Refresh Status alle 30 Sekunden via AJAX (nur wenn Schedules vorhanden)
<?php if (!empty($schedules)): ?>
var hasPending = <?php
$has_pending = false;
foreach ($schedules as $s) {
if ($s['time'] <= time() && $s['time'] > time() - 600) { // Letzte 10 Minuten
$has_pending = true;
break;
}
}
echo $has_pending ? 'true' : 'false';
?>;
if (hasPending) {
var refreshCount = 0;
var maxRefreshes = 20; // Max 10 Minuten (20 x 30s)
var autoRefreshInterval = setInterval(function() {
refreshCount++;
// Stoppe nach maxRefreshes
if (refreshCount > maxRefreshes) {
clearInterval(autoRefreshInterval);
console.log('PulseCast: Auto-Refresh gestoppt (Zeitlimit erreicht)');
return;
}
// AJAX Request um Status zu aktualisieren
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'pulsecast_ajax_refresh_status',
nonce: '<?php echo wp_create_nonce('pulsecast_ajax_refresh'); ?>'
},
success: function(response) {
if (response.success) {
var updated = response.data.updated || 0;
var hasPending = response.data.has_pending || false;
if (updated > 0) {
console.log('PulseCast: ✅ Status aktualisiert (' + updated + ' Broadcasts)');
// Sanftes Reload - nur wenn kein Input fokussiert ist
if (!$('input:focus, textarea:focus').length) {
// Fade out, reload, fade in
$('body').css('opacity', '0.7');
setTimeout(function() {
location.reload();
}, 300);
}
}
// Stoppe Auto-Refresh wenn keine ausstehenden Broadcasts mehr
if (!hasPending) {
clearInterval(autoRefreshInterval);
console.log('PulseCast: ✅ Auto-Refresh gestoppt (alle Broadcasts verarbeitet)');
}
}
},
error: function() {
console.log('PulseCast: ⚠️ Status-Update fehlgeschlagen');
}
});
}, 30000); // Alle 30 Sekunden
console.log('PulseCast: ⚡ Auto-Refresh aktiv (alle 30s für max ' + (maxRefreshes * 30 / 60) + ' Minuten)');
}
<?php endif; ?>
});
</script>
<div class="pulsecast-setup-examples">
<h4>📋 Konfigurations-Beispiele</h4>
<p><strong>Setup 1: Homenetzwerk mit Nginx Proxy Manager</strong></p>
<div style="background: #e8f5e9; padding: 10px; margin: 10px 0; border-left: 4px solid #4caf50;">
<p><strong>Nginx Proxy Manager Konfiguration:</strong></p>
<ul>
<li>Domain Names: <code>statusapi.viper.ipv64.net</code></li>
<li>Scheme: <code>http</code> ⚠️ <em>(nicht https!)</em></li>
<li>Forward Hostname/IP: <code>192.168.x.x</code> (lokale IP vom Server mit StatusAPI)</li>
<li>Forward Port: <code>9191</code></li>
<li>✅ SSL-Zertifikat aktivieren (Let's Encrypt)</li>
<li>✅ Force SSL aktivieren</li>
<li>✅ HTTP/2 Support aktivieren</li>
</ul>
<p><strong>WordPress Plugin Einstellungen:</strong></p>
<ul>
<li>Protokoll: <code>https</code></li>
<li>Host: <code>statusapi.viper.ipv64.net</code></li>
<li>Port: <em>(leer lassen oder <code>443</code>)</em></li>
<li>Pfad: <code>/broadcast</code></li>
</ul>
<p style="color: #d32f2f; font-weight: bold;">
⚠️ WICHTIG: Verwende NICHT Port 9191 in WordPress wenn du nginx proxy manager nutzt!<br>
Der nginx Proxy läuft auf Port 80/443 und leitet intern auf 9191 weiter.
</p>
</div>
<p><strong>Setup 2: Direkter Zugriff ohne Proxy (z.B. im lokalen Netzwerk)</strong></p>
<div style="background: #fff3e0; padding: 10px; margin: 10px 0; border-left: 4px solid #ff9800;">
<ul>
<li>Protokoll: <code>http</code></li>
<li>Host: <code>192.168.1.100</code> (lokale IP)</li>
<li>Port: <code>9191</code></li>
<li>Pfad: <code>/broadcast</code></li>
</ul>
</div>
<p><strong>Setup 3: Beide Server extern (ohne Proxy)</strong></p>
<div style="background: #e3f2fd; padding: 10px; margin: 10px 0; border-left: 4px solid #2196f3;">
<ul>
<li>Protokoll: <code>http</code> oder <code>https</code> (je nach Server-Konfiguration)</li>
<li>Host: <code>game-server.example.com</code> oder <code>123.45.67.89</code></li>
<li>Port: <code>9191</code></li>
<li>Pfad: <code>/broadcast</code></li>
</ul>
</div>
</div>
<div class="pulsecast-setup-examples" style="background: #fff3cd; border-color: #ffc107;">
<h4>🔧 Fehlerbehebung</h4>
<p><strong style="color: #d32f2f;">❌ Fehler: 403 Forbidden - "rejected"</strong></p>
<div style="background: #ffebee; padding: 10px; margin: 10px 0; border-left: 4px solid #f44336;">
<p><strong>Die StatusAPI lehnt die Anfrage ab. Häufigste Gründe:</strong></p>
<ol>
<li>
<strong>❌ API Key fehlt oder ist falsch</strong><br>
→ Prüfe ob in den Plugin-Einstellungen ein API Key eingetragen ist<br>
→ Stimmt der Key mit dem in der StatusAPI-Config überein?
</li>
<li>
<strong>❌ StatusAPI erwartet andere Feldnamen</strong><br>
Das Plugin sendet: <code>prefixColor</code>, <code>bracketColor</code>, <code>messageColor</code><br>
Möglicherweise erwartet deine API: <code>prefix_color</code>, etc. (mit Unterstrich)<br>
→ Schaue ins StatusAPI Server-Log um zu sehen, welche Felder erwartet werden
</li>
<li>
<strong>❌ IP-Whitelist</strong><br>
WordPress Server-IP: <code><?php echo esc_html($_SERVER['SERVER_ADDR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'N/A'); ?></code><br>
→ Ist diese IP in der StatusAPI Whitelist eingetragen?
</li>
<li>
<strong>❌ Pflichtfelder fehlen</strong><br>
→ Nutze den "🔍 Payload Vorschau" Button beim Sofortigen Broadcast<br>
→ Vergleiche das JSON mit den Anforderungen deiner StatusAPI
</li>
<li>
<strong>❌ API Key wird nicht korrekt übergeben</strong><br>
Das Plugin sendet den Key als HTTP-Header: <code>X-Api-Key</code><br>
Möglicherweise erwartet deine API den Key im JSON-Body als <code>apiKey</code>?<br>
→ Siehe StatusAPI Dokumentation
</li>
</ol>
<p><strong>🔍 Nächste Schritte zur Diagnose:</strong></p>
<ul>
<li>1. Klicke auf "🔍 Payload Vorschau" um das gesendete JSON zu sehen</li>
<li>2. Aktiviere Debug-Modus in den Plugin-Einstellungen</li>
<li>3. Schaue ins WordPress Debug-Log: <code>wp-content/debug.log</code></li>
<li>4. Schaue ins StatusAPI Server-Log für Details zur Ablehnung</li>
<li>5. Teste die API mit curl/Postman um das erwartete Format zu finden</li>
</ul>
<details style="margin-top: 15px;">
<summary style="cursor: pointer; font-weight: bold;">📝 Beispiel: API mit curl testen</summary>
<pre style="background: #f5f5f5; padding: 10px; margin-top: 10px; overflow-x: auto;">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"
}'</pre>
<p>Wenn curl funktioniert, aber das Plugin nicht, liegt es an den Einstellungen.</p>
</details>
</div>
<p><strong style="color: #d32f2f;">❌ Fehler: "cURL error 35: OpenSSL SSL_connect" oder Port 9191</strong></p>
<div style="background: #ffebee; padding: 10px; margin: 10px 0; border-left: 4px solid #f44336;">
<p><strong>Problem:</strong> Du versuchst mit HTTPS auf Port 9191 zuzugreifen, aber nginx Proxy Manager läuft auf Port 443.</p>
<p><strong>Lösung:</strong></p>
<ol>
<li>Ändere im Plugin die Einstellung:
<ul>
<li>Protokoll: <code>https</code></li>
<li>Host: <code>statusapi.viper.ipv64.net</code> (deine Domain)</li>
<li>Port: <strong>(leer lassen!)</strong> oder <code>443</code></li>
<li>Pfad: <code>/broadcast</code></li>
</ul>
</li>
<li>Im Nginx Proxy Manager:
<ul>
<li>Scheme: <code>http</code> (intern zum Server)</li>
<li>Forward Port: <code>9191</code></li>
<li>SSL-Zertifikat: ✅ aktiviert (für externe Verbindung)</li>
</ul>
</li>
</ol>
<p style="font-weight: bold; color: #d32f2f;">
Der Datenfluss ist: WordPress --https:443--> Nginx Proxy --http:9191--> StatusAPI
</p>
</div>
<?php
}
public function handle_test_connection() {
if (!current_user_can('manage_options')) wp_die('Forbidden');
check_admin_referer('pulsecast_test_connection');
$settings = get_option(self::OPTION_KEY, []);
$api_url = $this->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();