1509 lines
70 KiB
PHP
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 &code (wegen des vorherigen esc_html)
|
|
$pattern = '/&([0-9a-fk-or])/i';
|
|
|
|
$formatted = preg_replace_callback($pattern, function($matches) use ($colors, $formats) {
|
|
$code = strtolower($matches[1]);
|
|
|
|
// Reset (schließt alle offenen Tags)
|
|
if ($code === 'r') {
|
|
return '</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>&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>&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>&</code> (z. B. <code>&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();
|