307 lines
13 KiB
PHP
307 lines
13 KiB
PHP
<?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>';
|
||
}
|