display_name : 'Unbekannt'; // Kontext-Daten sammeln $title = ''; $url = ''; $forum_url = wbf_get_forum_url(); switch ( $type ) { case 'reply': case 'mention': $thread = WBF_DB::get_thread( (int) $object_id ); if ( $thread ) { $title = $thread->title; $url = $forum_url . '?forum_thread=' . (int) $thread->id; } break; case 'message': $title = 'Neue Privatnachricht'; $url = $forum_url . '?forum_dm=1'; break; } // Push an BungeeCord senden self::push_to_bungee( $mc_uuid, $type, $title, $actor_name, $url, $user_id ); } /** * Sendet die Benachrichtigung per HTTP POST an den BungeeCord StatusAPI Server. */ private static function push_to_bungee( $mc_uuid, $type, $title, $author, $url, $wp_user_id ) { $api_url = self::get_api_url(); $secret = self::get_api_secret(); $payload = wp_json_encode( [ 'player_uuid' => $mc_uuid, 'type' => $type, 'title' => $title, 'author' => $author, 'url' => $url, 'wp_user_id' => (int) $wp_user_id, ] ); $args = [ 'method' => 'POST', 'timeout' => 5, 'blocking' => false, // Non-blocking — Seite wartet nicht auf Antwort 'headers' => [ 'Content-Type' => 'application/json; charset=UTF-8', 'X-Api-Key' => $secret, ], 'body' => $payload, 'sslverify' => false, // Lokales Netzwerk braucht kein SSL ]; wp_remote_post( $api_url . '/forum/notify', $args ); } // ══════════════════════════════════════════════════════════════════════════ // ── REST API (für BungeeCord → WordPress) ───────────────────────────────── // ══════════════════════════════════════════════════════════════════════════ public static function register_rest_routes() { // POST /wp-json/mc-bridge/v1/verify-link // BungeeCord schickt Token + MC-UUID → WP verifiziert und speichert register_rest_route( 'mc-bridge/v1', '/verify-link', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_verify_link' ], 'permission_callback' => '__return_true', ] ); // POST /wp-json/mc-bridge/v1/unlink // BungeeCord kann Verknüpfung auch von der MC-Seite lösen register_rest_route( 'mc-bridge/v1', '/unlink', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_unlink' ], 'permission_callback' => '__return_true', ] ); // GET /wp-json/mc-bridge/v1/status // Verbindungstest register_rest_route( 'mc-bridge/v1', '/status', [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_status' ], 'permission_callback' => '__return_true', ] ); } /** * REST: Verknüpfung bestätigen. * BungeeCord sendet: { "token": "...", "mc_uuid": "...", "mc_name": "..." } */ public static function rest_verify_link( $request ) { // Rate Limiting: max 10 Versuche pro IP pro Minute $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $limit_key = 'wbf_mc_link_' . md5($ip); $attempts = (int) get_transient($limit_key); if ($attempts >= 10) { return new WP_REST_Response([ 'success' => false, 'error' => 'rate_limited', 'message' => 'Zu viele Versuche. Bitte warte eine Minute.' ], 429); } set_transient($limit_key, $attempts + 1, 60); // API-Secret prüfen $secret = self::get_api_secret(); if ( ! empty( $secret ) ) { $provided = $request->get_header( 'X-Api-Key' ); if ( $provided !== $secret ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 ); } } $token = sanitize_text_field( $request->get_param( 'token' ) ); $mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) ); $mc_name = sanitize_text_field( $request->get_param( 'mc_name' ) ); if ( empty( $token ) || empty( $mc_uuid ) ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_fields' ], 400 ); } // Token in forum_user_meta suchen global $wpdb; $meta_row = $wpdb->get_row( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s", self::META_LINK_TOKEN, $token ) ); if ( ! $meta_row ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'invalid_token' ], 404 ); } $forum_user_id = (int) $meta_row->user_id; // Ablauf prüfen $expiry_meta = WBF_DB::get_user_meta( $forum_user_id ); $expiry = $expiry_meta[ self::META_LINK_EXPIRY ] ?? '0'; if ( (int) $expiry < time() ) { // Token abgelaufen — aufräumen WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' ); WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' ); return new WP_REST_Response( [ 'success' => false, 'error' => 'token_expired' ], 410 ); } // Prüfen ob diese MC-UUID bereits mit einem anderen Account verknüpft ist $existing = $wpdb->get_row( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s", self::META_MC_UUID, $mc_uuid ) ); if ( $existing && (int) $existing->user_id !== $forum_user_id ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'uuid_already_linked', 'message' => 'Diese Minecraft-UUID ist bereits mit einem anderen Forum-Account verknüpft.', ], 409 ); } // Verknüpfung speichern WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, $mc_uuid ); WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, $mc_name ); // Token aufräumen WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_TOKEN, '' ); WBF_DB::set_user_meta( $forum_user_id, self::META_LINK_EXPIRY, '' ); // Forum-User-Info für BungeeCord zurückgeben $forum_user = WBF_DB::get_user( $forum_user_id ); return new WP_REST_Response( [ 'success' => true, 'forum_user_id' => $forum_user_id, 'display_name' => $forum_user ? $forum_user->display_name : '', 'username' => $forum_user ? $forum_user->username : '', ], 200 ); } /** * REST: Verknüpfung lösen (von BungeeCord-Seite). */ public static function rest_unlink( $request ) { $secret = self::get_api_secret(); if ( ! empty( $secret ) ) { $provided = $request->get_header( 'X-Api-Key' ); if ( $provided !== $secret ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'unauthorized' ], 403 ); } } $mc_uuid = sanitize_text_field( $request->get_param( 'mc_uuid' ) ); if ( empty( $mc_uuid ) ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'missing_mc_uuid' ], 400 ); } global $wpdb; $meta_row = $wpdb->get_row( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}forum_user_meta WHERE meta_key = %s AND meta_value = %s", self::META_MC_UUID, $mc_uuid ) ); if ( ! $meta_row ) { return new WP_REST_Response( [ 'success' => false, 'error' => 'not_linked' ], 404 ); } $forum_user_id = (int) $meta_row->user_id; WBF_DB::set_user_meta( $forum_user_id, self::META_MC_UUID, '' ); WBF_DB::set_user_meta( $forum_user_id, self::META_MC_NAME, '' ); return new WP_REST_Response( [ 'success' => true ], 200 ); } /** * REST: Status-Endpoint für Verbindungstest. */ public static function rest_status( $request ) { return new WP_REST_Response( [ 'success' => true, 'enabled' => self::is_enabled(), 'version' => defined( 'WBF_VERSION' ) ? WBF_VERSION : '?', 'plugin' => 'WP Business Forum', ], 200 ); } // ══════════════════════════════════════════════════════════════════════════ // ── AJAX: Token generieren ──────────────────────────────────────────────── // ══════════════════════════════════════════════════════════════════════════ /** * Generiert einen 8-stelligen Verknüpfungs-Token (15 Minuten gültig). * Der User gibt diesen Token dann ingame mit /forumlink ein. */ public static function ajax_generate_token() { check_ajax_referer( 'wbf_nonce', 'nonce' ); $user = WBF_Auth::get_current_user(); if ( ! $user ) { wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] ); } // Prüfen ob bereits verknüpft $existing_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID ); if ( ! empty( $existing_uuid ) ) { $mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME ); wp_send_json_error( [ 'message' => 'Dein Account ist bereits mit ' . esc_html( $mc_name ?: $existing_uuid ) . ' verknüpft.', 'linked' => true, 'mc_name' => $mc_name, 'mc_uuid' => $existing_uuid, ] ); } // Token generieren: 8 Zeichen, alphanumerisch, uppercase $token = strtoupper( substr( bin2hex( random_bytes( 5 ) ), 0, 8 ) ); $expiry = time() + ( 15 * 60 ); // 15 Minuten WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, $token ); WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, (string) $expiry ); wp_send_json_success( [ 'token' => $token, 'expires_in' => 15, // Minuten 'command' => '/forumlink ' . $token, ] ); } /** * AJAX: Verknüpfung lösen (von der Forum-Seite). */ public static function ajax_unlink() { check_ajax_referer( 'wbf_nonce', 'nonce' ); $user = WBF_Auth::get_current_user(); if ( ! $user ) { wp_send_json_error( [ 'message' => 'Nicht eingeloggt.' ] ); } $mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID ); // Meta löschen WBF_DB::set_user_meta( $user->id, self::META_MC_UUID, '' ); WBF_DB::set_user_meta( $user->id, self::META_MC_NAME, '' ); WBF_DB::set_user_meta( $user->id, self::META_LINK_TOKEN, '' ); WBF_DB::set_user_meta( $user->id, self::META_LINK_EXPIRY, '' ); // Optional: BungeeCord informieren if ( ! empty( $mc_uuid ) && self::is_enabled() ) { $api_url = self::get_api_url(); $secret = self::get_api_secret(); if ( ! empty( $api_url ) ) { wp_remote_post( $api_url . '/forum/unlink', [ 'method' => 'POST', 'timeout' => 3, 'blocking' => false, 'headers' => [ 'Content-Type' => 'application/json', 'X-Api-Key' => $secret, ], 'body' => wp_json_encode( [ 'mc_uuid' => $mc_uuid ] ), 'sslverify' => false, ] ); } } wp_send_json_success( [ 'message' => 'Minecraft-Verknüpfung wurde aufgehoben.' ] ); } /** * AJAX: Verknüpfungs-Status prüfen. * Wird vom Frontend-Polling alle 5 Sekunden nach Token-Generierung aufgerufen. * Gibt zurück ob der User bereits verknüpft ist (BungeeCord hat verify-link gesendet). */ public static function ajax_link_status() { check_ajax_referer( 'wbf_nonce', 'nonce' ); $user = WBF_Auth::get_current_user(); if ( ! $user ) { wp_send_json_error( [ 'message' => 'Nicht eingeloggt.', 'linked' => false ] ); return; } $mc_uuid = WBF_DB::get_user_meta_single( $user->id, self::META_MC_UUID ); $mc_name = WBF_DB::get_user_meta_single( $user->id, self::META_MC_NAME ); if ( ! empty( $mc_uuid ) ) { wp_send_json_success( [ 'linked' => true, 'mc_uuid' => $mc_uuid, 'mc_name' => $mc_name, ] ); } else { wp_send_json_success( [ 'linked' => false ] ); } } // ══════════════════════════════════════════════════════════════════════════ // ── Hilfsmethoden ───────────────────────────────────────────────────────── // ══════════════════════════════════════════════════════════════════════════ /** * Gibt die MC-UUID eines Forum-Users zurück (oder leer). */ public static function get_mc_uuid( $forum_user_id ) { return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_UUID ); } /** * Gibt den MC-Namen eines Forum-Users zurück (oder leer). */ public static function get_mc_name( $forum_user_id ) { return WBF_DB::get_user_meta_single( $forum_user_id, self::META_MC_NAME ); } /** * Prüft ob ein Forum-User mit MC verknüpft ist. */ public static function is_linked( $forum_user_id ) { $uuid = self::get_mc_uuid( $forum_user_id ); return ! empty( $uuid ); } /** * HTML-Badge für Profilansicht: Zeigt MC-Verknüpfung an. */ public static function profile_badge( $forum_user_id ) { if ( ! self::is_enabled() ) return ''; $mc_name = self::get_mc_name( $forum_user_id ); if ( empty( $mc_name ) ) return ''; $name_esc = esc_html( $mc_name ); $head_url = 'https://mc-heads.net/avatar/' . urlencode( $mc_name ) . '/24'; return "" . "\"\"" . "{$name_esc}" . ""; } } // Initialisierung add_action( 'init', [ 'WBF_MC_Bridge', 'init' ], 20 );