Files
Minecraft-Server-Status/minecraft-server-status.php

694 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Plugin Name: Minecraft Server Status
* Description: Minecraft Serverstatus Live-Update, PREFIX wird als Rang angezeigt ([Owner], [VIP], etc.), sonst "Spieler"
* Version: 1.0.0
* Author: M_Viper
* Requires at least: 6.7.2
* Tested up to: 6.7.2
* Requires PHP: 7.4
* License: GPL2
* Text Domain: wp-multi-mc-server
*/
if (!defined('ABSPATH')) exit;
define('MCSS_DIR', plugin_dir_path(__FILE__));
define('MCSS_URL', plugin_dir_url(__FILE__));
require_once MCSS_DIR . 'rcon/Rcon.php';
/* ---------------- Enqueue assets ---------------- */
add_action('admin_enqueue_scripts', function($hook){
global $pagenow;
if ($pagenow === 'admin.php' && isset($_GET['page']) && $_GET['page'] === 'mcss-settings') {
wp_enqueue_media();
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('wp-color-picker');
wp_enqueue_script('mcss-admin-js', MCSS_URL . 'js/admin.js', ['jquery', 'wp-color-picker'], '1.9.23', true);
}
});
add_action('wp_enqueue_scripts', function(){
wp_enqueue_style('mcss-style', MCSS_URL . 'css/style.css', [], '1.9.23');
wp_enqueue_script('mcss-frontend-js', MCSS_URL . 'js/mcss-frontend.js', ['jquery'], '1.9.23', true);
wp_localize_script('mcss-frontend-js', 'mcss_ajax_object', [
'ajax_url' => admin_url('admin-ajax.php'),
'refresh_interval' => max(10, intval(get_option('mcss_cache_ttl', 15))) * 1000
]);
});
/* ---------------- Settings registrieren ---------------- */
add_action('admin_menu', function(){
add_options_page('Minecraft Server', 'Minecraft Server', 'manage_options', 'mcss-settings', 'mcss_settings_page');
});
add_action('admin_init', function(){
register_setting('mcss_settings_group', 'mcss_host', ['sanitize_callback'=>'sanitize_text_field']);
register_setting('mcss_settings_group', 'mcss_rcon_port', ['sanitize_callback'=>'intval']);
register_setting('mcss_settings_group', 'mcss_rcon_pass', ['sanitize_callback'=>'sanitize_text_field']);
register_setting('mcss_settings_group', 'mcss_cache_ttl', ['sanitize_callback'=>'intval']);
register_setting('mcss_settings_group', 'mcss_show_motd', ['sanitize_callback'=>'boolval']);
register_setting('mcss_settings_group', 'mcss_ranks_json', ['sanitize_callback'=>'mcss_sanitize_ranks']);
register_setting('mcss_settings_group', 'mcss_server_logo_id', ['sanitize_callback'=>'absint']);
register_setting('mcss_settings_group', 'mcss_server_logo_url', ['sanitize_callback'=>'esc_url_raw']);
register_setting('mcss_settings_group', 'mcss_custom_text', ['sanitize_callback'=>'wp_kses_post']);
register_setting('mcss_settings_group', 'mcss_ip_color', ['default' => '#1f2937', 'sanitize_callback' => 'sanitize_hex_color']);
register_setting('mcss_settings_group', 'mcss_ct_color', ['default' => '#1e293b', 'sanitize_callback' => 'sanitize_hex_color']);
register_setting('mcss_settings_group', 'mcss_ip_size', ['default' => '1.5em', 'sanitize_callback' => 'sanitize_text_field']);
register_setting('mcss_settings_group', 'mcss_ct_size', ['default' => '1.05em', 'sanitize_callback' => 'sanitize_text_field']);
register_setting('mcss_settings_group', 'mcss_hide_port', ['default' => true, 'sanitize_callback' => 'rest_sanitize_boolean']);
register_setting('mcss_settings_group', 'mcss_player_port', ['default' => '', 'sanitize_callback' => 'sanitize_text_field']);
});
/* Sanitize Ranks */
function mcss_sanitize_ranks($input) {
$decoded = json_decode($input, true);
if (!is_array($decoded)) return '[]';
$out = [];
foreach ($decoded as $r) {
if (!is_array($r) || empty($r['name'])) continue;
$out[] = [
'name' => sanitize_text_field($r['name']),
'groups' => sanitize_text_field($r['groups'] ?? ''),
'color' => sanitize_text_field($r['color'] ?? '#6c5ce7')
];
}
return wp_json_encode($out);
}
/* Settings Page */
function mcss_settings_page() {
$logo_id = get_option('mcss_server_logo_id', 0);
$logo_url = get_option('mcss_server_logo_url', '');
$current_logo = $logo_id ? wp_get_attachment_image_url($logo_id, 'medium') : $logo_url;
if (!$current_logo) $current_logo = MCSS_URL . 'img/default-server-logo.png';
$custom_text = get_option('mcss_custom_text', '');
$ip_color = get_option('mcss_ip_color', '#1f2937');
$ct_color = get_option('mcss_ct_color', '#1e293b');
$ip_size = get_option('mcss_ip_size', '1.5em');
$ct_size = get_option('mcss_ct_size', '1.05em');
$hide_port = get_option('mcss_hide_port', true);
$player_port = get_option('mcss_player_port', '');
$font_sizes = [
'0.7em' => 'Sehr klein','0.85em' => 'Klein','1em' => 'Normal','1.2em' => 'Etwas größer',
'1.4em' => 'Groß','1.5em' => 'Sehr groß (Standard)','1.7em' => 'Extra groß','2em' => 'Riesig',
'2.5em' => 'Enorm','3em' => 'Gigantisch',
];
?>
<div class="wrap">
<h1>Minecraft Server Einstellungen</h1>
<form method="post" action="options.php">
<?php settings_fields('mcss_settings_group'); ?>
<table class="form-table">
<tr><th>Server Host</th><td><input name="mcss_host" value="<?php echo esc_attr(get_option('mcss_host','')); ?>" class="regular-text" /></td></tr>
<tr><th>RCON Port</th><td><input name="mcss_rcon_port" type="number" value="<?php echo esc_attr(get_option('mcss_rcon_port',25575)); ?>" class="small-text" /></td></tr>
<tr><th>RCON Passwort</th><td><input name="mcss_rcon_pass" type="password" value="<?php echo esc_attr(get_option('mcss_rcon_pass','')); ?>" class="regular-text" /></td></tr>
<tr><th>Cache TTL (Sekunden)</th><td><input name="mcss_cache_ttl" type="number" value="<?php echo esc_attr(get_option('mcss_cache_ttl',15)); ?>" class="small-text" /></td></tr>
<tr><th>MOTD anzeigen?</th><td><input name="mcss_show_motd" type="checkbox" value="1" <?php checked(get_option('mcss_show_motd',true),true); ?> /></td></tr>
<tr>
<th><label for="mcss_player_port"><strong>Spieler-Port (für Kopieren)</strong></label></th>
<td>
<input name="mcss_player_port" type="text" id="mcss_player_port" value="<?php echo esc_attr($player_port); ?>" class="regular-text" style="width:180px;" placeholder="z. B. 25565" />
<p class="description" style="margin-top:8px;">Wird <strong>nicht angezeigt</strong>, aber beim Klick mitkopiert.<br>Leer = kein Port wird kopiert.</p>
</td>
</tr>
<tr>
<th>Port in Adresse ausblenden?</th>
<td><input name="mcss_hide_port" type="checkbox" value="1" <?php checked($hide_port, true); ?> /> <label>Ja zeige nur den Host an</label></td>
</tr>
<tr>
<th><label>Server Logo</label></th>
<td>
<div id="mcss-logo-preview" style="margin-bottom:12px;">
<img src="<?php echo esc_url($current_logo); ?>" style="max-width:150px;max-height:150px;border-radius:8px;" />
</div>
<input type="hidden" name="mcss_server_logo_id" id="mcss_server_logo_id" value="<?php echo esc_attr($logo_id); ?>">
<input type="text" name="mcss_server_logo_url" id="mcss_server_logo_url" value="<?php echo esc_attr($logo_url); ?>" class="regular-text" placeholder="Oder direkte URL...">
<button type="button" class="button" id="mcss_upload_logo_button">Hochladen / auswählen</button>
<button type="button" class="button" id="mcss_remove_logo_button" <?php echo empty($logo_id) && empty($logo_url) ? 'style="display:none;"' : ''; ?>>Entfernen</button>
</td>
</tr>
<tr>
<th><label for="mcss_custom_text">Zusatztext unter der Adresse</label></th>
<td>
<input type="text" name="mcss_custom_text" id="mcss_custom_text" value="<?php echo esc_attr($custom_text); ?>" class="large-text" placeholder="1.21 Survival • Whitelist aktiv • Discord" />
<p class="description">HTML + Emojis erlaubt.</p>
</td>
</tr>
<tr>
<th>IP-Adresse Schrift</th>
<td>
<label>Farbe:</label>
<input type="text" name="mcss_ip_color" value="<?php echo esc_attr($ip_color); ?>" class="mcss-color-picker" data-default-color="#1f2937" style="width:100px;margin-left:8px;" />
<label style="margin-left:20px;">Größe:</label>
<select name="mcss_ip_size" style="width:200px;margin-left:8px;">
<?php foreach ($font_sizes as $value => $label): ?>
<option value="<?php echo esc_attr($value); ?>" <?php selected($ip_size, $value); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
<option value="<?php echo esc_attr($ip_size); ?>" <?php echo !array_key_exists($ip_size, $font_sizes) ? 'selected' : ''; ?>>Benutzerdefiniert (<?php echo esc_html($ip_size); ?>)</option>
</select>
</td>
</tr>
<tr>
<th>Zusatztext Schrift</th>
<td>
<label>Farbe:</label>
<input type="text" name="mcss_ct_color" value="<?php echo esc_attr($ct_color); ?>" class="mcss-color-picker" data-default-color="#1e293b" style="width:100px;margin-left:8px;" />
<label style="margin-left:20px;">Größe:</label>
<select name="mcss_ct_size" style="width:200px;margin-left:8px;">
<?php foreach ($font_sizes as $value => $label): ?>
<option value="<?php echo esc_attr($value); ?>" <?php selected($ct_size, $value); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
<option value="<?php echo esc_attr($ct_size); ?>" <?php echo !array_key_exists($ct_size, $font_sizes) ? 'selected' : ''; ?>>Benutzerdefiniert (<?php echo esc_html($ct_size); ?>)</option>
</select>
</td>
</tr>
</table>
<h2>Ränge (LuckPerms Zuordnung)</h2>
<p>Die Ränge werden automatisch über LuckPerms per RCON abgerufen.</p>
<div id="mcss-ranks-editor"></div>
<textarea id="mcss_ranks_json" name="mcss_ranks_json" style="display:none;"><?php echo esc_textarea(get_option('mcss_ranks_json','[]')); ?></textarea>
<?php submit_button(); ?>
</form>
<h2>Tools</h2>
<p><a class="button" href="<?php echo esc_url(add_query_arg('mcss_test_connection','1', admin_url('options-general.php?page=mcss-settings'))); ?>">RCON testen</a></p>
<?php if (isset($_GET['mcss_test_connection'])): ?>
<pre style="background:#fff;padding:12px;border-radius:8px;"><?php echo esc_html(print_r(mcss_test_rcon_now(), true)); ?></pre>
<?php endif; ?>
</div>
<script>
jQuery(function($){
$('.mcss-color-picker').wpColorPicker();
});
</script>
<?php
}
/* ---------------- Hilfsfunktionen ---------------- */
function mcss_test_rcon_now() {
$host = get_option('mcss_host', '');
$port = intval(get_option('mcss_rcon_port', 25575));
$pass = get_option('mcss_rcon_pass', '');
if (empty($host) || empty($pass)) return ['error' => 'Host oder Passwort nicht gesetzt'];
$rcon = new Rcon($host, $port, $pass, 3);
$out = ['connected' => false];
if ($rcon->connect()) {
$out['connected'] = true;
$out['list'] = $rcon->sendCommand('list');
$out['version_raw'] = $rcon->sendCommand('version');
$out['lp_listgroups'] = $rcon->sendCommand('lp listgroups');
$rcon->disconnect();
} else $out['error'] = 'RCON Verbindung fehlgeschlagen';
return $out;
}
function mcss_normalize_version($raw) {
$raw = trim((string)$raw);
if ($raw === '') return 'Unbekannt';
$raw = preg_replace('/^\s*v\s*/i', '', $raw);
$softwares = ['spigot','paper','bukkit'];
foreach ($softwares as $soft) {
if (stripos($raw, $soft) !== false) {
if (preg_match('/git-' . preg_quote($soft, '/') . '-(\d+\.\d+(?:\.\d+)?)/i', $raw, $m)) return ucfirst($soft) . ' ' . $m[1];
if (preg_match('/\b' . preg_quote($soft, '/') . '\b[^\d]{0,10}?(\d+\.\d+(?:\.\d+)?)/i', $raw, $m)) return ucfirst($soft) . ' ' . $m[1];
if (preg_match('/(\d+\.\d+(?:\.\d+)?)/', $raw, $m)) return ucfirst($soft) . ' ' . $m[1];
return ucfirst($soft) . ' Unbekannt';
}
}
if (preg_match('/\(mc:\s*(\d+\.\d+(?:\.\d+)?)\)/i', $raw, $m)) return 'Vanilla ' . $m[1];
if (preg_match('/minecraft\s+(\d+\.\d+(?:\.\d+)?)/i', $raw, $m)) return 'Vanilla ' . $m[1];
if (preg_match('/git-(spigot|paper|bukkit)-(\d+\.\d+(?:\.\d+)?)/i', $raw, $m)) return ucfirst($m[1]) . ' ' . $m[2];
if (preg_match('/(\d+\.\d+(?:\.\d+)?)/', $raw, $m)) return $m[1];
return trim($raw);
}
/* ---------------- LuckPerms helper ---------------- */
function mcss_fetch_luckperms_groups() {
$host = get_option('mcss_host', '');
$port = intval(get_option('mcss_rcon_port', 25575));
$pass = get_option('mcss_rcon_pass', '');
if (empty($host) || empty($pass)) return [];
$cache_key = 'mcss_lp_groups_' . md5($host . ':' . $port);
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
$rcon = new Rcon($host, $port, $pass, 3);
if (!$rcon->connect()) return [];
$groups = [];
$out = $rcon->sendCommand('lp listgroups');
if ($out) {
if (preg_match('/(?:Groups?:\s*)(.+)/i', $out, $m)) {
$names = array_map('trim', explode(',', $m[1]));
foreach ($names as $n) if ($n !== '') $groups[] = $n;
} else {
$lines = preg_split("/\r?\n/", $out);
foreach ($lines as $ln) {
$ln = trim(preg_replace('/^[\-\d\.\s]+/', '', $ln));
if ($ln !== '') $groups[] = $ln;
}
}
}
if (empty($groups)) {
$out2 = $rcon->sendCommand('lp info');
if ($out2 && preg_match_all('/Groups:\s*(.+)/i', $out2, $mm)) {
$names = array_map('trim', explode(',', implode(',', $mm[1])));
foreach ($names as $n) if ($n !== '') $groups[] = $n;
}
}
$groups = array_values(array_unique(array_filter($groups)));
$group_infos = [];
foreach ($groups as $g) {
$info_raw = $rcon->sendCommand('lp group ' . $g . ' info');
$weight = 0; $prefix = null;
if ($info_raw) {
if (preg_match('/weight[:\s]*([\-]?\d+)/i', $info_raw, $wm)) $weight = intval($wm[1]);
elseif (preg_match('/priority[:\s]*([\-]?\d+)/i', $info_raw, $wm2)) $weight = intval($wm2[1]);
if (preg_match('/prefix[:\s]*([^\r\n]+)/i', $info_raw, $pm)) $prefix = trim($pm[1]);
elseif (preg_match('/display name[:\s]*([^\r\n]+)/i', $info_raw, $pm2)) $prefix = trim($pm2[1]);
}
$color = '#6c5ce7';
if ($prefix) { $c = mcss_color_from_prefix($prefix); if ($c) $color = $c; }
$group_infos[] = ['group'=>$g,'weight'=>$weight,'prefix'=>$prefix,'color'=>$color];
}
$rcon->disconnect();
usort($group_infos, fn($a,$b) => $b['weight'] <=> $a['weight']);
$ranks = [];
foreach ($group_infos as $gi) {
$ranks[] = ['name'=>$gi['group'],'groups'=>$gi['group'],'color'=>$gi['color'],'prefix'=>$gi['prefix']];
}
set_transient($cache_key, $ranks, 12 * HOUR_IN_SECONDS);
return $ranks;
}
function mcss_color_from_prefix($prefix) {
if (!$prefix) return null;
if (preg_match('/[§&]([0-9a-fk-or])/i', $prefix, $m)) {
$code = strtolower($m[1]);
$map = ['0'=>'#000000','1'=>'#0000AA','2'=>'#00AA00','3'=>'#00AAAA','4'=>'#AA0000','5'=>'#AA00AA','6'=>'#FFAA00',
'7'=>'#AAAAAA','8'=>'#555555','9'=>'#5555FF','a'=>'#55FF55','b'=>'#55FFFF','c'=>'#FF5555','d'=>'#FF55FF',
'e'=>'#FFFF55','f'=>'#FFFFFF'];
if (isset($map[$code])) return $map[$code];
}
if (preg_match('/#([0-9a-f]{6})/i', $prefix, $m2)) return '#' . $m2[1];
return null;
}
/* ---------------- UUID-Resolver & Avatar ---------------- */
function mcss_get_uuid_from_name($name) {
if (empty($name) || !preg_match('/^[a-zA-Z0-9_]{3,16}$/', $name)) return false;
$key = 'mcss_uuid_' . strtolower($name);
$cached = get_transient($key);
if ($cached !== false && $cached !== 'invalid') return $cached;
$response = wp_remote_get("https://api.mojang.com/users/profiles/minecraft/" . rawurlencode($name), [
'timeout' => 6,
'user-agent' => 'MCSS-Plugin/1.9'
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
set_transient($key, 'invalid', 60);
return false;
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (empty($body['id'])) {
set_transient($key, 'invalid', 24 * HOUR_IN_SECONDS);
return false;
}
$uuid = $body['id'];
set_transient($key, $uuid, 30 * DAY_IN_SECONDS);
return $uuid;
}
function mcss_avatar($input, $size = 64) {
$size = intval($size);
if (preg_match('/^[0-9a-f]{32}$/i', $input) || preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $input)) {
$uuid = preg_replace('/-/', '', $input);
} else {
$uuid = mcss_get_uuid_from_name($input);
if (!$uuid) {
return "https://mc-heads.net/avatar/" . rawurlencode($input) . "/{$size}";
}
}
return "https://mc-heads.net/avatar/{$uuid}/{$size}";
}
/* ---------------- Hauptfunktion PREFIX ALS RANG! ---------------- */
add_action('wp_ajax_nopriv_mcss_fetch', 'mcss_ajax_fetch');
add_action('wp_ajax_mcss_fetch', 'mcss_ajax_fetch');
function mcss_ajax_fetch() {
wp_send_json(mcss_fetch_server_with_ranks());
}
function mcss_fetch_server_with_ranks() {
$host = get_option('mcss_host', '');
if (empty($host)) return ['online'=>false,'players'=>[],'address'=>'','version'=>'Unbekannt','ping'=>0,'motd'=>'','last_update'=>time()];
$port = intval(get_option('mcss_rcon_port', 25575));
$pass = get_option('mcss_rcon_pass', '');
$ttl = max(5, intval(get_option('mcss_cache_ttl', 15)));
$show_motd = (bool)get_option('mcss_show_motd', true);
$transient_key = 'mcss_data_' . md5($host . ':' . $port);
$cached = get_transient($transient_key);
if ($cached !== false) {
$cached['last_update'] = get_option($transient_key . '_time', time());
return $cached;
}
$result = [
'online' => false,
'players' => [],
'address' => $host . ':' . $port,
'version' => 'Unbekannt',
'ping' => 0,
'motd' => '',
'last_update' => time()
];
$raw_players = [];
if (!empty($pass)) {
$rcon = new Rcon($host, $port, $pass, 3);
if ($rcon->connect()) {
$list_raw = $rcon->sendCommand('list');
if (preg_match('/: (.*)$/', $list_raw, $m)) {
$list = trim($m[1]);
$entries = array_filter(array_map('trim', explode(',', $list)));
foreach ($entries as $entry) {
if (preg_match('/\s+([a-zA-Z0-9_]{3,16})$/', $entry, $n)) {
$clean_name = $n[1];
} else {
$clean_name = trim(preg_replace('/^[^a-zA-Z0-9_]*/', '', $entry));
$clean_name = preg_replace('/[^a-zA-Z0-9_].*$/', '', $clean_name);
}
if ($clean_name && preg_match('/^[a-zA-Z0-9_]{3,16}$/', $clean_name)) {
$raw_players[] = ['name' => $clean_name, 'raw_entry' => $entry];
}
}
}
$result['online'] = true;
$result['version'] = mcss_normalize_version($rcon->sendCommand('version') ?? '');
$rcon->disconnect();
}
}
$api_host = $port !== 25565 ? $host . ':' . $port : $host;
$resp = wp_remote_get('https://api.mcsrvstat.us/2/' . rawurlencode($api_host), ['timeout'=>7]);
if (!is_wp_error($resp) && wp_remote_retrieve_response_code($resp)==200) {
$body = json_decode(wp_remote_retrieve_body($resp), true);
if (!empty($body['online'])) {
$result['online'] = true;
if (empty($raw_players) && !empty($body['players']['list']) && is_array($body['players']['list'])) {
$raw_players = [];
foreach ($body['players']['list'] as $pl) {
if (!is_array($pl)) continue;
$name = $pl['name'] ?? '';
$uuid = $pl['uuid'] ?? '';
if ($name === '') continue;
$raw_players[] = ['name' => $name, 'uuid' => $uuid, 'raw_entry' => $name];
}
}
if ($result['version'] === 'Unbekannt') {
$candidate = $body['version'] ?? $body['software'] ?? '';
if ($candidate) $result['version'] = mcss_normalize_version($candidate);
}
$result['ping'] = intval($body['debug']['ping'] ?? $body['ping'] ?? 0);
if ($show_motd && !empty($body['motd']['clean'])) {
$motd = is_array($body['motd']['clean']) ? implode(' ', $body['motd']['clean']) : $body['motd']['clean'];
$result['motd'] = $motd;
}
}
}
$lp_groups = mcss_fetch_luckperms_groups();
$players_info = [];
foreach ($raw_players as $p) {
$name = $p['name'];
$uuid = $p['uuid'] ?? mcss_get_uuid_from_name($name);
$raw_entry = $p['raw_entry'] ?? $name;
// PREFIX extrahieren (z. B. "[Owner] M_Viper" → "[Owner]")
$prefix = '';
if (preg_match('/^(\[[^\]]+\])/', $raw_entry, $m)) {
$prefix = $m[1];
}
// Rang = Prefix oder "Spieler"
$rank = $prefix ?: 'Spieler';
$color = '#94a3b8'; // Default Spieler-Farbe
// Farbe aus LuckPerms holen
foreach ($lp_groups as $g) {
if ($g['prefix'] && stripos($prefix, $g['prefix']) !== false) {
$color = $g['color'];
break;
}
}
$players_info[] = [
'name' => $name,
'avatar' => mcss_avatar($uuid ?: $name, 64),
'rank' => $rank,
'color' => $color,
];
}
$result['players'] = $players_info;
$result['version'] = preg_replace('/^\s*v/i', '', trim($result['version']));
set_transient($transient_key, $result, $ttl);
update_option($transient_key . '_time', time());
return $result;
}
/* ---------------- Player rank detection (vereinfacht) ---------------- */
function mcss_get_player_rank($player, $ranks_map = []) {
return ['rank' => 'Spieler', 'color' => '#94a3b8'];
}
/* ---------------- Shortcode ---------------- */
add_shortcode('minecraft_status', 'mcss_server_card_shortcode');
add_shortcode('minecraft_server_detail', 'mcss_server_card_shortcode');
function mcss_server_card_shortcode($atts = []) {
$data = mcss_fetch_server_with_ranks();
$logo_id = get_option('mcss_server_logo_id', 0);
$logo_url = get_option('mcss_server_logo_url', '');
$logo = $logo_id ? wp_get_attachment_image_url($logo_id, 'full') : ($logo_url ?: ($atts['logo'] ?? MCSS_URL . 'img/default-server-logo.png'));
$custom_text = wp_kses_post(get_option('mcss_custom_text', ''));
$ip_color = get_option('mcss_ip_color', '#1f2937');
$ip_size = get_option('mcss_ip_size', '1.5em');
$ct_color = get_option('mcss_ct_color', '#1e293b');
$ct_size = get_option('mcss_ct_size', '1.05em');
$host = get_option('mcss_host', '');
$player_port = trim(get_option('mcss_player_port', ''));
$hide_port = (bool)get_option('mcss_hide_port', true);
$display_address = $host;
if (!$hide_port && $player_port !== '') {
$display_address .= ':' . $player_port;
}
$copy_address = $host . ($player_port !== '' ? ':' . $player_port : '');
$user_defined_ranks = json_decode(get_option('mcss_ranks_json','[]'), true);
$lp_present = is_array($user_defined_ranks) && count($user_defined_ranks) > 0;
$top_rank_name = $lp_present ? ($user_defined_ranks[0]['name'] ?? 'Spieler') : 'Spieler';
$uid = md5($host . '|' . $player_port);
ob_start(); ?>
<div id="mcss-server-widget-<?php echo esc_attr($uid); ?>" class="mcss-server-widget" style="max-width:920px;margin:30px auto;background:#fff;border-radius:20px;overflow:hidden;box-shadow:0 16px 40px rgba(0,0,0,0.12);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;border:1px solid #e2e8f0;position:relative;">
<div id="mcss-copy-toast-<?php echo esc_attr($uid); ?>" style="position:absolute;top:20px;right:20px;background:#10b981;color:#fff;padding:12px 24px;border-radius:12px;font-weight:600;font-size:0.95em;opacity:0;transform:translateY(-20px);transition:all .4s ease;z-index:100;box-shadow:0 8px 20px rgba(16,185,129,0.4);">Adresse kopiert!</div>
<div id="mcss-widget-content-<?php echo esc_attr($uid); ?>">
<div style="padding:28px 32px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:20px;">
<img id="mcss-logo-<?php echo esc_attr($uid); ?>" src="<?php echo esc_url($logo); ?>" alt="Logo" loading="lazy" style="width:80px;height:80px;border-radius:16px;box-shadow:0 8px 20px rgba(0,0,0,0.15);" onerror="this.src='<?php echo esc_js(MCSS_URL . 'img/default-server-logo.png'); ?>'" />
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
<span id="mcss-address-<?php echo esc_attr($uid); ?>" class="mcss-address" style="font-weight:800;font-size:<?php echo esc_attr($ip_size); ?>;color:<?php echo esc_attr($ip_color); ?>;cursor:pointer;user-select:none;" onclick="mcss_copy_address_<?php echo esc_attr($uid); ?>('<?php echo esc_js($copy_address); ?>')">
<?php echo esc_html($display_address); ?>
</span>
<span id="mcss-status-dot-<?php echo esc_attr($uid); ?>" style="width:14px;height:14px;border-radius:50%;background:<?php echo $data['online'] ? '#10b981' : '#ef4444'; ?>;box-shadow:0 0 16px <?php echo $data['online'] ? 'rgba(16,185,129,0.5)' : 'rgba(239,68,68,0.5)'; ?>;"></span>
<span id="mcss-online-text-<?php echo esc_attr($uid); ?>" style="font-weight:600;color:#475569;font-size:1em;"><?php echo $data['online'] ? 'Online' : 'Offline'; ?></span>
</div>
<?php if (!empty($custom_text)): ?>
<div id="mcss-custom-text-<?php echo esc_attr($uid); ?>" style="font-size:<?php echo esc_attr($ct_size); ?>;color:<?php echo esc_attr($ct_color); ?>;font-weight:600;margin-top:6px;"><?php echo $custom_text; ?></div>
<?php else: ?>
<div id="mcss-custom-text-<?php echo esc_attr($uid); ?>" style="display:none;"></div>
<?php endif; ?>
<?php if (!empty($data['motd']) && get_option('mcss_show_motd', true)): ?>
<div id="mcss-motd-<?php echo esc_attr($uid); ?>" style="margin-top:10px;font-size:0.95em;color:#64748b;"><?php echo esc_html($data['motd']); ?></div>
<?php else: ?>
<div id="mcss-motd-<?php echo esc_attr($uid); ?>" style="display:none;"></div>
<?php endif; ?>
</div>
</div>
<div style="padding:18px 32px;background:#f1f5f9;display:flex;justify-content:space-around;flex-wrap:wrap;gap:20px;font-size:0.98em;color:#374151;border-bottom:1px solid #e2e8f0;">
<div><strong>Version:</strong> <span id="mcss-version-<?php echo esc_attr($uid); ?>"><?php echo esc_html($data['version']); ?></span></div>
<?php if ($lp_present): ?>
<div><strong>Rang:</strong> <span id="mcss-players-count-<?php echo esc_attr($uid); ?>"><?php echo esc_html($top_rank_name); ?></span></div>
<?php else: ?>
<div><strong>Spieler:</strong> <span id="mcss-players-count-<?php echo esc_attr($uid); ?>"><?php echo count($data['players']); ?></span></div>
<?php endif; ?>
<div><strong>Ping:</strong> <span id="mcss-ping-<?php echo esc_attr($uid); ?>"><?php echo intval($data['ping']); ?></span> ms</div>
</div>
<div style="padding:24px 32px;">
<div style="margin-bottom:16px;font-weight:700;color:#1f2937;font-size:1.05em;">
Spieler online:
</div>
<div id="mcss-player-grid-<?php echo esc_attr($uid); ?>" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(80px,1fr));gap:16px;">
<?php if (!empty($data['players'])): ?>
<?php foreach ($data['players'] as $p): ?>
<div style="text-align:center;">
<img src="<?php echo esc_url($p['avatar']); ?>" alt="<?php echo esc_attr($p['name']); ?>" loading="lazy" style="width:56px;height:56px;border-radius:12px;margin-bottom:8px;" onerror="this.src='<?php echo esc_js(MCSS_URL . 'img/default-server-logo.png'); ?>'" />
<div style="font-weight:600;font-size:0.88em;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><?php echo esc_html($p['name']); ?></div>
<div style="font-size:0.8em;color:<?php echo esc_attr($p['color']); ?>;margin-top:4px;"><?php echo esc_html($p['rank']); ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div style="grid-column:1/-1;text-align:center;color:#94a3b8;padding:30px 0;font-size:1.1em;">Keine Spieler online</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script>
function mcss_copy_address_<?php echo esc_attr($uid); ?>(text) {
navigator.clipboard.writeText(text).then(function(){
var toast = document.getElementById('mcss-copy-toast-<?php echo esc_attr($uid); ?>');
if(!toast) return;
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
setTimeout(function(){
toast.style.opacity = '0';
toast.style.transform = 'translateY(-20px)';
}, 2000);
}).catch(function(){});
}
(function(){
var uid = '<?php echo esc_js($uid); ?>';
var ajaxUrl = (typeof mcss_ajax_object !== 'undefined' && mcss_ajax_object.ajax_url) ? mcss_ajax_object.ajax_url : '<?php echo admin_url('admin-ajax.php'); ?>';
var interval = (typeof mcss_ajax_object !== 'undefined' && mcss_ajax_object.refresh_interval) ? parseInt(mcss_ajax_object.refresh_interval,10) : 15000;
interval = Math.max(10000, interval);
function updateWidgetData() {
var body = new URLSearchParams();
body.append('action', 'mcss_fetch');
fetch(ajaxUrl, { method: 'POST', headers: {'Content-Type':'application/x-www-form-urlencoded'}, body: body.toString() })
.then(r => r.json())
.then(data => {
if(!data) return;
var dot = document.getElementById('mcss-status-dot-' + uid);
var onlineText = document.getElementById('mcss-online-text-' + uid);
if(dot) {
dot.style.background = data.online ? '#10b981' : '#ef4444';
dot.style.boxShadow = data.online ? '0 0 16px rgba(16,185,129,0.5)' : '0 0 16px rgba(239,68,68,0.5)';
}
if(onlineText) onlineText.textContent = data.online ? 'Online' : 'Offline';
var verEl = document.getElementById('mcss-version-' + uid);
if(verEl) verEl.textContent = (data.version || '');
var playersCnt = document.getElementById('mcss-players-count-' + uid);
if(playersCnt) {
var showCount = true;
if(Array.isArray(data.players) && data.players.length) {
for(var i=0;i<data.players.length;i++){
if(data.players[i] && data.players[i].rank && data.players[i].rank !== 'Spieler'){
showCount = false;
break;
}
}
}
if(showCount) playersCnt.textContent = (data.players ? data.players.length : 0);
}
var pingEl = document.getElementById('mcss-ping-' + uid);
if(pingEl) pingEl.textContent = (data.ping || 0);
var motdEl = document.getElementById('mcss-motd-' + uid);
if(motdEl) {
if(data.motd && data.motd.length) {
motdEl.style.display = '';
motdEl.textContent = data.motd;
} else {
motdEl.style.display = 'none';
}
}
var grid = document.getElementById('mcss-player-grid-' + uid);
if(grid) {
if(data.players && data.players.length) {
var html = '';
data.players.forEach(function(p){
var safeAvatar = p.avatar || '';
var safeName = p.name || '';
var safeRank = p.rank || '';
var safeColor = p.color || '#6c5ce7';
html += '<div style="text-align:center;">' +
'<img src="'+ safeAvatar +'" alt="'+ safeName +'" loading="lazy" style="width:56px;height:56px;border-radius:12px;margin-bottom:8px;" onerror="this.src=\''+ '<?php echo esc_js(MCSS_URL . 'img/default-server-logo.png'); ?>' +'\'" />' +
'<div style="font-weight:600;font-size:0.88em;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'+ escapeHtml(safeName) +'</div>' +
'<div style="font-size:0.8em;color:'+ safeColor +';margin-top:4px;">'+ escapeHtml(safeRank) +'</div>' +
'</div>';
});
grid.innerHTML = html;
} else {
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#94a3b8;padding:30px 0;font-size:1.1em;">Keine Spieler online</div>';
}
}
})
.catch(() => {});
}
function escapeHtml(text) {
if(!text) return '';
return text.replace(/[&<>"'`=\/]/g, function(s) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;','/':'&#x2F;','=':'&#x3D;','`':'&#x60'}[s];
});
}
setInterval(updateWidgetData, interval);
setTimeout(updateWidgetData, 700);
})();
</script>
<?php
return ob_get_clean();
}