.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); } } '; echo '
'; echo '
'; echo '

StatusPulse Monitor

'; echo '' . esc_html($status_text) . ''; echo '
'; echo '
'; echo '
'; echo '
'; echo 'Status'; echo '
' . esc_html($live_status['label']) . '
'; echo '
' . esc_html($live_status['meta']) . '
'; echo '
'; echo '
'; echo 'Letzte Meldung'; 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 '
' . esc_html($msg_title) . '
'; echo '
' . esc_html(substr($msg_text, 0, 50)) . '
'; } else { echo '
Keine Meldungen
'; } echo '
'; echo '
'; echo '
'; echo '
HTTP
' . esc_html($live_status['http_code'] > 0 ? (string) $live_status['http_code'] : '—') . '
'; echo '
Spieler
' . esc_html($live_status['metrics']['online_players']) . '
'; echo '
RAM
' . esc_html($live_status['metrics']['memory']) . '
'; echo '
Check
' . esc_html(self::format_timestamp($state['last_check_at'])) . '
'; echo '
'; echo ''; echo '
'; echo '
'; } 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 '
  • Noch keine Meldungen

    Sobald Statuswechsel oder Attack-Events erkannt werden, erscheinen sie hier.

  • '; } $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 .= '
  • '; $html .= '
    ' . esc_html($title) . '' . esc_html(self::format_timestamp($ts)) . '
    '; $html .= '

    ' . esc_html($text) . '

    '; $html .= '
  • '; } 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 '
    '; self::render_styles(); self::render_scripts($refresh_nonce); echo '
    '; echo '
    '; echo 'Proxy Control Surface'; echo '

    StatusPulse

    '; echo '

    Reduzierte Admin-Seite für Proxy-URL, Attack-Key und direkte Tests gegen dein StatusAPI-Plugin.

    '; echo '
    '; echo '
    '; echo 'Live Refresh'; echo 'alle 15s'; echo 'wartet auf erstes Update'; echo '
    '; echo '
    '; if ($notice !== null) { printf( '

    %2$s

    ', esc_attr($notice['type']), esc_html($notice['message']) ); } echo '
    '; 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 '
    '; echo '
    '; 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 '
    '; echo '
    '; echo '
    '; echo '

    🔍 DEBUG: API Response (klicken zum Ein-/Ausblenden)

    '; echo '
    '; echo ''; echo '
    '; echo '
    '; echo '
    '; echo '

    Konfiguration

    Die Werte werden lokal in WordPress gespeichert und als Snippets für deine Proxy-Dateien angezeigt.

    '; echo '
    '; settings_fields('statusapi_backend_helper_group'); echo '
    '; 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 '
    '; echo '
    '; submit_button('Einstellungen speichern', 'primary', 'submit', false); echo '
    '; echo '
    '; echo '
    '; echo '
    '; echo '

    Snippets

    Direkt für deine Proxy-Konfiguration und externe Tests vorbereitet.

    '; 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 '
    '; echo '
    '; echo '

    Aktionen

    Teste direkt aus dem Backend, ob dein Proxy erreichbar ist und Attack-Meldungen akzeptiert.

    '; echo '
    '; wp_nonce_field('statusapi_backend_helper_test'); echo ''; echo '
    '; submit_button('StatusAPI Verbindung prüfen', 'secondary', 'test_connection', false); submit_button('Test Attack-Meldung senden', 'secondary', 'test_attack', false); echo '
    '; echo '
    '; echo '
    '; echo '
    Gespeicherte Basis-URL' . esc_html($statusapi_base_url !== '' ? $statusapi_base_url : 'nicht gesetzt') . '
    '; echo '
    Attack-Key' . esc_html($attack_api_key !== '' ? 'gesetzt' : 'leer') . '
    '; echo '
    Quelle' . esc_html($attack_source !== '' ? $attack_source : 'WordPress') . '
    '; echo '
    '; echo '
    '; echo '
    '; echo '
    '; } 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 '
    '; self::render_styles(); self::render_scripts($refresh_nonce); echo '

    StatusPulse Meldungen

    '; echo '

    Dedizierte Seite für alle laufenden Status-, Attack- und Warnmeldungen.

    '; if ($notice !== null) { printf( '

    %2$s

    ', esc_attr($notice['type']), esc_html($notice['message']) ); } self::render_messages_center($messages_markup, self::MESSAGES_SLUG); echo '
    '; } 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 '
    '; self::render_styles(); echo '

    StatusPulse Angriffsversuche

    '; echo '

    Zeigt nur Spielername und UUID von erkannten Angriffsversuchen (inkl. Datum/Uhrzeit).

    '; if ($notice !== null) { printf( '

    %2$s

    ', esc_attr($notice['type']), esc_html($notice['message']) ); } echo '
    '; echo '

    Angriffs-Ereignisse

    Neueste Einträge oben.

    '; if ($list_error !== '') { echo '

    ' . esc_html($list_error) . '

    '; } if (empty($rows)) { echo '

    Keine Angriffs-Einträge mit Spielername/UUID vorhanden.

    '; } else { echo ''; echo ''; echo ''; 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 ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
    Datum / UhrzeitSpielernameIPUUID
    ' . esc_html($date) . '' . esc_html($player) . '' . esc_html($ip) . '' . esc_html($uuid) . '
    '; } echo '
    '; echo '
    '; } private static function render_messages_center($messages_markup, $redirect_page) { echo '
    '; echo '

    Meldungs-Center

    Hier siehst du laufend erkannte Status-, Attack- und Warnmeldungen aus deinem Proxy.

    '; echo '
    '; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
    '; wp_nonce_field('statusapi_backend_helper_clear_messages'); echo ''; echo ''; echo ''; echo '
    '; echo '
    '; echo ''; echo '
    '; } private static function render_input_field($label, $name, $value, $description) { echo ''; } private static function render_snippet_card($title, $content, $rows) { echo '
    '; echo '
    '; echo '

    ' . esc_html($title) . '

    '; echo ''; echo '
    '; printf( '', (int) $rows, esc_attr('statusapi-copy-' . md5($title . $content)), esc_textarea($content) ); echo '
    '; } 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 ''; echo '' . esc_html($title) . ''; echo '' . esc_html($value) . ''; echo '' . esc_html($meta) . ''; echo ''; } private static function render_metric_card($id, $title, $value, $meta) { echo '
    '; echo '' . esc_html($title) . ''; echo '' . esc_html($value) . ''; echo '' . esc_html($meta) . ''; echo '
    '; } 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 ''; } private static function render_scripts($refresh_nonce) { echo ''; } } StatusAPI_Backend_Helper::boot(); }