Upload via Git Manager GUI

This commit is contained in:
Git Manager GUI
2026-04-03 01:36:24 +02:00
parent 712b506f0e
commit a0ee37da24

View File

@@ -3,7 +3,7 @@
Plugin Name: StatusPulse
Plugin URI:https://git.viper.ipv64.net/M_Viper/StatusPulse
Description: Moderne WordPress-Admin-Seite für StatusAPI-Konfiguration, Attack-API-Key und Proxy-Tests.
Version: 1.0.0
Version: 1.0.1
Author: M_Viper
Author URI: https://m-viper.de
Requires at least: 6.8
@@ -290,7 +290,118 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return '';
}
return untrailingslashit(esc_url_raw($url));
$url = untrailingslashit(esc_url_raw($url));
// Falls aus anderen Plugins ein Endpoint eingetragen wurde,
// auf reine Basis-URL zurückführen.
$known_suffixes = array(
'/broadcast',
'/network/attack',
'/antibot/security-log',
'/health',
);
foreach ($known_suffixes as $suffix) {
if (substr($url, -strlen($suffix)) === $suffix) {
$url = substr($url, 0, -strlen($suffix));
break;
}
}
return untrailingslashit($url);
}
private static function get_pulsecast_api_url() {
$settings = get_option('pulsecast_settings', array());
if (!is_array($settings)) {
return '';
}
$protocol = isset($settings['api_protocol']) ? strtolower(trim((string) $settings['api_protocol'])) : 'http';
if ($protocol !== 'http' && $protocol !== 'https') {
$protocol = 'http';
}
$host = isset($settings['api_host']) ? trim((string) $settings['api_host']) : '';
if ($host === '') {
return '';
}
$port = isset($settings['api_port']) ? trim((string) $settings['api_port']) : '';
$path = isset($settings['api_path']) ? trim((string) $settings['api_path']) : '/broadcast';
if ($path === '') {
$path = '/broadcast';
}
if (substr($path, 0, 1) !== '/') {
$path = '/' . $path;
}
$url = $protocol . '://' . $host;
if ($port !== '' && !(($protocol === 'http' && $port === '80') || ($protocol === 'https' && $port === '443'))) {
$url .= ':' . $port;
}
// Wichtig: exakt wie PulseCast verwenden (inkl. konfiguriertem Pfad).
// So verhalten wir uns identisch zum funktionierenden Plugin.
$url .= $path;
return esc_url_raw($url);
}
private static function get_candidate_base_urls($base_url) {
$candidates = array();
$add = function($url) use (&$candidates) {
$u = trim((string) $url);
if ($u !== '' && !in_array($u, $candidates, true)) {
$candidates[] = $u;
}
};
$add_base_variants = function($url) use ($add) {
$u = trim((string) $url);
if ($u === '') {
return;
}
// Wenn nur Domain ohne Schema gespeichert wurde: wie in den anderen Plugins auf http normalisieren.
if (strpos($u, 'http://') !== 0 && strpos($u, 'https://') !== 0) {
$u = 'http://' . $u;
}
// Primär: exakt die konfigurierte URL testen.
$root = untrailingslashit($u);
$add($root);
// Direkt danach Port 9191 testen falls NPM nicht korrekt routet, finden
// wir den StatusAPI-Server so viel schneller (vor /health etc.)
$parts = wp_parse_url($u);
if (is_array($parts) && !empty($parts['host']) && empty($parts['port'])) {
$host_9191 = 'http://' . $parts['host'] . ':9191';
$add($host_9191);
$add($host_9191 . '/');
$add($host_9191 . '/health');
}
$add($root . '/');
$add($root . '/health');
// Falls https gesetzt ist, auch http testen (häufiger NPM/SSL Sonderfall).
if (strpos($root, 'https://') === 0) {
$http_root = 'http://' . substr($root, 8);
$add($http_root);
$add($http_root . '/');
$add($http_root . '/health');
}
};
$primary = trim((string) $base_url);
if ($primary !== '') {
$add_base_variants($primary);
}
$pulsecast_url = self::get_pulsecast_api_url();
if ($pulsecast_url !== '') {
$add_base_variants($pulsecast_url);
}
return $candidates;
}
private static function get_options() {
@@ -404,13 +515,22 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
$live_status = self::probe_statusapi($statusapi_base_url);
$state = self::get_state();
$notice = self::build_notice();
$status_endpoint = $statusapi_base_url !== '' ? $statusapi_base_url : 'http://dein-proxy:9191';
$candidate_urls_for_ui = self::get_candidate_base_urls($statusapi_base_url);
$status_endpoint = !empty($candidate_urls_for_ui)
? $candidate_urls_for_ui[0]
: '';
$attack_endpoint = $status_endpoint . '/network/attack';
$status_json_snippet = "Status JSON: {$status_endpoint}";
$network_guard_snippet = "networkinfo.attack.api_key={$attack_api_key}\nnetworkinfo.attack.source={$attack_source}";
$curl_examples = self::build_curl_examples($status_endpoint, $attack_endpoint, $attack_api_key, $attack_source);
$refresh_nonce = wp_create_nonce('statusapi_backend_helper_refresh');
$debug_data = isset($live_status['debug']) && is_array($live_status['debug']) ? $live_status['debug'] : array();
$debug_raw_data = isset($debug_data['raw_data']) ? (string) $debug_data['raw_data'] : '';
$debug_error = isset($debug_data['error']) ? (string) $debug_data['error'] : '';
$debug_url_used = isset($debug_data['url_used']) ? (string) $debug_data['url_used'] : '';
$debug_attempts = isset($debug_data['attempts']) && is_array($debug_data['attempts']) ? $debug_data['attempts'] : array();
echo '<div class="wrap statusapi-admin">';
self::render_styles();
self::render_scripts($refresh_nonce);
@@ -473,13 +593,37 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
self::render_metric_card('statusapi-metric-memory', 'RAM Nutzung', $live_status['metrics']['memory'], $live_status['metrics']['memory_meta']);
echo '</div>';
echo '<div class="statusapi-card" style="background: #f9f9f9; border: 2px solid #cbd5e1; margin: 20px 0;">';
echo '<div class="statusapi-card-head" style="cursor:pointer;user-select:none;" onclick="var d=document.getElementById(\'statuspulse-debug-body\');d.style.display=(d.style.display===\'none\'?\'block\':\'none\')">';
echo '<h2 style="color:#64748b;font-size:16px;">🔍 DEBUG: API Response <span style="font-size:12px;font-weight:normal;">(klicken zum Ein-/Ausblenden)</span></h2>';
echo '</div>';
echo '<div id="statuspulse-debug-body" style="display:none;">';
echo '<div style="padding: 0 15px 10px 15px; font-size: 12px; color: #333;">';
echo '<div><strong>URL:</strong> <span id="statuspulse-debug-url">' . esc_html($debug_url_used !== '' ? $debug_url_used : $statusapi_base_url) . '</span></div>';
if (!empty($debug_attempts)) {
echo '<div><strong>Versuche:</strong> <span id="statuspulse-debug-attempts">' . esc_html(implode(' | ', $debug_attempts)) . '</span></div>';
} else {
echo '<div><strong>Versuche:</strong> <span id="statuspulse-debug-attempts"></span></div>';
}
if ($debug_error !== '') {
echo '<div id="statuspulse-debug-error" style="color:#c0392b;font-weight:600;margin-top:4px;"><strong>⚠ Fehler:</strong> ' . esc_html($debug_error) . '</div>';
} else {
echo '<div id="statuspulse-debug-error" style="color:#c0392b;font-weight:600;margin-top:4px;display:none;"></div>';
}
echo '</div>';
echo '<pre id="statuspulse-debug-raw" style="background: #222; color: #0f0; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; max-height: 400px; border: 1px solid #444;">';
echo esc_html($debug_raw_data !== '' ? $debug_raw_data : '(keine Response)');
echo '</pre>';
echo '</div>';
echo '</div>';
echo '<div class="statusapi-grid">';
echo '<div class="statusapi-card statusapi-card-form">';
echo '<div class="statusapi-card-head"><h2>Konfiguration</h2><p>Die Werte werden lokal in WordPress gespeichert und als Snippets für deine Proxy-Dateien angezeigt.</p></div>';
echo '<form method="post" action="options.php">';
settings_fields('statusapi_backend_helper_group');
echo '<div class="statusapi-fields">';
self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'z. B. http://127.0.0.1:9191');
self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'Bei NPM: https://deine-domain.tld (ohne :9191, ohne /broadcast)');
self::render_input_field('Attack API-Key', self::OPTION_KEY . '[attack_api_key]', $attack_api_key, 'Entspricht networkinfo.attack.api_key in network-guard.properties.');
self::render_input_field('Attack Source', self::OPTION_KEY . '[attack_source]', $attack_source, 'Wird in Testmeldungen als Quelle angezeigt.');
echo '</div>';
@@ -513,6 +657,7 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
echo '</div>';
echo '</div>';
echo '</div>';
echo '</div>';
}
public static function render_messages_page() {
@@ -690,6 +835,160 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return "GET {$status_endpoint}\n\nPOST {$attack_endpoint}\nHeader: X-API-Key: {$attack_api_key}\nBody: {$payload}";
}
private static function is_supported_api_payload($decoded) {
if (!is_array($decoded)) {
return false;
}
$is_statusapi = isset($decoded['network']) && is_array($decoded['network']);
$has_players_array = isset($decoded['players']) && is_array($decoded['players']);
$has_online_flag = isset($decoded['online'])
&& (is_bool($decoded['online']) || $decoded['online'] === 0 || $decoded['online'] === 1 || $decoded['online'] === '0' || $decoded['online'] === '1' || $decoded['online'] === 'true' || $decoded['online'] === 'false');
$is_bungeecord = $has_online_flag && isset($decoded['players']) && is_array($decoded['players']);
// Wie in mc-player-history: players[] gilt bereits als verwertbares StatusAPI-JSON.
return $is_statusapi || $is_bungeecord || $has_players_array;
}
private static function should_try_stream_fallback($error_message) {
$msg = strtolower((string) $error_message);
return strpos($msg, 'curl error 35') !== false
|| strpos($msg, 'tlsv1 unrecognized name') !== false
|| strpos($msg, 'ssl routines') !== false;
}
private static function stream_http_request($url, $method, $headers, $body, $timeout) {
$parts = wp_parse_url($url);
if (!is_array($parts) || empty($parts['host'])) {
return new WP_Error('statuspulse_stream_bad_url', 'Ungueltige URL fuer Stream-Fallback.');
}
$scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) : 'http';
$host = (string) $parts['host'];
$port = isset($parts['port']) ? (int) $parts['port'] : ($scheme === 'https' ? 443 : 80);
$path = isset($parts['path']) ? (string) $parts['path'] : '/';
if ($path === '') {
$path = '/';
}
if (!empty($parts['query'])) {
$path .= '?' . $parts['query'];
}
$transport = $scheme === 'https' ? 'ssl://' : 'tcp://';
$context_options = array();
if ($scheme === 'https') {
$context_options['ssl'] = array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
// Workaround fuer Umgebungen mit problematischem SNI/TLS Handshake via cURL.
'SNI_enabled' => false,
);
}
$context = stream_context_create($context_options);
$errno = 0;
$errstr = '';
$fp = @stream_socket_client($transport . $host . ':' . $port, $errno, $errstr, (float) $timeout, STREAM_CLIENT_CONNECT, $context);
if (!$fp) {
return new WP_Error('statuspulse_stream_connect_failed', 'Stream-Verbindung fehlgeschlagen: ' . $errstr);
}
stream_set_timeout($fp, (int) max(1, (int) $timeout));
$request_headers = array(
'Host' => $host,
'Connection' => 'close',
'User-Agent' => 'StatusPulse/1.0',
'Accept' => 'application/json, */*',
);
foreach ((array) $headers as $hk => $hv) {
$request_headers[(string) $hk] = (string) $hv;
}
$payload = (string) $body;
if ($payload !== '') {
$request_headers['Content-Length'] = (string) strlen($payload);
}
$raw = strtoupper($method) . ' ' . $path . " HTTP/1.1\r\n";
foreach ($request_headers as $hk => $hv) {
$raw .= $hk . ': ' . $hv . "\r\n";
}
$raw .= "\r\n";
if ($payload !== '') {
$raw .= $payload;
}
fwrite($fp, $raw);
$response_raw = '';
while (!feof($fp)) {
$chunk = fread($fp, 8192);
if ($chunk === false) {
break;
}
$response_raw .= $chunk;
}
fclose($fp);
if ($response_raw === '') {
return new WP_Error('statuspulse_stream_empty', 'Leere Antwort vom Stream-Fallback.');
}
$parts_resp = explode("\r\n\r\n", $response_raw, 2);
$head = isset($parts_resp[0]) ? $parts_resp[0] : '';
$resp_body = isset($parts_resp[1]) ? $parts_resp[1] : '';
$status_code = 0;
$head_lines = explode("\r\n", $head);
if (!empty($head_lines)) {
if (preg_match('#HTTP/\d\.\d\s+(\d{3})#', (string) $head_lines[0], $m)) {
$status_code = (int) $m[1];
}
}
return array(
'code' => $status_code,
'body' => $resp_body,
'error' => '',
'via' => 'stream',
);
}
private static function perform_request_with_fallback($url, $method, $headers = array(), $body = '', $timeout = 5) {
$args = array(
'timeout' => $timeout,
'sslverify' => false,
'headers' => is_array($headers) ? $headers : array(),
);
if (strtoupper($method) !== 'GET') {
$args['body'] = (string) $body;
}
$response = wp_remote_request($url, array_merge($args, array('method' => strtoupper($method))));
if (!is_wp_error($response)) {
return array(
'code' => (int) wp_remote_retrieve_response_code($response),
'body' => (string) wp_remote_retrieve_body($response),
'error' => '',
'via' => 'wp',
);
}
$error_message = $response->get_error_message();
if (!self::should_try_stream_fallback($error_message)) {
return array('code' => 0, 'body' => '', 'error' => $error_message, 'via' => 'wp');
}
$fallback = self::stream_http_request($url, $method, $headers, $body, $timeout);
if (is_wp_error($fallback)) {
return array('code' => 0, 'body' => '', 'error' => $error_message . ' | Stream-Fallback: ' . $fallback->get_error_message(), 'via' => 'wp+stream');
}
return $fallback;
}
private static function probe_statusapi($base_url) {
$state = self::get_state();
$previous_status = isset($state['last_status']) ? (string) $state['last_status'] : 'unknown';
@@ -697,43 +996,113 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
$previous_memory_high = !empty($state['last_memory_high']);
$previous_player_high = !empty($state['last_player_high']);
if ($base_url === '') {
$candidate_urls = self::get_candidate_base_urls($base_url);
if (empty($candidate_urls)) {
return array(
'status' => 'unknown',
'label' => 'Nicht konfiguriert',
'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen.',
'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen (oder PulseCast konfigurieren).',
'http_code' => 0,
'checked_at_human' => self::format_timestamp($state['last_check_at']),
'metrics' => self::empty_metrics(),
'debug' => array(
'url_used' => '',
'attempts' => array(),
'error' => 'Keine Basis-URL gesetzt.',
'raw_data' => '',
),
);
}
$response = wp_remote_get($base_url, array('timeout' => 5));
$response_url = $candidate_urls[0];
$response = null;
$last_error = '';
$is_valid_response = false;
$is_reachable = false;
$reachable_code = 0;
$attempts = array();
$raw_fallback = '';
foreach ($candidate_urls as $try_url) {
$response = self::perform_request_with_fallback($try_url, 'GET', array(), '', 5);
if (!empty($response['error'])) {
$last_error = $response['error'];
$attempts[] = $try_url . ' -> ERROR: ' . $response['error'];
continue;
}
$code_tmp = (int) $response['code'];
$body_tmp = (string) $response['body'];
$decoded_tmp = json_decode($body_tmp, true);
if ($code_tmp > 0 && $code_tmp < 500 && !$is_reachable) {
$is_reachable = true;
$reachable_code = $code_tmp;
$response_url = $try_url;
}
if ($code_tmp >= 200 && $code_tmp < 300 && self::is_supported_api_payload($decoded_tmp)) {
$response_url = $try_url;
$is_valid_response = true;
$last_error = '';
$attempts[] = $try_url . ' -> HTTP ' . $code_tmp . ' (StatusAPI JSON)';
break;
}
if ($code_tmp >= 200 && $code_tmp < 300 && !self::is_supported_api_payload($decoded_tmp)) {
$attempts[] = $try_url . ' -> HTTP ' . $code_tmp . ' (kein StatusAPI JSON)';
if ($raw_fallback === '') {
$raw_fallback = $body_tmp;
}
if (is_string($body_tmp) && stripos($body_tmp, 'successfully started the Nginx Proxy Manager') !== false) {
// NPM-Domain ist nicht korrekt auf Port 9191 geroutet
$parsed_npm = wp_parse_url($try_url);
$npm_host = is_array($parsed_npm) && !empty($parsed_npm['host']) ? $parsed_npm['host'] : '';
$last_error = 'Nginx Proxy Manager Default-Seite erkannt Proxy-Host für diese Domain fehlt.'
. ($npm_host !== '' ? ' Trage stattdessen direkt ein: http://' . $npm_host . ':9191' : '');
} else {
$last_error = 'Antwort ist kein StatusAPI-JSON.';
}
} else {
$attempts[] = $try_url . ' -> HTTP ' . $code_tmp;
$last_error = 'HTTP ' . $code_tmp;
}
}
$now = time();
if (is_wp_error($response)) {
if (!$is_valid_response) {
$state['last_check_at'] = $now;
$state['last_http_code'] = 0;
$state['last_http_code'] = $is_reachable ? $reachable_code : 0;
$state['last_status'] = 'error';
$state['last_error'] = $response->get_error_message();
$state['last_error'] = $last_error !== '' ? $last_error : 'Ungueltige Antwort vom Proxy';
if ($previous_status !== 'error') {
self::push_message($state, 'error', 'Proxy offline', 'StatusAPI ist nicht erreichbar: ' . $response->get_error_message());
self::push_message($state, 'error', 'Proxy-Fehlkonfiguration', 'StatusAPI liefert keine gueltigen JSON-Daten. Pruefe Nginx Proxy Host fuer die Domain.');
}
self::save_state($state);
error_log('StatusPulse: Fehler fuer ' . implode(', ', $candidate_urls) . ': ' . $state['last_error']);
return array(
'status' => 'error',
'label' => 'Offline',
'meta' => $response->get_error_message(),
'label' => 'Fehlkonfiguration',
'meta' => $state['last_error'],
'http_code' => 0,
'checked_at_human' => self::format_timestamp($now),
'metrics' => self::empty_metrics(),
'debug' => array(
'url_used' => $response_url,
'attempts' => $attempts,
'error' => $state['last_error'],
'raw_data' => $raw_fallback,
),
);
}
$code = (int) wp_remote_retrieve_response_code($response);
$code = (int) $response['code'];
$ok = $code >= 200 && $code < 300;
$body = wp_remote_retrieve_body($response);
$body = (string) $response['body'];
$decoded = json_decode($body, true);
$state['last_check_at'] = $now;
$state['last_http_code'] = $code;
@@ -800,10 +1169,16 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return array(
'status' => $ok ? 'ok' : 'error',
'label' => $ok ? 'Online' : 'Fehler',
'meta' => $ok ? 'Antwort von ' . untrailingslashit($base_url) : 'HTTP ' . $code,
'meta' => $ok ? 'Antwort von ' . untrailingslashit($response_url) : 'HTTP ' . $code,
'http_code' => $code,
'checked_at_human' => self::format_timestamp($now),
'metrics' => $ok ? self::extract_metrics($decoded) : self::empty_metrics(),
'debug' => array(
'url_used' => $response_url,
'attempts' => $attempts,
'error' => $ok ? '' : 'HTTP ' . $code,
'raw_data' => $body,
),
);
}
@@ -812,19 +1187,33 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return self::empty_metrics();
}
$players_list_count = self::count_players_fallback($decoded);
// StatusAPI-Format
$network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
if (!empty($network)) {
$players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array();
$memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array();
$online_players = self::array_value($players, 'online', self::count_players_fallback($decoded));
$max_players = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a'));
// Wie in MC-Player-History: players[] als primäre Quelle bevorzugen.
$online_players = $players_list_count !== 'n/a'
? $players_list_count
: self::array_value($players, 'online', 'n/a');
$max_players_raw = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a'));
$max_players = self::normalize_max_players($max_players_raw);
$occupancy = self::array_value($players, 'occupancy_percent', null);
$uptime_human = self::array_value($network, 'uptime_human', 'n/a');
$uptime_seconds = self::array_value($network, 'uptime_seconds', null);
$memory_percent = self::array_value($memory, 'usage_percent', 'n/a');
$memory_percent = self::array_value($memory, 'usage_percent', null);
$memory_used = self::array_value($memory, 'used_mb', 'n/a');
$memory_max = self::array_value($memory, 'max_mb', 'n/a');
$memory_value = 'n/a';
if ($memory_percent !== null && $memory_percent !== 'n/a' && $memory_percent !== '') {
$memory_value = ((string) $memory_percent) . '%';
}
return array(
'online_players' => (string) $online_players,
'online_meta' => $occupancy !== null ? 'Auslastung ' . $occupancy . '%' : 'Aktuell verbundene Spieler',
@@ -832,11 +1221,48 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
'max_meta' => 'Player-Limit laut Proxy',
'uptime' => (string) $uptime_human,
'uptime_meta' => $uptime_seconds !== null ? ((string) $uptime_seconds) . ' Sekunden' : 'Keine Uptime-Daten',
'memory' => ((string) $memory_percent) . '%',
'memory' => $memory_value,
'memory_meta' => $memory_used . ' MB von ' . $memory_max . ' MB',
);
}
// BungeeCord-Format
$is_bungeecord = isset($decoded['players']) && is_array($decoded['players']) && isset($decoded['online']);
if ($is_bungeecord) {
return self::extract_metrics_bungeecord($decoded);
}
return self::empty_metrics();
}
private static function extract_metrics_bungeecord($decoded) {
if (!is_array($decoded)) {
return self::empty_metrics();
}
$players_array = isset($decoded['players']) && is_array($decoded['players']) ? $decoded['players'] : array();
$online_raw = self::array_value($decoded, 'online', null);
$is_online = $online_raw === true || $online_raw === 1 || $online_raw === '1' || $online_raw === 'true';
$online_count = $is_online ? count($players_array) : '0';
$max_players = self::array_value($decoded, 'max_players', 'n/a');
if ($max_players === 'n/a' && !empty($players_array) && is_array($players_array[0]) && isset($players_array[0]['max'])) {
$max_players = (int) $players_array[0]['max'];
}
$max_players = self::normalize_max_players($max_players);
return array(
'online_players' => (string) $online_count,
'online_meta' => 'Spieler online (BungeeCord)',
'max_players' => (string) $max_players,
'max_meta' => 'Max Spieler',
'uptime' => 'n/a',
'uptime_meta' => 'BungeeCord Format (nicht verfügbar)',
'memory' => 'n/a',
'memory_meta' => 'BungeeCord Format (nicht verfügbar)',
);
}
private static function empty_metrics() {
return array(
'online_players' => 'n/a',
@@ -864,6 +1290,20 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return 'n/a';
}
private static function normalize_max_players($value) {
// -1 bedeutet: BungeeCord hat keinen Listener-Wert und kein globales Limit.
// Wird ab StatusAPI 4.1+ eigentlich nicht mehr auftreten da Java den Listener-Wert liefert.
if ($value === -1 || $value === '-1' || (is_numeric($value) && (int) $value === -1)) {
return '∞';
}
if (is_numeric($value) && (int) $value <= 0) {
return 'n/a';
}
return $value;
}
private static function format_timestamp($timestamp) {
$timestamp = (int) $timestamp;
if ($timestamp <= 0) {
@@ -892,7 +1332,12 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return array('type' => 'error', 'message' => 'Bitte zuerst einen Attack API-Key speichern.');
}
if ($code === 'connection_failed') {
return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.');
$state = self::get_state();
$suffix = '';
if (!empty($state['last_error'])) {
$suffix = ' Letzter Fehler: ' . (string) $state['last_error'];
}
return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.' . $suffix);
}
if ($code === 'attack_failed') {
return array('type' => 'error', 'message' => 'Test Attack-Meldung konnte nicht gesendet werden. API-Key und Proxy-Endpunkt prüfen.');
@@ -918,13 +1363,16 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
}
if (isset($_POST['test_connection'])) {
$ok = self::request_ok($base_url);
$probe = self::probe_statusapi($base_url);
$ok = isset($probe['status']) && $probe['status'] === 'ok';
$state = self::get_state();
self::push_message(
$state,
$ok ? 'success' : 'error',
$ok ? 'Verbindungstest erfolgreich' : 'Verbindungstest fehlgeschlagen',
$ok ? 'Manueller Verbindungstest im WordPress-Backend war erfolgreich.' : 'Manueller Verbindungstest im WordPress-Backend war nicht erfolgreich.'
$ok
? 'Manueller Verbindungstest im WordPress-Backend war erfolgreich.'
: 'Manueller Verbindungstest im WordPress-Backend war nicht erfolgreich. ' . (isset($probe['meta']) ? (string) $probe['meta'] : '')
);
self::save_state($state);
self::redirect_with_notice($ok ? 'connection_ok' : 'connection_failed');
@@ -943,27 +1391,29 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
'connectionsBlocked' => 1800,
);
$response = wp_remote_post($base_url . '/network/attack', array(
'timeout' => 8,
'headers' => array(
$response = self::perform_request_with_fallback(
$base_url . '/network/attack',
'POST',
array(
'Content-Type' => 'application/json; charset=utf-8',
'X-API-Key' => $options['attack_api_key'],
),
'body' => wp_json_encode($payload),
));
wp_json_encode($payload),
8
);
if (is_wp_error($response)) {
if (!empty($response['error'])) {
$state = self::get_state();
$state['last_check_at'] = time();
$state['last_http_code'] = 0;
$state['last_status'] = 'error';
$state['last_error'] = $response->get_error_message();
self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response->get_error_message());
$state['last_error'] = $response['error'];
self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response['error']);
self::save_state($state);
self::redirect_with_notice('attack_failed');
}
$code = (int) wp_remote_retrieve_response_code($response);
$code = (int) $response['code'];
$state = self::get_state();
$state['last_check_at'] = time();
$state['last_http_code'] = $code;
@@ -1020,9 +1470,17 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
$live_status = self::probe_statusapi($options['statusapi_base_url']);
$state = self::get_state();
$debug_data = isset($live_status['debug']) && is_array($live_status['debug']) ? $live_status['debug'] : array();
wp_send_json_success(array(
'refresh_state' => 'Aktualisiert ' . self::format_timestamp($state['last_check_at']),
'messages_html' => self::build_messages_markup($state),
'debug' => array(
'url_used' => isset($debug_data['url_used']) ? (string) $debug_data['url_used'] : '',
'attempts' => isset($debug_data['attempts']) && is_array($debug_data['attempts']) ? $debug_data['attempts'] : array(),
'error' => isset($debug_data['error']) ? (string) $debug_data['error'] : '',
'raw_data' => isset($debug_data['raw_data']) ? (string) $debug_data['raw_data'] : '',
),
'status_cards' => array(
'proxy' => array(
'value' => $live_status['label'],
@@ -1067,52 +1525,108 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
}
private static function request_ok($url) {
$response = wp_remote_get($url, array('timeout' => 8));
if (is_wp_error($response)) {
$candidate_urls = self::get_candidate_base_urls($url);
if (empty($candidate_urls)) {
return false;
}
$code = (int) wp_remote_retrieve_response_code($response);
return $code >= 200 && $code < 300;
foreach ($candidate_urls as $try_url) {
$response = self::perform_request_with_fallback($try_url, 'GET', array(), '', 8);
if (!empty($response['error'])) {
error_log('StatusPulse: request_ok() fehlgeschlagen fuer ' . $try_url . ' - ' . $response['error']);
continue;
}
$code = (int) $response['code'];
$decoded = json_decode((string) $response['body'], true);
error_log('StatusPulse: request_ok() fuer ' . $try_url . ' -> HTTP ' . $code);
if ($code >= 200 && $code < 300 && self::is_supported_api_payload($decoded)) {
return true;
}
}
return false;
}
private static function fetch_attacker_entries($base_url) {
if ($base_url === '') {
// Schritt 1: Arbeitsfähige Basis-URL ermitteln (erste die valide antwortet)
$candidate_urls = self::get_candidate_base_urls($base_url);
if (empty($candidate_urls)) {
return array(
'entries' => array(),
'error' => 'StatusAPI Basis-URL ist nicht konfiguriert.',
);
}
$endpoint = untrailingslashit($base_url) . '/antibot/security-log';
$response = wp_remote_get($endpoint, array('timeout' => 8));
if (is_wp_error($response)) {
// Finde die funktionierende Basis-URL (die echtes StatusAPI-JSON liefert)
$working_base = '';
foreach ($candidate_urls as $try_url) {
// Entferne bekannte Pfad-Suffixe damit wir nur die Root-URL testen
$root_url = rtrim(preg_replace('#/(health|antibot/security-log|broadcast|network/attack)$#', '', $try_url), '/');
if ($root_url === '') {
continue;
}
$probe = self::perform_request_with_fallback($root_url, 'GET', array(), '', 8);
if (empty($probe['error'])) {
$probe_code = (int) $probe['code'];
$probe_decoded = json_decode((string) $probe['body'], true);
if ($probe_code >= 200 && $probe_code < 300 && self::is_supported_api_payload($probe_decoded)) {
$working_base = $root_url;
break;
}
}
}
if ($working_base === '') {
return array(
'entries' => array(),
'error' => 'Konnte Angriffsdaten nicht laden: ' . $response->get_error_message(),
'error' => 'StatusAPI konnte nicht erreicht werden. Bitte Basis-URL und Erreichbarkeit prüfen.',
);
}
$code = (int) wp_remote_retrieve_response_code($response);
// Schritt 2: Dedizierter Endpunkt /antibot/security-log abrufen
$security_log_url = rtrim($working_base, '/') . '/antibot/security-log';
$response = self::perform_request_with_fallback($security_log_url, 'GET', array(), '', 8);
if (!empty($response['error'])) {
return array(
'entries' => array(),
'error' => 'Konnte Security-Log nicht laden: ' . $response['error'],
);
}
$code = (int) $response['code'];
if ($code < 200 || $code >= 300) {
return array(
'entries' => array(),
'error' => 'StatusAPI lieferte HTTP ' . $code . ' für /antibot/security-log.',
'error' => 'StatusAPI /antibot/security-log lieferte HTTP ' . $code . '.',
);
}
$body = wp_remote_retrieve_body($response);
$body = (string) $response['body'];
$decoded = json_decode($body, true);
if (!is_array($decoded) || !isset($decoded['events']) || !is_array($decoded['events'])) {
if (!is_array($decoded)) {
return array(
'entries' => array(),
'error' => 'Antwort von /antibot/security-log ist ungültig.',
'error' => 'Antwort vom Security-Log-Endpunkt ist kein gültiges JSON.',
);
}
$entries = array();
// StatusAPI liefert { "success": true, "events": [...] }
if (isset($decoded['events']) && is_array($decoded['events'])) {
foreach (array_slice($decoded['events'], 0, 100) as $entry) {
if (is_array($entry)) {
$entries[] = $entry;
}
}
}
return array(
'entries' => $decoded['events'],
'error' => '',
'entries' => $entries,
'error' => count($entries) === 0 ? 'Keine Angriffsversuche im Log vorhanden.' : '',
);
}
@@ -1699,6 +2213,22 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
applyMetricCard("statusapi-metric-uptime", result.data.metric_cards.uptime);
applyMetricCard("statusapi-metric-memory", result.data.metric_cards.memory);
// Debug-Panel live aktualisieren
if (result.data.debug) {
var dbg = result.data.debug;
var urlEl = document.getElementById("statuspulse-debug-url");
var attEl = document.getElementById("statuspulse-debug-attempts");
var rawEl = document.getElementById("statuspulse-debug-raw");
var errEl = document.getElementById("statuspulse-debug-error");
if (urlEl) urlEl.textContent = dbg.url_used || "(nicht gesetzt)";
if (attEl) attEl.textContent = dbg.attempts && dbg.attempts.length ? dbg.attempts.join(" | ") : "";
if (rawEl) rawEl.textContent = dbg.raw_data || "(keine Response)";
if (errEl) {
errEl.textContent = dbg.error || "";
errEl.style.display = dbg.error ? "block" : "none";
}
}
var messageList = document.getElementById("statusapi-message-list");
if (messageList && typeof result.data.messages_html !== "undefined") {
messageList.innerHTML = result.data.messages_html;