diff --git a/statuspulse.php b/statuspulse.php
index 472f5e2..fcb78bf 100644
--- a/statuspulse.php
+++ b/statuspulse.php
@@ -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 '
';
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 '
';
+ echo '';
+ echo '
';
+ echo '
🔍 DEBUG: API Response (klicken zum Ein-/Ausblenden)
';
+ echo '';
+ echo '
';
+ echo '
';
+ echo '
URL: ' . esc_html($debug_url_used !== '' ? $debug_url_used : $statusapi_base_url) . '
';
+ if (!empty($debug_attempts)) {
+ echo '
Versuche: ' . esc_html(implode(' | ', $debug_attempts)) . '
';
+ } else {
+ echo '
Versuche:
';
+ }
+ if ($debug_error !== '') {
+ echo '
⚠ Fehler: ' . esc_html($debug_error) . '
';
+ } else {
+ echo '
';
+ }
+ echo '
';
+ echo '
';
+ echo esc_html($debug_raw_data !== '' ? $debug_raw_data : '(keine Response)');
+ echo '
';
+ echo '
';
+ echo '
';
+
echo '';
echo '';
+ echo '';
}
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,28 +1187,79 @@ if (!class_exists('StatusAPI_Backend_Helper')) {
return self::empty_metrics();
}
- $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
- $players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array();
- $memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array();
+ $players_list_count = self::count_players_fallback($decoded);
- $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'));
- $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_used = self::array_value($memory, 'used_mb', 'n/a');
- $memory_max = self::array_value($memory, 'max_mb', 'n/a');
+ // 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();
+
+ // 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', 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',
+ 'max_players' => (string) $max_players,
+ 'max_meta' => 'Player-Limit laut Proxy',
+ 'uptime' => (string) $uptime_human,
+ 'uptime_meta' => $uptime_seconds !== null ? ((string) $uptime_seconds) . ' Sekunden' : 'Keine Uptime-Daten',
+ '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_players,
- 'online_meta' => $occupancy !== null ? 'Auslastung ' . $occupancy . '%' : 'Aktuell verbundene Spieler',
+ 'online_players' => (string) $online_count,
+ 'online_meta' => 'Spieler online (BungeeCord)',
'max_players' => (string) $max_players,
- '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_meta' => $memory_used . ' MB von ' . $memory_max . ' MB',
+ 'max_meta' => 'Max Spieler',
+ 'uptime' => 'n/a',
+ 'uptime_meta' => 'BungeeCord Format (nicht verfügbar)',
+ 'memory' => 'n/a',
+ 'memory_meta' => 'BungeeCord Format (nicht verfügbar)',
);
}
@@ -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;