2257 lines
100 KiB
PHP
2257 lines
100 KiB
PHP
<?php
|
||
/*
|
||
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.1
|
||
Author: M_Viper
|
||
Author URI: https://m-viper.de
|
||
Requires at least: 6.8
|
||
Tested up to: 6.8
|
||
PHP Version: 7.4
|
||
License: GPL2
|
||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||
Text Domain: statuspulse
|
||
Tags: status, statusapi, monitoring, admin, dashboard, proxy, api, tools
|
||
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
||
Support: [Telegram Support](https://t.me/M_Viper04)
|
||
*/
|
||
|
||
|
||
|
||
if (!defined('ABSPATH')) {
|
||
exit;
|
||
}
|
||
|
||
if (!class_exists('StatusAPI_Backend_Helper')) {
|
||
class StatusAPI_Backend_Helper {
|
||
const OPTION_KEY = 'statusapi_backend_helper_options';
|
||
const STATE_OPTION_KEY = 'statusapi_backend_helper_state';
|
||
const HISTORY_OPTION_KEY = 'statusapi_backend_helper_history';
|
||
const MENU_SLUG = 'statuspulse';
|
||
const MESSAGES_SLUG = 'statuspulse-meldungen';
|
||
const LIST_SLUG = 'statuspulse-liste';
|
||
|
||
public static function boot() {
|
||
add_action('admin_menu', array(__CLASS__, 'register_menu'));
|
||
add_action('admin_init', array(__CLASS__, 'register_settings'));
|
||
add_action('wp_dashboard_setup', array(__CLASS__, 'register_dashboard_widget'));
|
||
add_action('admin_post_statusapi_backend_helper_test', array(__CLASS__, 'handle_test_action'));
|
||
add_action('admin_post_statusapi_backend_helper_clear_messages', array(__CLASS__, 'handle_clear_messages'));
|
||
add_action('wp_ajax_statusapi_backend_helper_refresh', array(__CLASS__, 'handle_ajax_refresh'));
|
||
}
|
||
|
||
public static function register_dashboard_widget() {
|
||
if (!current_user_can('manage_options')) {
|
||
return;
|
||
}
|
||
|
||
wp_add_dashboard_widget(
|
||
'statuspulse_dashboard_widget',
|
||
'StatusPulse - Live-Status',
|
||
array(__CLASS__, 'render_dashboard_widget')
|
||
);
|
||
}
|
||
|
||
public static function render_dashboard_widget() {
|
||
$options = self::get_options();
|
||
$live_status = self::probe_statusapi($options['statusapi_base_url']);
|
||
$state = self::get_state();
|
||
$messages = isset($state['messages']) && is_array($state['messages']) ? $state['messages'] : array();
|
||
$last_message = !empty($messages) ? $messages[0] : null;
|
||
|
||
$status_color = '#dc3545';
|
||
$status_text = 'OFFLINE';
|
||
if ($live_status['status'] === 'ok') {
|
||
$status_color = '#28a745';
|
||
$status_text = 'ONLINE';
|
||
} elseif ($live_status['status'] === 'warn') {
|
||
$status_color = '#ffc107';
|
||
$status_text = 'WARNUNG';
|
||
}
|
||
|
||
echo '<style>
|
||
.spw-widget {
|
||
background: #fff;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
}
|
||
.spw-header {
|
||
background: #f5f5f5;
|
||
border-bottom: 1px solid #ddd;
|
||
padding: 8px 12px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.spw-title {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: #333;
|
||
margin: 0;
|
||
}
|
||
.spw-status {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 3px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
background: ' . esc_attr($status_color) . ';
|
||
color: #fff;
|
||
}
|
||
.spw-body {
|
||
padding: 12px;
|
||
}
|
||
.spw-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.spw-item {
|
||
background: #f9f9f9;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
padding: 8px;
|
||
}
|
||
.spw-item-label {
|
||
font-size: 11px;
|
||
color: #666;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
margin-bottom: 3px;
|
||
display: block;
|
||
}
|
||
.spw-item-value {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #000;
|
||
}
|
||
.spw-item-meta {
|
||
font-size: 11px;
|
||
color: #999;
|
||
margin-top: 2px;
|
||
}
|
||
.spw-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.spw-metric {
|
||
background: #f9f9f9;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
padding: 6px;
|
||
text-align: center;
|
||
}
|
||
.spw-metric-label {
|
||
font-size: 10px;
|
||
color: #666;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
margin-bottom: 2px;
|
||
}
|
||
.spw-metric-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #000;
|
||
}
|
||
.spw-footer {
|
||
display: flex;
|
||
gap: 8px;
|
||
border-top: 1px solid #eee;
|
||
padding-top: 8px;
|
||
}
|
||
.spw-link {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 6px 8px;
|
||
background: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
border-radius: 3px;
|
||
font-size: 11px;
|
||
color: #0073aa;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
transition: background 0.2s;
|
||
}
|
||
.spw-link:hover {
|
||
background: #e5e5e5;
|
||
color: #005a87;
|
||
text-decoration: none;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.spw-row { grid-template-columns: 1fr; }
|
||
.spw-metrics { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
</style>';
|
||
|
||
echo '<div class="spw-widget">';
|
||
|
||
echo '<div class="spw-header">';
|
||
echo '<h3 class="spw-title">StatusPulse Monitor</h3>';
|
||
echo '<span class="spw-status">' . esc_html($status_text) . '</span>';
|
||
echo '</div>';
|
||
|
||
echo '<div class="spw-body">';
|
||
|
||
echo '<div class="spw-row">';
|
||
|
||
echo '<div class="spw-item">';
|
||
echo '<span class="spw-item-label">Status</span>';
|
||
echo '<div class="spw-item-value">' . esc_html($live_status['label']) . '</div>';
|
||
echo '<div class="spw-item-meta">' . esc_html($live_status['meta']) . '</div>';
|
||
echo '</div>';
|
||
|
||
echo '<div class="spw-item">';
|
||
echo '<span class="spw-item-label">Letzte Meldung</span>';
|
||
if ($last_message !== null) {
|
||
$msg_title = isset($last_message['title']) ? (string) $last_message['title'] : 'Meldung';
|
||
$msg_text = isset($last_message['message']) ? (string) $last_message['message'] : '';
|
||
echo '<div class="spw-item-value" style="font-size: 14px;">' . esc_html($msg_title) . '</div>';
|
||
echo '<div class="spw-item-meta">' . esc_html(substr($msg_text, 0, 50)) . '</div>';
|
||
} else {
|
||
echo '<div class="spw-item-meta">Keine Meldungen</div>';
|
||
}
|
||
echo '</div>';
|
||
|
||
echo '</div>';
|
||
|
||
echo '<div class="spw-metrics">';
|
||
echo '<div class="spw-metric"><div class="spw-metric-label">HTTP</div><div class="spw-metric-value">' . esc_html($live_status['http_code'] > 0 ? (string) $live_status['http_code'] : '—') . '</div></div>';
|
||
echo '<div class="spw-metric"><div class="spw-metric-label">Spieler</div><div class="spw-metric-value">' . esc_html($live_status['metrics']['online_players']) . '</div></div>';
|
||
echo '<div class="spw-metric"><div class="spw-metric-label">RAM</div><div class="spw-metric-value">' . esc_html($live_status['metrics']['memory']) . '</div></div>';
|
||
echo '<div class="spw-metric"><div class="spw-metric-label">Check</div><div class="spw-metric-value" style="font-size: 12px;">' . esc_html(self::format_timestamp($state['last_check_at'])) . '</div></div>';
|
||
echo '</div>';
|
||
|
||
echo '<div class="spw-footer">';
|
||
echo '<a class="spw-link" href="' . esc_url(admin_url('admin.php?page=' . self::MENU_SLUG)) . '">Einstellungen</a>';
|
||
echo '<a class="spw-link" href="' . esc_url(admin_url('admin.php?page=' . self::MESSAGES_SLUG)) . '">Meldungen</a>';
|
||
echo '<a class="spw-link" href="' . esc_url(admin_url('admin.php?page=' . self::LIST_SLUG)) . '">Angriffe</a>';
|
||
echo '</div>';
|
||
|
||
echo '</div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
public static function register_menu() {
|
||
add_menu_page(
|
||
'StatusPulse',
|
||
'StatusPulse',
|
||
'manage_options',
|
||
self::MENU_SLUG,
|
||
array(__CLASS__, 'render_page'),
|
||
'dashicons-shield-alt',
|
||
81
|
||
);
|
||
|
||
add_submenu_page(
|
||
self::MENU_SLUG,
|
||
'Meldungen',
|
||
'Meldungen',
|
||
'manage_options',
|
||
self::MESSAGES_SLUG,
|
||
array(__CLASS__, 'render_messages_page')
|
||
);
|
||
|
||
add_submenu_page(
|
||
self::MENU_SLUG,
|
||
'Angriffsversuche',
|
||
'Angriffsversuche',
|
||
'manage_options',
|
||
self::LIST_SLUG,
|
||
array(__CLASS__, 'render_list_page')
|
||
);
|
||
}
|
||
|
||
public static function register_settings() {
|
||
register_setting(
|
||
'statusapi_backend_helper_group',
|
||
self::OPTION_KEY,
|
||
array(__CLASS__, 'sanitize_options')
|
||
);
|
||
}
|
||
|
||
public static function sanitize_options($input) {
|
||
$input = is_array($input) ? $input : array();
|
||
|
||
return array(
|
||
'statusapi_base_url' => self::sanitize_url(isset($input['statusapi_base_url']) ? $input['statusapi_base_url'] : ''),
|
||
'attack_api_key' => sanitize_text_field(isset($input['attack_api_key']) ? $input['attack_api_key'] : ''),
|
||
'attack_source' => sanitize_text_field(isset($input['attack_source']) ? $input['attack_source'] : 'WordPress'),
|
||
);
|
||
}
|
||
|
||
private static function sanitize_url($url) {
|
||
$url = trim((string) $url);
|
||
if ($url === '') {
|
||
return '';
|
||
}
|
||
|
||
$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() {
|
||
$stored = get_option(self::OPTION_KEY, array());
|
||
$stored = is_array($stored) ? $stored : array();
|
||
|
||
return wp_parse_args($stored, array(
|
||
'statusapi_base_url' => '',
|
||
'attack_api_key' => '',
|
||
'attack_source' => 'WordPress',
|
||
));
|
||
}
|
||
|
||
private static function get_state() {
|
||
$stored = get_option(self::STATE_OPTION_KEY, array());
|
||
$stored = is_array($stored) ? $stored : array();
|
||
|
||
return wp_parse_args($stored, array(
|
||
'last_check_at' => 0,
|
||
'last_http_code' => 0,
|
||
'last_success_at' => 0,
|
||
'last_success_code' => 0,
|
||
'last_status' => 'unknown',
|
||
'last_error' => '',
|
||
'last_attack_mode' => null,
|
||
'last_memory_high' => false,
|
||
'last_player_high' => false,
|
||
'messages' => array(),
|
||
));
|
||
}
|
||
|
||
private static function save_state($state) {
|
||
update_option(self::STATE_OPTION_KEY, $state, false);
|
||
}
|
||
|
||
private static function push_message(&$state, $level, $title, $message) {
|
||
if (!isset($state['messages']) || !is_array($state['messages'])) {
|
||
$state['messages'] = array();
|
||
}
|
||
|
||
array_unshift($state['messages'], array(
|
||
'ts' => time(),
|
||
'level' => $level,
|
||
'title' => $title,
|
||
'message' => $message,
|
||
));
|
||
|
||
if (count($state['messages']) > 60) {
|
||
$state['messages'] = array_slice($state['messages'], 0, 60);
|
||
}
|
||
|
||
self::push_history_entry($level, $title, $message);
|
||
}
|
||
|
||
private static function get_history() {
|
||
$stored = get_option(self::HISTORY_OPTION_KEY, array());
|
||
return is_array($stored) ? $stored : array();
|
||
}
|
||
|
||
private static function save_history($history) {
|
||
update_option(self::HISTORY_OPTION_KEY, is_array($history) ? $history : array(), false);
|
||
}
|
||
|
||
private static function push_history_entry($level, $title, $message) {
|
||
$history = self::get_history();
|
||
array_unshift($history, array(
|
||
'ts' => time(),
|
||
'level' => (string) $level,
|
||
'title' => (string) $title,
|
||
'message' => (string) $message,
|
||
));
|
||
|
||
if (count($history) > 1000) {
|
||
$history = array_slice($history, 0, 1000);
|
||
}
|
||
|
||
self::save_history($history);
|
||
}
|
||
|
||
private static function build_messages_markup($state) {
|
||
$messages = isset($state['messages']) && is_array($state['messages']) ? $state['messages'] : array();
|
||
if (empty($messages)) {
|
||
return '<li data-level="empty" class="statusapi-message-item statusapi-message-empty"><div><strong>Noch keine Meldungen</strong><p>Sobald Statuswechsel oder Attack-Events erkannt werden, erscheinen sie hier.</p></div></li>';
|
||
}
|
||
|
||
$html = '';
|
||
foreach (array_slice($messages, 0, 20) as $msg) {
|
||
$level = isset($msg['level']) ? (string) $msg['level'] : 'info';
|
||
$title = isset($msg['title']) ? (string) $msg['title'] : 'Meldung';
|
||
$text = isset($msg['message']) ? (string) $msg['message'] : '';
|
||
$ts = isset($msg['ts']) ? (int) $msg['ts'] : 0;
|
||
|
||
$html .= '<li data-level="' . esc_attr($level) . '" class="statusapi-message-item statusapi-level-' . esc_attr($level) . '">';
|
||
$html .= '<div class="statusapi-message-head"><strong>' . esc_html($title) . '</strong><span>' . esc_html(self::format_timestamp($ts)) . '</span></div>';
|
||
$html .= '<p>' . esc_html($text) . '</p>';
|
||
$html .= '</li>';
|
||
}
|
||
|
||
return $html;
|
||
}
|
||
|
||
public static function render_page() {
|
||
if (!current_user_can('manage_options')) {
|
||
return;
|
||
}
|
||
|
||
$options = self::get_options();
|
||
$statusapi_base_url = $options['statusapi_base_url'];
|
||
$attack_api_key = $options['attack_api_key'];
|
||
$attack_source = $options['attack_source'];
|
||
$live_status = self::probe_statusapi($statusapi_base_url);
|
||
$state = self::get_state();
|
||
$notice = self::build_notice();
|
||
$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);
|
||
echo '<div class="statusapi-hero">';
|
||
echo '<div>';
|
||
echo '<span class="statusapi-kicker">Proxy Control Surface</span>';
|
||
echo '<h1>StatusPulse</h1>';
|
||
echo '<p>Reduzierte Admin-Seite für Proxy-URL, Attack-Key und direkte Tests gegen dein StatusAPI-Plugin.</p>';
|
||
echo '</div>';
|
||
echo '<div class="statusapi-hero-badge">';
|
||
echo '<span>Live Refresh</span>';
|
||
echo '<strong>alle 15s</strong>';
|
||
echo '<em id="statusapi-refresh-state">wartet auf erstes Update</em>';
|
||
echo '</div>';
|
||
echo '</div>';
|
||
|
||
if ($notice !== null) {
|
||
printf(
|
||
'<div class="notice notice-%1$s statusapi-notice"><p>%2$s</p></div>',
|
||
esc_attr($notice['type']),
|
||
esc_html($notice['message'])
|
||
);
|
||
}
|
||
|
||
echo '<div class="statusapi-status-grid">';
|
||
self::render_status_card_named(
|
||
'statusapi-card-proxy',
|
||
'Proxy Status',
|
||
$live_status['label'],
|
||
$live_status['meta'],
|
||
$live_status['status']
|
||
);
|
||
self::render_status_card_named(
|
||
'statusapi-card-http',
|
||
'Letzter HTTP-Code',
|
||
$live_status['http_code'] > 0 ? (string) $live_status['http_code'] : 'n/a',
|
||
$live_status['checked_at_human'],
|
||
$live_status['status']
|
||
);
|
||
self::render_status_card_named(
|
||
'statusapi-card-success',
|
||
'Letzte erfolgreiche Prüfung',
|
||
self::format_timestamp($state['last_success_at']),
|
||
$state['last_success_code'] > 0 ? 'HTTP ' . $state['last_success_code'] : 'Noch kein erfolgreicher Check gespeichert',
|
||
$state['last_success_at'] > 0 ? 'ok' : 'unknown'
|
||
);
|
||
self::render_status_card_named(
|
||
'statusapi-card-lastcheck',
|
||
'Zuletzt geprüft',
|
||
self::format_timestamp($state['last_check_at']),
|
||
$state['last_error'] !== '' ? $state['last_error'] : 'Keine Fehler gespeichert',
|
||
$state['last_status']
|
||
);
|
||
echo '</div>';
|
||
|
||
echo '<div class="statusapi-metric-grid">';
|
||
self::render_metric_card('statusapi-metric-online', 'Spieler online', $live_status['metrics']['online_players'], $live_status['metrics']['online_meta']);
|
||
self::render_metric_card('statusapi-metric-max', 'Max. Spieler', $live_status['metrics']['max_players'], $live_status['metrics']['max_meta']);
|
||
self::render_metric_card('statusapi-metric-uptime', 'Uptime', $live_status['metrics']['uptime'], $live_status['metrics']['uptime_meta']);
|
||
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, '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>';
|
||
echo '<div class="statusapi-actions">';
|
||
submit_button('Einstellungen speichern', 'primary', 'submit', false);
|
||
echo '</div>';
|
||
echo '</form>';
|
||
echo '</div>';
|
||
|
||
echo '<div class="statusapi-card">';
|
||
echo '<div class="statusapi-card-head"><h2>Snippets</h2><p>Direkt für deine Proxy-Konfiguration und externe Tests vorbereitet.</p></div>';
|
||
self::render_snippet_card('Für network-guard.properties', $network_guard_snippet, 4);
|
||
self::render_snippet_card('Wichtiger Endpunkt', $status_json_snippet . "\nAttack POST: {$attack_endpoint}", 4);
|
||
self::render_snippet_card('Test-Requests', $curl_examples, 8);
|
||
echo '</div>';
|
||
|
||
echo '<div class="statusapi-card">';
|
||
echo '<div class="statusapi-card-head"><h2>Aktionen</h2><p>Teste direkt aus dem Backend, ob dein Proxy erreichbar ist und Attack-Meldungen akzeptiert.</p></div>';
|
||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="statusapi-test-form">';
|
||
wp_nonce_field('statusapi_backend_helper_test');
|
||
echo '<input type="hidden" name="action" value="statusapi_backend_helper_test" />';
|
||
echo '<div class="statusapi-test-buttons">';
|
||
submit_button('StatusAPI Verbindung prüfen', 'secondary', 'test_connection', false);
|
||
submit_button('Test Attack-Meldung senden', 'secondary', 'test_attack', false);
|
||
echo '</div>';
|
||
echo '</form>';
|
||
echo '<div class="statusapi-meta">';
|
||
echo '<div><span>Gespeicherte Basis-URL</span><strong>' . esc_html($statusapi_base_url !== '' ? $statusapi_base_url : 'nicht gesetzt') . '</strong></div>';
|
||
echo '<div><span>Attack-Key</span><strong>' . esc_html($attack_api_key !== '' ? 'gesetzt' : 'leer') . '</strong></div>';
|
||
echo '<div><span>Quelle</span><strong>' . esc_html($attack_source !== '' ? $attack_source : 'WordPress') . '</strong></div>';
|
||
echo '</div>';
|
||
echo '</div>';
|
||
echo '</div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
public static function render_messages_page() {
|
||
if (!current_user_can('manage_options')) {
|
||
return;
|
||
}
|
||
|
||
$state = self::get_state();
|
||
$notice = self::build_notice();
|
||
$messages_markup = self::build_messages_markup($state);
|
||
$refresh_nonce = wp_create_nonce('statusapi_backend_helper_refresh');
|
||
|
||
echo '<div class="wrap statusapi-admin">';
|
||
self::render_styles();
|
||
self::render_scripts($refresh_nonce);
|
||
echo '<h1>StatusPulse Meldungen</h1>';
|
||
echo '<p>Dedizierte Seite für alle laufenden Status-, Attack- und Warnmeldungen.</p>';
|
||
|
||
if ($notice !== null) {
|
||
printf(
|
||
'<div class="notice notice-%1$s statusapi-notice"><p>%2$s</p></div>',
|
||
esc_attr($notice['type']),
|
||
esc_html($notice['message'])
|
||
);
|
||
}
|
||
|
||
self::render_messages_center($messages_markup, self::MESSAGES_SLUG);
|
||
echo '</div>';
|
||
}
|
||
|
||
public static function render_list_page() {
|
||
if (!current_user_can('manage_options')) {
|
||
return;
|
||
}
|
||
|
||
$options = self::get_options();
|
||
$notice = self::build_notice();
|
||
$attacker_data = self::fetch_attacker_entries($options['statusapi_base_url']);
|
||
$rows = isset($attacker_data['entries']) && is_array($attacker_data['entries']) ? $attacker_data['entries'] : array();
|
||
$list_error = isset($attacker_data['error']) ? (string) $attacker_data['error'] : '';
|
||
|
||
echo '<div class="wrap statusapi-admin">';
|
||
self::render_styles();
|
||
|
||
echo '<h1>StatusPulse Angriffsversuche</h1>';
|
||
echo '<p>Zeigt nur Spielername und UUID von erkannten Angriffsversuchen (inkl. Datum/Uhrzeit).</p>';
|
||
|
||
if ($notice !== null) {
|
||
printf(
|
||
'<div class="notice notice-%1$s statusapi-notice"><p>%2$s</p></div>',
|
||
esc_attr($notice['type']),
|
||
esc_html($notice['message'])
|
||
);
|
||
}
|
||
|
||
echo '<div class="statusapi-card statusapi-card-messages">';
|
||
echo '<div class="statusapi-card-head"><h2>Angriffs-Ereignisse</h2><p>Neueste Einträge oben.</p></div>';
|
||
|
||
if ($list_error !== '') {
|
||
echo '<div class="notice notice-error inline"><p>' . esc_html($list_error) . '</p></div>';
|
||
}
|
||
|
||
if (empty($rows)) {
|
||
echo '<p>Keine Angriffs-Einträge mit Spielername/UUID vorhanden.</p>';
|
||
} else {
|
||
echo '<table class="widefat striped">';
|
||
echo '<thead><tr><th style="width:190px;">Datum / Uhrzeit</th><th style="width:220px;">Spielername</th><th style="width:180px;">IP</th><th>UUID</th></tr></thead>';
|
||
echo '<tbody>';
|
||
foreach ($rows as $row) {
|
||
$date = isset($row['datetime']) ? (string) $row['datetime'] : '';
|
||
$player = isset($row['player']) ? (string) $row['player'] : '-';
|
||
$ip = isset($row['ip']) ? (string) $row['ip'] : '-';
|
||
$uuid = isset($row['uuid']) ? (string) $row['uuid'] : '-';
|
||
|
||
echo '<tr>';
|
||
echo '<td>' . esc_html($date) . '</td>';
|
||
echo '<td><strong>' . esc_html($player) . '</strong></td>';
|
||
echo '<td>' . esc_html($ip) . '</td>';
|
||
echo '<td>' . esc_html($uuid) . '</td>';
|
||
echo '</tr>';
|
||
}
|
||
echo '</tbody></table>';
|
||
}
|
||
|
||
echo '</div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
private static function render_messages_center($messages_markup, $redirect_page) {
|
||
echo '<div class="statusapi-card statusapi-card-messages">';
|
||
echo '<div class="statusapi-card-head"><h2>Meldungs-Center</h2><p>Hier siehst du laufend erkannte Status-, Attack- und Warnmeldungen aus deinem Proxy.</p></div>';
|
||
echo '<div class="statusapi-message-filters">';
|
||
echo '<button type="button" class="button statusapi-filter-btn is-active" data-filter="all">Alle</button>';
|
||
echo '<button type="button" class="button statusapi-filter-btn" data-filter="error">Fehler</button>';
|
||
echo '<button type="button" class="button statusapi-filter-btn" data-filter="warning">Warnung</button>';
|
||
echo '<button type="button" class="button statusapi-filter-btn" data-filter="success">Erfolg</button>';
|
||
echo '<button type="button" class="button statusapi-filter-btn" data-filter="info">Info</button>';
|
||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="statusapi-clear-form">';
|
||
wp_nonce_field('statusapi_backend_helper_clear_messages');
|
||
echo '<input type="hidden" name="action" value="statusapi_backend_helper_clear_messages" />';
|
||
echo '<input type="hidden" name="redirect_page" value="' . esc_attr($redirect_page) . '" />';
|
||
echo '<button type="submit" class="button statusapi-clear-btn">Meldungen leeren</button>';
|
||
echo '</form>';
|
||
echo '</div>';
|
||
echo '<ul id="statusapi-message-list" class="statusapi-message-list">' . $messages_markup . '</ul>';
|
||
echo '</div>';
|
||
}
|
||
|
||
private static function render_input_field($label, $name, $value, $description) {
|
||
echo '<label class="statusapi-field" for="' . esc_attr($name) . '">';
|
||
echo '<span class="statusapi-field-label">' . esc_html($label) . '</span>';
|
||
printf(
|
||
'<input class="statusapi-input" type="text" id="%1$s" name="%1$s" value="%2$s" />',
|
||
esc_attr($name),
|
||
esc_attr($value)
|
||
);
|
||
if ($description !== '') {
|
||
echo '<span class="statusapi-field-description">' . esc_html($description) . '</span>';
|
||
}
|
||
echo '</label>';
|
||
}
|
||
|
||
private static function render_snippet_card($title, $content, $rows) {
|
||
echo '<div class="statusapi-snippet-card">';
|
||
echo '<div class="statusapi-snippet-head">';
|
||
echo '<h3>' . esc_html($title) . '</h3>';
|
||
echo '<button type="button" class="button button-secondary statusapi-copy" data-copy-target="statusapi-copy-' . esc_attr(md5($title . $content)) . '">Kopieren</button>';
|
||
echo '</div>';
|
||
printf(
|
||
'<textarea readonly rows="%1$d" id="%2$s" class="statusapi-snippet">%3$s</textarea>',
|
||
(int) $rows,
|
||
esc_attr('statusapi-copy-' . md5($title . $content)),
|
||
esc_textarea($content)
|
||
);
|
||
echo '</div>';
|
||
}
|
||
|
||
private static function render_status_card($title, $value, $meta, $status) {
|
||
self::render_status_card_named('', $title, $value, $meta, $status);
|
||
}
|
||
|
||
private static function render_status_card_named($id, $title, $value, $meta, $status) {
|
||
$status_class = 'statusapi-state-unknown';
|
||
if ($status === 'ok') {
|
||
$status_class = 'statusapi-state-ok';
|
||
} elseif ($status === 'error') {
|
||
$status_class = 'statusapi-state-error';
|
||
}
|
||
|
||
$id_attr = $id !== '' ? ' id="' . esc_attr($id) . '"' : '';
|
||
echo '<div' . $id_attr . ' class="statusapi-status-card ' . esc_attr($status_class) . '">';
|
||
echo '<span class="statusapi-status-title">' . esc_html($title) . '</span>';
|
||
echo '<strong class="statusapi-status-value" data-role="value">' . esc_html($value) . '</strong>';
|
||
echo '<span class="statusapi-status-meta" data-role="meta">' . esc_html($meta) . '</span>';
|
||
echo '</div>';
|
||
}
|
||
|
||
private static function render_metric_card($id, $title, $value, $meta) {
|
||
echo '<div id="' . esc_attr($id) . '" class="statusapi-metric-card">';
|
||
echo '<span class="statusapi-metric-title">' . esc_html($title) . '</span>';
|
||
echo '<strong class="statusapi-metric-value" data-role="value">' . esc_html($value) . '</strong>';
|
||
echo '<span class="statusapi-metric-meta" data-role="meta">' . esc_html($meta) . '</span>';
|
||
echo '</div>';
|
||
}
|
||
|
||
private static function build_curl_examples($status_endpoint, $attack_endpoint, $attack_api_key, $attack_source) {
|
||
$payload = wp_json_encode(array(
|
||
'event' => 'detected',
|
||
'source' => $attack_source,
|
||
'connectionsPerSecond' => 250,
|
||
'ipAddressesBlocked' => 12,
|
||
'connectionsBlocked' => 1800,
|
||
));
|
||
|
||
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';
|
||
$previous_attack_mode = array_key_exists('last_attack_mode', $state) ? $state['last_attack_mode'] : null;
|
||
$previous_memory_high = !empty($state['last_memory_high']);
|
||
$previous_player_high = !empty($state['last_player_high']);
|
||
|
||
$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 (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_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_valid_response) {
|
||
$state['last_check_at'] = $now;
|
||
$state['last_http_code'] = $is_reachable ? $reachable_code : 0;
|
||
$state['last_status'] = 'error';
|
||
$state['last_error'] = $last_error !== '' ? $last_error : 'Ungueltige Antwort vom Proxy';
|
||
if ($previous_status !== 'error') {
|
||
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' => '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) $response['code'];
|
||
$ok = $code >= 200 && $code < 300;
|
||
$body = (string) $response['body'];
|
||
$decoded = json_decode($body, true);
|
||
$state['last_check_at'] = $now;
|
||
$state['last_http_code'] = $code;
|
||
$state['last_status'] = $ok ? 'ok' : 'error';
|
||
$state['last_error'] = $ok ? '' : 'HTTP ' . $code;
|
||
|
||
if ($ok && $previous_status === 'error') {
|
||
self::push_message($state, 'success', 'Proxy wieder online', 'StatusAPI antwortet wieder erfolgreich.');
|
||
}
|
||
if (!$ok && $previous_status !== 'error') {
|
||
self::push_message($state, 'error', 'Proxy-Fehler', 'StatusAPI antwortet mit HTTP ' . $code . '.');
|
||
}
|
||
|
||
if ($ok && is_array($decoded)) {
|
||
$network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array();
|
||
$antibot = isset($decoded['antibot']) && is_array($decoded['antibot']) ? $decoded['antibot'] : array();
|
||
|
||
if (array_key_exists('attack_mode', $antibot)) {
|
||
$attack_mode = (bool) $antibot['attack_mode'];
|
||
if ($previous_attack_mode !== null && $attack_mode !== (bool) $previous_attack_mode) {
|
||
if ($attack_mode) {
|
||
$cps = isset($antibot['last_cps']) ? $antibot['last_cps'] : 'n/a';
|
||
self::push_message($state, 'warning', 'Attack erkannt', 'AntiBot hat den Attack-Mode aktiviert (CPS: ' . $cps . ').');
|
||
} else {
|
||
self::push_message($state, 'success', 'Attack beendet', 'AntiBot hat den Attack-Mode beendet.');
|
||
}
|
||
}
|
||
$state['last_attack_mode'] = $attack_mode;
|
||
}
|
||
|
||
$memory_percent = null;
|
||
if (isset($network['memory']) && is_array($network['memory']) && isset($network['memory']['usage_percent'])) {
|
||
$memory_percent = (int) $network['memory']['usage_percent'];
|
||
}
|
||
$is_memory_high = $memory_percent !== null && $memory_percent >= 90;
|
||
if ($is_memory_high && !$previous_memory_high) {
|
||
self::push_message($state, 'warning', 'RAM-Warnung', 'RAM-Auslastung liegt bei ' . $memory_percent . '%.');
|
||
}
|
||
if (!$is_memory_high && $previous_memory_high) {
|
||
self::push_message($state, 'success', 'RAM normalisiert', 'RAM-Auslastung ist wieder unter 90%.');
|
||
}
|
||
$state['last_memory_high'] = $is_memory_high;
|
||
|
||
$occupancy = null;
|
||
if (isset($network['players']) && is_array($network['players']) && isset($network['players']['occupancy_percent'])) {
|
||
$occupancy = (int) $network['players']['occupancy_percent'];
|
||
}
|
||
$is_player_high = $occupancy !== null && $occupancy >= 95;
|
||
if ($is_player_high && !$previous_player_high) {
|
||
self::push_message($state, 'warning', 'Spieler-Auslastung hoch', 'Spieler-Auslastung liegt bei ' . $occupancy . '%.');
|
||
}
|
||
if (!$is_player_high && $previous_player_high) {
|
||
self::push_message($state, 'success', 'Spieler-Auslastung normalisiert', 'Spieler-Auslastung ist wieder unter 95%.');
|
||
}
|
||
$state['last_player_high'] = $is_player_high;
|
||
}
|
||
|
||
if ($ok) {
|
||
$state['last_success_at'] = $now;
|
||
$state['last_success_code'] = $code;
|
||
}
|
||
self::save_state($state);
|
||
|
||
return array(
|
||
'status' => $ok ? 'ok' : 'error',
|
||
'label' => $ok ? 'Online' : 'Fehler',
|
||
'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,
|
||
),
|
||
);
|
||
}
|
||
|
||
private static function extract_metrics($decoded) {
|
||
if (!is_array($decoded)) {
|
||
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();
|
||
|
||
// 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_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',
|
||
'online_meta' => 'Keine Live-Daten',
|
||
'max_players' => 'n/a',
|
||
'max_meta' => 'Keine Live-Daten',
|
||
'uptime' => 'n/a',
|
||
'uptime_meta' => 'Keine Live-Daten',
|
||
'memory' => 'n/a',
|
||
'memory_meta' => 'Keine Live-Daten',
|
||
);
|
||
}
|
||
|
||
private static function array_value($array, $key, $fallback) {
|
||
if (!is_array($array) || !array_key_exists($key, $array)) {
|
||
return $fallback;
|
||
}
|
||
return $array[$key];
|
||
}
|
||
|
||
private static function count_players_fallback($decoded) {
|
||
if (isset($decoded['players']) && is_array($decoded['players'])) {
|
||
return count($decoded['players']);
|
||
}
|
||
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) {
|
||
return 'Noch kein Wert';
|
||
}
|
||
|
||
return wp_date('d.m.Y H:i:s', $timestamp);
|
||
}
|
||
|
||
private static function build_notice() {
|
||
if (!isset($_GET['statusapi_notice'])) {
|
||
return null;
|
||
}
|
||
|
||
$code = sanitize_text_field(wp_unslash($_GET['statusapi_notice']));
|
||
if ($code === 'connection_ok') {
|
||
return array('type' => 'success', 'message' => 'StatusAPI ist erreichbar.');
|
||
}
|
||
if ($code === 'attack_ok') {
|
||
return array('type' => 'success', 'message' => 'Test Attack-Meldung wurde erfolgreich an den Proxy gesendet.');
|
||
}
|
||
if ($code === 'missing_url') {
|
||
return array('type' => 'error', 'message' => 'Bitte zuerst die StatusAPI Basis-URL eintragen.');
|
||
}
|
||
if ($code === 'missing_attack_key') {
|
||
return array('type' => 'error', 'message' => 'Bitte zuerst einen Attack API-Key speichern.');
|
||
}
|
||
if ($code === 'connection_failed') {
|
||
$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.');
|
||
}
|
||
if ($code === 'messages_cleared') {
|
||
return array('type' => 'success', 'message' => 'Meldungen wurden erfolgreich geleert.');
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public static function handle_test_action() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('Nicht erlaubt.');
|
||
}
|
||
|
||
check_admin_referer('statusapi_backend_helper_test');
|
||
|
||
$options = self::get_options();
|
||
$base_url = $options['statusapi_base_url'];
|
||
if ($base_url === '') {
|
||
self::redirect_with_notice('missing_url');
|
||
}
|
||
|
||
if (isset($_POST['test_connection'])) {
|
||
$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. ' . (isset($probe['meta']) ? (string) $probe['meta'] : '')
|
||
);
|
||
self::save_state($state);
|
||
self::redirect_with_notice($ok ? 'connection_ok' : 'connection_failed');
|
||
}
|
||
|
||
if (isset($_POST['test_attack'])) {
|
||
if ($options['attack_api_key'] === '') {
|
||
self::redirect_with_notice('missing_attack_key');
|
||
}
|
||
|
||
$payload = array(
|
||
'event' => 'detected',
|
||
'source' => $options['attack_source'] !== '' ? $options['attack_source'] : 'WordPress',
|
||
'connectionsPerSecond' => 250,
|
||
'ipAddressesBlocked' => 12,
|
||
'connectionsBlocked' => 1800,
|
||
);
|
||
|
||
$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'],
|
||
),
|
||
wp_json_encode($payload),
|
||
8
|
||
);
|
||
|
||
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['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) $response['code'];
|
||
$state = self::get_state();
|
||
$state['last_check_at'] = time();
|
||
$state['last_http_code'] = $code;
|
||
$state['last_status'] = ($code >= 200 && $code < 300) ? 'ok' : 'error';
|
||
$state['last_error'] = ($code >= 200 && $code < 300) ? '' : 'HTTP ' . $code;
|
||
if ($code >= 200 && $code < 300) {
|
||
$state['last_success_at'] = $state['last_check_at'];
|
||
$state['last_success_code'] = $code;
|
||
self::push_message($state, 'success', 'Test-Attack gesendet', 'Die Test-Attack-Meldung wurde erfolgreich an den Proxy gesendet.');
|
||
} else {
|
||
self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Der Proxy antwortete mit HTTP ' . $code . '.');
|
||
}
|
||
self::save_state($state);
|
||
self::redirect_with_notice($code >= 200 && $code < 300 ? 'attack_ok' : 'attack_failed');
|
||
}
|
||
|
||
self::redirect_with_notice('connection_failed');
|
||
}
|
||
|
||
public static function handle_clear_messages() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('Nicht erlaubt.');
|
||
}
|
||
|
||
check_admin_referer('statusapi_backend_helper_clear_messages');
|
||
|
||
$state = self::get_state();
|
||
$redirect_page = isset($_POST['redirect_page']) ? sanitize_text_field(wp_unslash($_POST['redirect_page'])) : self::MENU_SLUG;
|
||
|
||
if ($redirect_page === self::LIST_SLUG) {
|
||
self::save_history(array());
|
||
} else {
|
||
$state['messages'] = array();
|
||
}
|
||
self::save_state($state);
|
||
|
||
if ($redirect_page !== self::MESSAGES_SLUG) {
|
||
if ($redirect_page !== self::LIST_SLUG) {
|
||
$redirect_page = self::MENU_SLUG;
|
||
}
|
||
}
|
||
|
||
self::redirect_with_notice('messages_cleared', $redirect_page);
|
||
}
|
||
|
||
public static function handle_ajax_refresh() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_send_json_error(array('message' => 'forbidden'), 403);
|
||
}
|
||
|
||
check_ajax_referer('statusapi_backend_helper_refresh');
|
||
|
||
$options = self::get_options();
|
||
$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'],
|
||
'meta' => $live_status['meta'],
|
||
'status' => $live_status['status'],
|
||
),
|
||
'http' => array(
|
||
'value' => $live_status['http_code'] > 0 ? (string) $live_status['http_code'] : 'n/a',
|
||
'meta' => $live_status['checked_at_human'],
|
||
'status' => $live_status['status'],
|
||
),
|
||
'success' => array(
|
||
'value' => self::format_timestamp($state['last_success_at']),
|
||
'meta' => $state['last_success_code'] > 0 ? 'HTTP ' . $state['last_success_code'] : 'Noch kein erfolgreicher Check gespeichert',
|
||
'status' => $state['last_success_at'] > 0 ? 'ok' : 'unknown',
|
||
),
|
||
'lastcheck' => array(
|
||
'value' => self::format_timestamp($state['last_check_at']),
|
||
'meta' => $state['last_error'] !== '' ? $state['last_error'] : 'Keine Fehler gespeichert',
|
||
'status' => $state['last_status'],
|
||
),
|
||
),
|
||
'metric_cards' => array(
|
||
'online' => array(
|
||
'value' => $live_status['metrics']['online_players'],
|
||
'meta' => $live_status['metrics']['online_meta'],
|
||
),
|
||
'max' => array(
|
||
'value' => $live_status['metrics']['max_players'],
|
||
'meta' => $live_status['metrics']['max_meta'],
|
||
),
|
||
'uptime' => array(
|
||
'value' => $live_status['metrics']['uptime'],
|
||
'meta' => $live_status['metrics']['uptime_meta'],
|
||
),
|
||
'memory' => array(
|
||
'value' => $live_status['metrics']['memory'],
|
||
'meta' => $live_status['metrics']['memory_meta'],
|
||
),
|
||
),
|
||
));
|
||
}
|
||
|
||
private static function request_ok($url) {
|
||
$candidate_urls = self::get_candidate_base_urls($url);
|
||
if (empty($candidate_urls)) {
|
||
return false;
|
||
}
|
||
|
||
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) {
|
||
// 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.',
|
||
);
|
||
}
|
||
|
||
// 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' => 'StatusAPI konnte nicht erreicht werden. Bitte Basis-URL und Erreichbarkeit prüfen.',
|
||
);
|
||
}
|
||
|
||
// 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 /antibot/security-log lieferte HTTP ' . $code . '.',
|
||
);
|
||
}
|
||
|
||
$body = (string) $response['body'];
|
||
$decoded = json_decode($body, true);
|
||
if (!is_array($decoded)) {
|
||
return array(
|
||
'entries' => array(),
|
||
'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' => $entries,
|
||
'error' => count($entries) === 0 ? 'Keine Angriffsversuche im Log vorhanden.' : '',
|
||
);
|
||
}
|
||
|
||
private static function redirect_with_notice($code, $page = self::MENU_SLUG) {
|
||
$target_page = self::MENU_SLUG;
|
||
if ($page === self::MESSAGES_SLUG) {
|
||
$target_page = self::MESSAGES_SLUG;
|
||
} elseif ($page === self::LIST_SLUG) {
|
||
$target_page = self::LIST_SLUG;
|
||
}
|
||
$url = add_query_arg(
|
||
array(
|
||
'page' => $target_page,
|
||
'statusapi_notice' => $code,
|
||
),
|
||
admin_url('admin.php')
|
||
);
|
||
|
||
wp_safe_redirect($url);
|
||
exit;
|
||
}
|
||
|
||
private static function render_styles() {
|
||
echo '<style>
|
||
.statusapi-admin {
|
||
max-width: 1280px;
|
||
}
|
||
.statusapi-hero {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 24px;
|
||
align-items: flex-end;
|
||
padding: 32px;
|
||
margin: 20px 0 24px;
|
||
border-radius: 24px;
|
||
background: linear-gradient(135deg, #0f172a 0%, #1d4ed8 55%, #38bdf8 100%);
|
||
color: #f8fafc;
|
||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22);
|
||
}
|
||
.statusapi-hero h1 {
|
||
margin: 6px 0 10px;
|
||
color: #ffffff;
|
||
font-size: 34px;
|
||
line-height: 1.1;
|
||
}
|
||
.statusapi-hero p {
|
||
margin: 0;
|
||
max-width: 760px;
|
||
color: rgba(248, 250, 252, 0.96);
|
||
font-size: 15px;
|
||
}
|
||
.statusapi-kicker {
|
||
display: inline-block;
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.14);
|
||
color: #eff6ff;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
.statusapi-hero-badge {
|
||
min-width: 220px;
|
||
padding: 18px 20px;
|
||
border-radius: 18px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.statusapi-hero-badge span {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: #dbeafe;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
.statusapi-hero-badge strong {
|
||
font-size: 22px;
|
||
color: #ffffff;
|
||
}
|
||
.statusapi-hero-badge em {
|
||
display: block;
|
||
margin-top: 6px;
|
||
color: rgba(255, 255, 255, 0.92);
|
||
font-style: normal;
|
||
font-size: 12px;
|
||
}
|
||
.statusapi-grid {
|
||
display: grid;
|
||
grid-template-columns: 1.05fr 0.95fr;
|
||
gap: 24px;
|
||
}
|
||
.statusapi-metric-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 16px;
|
||
margin: 0 0 24px;
|
||
}
|
||
.statusapi-status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 16px;
|
||
margin: 0 0 24px;
|
||
}
|
||
.statusapi-status-card {
|
||
position: relative;
|
||
overflow: hidden;
|
||
padding: 18px 20px;
|
||
border-radius: 20px;
|
||
background: #ffffff;
|
||
border: 1px solid #dbe4f0;
|
||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
|
||
}
|
||
.statusapi-status-card::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0 auto 0 0;
|
||
width: 5px;
|
||
border-radius: 20px 0 0 20px;
|
||
background: #94a3b8;
|
||
}
|
||
.statusapi-state-ok::after {
|
||
background: #16a34a;
|
||
}
|
||
.statusapi-state-error::after {
|
||
background: #dc2626;
|
||
}
|
||
.statusapi-state-unknown::after {
|
||
background: #f59e0b;
|
||
}
|
||
.statusapi-status-title {
|
||
display: block;
|
||
margin-bottom: 10px;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
}
|
||
.statusapi-status-value {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #0f172a;
|
||
font-size: 24px;
|
||
line-height: 1.2;
|
||
}
|
||
.statusapi-status-meta {
|
||
display: block;
|
||
color: #334155;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
.statusapi-metric-card {
|
||
padding: 20px;
|
||
border-radius: 20px;
|
||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||
border: 1px solid #dbe4f0;
|
||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
|
||
}
|
||
.statusapi-metric-title {
|
||
display: block;
|
||
margin-bottom: 10px;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
}
|
||
.statusapi-metric-value {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #0f172a;
|
||
font-size: 24px;
|
||
line-height: 1.2;
|
||
}
|
||
.statusapi-metric-meta {
|
||
display: block;
|
||
color: #334155;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
.statusapi-card {
|
||
background: #ffffff;
|
||
border: 1px solid #dbe4f0;
|
||
border-radius: 22px;
|
||
padding: 24px;
|
||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||
}
|
||
.statusapi-card-form {
|
||
grid-row: span 2;
|
||
}
|
||
.statusapi-card-head h2 {
|
||
margin: 0 0 6px;
|
||
font-size: 22px;
|
||
}
|
||
.statusapi-card-head p {
|
||
margin: 0 0 20px;
|
||
color: #334155;
|
||
}
|
||
.statusapi-fields {
|
||
display: grid;
|
||
gap: 18px;
|
||
}
|
||
.statusapi-field {
|
||
display: block;
|
||
}
|
||
.statusapi-field-label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #0f172a;
|
||
font-weight: 600;
|
||
}
|
||
.statusapi-field-description {
|
||
display: block;
|
||
margin-top: 8px;
|
||
color: #334155;
|
||
font-size: 13px;
|
||
}
|
||
.statusapi-input {
|
||
width: 100%;
|
||
min-height: 48px;
|
||
padding: 12px 14px;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 14px;
|
||
background: #f8fafc;
|
||
font-size: 14px;
|
||
}
|
||
.statusapi-input:focus {
|
||
border-color: #2563eb;
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.16);
|
||
outline: none;
|
||
background: #ffffff;
|
||
}
|
||
.statusapi-actions {
|
||
margin-top: 20px;
|
||
}
|
||
.statusapi-snippet-card + .statusapi-snippet-card {
|
||
margin-top: 18px;
|
||
}
|
||
.statusapi-snippet-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
.statusapi-snippet-head h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
}
|
||
.statusapi-snippet {
|
||
width: 100%;
|
||
padding: 14px;
|
||
border: 1px solid #dbe4f0;
|
||
border-radius: 16px;
|
||
background: #0b1220 !important;
|
||
color: #f8fafc !important;
|
||
font-family: Consolas, Monaco, monospace;
|
||
resize: vertical;
|
||
opacity: 1 !important;
|
||
-webkit-text-fill-color: #f8fafc;
|
||
}
|
||
.statusapi-snippet:focus {
|
||
background: #0b1220 !important;
|
||
color: #f8fafc !important;
|
||
}
|
||
.statusapi-test-form {
|
||
margin-top: 4px;
|
||
}
|
||
.statusapi-test-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.statusapi-meta {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
.statusapi-meta div {
|
||
padding: 14px 16px;
|
||
border-radius: 16px;
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
.statusapi-meta span {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: #334155;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
.statusapi-meta strong {
|
||
color: #0f172a;
|
||
font-size: 15px;
|
||
}
|
||
.statusapi-notice {
|
||
margin: 0 0 16px;
|
||
border-radius: 14px;
|
||
overflow: hidden;
|
||
}
|
||
.statusapi-notice p {
|
||
color: #0f172a !important;
|
||
font-weight: 600;
|
||
}
|
||
.statusapi-card-messages {
|
||
margin-bottom: 24px;
|
||
}
|
||
.statusapi-message-list {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
display: grid;
|
||
gap: 10px;
|
||
max-height: 300px;
|
||
overflow: auto;
|
||
}
|
||
.statusapi-message-filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.statusapi-filter-btn {
|
||
border-radius: 999px !important;
|
||
}
|
||
.statusapi-filter-btn.is-active {
|
||
background: #0f172a !important;
|
||
border-color: #0f172a !important;
|
||
color: #ffffff !important;
|
||
}
|
||
.statusapi-clear-form {
|
||
margin-left: auto;
|
||
}
|
||
.statusapi-clear-btn {
|
||
border-color: #dc2626 !important;
|
||
color: #dc2626 !important;
|
||
}
|
||
.statusapi-clear-btn:hover {
|
||
border-color: #b91c1c !important;
|
||
color: #b91c1c !important;
|
||
}
|
||
.statusapi-message-item {
|
||
border: 1px solid #dbe4f0;
|
||
border-left-width: 5px;
|
||
border-radius: 12px;
|
||
background: #f8fafc;
|
||
padding: 10px 12px;
|
||
}
|
||
.statusapi-message-item.is-hidden {
|
||
display: none;
|
||
}
|
||
.statusapi-message-item p {
|
||
margin: 6px 0 0;
|
||
color: #334155;
|
||
}
|
||
.statusapi-message-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
align-items: baseline;
|
||
}
|
||
.statusapi-message-head strong {
|
||
color: #0f172a;
|
||
}
|
||
.statusapi-message-head span {
|
||
color: #475569;
|
||
font-size: 12px;
|
||
}
|
||
.statusapi-level-success {
|
||
border-left-color: #16a34a;
|
||
}
|
||
.statusapi-level-warning {
|
||
border-left-color: #d97706;
|
||
}
|
||
.statusapi-level-error {
|
||
border-left-color: #dc2626;
|
||
}
|
||
.statusapi-level-info,
|
||
.statusapi-message-empty {
|
||
border-left-color: #2563eb;
|
||
}
|
||
.statusapi-level-pill {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: .04em;
|
||
}
|
||
.statusapi-level-pill.statusapi-level-success {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
.statusapi-level-pill.statusapi-level-warning {
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
}
|
||
.statusapi-level-pill.statusapi-level-error {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
.statusapi-level-pill.statusapi-level-info {
|
||
background: #dbeafe;
|
||
color: #1e40af;
|
||
}
|
||
@media (max-width: 1100px) {
|
||
.statusapi-metric-grid,
|
||
.statusapi-status-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
.statusapi-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.statusapi-card-form {
|
||
grid-row: auto;
|
||
}
|
||
}
|
||
@media (max-width: 782px) {
|
||
.statusapi-metric-grid,
|
||
.statusapi-status-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.statusapi-hero {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.statusapi-snippet-head,
|
||
.statusapi-test-buttons {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.statusapi-message-filters {
|
||
gap: 6px;
|
||
}
|
||
.statusapi-clear-form {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
</style>';
|
||
}
|
||
|
||
private static function render_scripts($refresh_nonce) {
|
||
echo '<script>
|
||
window.StatusApiBridgeConfig = {
|
||
ajaxUrl: ' . wp_json_encode(admin_url('admin-ajax.php')) . ',
|
||
nonce: ' . wp_json_encode($refresh_nonce) . ',
|
||
intervalMs: 15000
|
||
};
|
||
|
||
document.addEventListener("click", function(event) {
|
||
var button = event.target.closest(".statusapi-copy");
|
||
if (!button) {
|
||
return;
|
||
}
|
||
var targetId = button.getAttribute("data-copy-target");
|
||
var target = document.getElementById(targetId);
|
||
if (!target) {
|
||
return;
|
||
}
|
||
target.select();
|
||
target.setSelectionRange(0, 99999);
|
||
try {
|
||
document.execCommand("copy");
|
||
button.textContent = "Kopiert";
|
||
window.setTimeout(function() {
|
||
button.textContent = "Kopieren";
|
||
}, 1400);
|
||
} catch (error) {
|
||
button.textContent = "Fehlgeschlagen";
|
||
}
|
||
});
|
||
|
||
document.addEventListener("DOMContentLoaded", function() {
|
||
var config = window.StatusApiBridgeConfig || null;
|
||
if (!config) {
|
||
return;
|
||
}
|
||
|
||
var refreshState = document.getElementById("statusapi-refresh-state");
|
||
var currentFilter = "all";
|
||
try {
|
||
var remembered = window.localStorage.getItem("statuspulse_message_filter");
|
||
if (remembered) {
|
||
currentFilter = remembered;
|
||
}
|
||
} catch (error) {
|
||
}
|
||
|
||
function applyStatusCard(id, payload) {
|
||
var card = document.getElementById(id);
|
||
if (!card || !payload) {
|
||
return;
|
||
}
|
||
var valueNode = card.querySelector("[data-role=value]");
|
||
var metaNode = card.querySelector("[data-role=meta]");
|
||
if (valueNode && typeof payload.value !== "undefined") {
|
||
valueNode.textContent = payload.value;
|
||
}
|
||
if (metaNode && typeof payload.meta !== "undefined") {
|
||
metaNode.textContent = payload.meta;
|
||
}
|
||
card.classList.remove("statusapi-state-ok", "statusapi-state-error", "statusapi-state-unknown");
|
||
if (payload.status === "ok") {
|
||
card.classList.add("statusapi-state-ok");
|
||
} else if (payload.status === "error") {
|
||
card.classList.add("statusapi-state-error");
|
||
} else {
|
||
card.classList.add("statusapi-state-unknown");
|
||
}
|
||
}
|
||
|
||
function applyMetricCard(id, payload) {
|
||
var card = document.getElementById(id);
|
||
if (!card || !payload) {
|
||
return;
|
||
}
|
||
var valueNode = card.querySelector("[data-role=value]");
|
||
var metaNode = card.querySelector("[data-role=meta]");
|
||
if (valueNode && typeof payload.value !== "undefined") {
|
||
valueNode.textContent = payload.value;
|
||
}
|
||
if (metaNode && typeof payload.meta !== "undefined") {
|
||
metaNode.textContent = payload.meta;
|
||
}
|
||
}
|
||
|
||
function applyMessageFilter(filter) {
|
||
currentFilter = filter || "all";
|
||
try {
|
||
window.localStorage.setItem("statuspulse_message_filter", currentFilter);
|
||
} catch (error) {
|
||
}
|
||
|
||
var buttons = document.querySelectorAll(".statusapi-filter-btn");
|
||
buttons.forEach(function(btn) {
|
||
var isActive = btn.getAttribute("data-filter") === currentFilter;
|
||
btn.classList.toggle("is-active", isActive);
|
||
});
|
||
|
||
var items = document.querySelectorAll("#statusapi-message-list .statusapi-message-item");
|
||
items.forEach(function(item) {
|
||
var level = item.getAttribute("data-level") || "info";
|
||
var show = currentFilter === "all" || level === "empty" || level === currentFilter;
|
||
item.classList.toggle("is-hidden", !show);
|
||
});
|
||
}
|
||
|
||
document.addEventListener("click", function(event) {
|
||
var filterBtn = event.target.closest(".statusapi-filter-btn");
|
||
if (!filterBtn) {
|
||
return;
|
||
}
|
||
applyMessageFilter(filterBtn.getAttribute("data-filter") || "all");
|
||
});
|
||
|
||
function refreshDashboard() {
|
||
var formData = new window.FormData();
|
||
formData.append("action", "statusapi_backend_helper_refresh");
|
||
formData.append("_ajax_nonce", config.nonce);
|
||
|
||
if (refreshState) {
|
||
refreshState.textContent = "aktualisiert...";
|
||
}
|
||
|
||
window.fetch(config.ajaxUrl, {
|
||
method: "POST",
|
||
credentials: "same-origin",
|
||
body: formData
|
||
}).then(function(response) {
|
||
return response.json();
|
||
}).then(function(result) {
|
||
if (!result || !result.success || !result.data) {
|
||
throw new Error("invalid_response");
|
||
}
|
||
|
||
applyStatusCard("statusapi-card-proxy", result.data.status_cards.proxy);
|
||
applyStatusCard("statusapi-card-http", result.data.status_cards.http);
|
||
applyStatusCard("statusapi-card-success", result.data.status_cards.success);
|
||
applyStatusCard("statusapi-card-lastcheck", result.data.status_cards.lastcheck);
|
||
|
||
applyMetricCard("statusapi-metric-online", result.data.metric_cards.online);
|
||
applyMetricCard("statusapi-metric-max", result.data.metric_cards.max);
|
||
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;
|
||
applyMessageFilter(currentFilter);
|
||
}
|
||
|
||
if (refreshState && result.data.refresh_state) {
|
||
refreshState.textContent = result.data.refresh_state;
|
||
}
|
||
}).catch(function() {
|
||
if (refreshState) {
|
||
refreshState.textContent = "Auto-Refresh fehlgeschlagen";
|
||
}
|
||
});
|
||
}
|
||
|
||
applyMessageFilter(currentFilter);
|
||
window.setTimeout(refreshDashboard, 1200);
|
||
window.setInterval(refreshDashboard, config.intervalMs);
|
||
});
|
||
</script>';
|
||
}
|
||
}
|
||
|
||
StatusAPI_Backend_Helper::boot();
|
||
} |