Files
2026-10-03 14:20:38 +00:00

307 lines
13 KiB
PHP
Raw Permalink 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
/**
* AeroMC-Umhänge: eigene (auch HD-)Umhänge, die Spieler im Launcher gestalten.
*
* POST /aeromc/v1/cape { uuid, image } Umhang hochladen/ersetzen (Anmeldung Pflicht)
* POST /aeromc/v1/cape/remove { uuid } eigenen Umhang entfernen (Anmeldung Pflicht)
* GET /aeromc/v1/cape?uuid= Umhang eines Spielers (öffentlich)
* GET /aeromc/v1/capes?uuids=a,b,c mehrere auf einmal, für Launcher und Mod (öffentlich)
*
* Bilder werden streng geprüft (echtes PNG, Seitenverhältnis 2:1, 64×32 bis
* 1024×512) und – falls GD verfügbar ist – neu kodiert, damit keine fremden
* Daten im Bild mitreisen. Der Dateiname enthält einen Teil der Prüfsumme, so
* ändert sich die Adresse bei jedem neuen Umhang (kein veralteter Zwischenspeicher).
* Admins können Umhänge unter "AeroMC Fehler → Umhänge" löschen oder sperren.
*/
if (!defined('ABSPATH')) {
exit;
}
define('AEROMC_FRIENDS_CAPE_DIR', 'aeromc-umhaenge');
define('AEROMC_FRIENDS_CAPE_MAX_BYTES', 3000000);
define('AEROMC_FRIENDS_CAPE_MIN_GAP', 15); // Sekunden zwischen zwei Uploads
define('AEROMC_FRIENDS_CAPE_MAX_UUIDS', 100); // je Sammelabfrage
function aeromc_friends_capes_table() {
global $wpdb;
return $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE;
}
function aeromc_friends_cape_dir() {
$uploads = wp_upload_dir();
return [
'pfad' => trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_CAPE_DIR,
'url' => trailingslashit($uploads['baseurl']) . AEROMC_FRIENDS_CAPE_DIR,
];
}
/** Datei eines Umhangs löschen (nur Namen in unserem Schema). */
function aeromc_friends_cape_delete_file($datei) {
if (!$datei || !preg_match('/^[a-f0-9]{32}-[a-f0-9]{8}\.png$/', (string) $datei)) {
return;
}
$ort = aeromc_friends_cape_dir();
$voll = $ort['pfad'] . '/' . $datei;
if (file_exists($voll)) {
@unlink($voll);
}
}
/** Zeile -> öffentliche Angaben */
function aeromc_friends_cape_info($r) {
$ort = aeromc_friends_cape_dir();
return [
'url' => $ort['url'] . '/' . $r->datei,
'width' => (int) $r->breite,
'height' => (int) $r->hoehe,
'hash' => $r->hash,
'updated' => $r->updated_at,
];
}
/**
* Prüft ein Umhang-Bild und gibt [daten, breite, hoehe] zurück oder WP_Error.
*/
function aeromc_friends_cape_check_image($base64) {
$daten = base64_decode(preg_replace('/^data:image\/png;base64,/', '', (string) $base64), true);
if ($daten === false || strlen($daten) < 60) {
return new WP_Error('aeromc_invalid', 'Ungültiges Bild.', ['status' => 400]);
}
if (strlen($daten) > AEROMC_FRIENDS_CAPE_MAX_BYTES) {
return new WP_Error('aeromc_too_big', 'Der Umhang ist zu groß (Datei).', ['status' => 413]);
}
if (strncmp($daten, "\x89PNG\r\n\x1A\n", 8) !== 0 || substr($daten, 12, 4) !== 'IHDR') {
return new WP_Error('aeromc_invalid', 'Nur PNG-Bilder.', ['status' => 400]);
}
$kopf = unpack('Nbreite/Nhoehe', substr($daten, 16, 8));
$breite = (int) $kopf['breite'];
$hoehe = (int) $kopf['hoehe'];
if (!in_array($breite, [64, 128, 256, 512, 1024], true) || $breite !== 2 * $hoehe) {
return new WP_Error('aeromc_invalid', 'Ein Umhang muss 64×32, 128×64, 256×128, 512×256 oder 1024×512 Pixel groß sein.', ['status' => 400]);
}
// neu kodieren: entfernt alles außer den Bildpunkten
if (function_exists('imagecreatefromstring')) {
$bild = @imagecreatefromstring($daten);
if (!$bild || imagesx($bild) !== $breite || imagesy($bild) !== $hoehe) {
return new WP_Error('aeromc_invalid', 'Das Bild ist beschädigt.', ['status' => 400]);
}
imagealphablending($bild, false);
imagesavealpha($bild, true);
ob_start();
imagepng($bild, null, 9);
$daten = ob_get_clean();
imagedestroy($bild);
}
return [$daten, $breite, $hoehe];
}
add_action('rest_api_init', function () {
register_rest_route('aeromc/v1', '/cape', [
[
'methods' => 'POST',
'callback' => 'aeromc_friends_route_cape_upload',
'permission_callback' => '__return_true',
],
[
'methods' => 'GET',
'callback' => 'aeromc_friends_route_cape_get',
'permission_callback' => '__return_true',
],
]);
register_rest_route('aeromc/v1', '/cape/remove', [
'methods' => 'POST',
'callback' => 'aeromc_friends_route_cape_remove',
'permission_callback' => '__return_true',
]);
register_rest_route('aeromc/v1', '/capes', [
'methods' => 'GET',
'callback' => 'aeromc_friends_route_capes',
'permission_callback' => '__return_true',
]);
});
/** Anmeldung ist hier immer Pflicht – auch im Übergangsmodus (öffentlich sichtbare Bilder). */
function aeromc_friends_cape_auth($request, $uuid) {
$sitzung = aeromc_friends_check_auth($request, $uuid);
if (is_wp_error($sitzung)) {
return $sitzung;
}
if (!$sitzung) {
return new WP_Error('aeromc_auth', 'Anmeldung erforderlich.', ['status' => 401]);
}
return $sitzung;
}
function aeromc_friends_route_cape_upload($request) {
global $wpdb;
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
if (!$uuid) {
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
}
$sitzung = aeromc_friends_cape_auth($request, $uuid);
if (is_wp_error($sitzung)) {
return $sitzung;
}
$table = aeromc_friends_capes_table();
$alt = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid));
if ($alt && $alt->status === 'gesperrt') {
return new WP_Error('aeromc_blocked', 'Dein Umhang wurde vom Betreiber gesperrt.', ['status' => 403]);
}
if ($alt && time() - strtotime($alt->updated_at . ' UTC') < AEROMC_FRIENDS_CAPE_MIN_GAP) {
return new WP_Error('aeromc_too_fast', 'Bitte einen Moment warten, bevor du den Umhang erneut hochlädst.', ['status' => 429]);
}
$geprueft = aeromc_friends_cape_check_image($request->get_param('image'));
if (is_wp_error($geprueft)) {
return $geprueft;
}
list($daten, $breite, $hoehe) = $geprueft;
$hash = sha1($daten);
$ort = aeromc_friends_cape_dir();
if (!wp_mkdir_p($ort['pfad'])) {
return new WP_Error('aeromc_error', 'Umhang konnte nicht gespeichert werden.', ['status' => 500]);
}
if (!file_exists($ort['pfad'] . '/index.html')) {
file_put_contents($ort['pfad'] . '/index.html', '');
}
$datei = $uuid . '-' . substr($hash, 0, 8) . '.png';
if (file_put_contents($ort['pfad'] . '/' . $datei, $daten) === false) {
return new WP_Error('aeromc_error', 'Umhang konnte nicht gespeichert werden.', ['status' => 500]);
}
if ($alt && $alt->datei !== $datei) {
aeromc_friends_cape_delete_file($alt->datei);
}
$zeile = [
'uuid' => $uuid,
'username' => $sitzung->username,
'datei' => $datei,
'breite' => $breite,
'hoehe' => $hoehe,
'hash' => $hash,
'status' => 'aktiv',
'updated_at' => current_time('mysql', true),
];
$wpdb->replace($table, $zeile);
return ['ok' => true, 'cape' => aeromc_friends_cape_info((object) $zeile)];
}
function aeromc_friends_route_cape_remove($request) {
global $wpdb;
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
if (!$uuid) {
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
}
$sitzung = aeromc_friends_cape_auth($request, $uuid);
if (is_wp_error($sitzung)) {
return $sitzung;
}
$table = aeromc_friends_capes_table();
$alt = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid));
if ($alt) {
aeromc_friends_cape_delete_file($alt->datei);
if ($alt->status === 'gesperrt') {
// Sperre bleibt bestehen, nur das Bild ist weg
$wpdb->update($table, ['datei' => '', 'hash' => ''], ['uuid' => $uuid]);
} else {
$wpdb->delete($table, ['uuid' => $uuid]);
}
}
return ['ok' => true];
}
function aeromc_friends_route_cape_get($request) {
global $wpdb;
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
if (!$uuid) {
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
}
$table = aeromc_friends_capes_table();
$r = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s AND status = 'aktiv'", $uuid));
return ['ok' => true, 'cape' => $r ? aeromc_friends_cape_info($r) : null];
}
function aeromc_friends_route_capes($request) {
global $wpdb;
$liste = array_filter(array_map('aeromc_friends_normalize_uuid', explode(',', (string) $request->get_param('uuids'))));
$liste = array_slice(array_values(array_unique($liste)), 0, AEROMC_FRIENDS_CAPE_MAX_UUIDS);
$ergebnis = [];
if ($liste) {
$table = aeromc_friends_capes_table();
$platz = implode(',', array_fill(0, count($liste), '%s'));
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table WHERE status = 'aktiv' AND uuid IN ($platz)", $liste
));
foreach ($rows as $r) {
$ergebnis[$r->uuid] = aeromc_friends_cape_info($r);
}
}
$antwort = new WP_REST_Response(['ok' => true, 'capes' => (object) $ergebnis]);
// kurz zwischenspeichern lassen – der Mod fragt beim Betreten eines Servers viele auf einmal ab
$antwort->header('Cache-Control', 'public, max-age=60');
return $antwort;
}
// ---------- Admin: Umhänge prüfen, löschen, sperren ----------
if (is_admin()) {
add_action('admin_menu', function () {
add_submenu_page('aeromc-reports', 'AeroMC Umhänge', 'Umhänge', 'manage_options', 'aeromc-capes', 'aeromc_friends_capes_page');
}, 20);
}
function aeromc_friends_capes_page() {
global $wpdb;
if (!current_user_can('manage_options')) {
return;
}
$table = aeromc_friends_capes_table();
if (isset($_POST['aeromc_cape'], $_POST['aeromc_aktion']) && check_admin_referer('aeromc_capes')) {
$uuid = aeromc_friends_normalize_uuid($_POST['aeromc_cape']);
$r = $uuid ? $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid)) : null;
if ($r) {
$aktion = (string) $_POST['aeromc_aktion'];
if ($aktion === 'loeschen') {
aeromc_friends_cape_delete_file($r->datei);
$wpdb->delete($table, ['uuid' => $uuid]);
echo '<div class="notice notice-success"><p>Umhang von ' . esc_html($r->username) . ' gelöscht.</p></div>';
} elseif ($aktion === 'sperren') {
aeromc_friends_cape_delete_file($r->datei);
$wpdb->update($table, ['status' => 'gesperrt', 'datei' => '', 'hash' => ''], ['uuid' => $uuid]);
echo '<div class="notice notice-success"><p>' . esc_html($r->username) . ' gesperrt – der Umhang ist entfernt, neue Uploads werden abgelehnt.</p></div>';
} elseif ($aktion === 'entsperren') {
$wpdb->delete($table, ['uuid' => $uuid]);
echo '<div class="notice notice-success"><p>Sperre für ' . esc_html($r->username) . ' aufgehoben.</p></div>';
}
}
}
$ort = aeromc_friends_cape_dir();
$rows = $wpdb->get_results("SELECT * FROM $table ORDER BY updated_at DESC LIMIT 300");
echo '<div class="wrap"><h1>AeroMC Umhänge</h1>';
echo '<p>Eigene Umhänge, die Spieler im AeroMC-Skin-Editor hochgeladen haben. „Sperren“ entfernt den Umhang und verhindert neue Uploads dieses Spielers.</p>';
if (!$rows) {
echo '<p>Noch keine Umhänge hochgeladen.</p></div>';
return;
}
echo '<table class="widefat striped"><thead><tr><th style="width:150px">Umhang</th><th>Spieler</th><th>Größe</th><th>Geändert (UTC)</th><th>Status</th><th></th></tr></thead><tbody>';
foreach ($rows as $r) {
$bild = $r->datei
? '<img src="' . esc_url($ort['url'] . '/' . $r->datei) . '" alt="" style="width:128px;height:64px;image-rendering:pixelated;background:#ddd;border:1px solid #ccc" />'
: '–';
echo '<tr><td>' . $bild . '</td><td>' . esc_html($r->username) . '<br /><small>' . esc_html($r->uuid) . '</small></td>'
. '<td>' . esc_html($r->breite . '×' . $r->hoehe) . '</td><td>' . esc_html($r->updated_at) . '</td><td>' . esc_html($r->status) . '</td><td>';
echo '<form method="post" style="display:flex;gap:6px">';
wp_nonce_field('aeromc_capes');
echo '<input type="hidden" name="aeromc_cape" value="' . esc_attr($r->uuid) . '" />';
if ($r->status === 'gesperrt') {
echo '<button class="button" name="aeromc_aktion" value="entsperren">Sperre aufheben</button>';
} else {
echo '<button class="button" name="aeromc_aktion" value="loeschen" onclick="return confirm(\'Umhang löschen?\')">Löschen</button>';
echo '<button class="button button-link-delete" name="aeromc_aktion" value="sperren" onclick="return confirm(\'Spieler sperren? Der Umhang wird entfernt und neue Uploads abgelehnt.\')">Sperren</button>';
}
echo '</form></td></tr>';
}
echo '</tbody></table></div>';
}