Update from Git Manager GUI
This commit is contained in:
@@ -2,72 +2,12 @@
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
class WBF_Ajax {
|
||||
// ── Discord-Rollen-Sync manuell anstoßen ────────────────────────────────
|
||||
public static function handle_manual_discord_sync() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if (!$user || WBF_Roles::level($user->role) < 80) {
|
||||
wp_send_json_error(['message' => 'Keine Berechtigung.']);
|
||||
}
|
||||
if (!function_exists('wbf_run_discord_role_sync')) {
|
||||
wp_send_json_error(['message' => 'Sync-Funktion nicht gefunden.']);
|
||||
}
|
||||
// Sync anstoßen (läuft synchron, kann bei vielen Usern etwas dauern)
|
||||
wbf_run_discord_role_sync();
|
||||
wp_send_json_success(['message' => 'Discord-Rollen-Sync wurde ausgeführt.']);
|
||||
}
|
||||
|
||||
// ── Discord-Rollen-Sync für einzelnen Nutzer ─────────────────────────────
|
||||
public static function handle_discord_sync_user() {
|
||||
self::verify();
|
||||
$admin = WBF_Auth::get_current_user();
|
||||
if ( ! $admin || WBF_Roles::level( $admin->role ) < 80 ) {
|
||||
wp_send_json_error( [ 'message' => 'Keine Berechtigung.' ] );
|
||||
}
|
||||
|
||||
$target_id = (int) ( $_POST['user_id'] ?? 0 );
|
||||
if ( ! $target_id ) {
|
||||
wp_send_json_error( [ 'message' => 'Keine Nutzer-ID.' ] );
|
||||
}
|
||||
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
$token = trim( $s['discord_bot_token'] ?? '' );
|
||||
$guild = trim( $s['discord_guild_id'] ?? '' );
|
||||
$role_map = json_decode( $s['discord_role_map'] ?? '{}', true ) ?: [];
|
||||
|
||||
if ( ! $token || ! $guild || empty( $role_map ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Discord nicht konfiguriert.' ] );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$discord_uid = $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT meta_value FROM {$wpdb->prefix}forum_user_meta
|
||||
WHERE user_id = %d AND meta_key = 'discord_user_id'",
|
||||
$target_id
|
||||
) );
|
||||
|
||||
if ( ! $discord_uid ) {
|
||||
wp_send_json_error( [ 'message' => 'Nutzer hat kein verknüpftes Discord-Konto.' ] );
|
||||
}
|
||||
|
||||
// Beide Richtungen: Discord → Forum
|
||||
if ( function_exists( 'wbf_sync_discord_role_for_user' ) ) {
|
||||
wbf_sync_discord_role_for_user( $target_id, $discord_uid, $token, $guild, $role_map );
|
||||
}
|
||||
|
||||
// Frisch geladene Rolle zurückgeben damit die UI sofort aktualisiert werden kann
|
||||
$updated = WBF_DB::get_user( $target_id );
|
||||
wp_send_json_success( [
|
||||
'message' => 'Sync abgeschlossen.',
|
||||
'new_role' => $updated ? $updated->role : '',
|
||||
] );
|
||||
}
|
||||
|
||||
public static function init() {
|
||||
$actions = [
|
||||
'wbf_login', 'wbf_register', 'wbf_logout',
|
||||
'wbf_new_thread', 'wbf_new_post', 'wbf_toggle_like',
|
||||
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_banner', 'wbf_upload_post_image',
|
||||
'wbf_update_profile', 'wbf_upload_avatar', 'wbf_upload_post_image', 'wbf_upload_banner',
|
||||
'wbf_forgot_password', 'wbf_reset_password', 'wbf_load_more_messages',
|
||||
'wbf_create_invite', 'wbf_delete_invite',
|
||||
'wbf_toggle_subscribe', 'wbf_restore_content', 'wbf_toggle_profile_visibility',
|
||||
@@ -84,8 +24,10 @@ class WBF_Ajax {
|
||||
'wbf_save_discord',
|
||||
'wbf_discord_send_code',
|
||||
'wbf_discord_verify_code',
|
||||
'wbf_manual_discord_sync',
|
||||
'wbf_discord_sync_user',
|
||||
'wbf_2fa_setup_begin',
|
||||
'wbf_2fa_setup_verify',
|
||||
'wbf_2fa_disable',
|
||||
'wbf_2fa_verify_login',
|
||||
];
|
||||
foreach ($actions as $action) {
|
||||
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
|
||||
@@ -121,7 +63,8 @@ class WBF_Ajax {
|
||||
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
|
||||
$result = WBF_Auth::login(
|
||||
sanitize_text_field($_POST['username'] ?? ''),
|
||||
$_POST['password'] ?? ''
|
||||
$_POST['password'] ?? '',
|
||||
! empty($_POST['remember_me'])
|
||||
);
|
||||
if ($result['success']) {
|
||||
// Erfolgreicher Login: Fehlzähler löschen
|
||||
@@ -131,6 +74,9 @@ class WBF_Ajax {
|
||||
WBF_Auth::set_remember_cookie($u->id);
|
||||
}
|
||||
wp_send_json_success(['display_name'=>$u->display_name,'avatar_url'=>$u->avatar_url,'user_id'=>$u->id]);
|
||||
} elseif ( ! empty($result['2fa_required']) ) {
|
||||
// 2FA erforderlich — kein Fehlerzähler erhöhen, kein Fehlermeldung
|
||||
wp_send_json_error(['2fa_required' => true]);
|
||||
} else {
|
||||
// Fehlversuch zählen — außer bei gesperrten Konten (kein Passwortfehler)
|
||||
if ( empty($result['banned']) ) {
|
||||
@@ -202,7 +148,10 @@ class WBF_Ajax {
|
||||
}
|
||||
|
||||
public static function handle_logout() {
|
||||
// Kein Nonce-Check für Logout nötig — Session-Clearing ist sicher
|
||||
// Nonce-Check für Logout
|
||||
if ( ! isset($_POST['nonce']) || ! check_ajax_referer('wbf_nonce', 'nonce', false) ) {
|
||||
wp_send_json_error(['message' => 'invalid_nonce'], 403);
|
||||
}
|
||||
WBF_Auth::logout();
|
||||
wp_send_json_success(['message' => 'logged_out']);
|
||||
}
|
||||
@@ -250,6 +199,10 @@ class WBF_Ajax {
|
||||
'content' => WBF_DB::apply_word_filter($content),
|
||||
'prefix_id' => $prefix_id,
|
||||
]);
|
||||
// Ingame-Benachrichtigung
|
||||
if (function_exists('wbf_notify_ingame')) {
|
||||
wbf_notify_ingame($user->username, 'Neuer Thread: ' . mb_substr($title, 0, 80));
|
||||
}
|
||||
|
||||
// Tags speichern
|
||||
$raw_tags = sanitize_text_field( $_POST['tags'] ?? '' );
|
||||
@@ -572,25 +525,21 @@ class WBF_Ajax {
|
||||
wp_send_json_success(['avatar_url'=>$url]);
|
||||
}
|
||||
|
||||
// ── Banner Upload ────────────────────────────────────────────────────────
|
||||
|
||||
public static function handle_upload_banner() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
|
||||
if (empty($_FILES['banner'])) wp_send_json_error(['message'=>'Keine Datei.']);
|
||||
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
|
||||
if ( empty($_FILES['banner']) ) wp_send_json_error(['message' => 'Keine Datei.']);
|
||||
|
||||
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||
|
||||
// Max 4 MB für Banner (größer als Avatar)
|
||||
if ( $_FILES['banner']['size'] > 4 * 1024 * 1024 ) {
|
||||
wp_send_json_error(['message'=>'Maximale Dateigröße: 4 MB.']);
|
||||
if ( $_FILES['banner']['size'] > 3 * 1024 * 1024 ) {
|
||||
wp_send_json_error(['message' => 'Maximale Dateigröße: 3 MB.']);
|
||||
}
|
||||
|
||||
// Server-seitige MIME-Typ-Prüfung
|
||||
$tmp = $_FILES['banner']['tmp_name'] ?? '';
|
||||
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
|
||||
wp_send_json_error(['message'=>'Ungültiger Datei-Upload.']);
|
||||
wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']);
|
||||
}
|
||||
if ( function_exists('finfo_open') ) {
|
||||
$finfo = finfo_open( FILEINFO_MIME_TYPE );
|
||||
@@ -607,7 +556,7 @@ class WBF_Ajax {
|
||||
$real_mime = $et_map[$et] ?? '';
|
||||
}
|
||||
if ( ! in_array( $real_mime, $allowed_types, true ) ) {
|
||||
wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
@@ -615,11 +564,11 @@ class WBF_Ajax {
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
|
||||
$id = media_handle_upload('banner', 0);
|
||||
if (is_wp_error($id)) wp_send_json_error(['message'=>$id->get_error_message()]);
|
||||
if ( is_wp_error($id) ) wp_send_json_error(['message' => $id->get_error_message()]);
|
||||
|
||||
$url = wp_get_attachment_url($id);
|
||||
WBF_DB::update_user($user->id, ['banner_url'=>$url]);
|
||||
wp_send_json_success(['banner_url'=>$url]);
|
||||
WBF_DB::update_user($user->id, ['banner_url' => $url]);
|
||||
wp_send_json_success(['banner_url' => $url]);
|
||||
}
|
||||
|
||||
// ── Report ────────────────────────────────────────────────────────────────
|
||||
@@ -1738,6 +1687,143 @@ class WBF_Ajax {
|
||||
return ( ! is_wp_error($msg_res) && wp_remote_retrieve_response_code($msg_res) === 200 );
|
||||
}
|
||||
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── 2FA / TOTP ────────────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Setup-Schritt 1: Neues Secret generieren und als "pending" speichern.
|
||||
* Gibt Secret (zur manuellen Eingabe) und otpauth:// URI (für QR-Code) zurück.
|
||||
*/
|
||||
public static function handle_2fa_setup_begin() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||
|
||||
$secret = WBF_TOTP::generate_secret();
|
||||
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_PENDING, $secret );
|
||||
|
||||
wp_send_json_success( [
|
||||
"secret" => $secret,
|
||||
"uri" => WBF_TOTP::get_otpauth_uri( $user->username, $secret ),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup-Schritt 2: Code verifizieren und 2FA aktivieren.
|
||||
*/
|
||||
public static function handle_2fa_setup_verify() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||
|
||||
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_PENDING );
|
||||
|
||||
if ( empty($secret) ) {
|
||||
wp_send_json_error( ["message" => "Kein ausstehender 2FA-Setup. Bitte neu starten."] );
|
||||
}
|
||||
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||
wp_send_json_error( ["message" => "Ungültiger Code. Bitte Uhrzeit prüfen und erneut versuchen."] );
|
||||
}
|
||||
|
||||
WBF_DB::set_user_meta( $user->id, WBF_TOTP::META_SECRET, $secret );
|
||||
global $wpdb;
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_user_meta",
|
||||
["user_id" => $user->id, "meta_key" => WBF_TOTP::META_PENDING], ["%d", "%s"] );
|
||||
|
||||
wp_send_json_success( ["message" => "2FA erfolgreich aktiviert!"] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 2FA deaktivieren (User-seitig).
|
||||
* Erfordert aktuelles Passwort + gültigen TOTP-Code.
|
||||
*/
|
||||
public static function handle_2fa_disable() {
|
||||
self::verify();
|
||||
$user = WBF_Auth::get_current_user();
|
||||
if ( ! $user ) wp_send_json_error( ["message" => "Nicht eingeloggt."] );
|
||||
if ( ! class_exists("WBF_TOTP") ) wp_send_json_error( ["message" => "2FA-Modul nicht verfügbar."] );
|
||||
|
||||
$password = $_POST["password"] ?? "";
|
||||
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||
|
||||
$fresh = WBF_DB::get_user( $user->id );
|
||||
if ( ! $fresh || ! password_verify( $password, $fresh->password ) ) {
|
||||
wp_send_json_error( ["message" => "Falsches Passwort."] );
|
||||
}
|
||||
|
||||
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
|
||||
if ( empty($secret) ) {
|
||||
wp_send_json_error( ["message" => "2FA ist nicht aktiv."] );
|
||||
}
|
||||
if ( ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||
wp_send_json_error( ["message" => "Ungültiger Authenticator-Code."] );
|
||||
}
|
||||
|
||||
WBF_TOTP::disable_for( $user->id );
|
||||
wp_send_json_success( ["message" => "2FA wurde deaktiviert."] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Login-Schritt 2: TOTP-Code nach erfolgreichem Passwort prüfen.
|
||||
* Kein Nonce — ausstehende Session-ID ist der Auth-Beweis.
|
||||
* Brute-Force-Schutz: max. 5 Versuche / IP / 10 Min.
|
||||
*/
|
||||
public static function handle_2fa_verify_login() {
|
||||
WBF_Auth::init();
|
||||
|
||||
$ip_key = "wbf_2fa_fail_" . md5( $_SERVER["REMOTE_ADDR"] ?? "unknown" );
|
||||
$fails = (int) get_transient( $ip_key );
|
||||
if ( $fails >= 5 ) {
|
||||
wp_send_json_error( ["message" => "Zu viele Fehlversuche. Bitte warte 10 Minuten.", "locked" => true] );
|
||||
}
|
||||
|
||||
$pending = (int) ( $_SESSION[ WBF_TOTP::SESSION_PENDING ] ?? 0 );
|
||||
if ( ! $pending ) {
|
||||
wp_send_json_error( ["message" => "Keine ausstehende Anmeldung. Bitte erneut einloggen."] );
|
||||
}
|
||||
|
||||
$code = preg_replace( "/\s+/", "", sanitize_text_field( $_POST["code"] ?? "" ) );
|
||||
$user = WBF_DB::get_user( $pending );
|
||||
|
||||
if ( ! $user ) {
|
||||
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||
wp_send_json_error( ["message" => "Ungültige Sitzung."] );
|
||||
}
|
||||
|
||||
$secret = WBF_DB::get_user_meta_single( $user->id, WBF_TOTP::META_SECRET );
|
||||
if ( empty($secret) || ! WBF_TOTP::verify( $secret, $code ) ) {
|
||||
set_transient( $ip_key, $fails + 1, 10 * MINUTE_IN_SECONDS );
|
||||
wp_send_json_error( ["message" => "Ungültiger Code. Bitte erneut versuchen."] );
|
||||
}
|
||||
|
||||
delete_transient( $ip_key );
|
||||
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||
|
||||
if ( WBF_Roles::level($user->role) < 0 ) {
|
||||
wp_send_json_error( ["message" => "Dein Konto ist gesperrt."] );
|
||||
}
|
||||
|
||||
if ( session_id() ) session_regenerate_id( true );
|
||||
$_SESSION[ WBF_Auth::SESSION_KEY ] = $user->id;
|
||||
WBF_DB::touch_last_active( $user->id );
|
||||
|
||||
if ( ! empty( $_SESSION["wbf_2fa_remember"] ) ) {
|
||||
WBF_Auth::set_remember_cookie( $user->id );
|
||||
unset( $_SESSION["wbf_2fa_remember"] );
|
||||
}
|
||||
|
||||
wp_send_json_success( [
|
||||
"display_name" => $user->display_name,
|
||||
"avatar_url" => $user->avatar_url,
|
||||
"user_id" => $user->id,
|
||||
] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
add_action( 'init', [ 'WBF_Ajax', 'init' ] );
|
||||
@@ -11,8 +11,6 @@ class WBF_Auth {
|
||||
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
|
||||
if ( ! session_id() ) {
|
||||
if ( headers_sent() ) {
|
||||
// Headers bereits gesendet — Session kann nicht sicher gestartet werden.
|
||||
// Passiert z.B. wenn WP_DEBUG=true und PHP Notices vor dem Hook ausgegeben hat.
|
||||
return;
|
||||
}
|
||||
$session_opts = [
|
||||
@@ -20,7 +18,6 @@ class WBF_Auth {
|
||||
'cookie_samesite' => 'Lax',
|
||||
'use_strict_mode' => true,
|
||||
];
|
||||
// cookie_secure nur setzen wenn HTTPS aktiv — verhindert Session-Verlust bei HTTP
|
||||
if ( is_ssl() || ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ) {
|
||||
$session_opts['cookie_secure'] = true;
|
||||
}
|
||||
@@ -50,7 +47,7 @@ class WBF_Auth {
|
||||
return WBF_DB::get_user( (int) $_SESSION[ self::SESSION_KEY ] );
|
||||
}
|
||||
|
||||
public static function login( $username_or_email, $password ) {
|
||||
public static function login( $username_or_email, $password, $remember = false ) {
|
||||
self::init();
|
||||
$user = WBF_DB::get_user_by( 'username', $username_or_email );
|
||||
if ( ! $user ) {
|
||||
@@ -60,6 +57,19 @@ class WBF_Auth {
|
||||
if ( ! password_verify( $password, $user->password ) ) {
|
||||
return array( 'success' => false, 'message' => 'Falsches Passwort.' );
|
||||
}
|
||||
|
||||
// ── 2FA-Check ─────────────────────────────────────────────────────────
|
||||
// Wenn 2FA aktiv: Login pausieren und TOTP-Code anfordern.
|
||||
// remember-Flag in Session merken, damit es nach 2FA-Verifikation gesetzt wird.
|
||||
if ( class_exists('WBF_TOTP') && WBF_TOTP::is_enabled_for( $user->id ) ) {
|
||||
$_SESSION[ WBF_TOTP::SESSION_PENDING ] = $user->id;
|
||||
if ( $remember ) {
|
||||
$_SESSION['wbf_2fa_remember'] = true;
|
||||
}
|
||||
return array( 'success' => false, '2fa_required' => true );
|
||||
}
|
||||
// ── Ende 2FA-Check ────────────────────────────────────────────────────
|
||||
|
||||
if ( WBF_Roles::level($user->role) < 0 ) {
|
||||
// Zeitlich begrenzte Sperre prüfen — automatisch aufheben wenn abgelaufen
|
||||
if ( ! empty($user->ban_until) && strtotime($user->ban_until) <= time() ) {
|
||||
@@ -70,22 +80,20 @@ class WBF_Auth {
|
||||
'ban_until' => null,
|
||||
'pre_ban_role' => '',
|
||||
]);
|
||||
// Frisch laden und einloggen
|
||||
$user = WBF_DB::get_user( $user->id );
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
if ( session_id() ) session_regenerate_id( true );
|
||||
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
||||
WBF_DB::touch_last_active( $user->id );
|
||||
return array( 'success' => true, 'user' => $user );
|
||||
}
|
||||
$reason = !empty($user->ban_reason) ? $user->ban_reason : 'Dein Konto wurde gesperrt.';
|
||||
// Zeitstempel anhängen wenn temporäre Sperre
|
||||
if ( ! empty($user->ban_until) ) {
|
||||
$until_fmt = date_i18n( 'd.m.Y \u\m H:i \U\h\r', strtotime($user->ban_until) );
|
||||
$reason .= ' (Gesperrt bis: ' . $until_fmt . ')';
|
||||
}
|
||||
return array( 'success' => false, 'banned' => true, 'message' => $reason );
|
||||
}
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
if ( session_id() ) session_regenerate_id( true );
|
||||
$_SESSION[ self::SESSION_KEY ] = $user->id;
|
||||
WBF_DB::touch_last_active( $user->id );
|
||||
return array( 'success' => true, 'user' => $user );
|
||||
@@ -115,7 +123,7 @@ class WBF_Auth {
|
||||
'avatar_url' => $avatar,
|
||||
));
|
||||
|
||||
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
|
||||
if ( session_id() ) session_regenerate_id( true );
|
||||
$_SESSION[ self::SESSION_KEY ] = $id;
|
||||
return array('success'=>true,'user'=>WBF_DB::get_user($id));
|
||||
}
|
||||
@@ -124,10 +132,14 @@ class WBF_Auth {
|
||||
self::init();
|
||||
$user_id = $_SESSION[ self::SESSION_KEY ] ?? 0;
|
||||
unset( $_SESSION[ self::SESSION_KEY ] );
|
||||
// 2FA-Pending-State ebenfalls löschen
|
||||
if ( class_exists('WBF_TOTP') ) {
|
||||
unset( $_SESSION[ WBF_TOTP::SESSION_PENDING ] );
|
||||
unset( $_SESSION['wbf_2fa_remember'] );
|
||||
}
|
||||
if ( $user_id ) {
|
||||
WBF_DB::delete_remember_token( (int)$user_id );
|
||||
}
|
||||
// Remove cookie
|
||||
if ( isset($_COOKIE['wbf_remember']) ) {
|
||||
setcookie( 'wbf_remember', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
|
||||
}
|
||||
|
||||
@@ -151,7 +151,6 @@ class WBF_DB {
|
||||
dbDelta( $sql_threads );
|
||||
dbDelta( $sql_posts );
|
||||
dbDelta( $sql_likes );
|
||||
dbDelta( $sql_reports );
|
||||
dbDelta( $sql_tags );
|
||||
dbDelta( $sql_thread_tags );
|
||||
dbDelta( $sql_messages );
|
||||
@@ -201,7 +200,6 @@ class WBF_DB {
|
||||
) $charset;";
|
||||
|
||||
// Ensure reports + notifications tables exist on existing installs
|
||||
dbDelta( $sql_reports );
|
||||
dbDelta( $sql_notifications );
|
||||
|
||||
// Einladungs-Tabelle
|
||||
@@ -604,10 +602,24 @@ class WBF_DB {
|
||||
) );
|
||||
}
|
||||
}
|
||||
// Posts zählen, User-IDs sammeln
|
||||
$posts = $wpdb->get_results($wpdb->prepare("SELECT user_id FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $id));
|
||||
$post_count = count($posts);
|
||||
$user_post_counts = [];
|
||||
foreach ($posts as $p) {
|
||||
$uid = (int)$p->user_id;
|
||||
if (!isset($user_post_counts[$uid])) $user_post_counts[$uid] = 0;
|
||||
$user_post_counts[$uid]++;
|
||||
}
|
||||
// Posts löschen
|
||||
$wpdb->delete("{$wpdb->prefix}forum_posts", ['thread_id' => $id]);
|
||||
$wpdb->delete("{$wpdb->prefix}forum_threads", ['id' => $id]);
|
||||
// Zähler anpassen
|
||||
if ( $thread->status !== 'archived' ) {
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0) WHERE id=%d", $thread->category_id));
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET thread_count=GREATEST(thread_count-1,0), post_count=GREATEST(post_count-%d,0) WHERE id=%d", $post_count, $thread->category_id));
|
||||
}
|
||||
foreach ($user_post_counts as $uid => $cnt) {
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-%d,0) WHERE id=%d", $cnt, $uid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,6 +871,8 @@ class WBF_DB {
|
||||
'object_id' => $object_id,
|
||||
'actor_id' => $actor_id,
|
||||
] );
|
||||
// MC Bridge: Ingame-Benachrichtigung auslösen wenn Spieler verknüpft ist
|
||||
do_action( 'wbf_notification_created', $user_id, $type, $object_id, $actor_id );
|
||||
}
|
||||
|
||||
public static function get_notifications( $user_id, $limit = 20 ) {
|
||||
@@ -1253,12 +1267,13 @@ class WBF_DB {
|
||||
public static function create_remember_token( $user_id ) {
|
||||
global $wpdb;
|
||||
$token = bin2hex( random_bytes(32) );
|
||||
$token_hash = hash('sha256', $token);
|
||||
$expires = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||
// Delete existing tokens for this user first
|
||||
$wpdb->delete( "{$wpdb->prefix}forum_remember_tokens", ['user_id' => $user_id] );
|
||||
$wpdb->insert( "{$wpdb->prefix}forum_remember_tokens", [
|
||||
'user_id' => $user_id,
|
||||
'token' => $token,
|
||||
'token' => $token_hash,
|
||||
'expires_at' => $expires,
|
||||
] );
|
||||
return $token;
|
||||
@@ -1268,10 +1283,11 @@ class WBF_DB {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}forum_remember_tokens";
|
||||
if ( $wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table ) return null;
|
||||
$token_hash = hash('sha256', sanitize_text_field($token));
|
||||
return $wpdb->get_row( $wpdb->prepare(
|
||||
"SELECT user_id FROM {$wpdb->prefix}forum_remember_tokens
|
||||
WHERE token=%s AND expires_at > NOW()",
|
||||
sanitize_text_field($token)
|
||||
$token_hash
|
||||
) );
|
||||
}
|
||||
|
||||
@@ -1460,11 +1476,25 @@ class WBF_DB {
|
||||
|
||||
public static function soft_delete_post( $post_id ) {
|
||||
global $wpdb;
|
||||
// Soft-Delete setzen
|
||||
$wpdb->update(
|
||||
"{$wpdb->prefix}forum_posts",
|
||||
['deleted_at' => current_time('mysql')],
|
||||
['id' => (int)$post_id]
|
||||
);
|
||||
// Zähler anpassen
|
||||
$post = $wpdb->get_row($wpdb->prepare("SELECT thread_id, user_id FROM {$wpdb->prefix}forum_posts WHERE id=%d", $post_id));
|
||||
if ($post) {
|
||||
// Thread reply_count -1
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_threads SET reply_count=GREATEST(reply_count-1,0) WHERE id=%d", $post->thread_id));
|
||||
// User post_count -1
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_users SET post_count=GREATEST(post_count-1,0) WHERE id=%d", $post->user_id));
|
||||
// Kategorie post_count -1
|
||||
$cat_id = $wpdb->get_var($wpdb->prepare("SELECT category_id FROM {$wpdb->prefix}forum_threads WHERE id=%d", $post->thread_id));
|
||||
if ($cat_id) {
|
||||
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}forum_categories SET post_count=GREATEST(post_count-1,0) WHERE id=%d", $cat_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function restore_thread( $thread_id ) {
|
||||
@@ -1587,6 +1617,18 @@ class WBF_DB {
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen einzelnen Meta-Wert zurück (oder leeren String wenn nicht vorhanden).
|
||||
*/
|
||||
public static function get_user_meta_single( $user_id, $key ) {
|
||||
global $wpdb;
|
||||
$value = $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT meta_value FROM {$wpdb->prefix}forum_user_meta WHERE user_id = %d AND meta_key = %s LIMIT 1",
|
||||
(int) $user_id, $key
|
||||
) );
|
||||
return $value !== null ? $value : '';
|
||||
}
|
||||
|
||||
public static function set_user_meta( $user_id, $key, $value ) {
|
||||
global $wpdb;
|
||||
$wpdb->replace(
|
||||
|
||||
480
includes/class-forum-mc-bridge.php
Normal file
480
includes/class-forum-mc-bridge.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
/**
|
||||
* WBF_MC_Bridge — Minecraft ↔ Forum Verknüpfung & Ingame-Benachrichtigungen
|
||||
*
|
||||
* Dieses Modul verbindet das WP Business Forum mit dem BungeeCord StatusAPI Plugin.
|
||||
*
|
||||
* Features:
|
||||
* - Account-Verknüpfung: Forum-User ↔ MC-UUID (über Token-System)
|
||||
* - Push-Benachrichtigungen: Neue Antwort/Erwähnung/PN → Ingame-Nachricht
|
||||
* - REST API Endpoints für die BungeeCord-Seite
|
||||
*
|
||||
* Einbindung in wp-business-forum.php:
|
||||
* require_once WBF_PATH . 'includes/class-forum-mc-bridge.php';
|
||||
*
|
||||
* Konfiguration in WBF-Einstellungen (Admin → Forum → Einstellungen):
|
||||
* mc_bridge_enabled = true/false
|
||||
* mc_bridge_api_url = http://server-ip:9191 (StatusAPI URL)
|
||||
* mc_bridge_api_secret = Shared Secret für API-Authentifizierung
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
class WBF_MC_Bridge {
|
||||
|
||||
/** Meta-Keys in forum_user_meta */
|
||||
const META_MC_UUID = 'mc_uuid';
|
||||
const META_MC_NAME = 'mc_name';
|
||||
const META_LINK_TOKEN = 'mc_link_token';
|
||||
const META_LINK_EXPIRY = 'mc_link_token_expires';
|
||||
|
||||
/**
|
||||
* Hooks registrieren — wird beim Plugin-Laden aufgerufen.
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook: Wird in der modifizierten WBF_DB::create_notification() gefeuert
|
||||
add_action( 'wbf_notification_created', [ __CLASS__, 'on_notification' ], 10, 4 );
|
||||
|
||||
// REST API Endpoints für BungeeCord
|
||||
add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );
|
||||
|
||||
// AJAX: Token generieren (für eingeloggte Forum-User)
|
||||
add_action( 'wp_ajax_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
|
||||
add_action( 'wp_ajax_nopriv_wbf_mc_generate_token', [ __CLASS__, 'ajax_generate_token' ] );
|
||||
|
||||
// AJAX: Verknüpfung lösen
|
||||
add_action( 'wp_ajax_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
|
||||
add_action( 'wp_ajax_nopriv_wbf_mc_unlink', [ __CLASS__, 'ajax_unlink' ] );
|
||||
|
||||
// AJAX: Link-Status prüfen (Polling im Profil nach Token-Generierung)
|
||||
add_action( 'wp_ajax_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
|
||||
add_action( 'wp_ajax_nopriv_wbf_mc_link_status', [ __CLASS__, 'ajax_link_status' ] );
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── Einstellungen ─────────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Prüft ob die MC-Bridge aktiviert ist.
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
return ! empty( $s['mc_bridge_enabled'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die StatusAPI-URL zurück (z.B. http://192.168.1.100:9191).
|
||||
*/
|
||||
private static function get_api_url() {
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
return rtrim( $s['mc_bridge_api_url'] ?? '', '/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Shared Secret zurück.
|
||||
*/
|
||||
private static function get_api_secret() {
|
||||
$s = function_exists( 'wbf_get_settings' ) ? wbf_get_settings() : [];
|
||||
return $s['mc_bridge_api_secret'] ?? '';
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ── Notification Hook ─────────────────────────────────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Wird aufgerufen wenn eine Forum-Notification erstellt wird.
|
||||
* Prüft ob der Empfänger eine MC-UUID hat und pusht die Nachricht an BungeeCord.
|
||||
*
|
||||
* @param int $user_id Forum-User ID des Empfängers
|
||||
* @param string $type Typ: 'reply', 'mention', 'message'
|
||||
* @param int $object_id Thread-ID (bei reply/mention) oder Message-ID (bei message)
|
||||
* @param int $actor_id Forum-User ID des Auslösers
|
||||
*/
|
||||
public static function on_notification( $user_id, $type, $object_id, $actor_id ) {
|
||||
if ( ! self::is_enabled() ) return;
|
||||
|
||||
$api_url = self::get_api_url();
|
||||
if ( empty( $api_url ) ) return;
|
||||
|
||||
// MC-UUID des Empfängers prüfen
|
||||
$mc_uuid = WBF_DB::get_user_meta_single( $user_id, self::META_MC_UUID );
|
||||
if ( empty( $mc_uuid ) ) return;
|
||||
|
||||
// Actor-Info laden
|
||||
$actor = WBF_DB::get_user( (int) $actor_id );
|
||||
$actor_name = $actor ? $actor->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 <token> 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 "<span class=\"wbf-mc-badge\" title=\"Minecraft: {$name_esc}\">"
|
||||
. "<img src=\"{$head_url}\" alt=\"\" width=\"16\" height=\"16\" style=\"border-radius:2px;vertical-align:middle;margin-right:4px\">"
|
||||
. "<span style=\"color:#55ff55\">{$name_esc}</span>"
|
||||
. "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierung
|
||||
add_action( 'init', [ 'WBF_MC_Bridge', 'init' ], 20 );
|
||||
@@ -928,12 +928,19 @@ class WBF_Shortcodes {
|
||||
// Aktiven Tab aus URL lesen (tab=1|2|3), Standard: 1 für eigenes, 2 für fremdes
|
||||
// Tab-ID: numerisch (1–4) oder String-Slug (z.B. 'mc' von der Forum-Bridge)
|
||||
$ptab_raw = $_GET['ptab'] ?? ($is_own ? 1 : 2);
|
||||
$shop_active = class_exists('WIS_DB');
|
||||
$shop_tab_id = 'shop';
|
||||
$allowed_tabs = [1,2,3,4];
|
||||
if ($is_own && $shop_active) $allowed_tabs[] = $shop_tab_id;
|
||||
$active_tab = ctype_digit( (string) $ptab_raw ) ? (int) $ptab_raw : sanitize_key( $ptab_raw );
|
||||
if ( is_int($active_tab) && ! in_array($active_tab, [1,2,3,4]) ) {
|
||||
if (is_int($active_tab) && !in_array($active_tab, [1,2,3,4])) {
|
||||
$active_tab = $is_own ? 1 : 2;
|
||||
}
|
||||
// Tab 1, 3, 4 und String-Tabs nur für eigenes Profil (außer Tab 2 = Aktivität)
|
||||
if ( ! $is_own && $active_tab !== 2 ) $active_tab = 2;
|
||||
if (!is_int($active_tab) && $active_tab !== $shop_tab_id && $active_tab !== 'mc') {
|
||||
$active_tab = $is_own ? 1 : 2;
|
||||
}
|
||||
// Tab 1, 3, 4, "shop" und String-Tabs nur für eigenes Profil (außer Tab 2 = Aktivität)
|
||||
if (!$is_own && $active_tab !== 2) $active_tab = 2;
|
||||
|
||||
ob_start(); ?>
|
||||
<div class="wbf-wrap">
|
||||
@@ -948,6 +955,21 @@ class WBF_Shortcodes {
|
||||
|
||||
<!-- ── SIDEBAR ─────────────────────────────────────────── -->
|
||||
<aside class="wbf-profile-sidebar">
|
||||
<!-- Banner -->
|
||||
<div class="wbf-profile-banner" id="wbfProfileBannerWrap">
|
||||
<?php if ( ! empty($profile->banner_url) ) : ?>
|
||||
<img src="<?php echo esc_url($profile->banner_url); ?>"
|
||||
alt="" id="wbfProfileBanner" class="wbf-profile-banner__img">
|
||||
<?php else : ?>
|
||||
<div class="wbf-profile-banner__placeholder"></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($is_own) : ?>
|
||||
<label class="wbf-banner-upload-btn" title="Banner ändern">
|
||||
<i class="fas fa-image"></i>
|
||||
<input type="file" id="wbfBannerFile" accept="image/*" style="display:none">
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wbf-profile-sidebar__avatar-wrap">
|
||||
<img src="<?php echo esc_url($profile->avatar_url); ?>"
|
||||
alt="<?php echo esc_attr($profile->display_name); ?>"
|
||||
@@ -994,13 +1016,13 @@ class WBF_Shortcodes {
|
||||
<?php if (!empty($profile->bio)): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-align-left"></i> Bio</span>
|
||||
<p><?php echo WBF_BBCode::render($profile->bio); ?></p>
|
||||
<div class="wbf-profile-sidebar__bio-text"><?php echo WBF_BBCode::render($profile->bio); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($profile->signature)): ?>
|
||||
<div class="wbf-profile-sidebar__section">
|
||||
<span class="wbf-profile-sidebar__section-label"><i class="fas fa-pen-nib"></i> Signatur</span>
|
||||
<p class="wbf-profile-sidebar__sig"><?php echo WBF_BBCode::render($profile->signature); ?></p>
|
||||
<div class="wbf-profile-sidebar__sig"><?php echo WBF_BBCode::render($profile->signature); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- Öffentliche Custom Fields — nach Kategorie gruppiert -->
|
||||
@@ -1113,6 +1135,12 @@ class WBF_Shortcodes {
|
||||
class="wbf-profile-tab<?php echo $active_tab===4?' active':''; ?>">
|
||||
<i class="fas fa-lock"></i> Sicherheit
|
||||
</a>
|
||||
<?php if ($shop_active): ?>
|
||||
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=shop"
|
||||
class="wbf-profile-tab<?php echo $active_tab==='shop'?' active':''; ?>">
|
||||
<i class="fas fa-shopping-cart"></i> Käufe
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// „Verbindungen" Tab — immer sichtbar (Discord eingebaut, MC optional)
|
||||
$wbf_has_connections = true;
|
||||
@@ -1124,6 +1152,116 @@ class WBF_Shortcodes {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB SHOP — Käufe (Shop-Plugin)
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ($is_own && $active_tab === 'shop' && $shop_active): ?>
|
||||
<div class="wbf-profile-card">
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-shopping-cart"></i> Deine Käufe
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
<?php
|
||||
$orders = [];
|
||||
if (class_exists('WIS_DB')) {
|
||||
global $wpdb;
|
||||
$username = $profile->username;
|
||||
$orders = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}wis_orders WHERE player_name = %s ORDER BY created_at DESC",
|
||||
$username
|
||||
));
|
||||
}
|
||||
?>
|
||||
<?php if (empty($orders)): ?>
|
||||
<p class="wbf-profile-empty">Du hast noch keine Käufe getätigt.</p>
|
||||
<?php else: ?>
|
||||
<div class="wbf-shop-orders-list">
|
||||
<table class="wbf-shop-orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">Datum</th>
|
||||
<th style="text-align:center">Anzahl</th>
|
||||
<th style="text-align:right">Gesamtpreis</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orders as $i => $order):
|
||||
$is_cancelled = strtolower($order->status) === 'cancelled' || strtolower($order->status) === 'storniert';
|
||||
$row_class = $is_cancelled ? 'wbf-shop-order-cancelled' : '';
|
||||
?>
|
||||
<tr class="wbf-shop-order-row <?php echo $row_class; ?>" data-idx="<?php echo $i; ?>">
|
||||
<td><?php echo date_i18n('d.m.Y H:i', strtotime($order->created_at)); ?></td>
|
||||
<td style="text-align:center"><?php echo (int)$order->quantity; ?></td>
|
||||
<td style="text-align:right"><?php echo number_format($order->price * $order->quantity); ?> <?php echo esc_html(get_option('wis_currency_name', 'Coins')); ?></td>
|
||||
<td style="text-align:center">
|
||||
<button class="wbf-btn wbf-btn--sm wbf-shop-order-toggle" data-idx="<?php echo $i; ?>"><i class="fas fa-chevron-down"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wbf-shop-order-details" id="wbf-shop-order-details-<?php echo $i; ?>" style="display:none">
|
||||
<td colspan="4">
|
||||
<div class="wbf-shop-order-details-inner">
|
||||
<?php /* Artikel-Zeile entfernt, da Item-Liste folgt */ ?>
|
||||
<strong>Einzelpreis:</strong> <?php echo number_format($order->price); ?> <?php echo esc_html(get_option('wis_currency_name', 'Coins')); ?><br>
|
||||
<strong>Status:</strong> <?php echo esc_html(ucfirst($order->status)); ?><br>
|
||||
<?php if (!empty($order->server)): ?><strong>Server:</strong> <?php echo esc_html($order->server); ?><br><?php endif; ?>
|
||||
<?php
|
||||
// Antwort als JSON-Items/Coupon anzeigen
|
||||
$response = $order->response;
|
||||
$decoded = null;
|
||||
if (!empty($response)) {
|
||||
$decoded = json_decode($response, true);
|
||||
}
|
||||
if (is_array($decoded) && isset($decoded['items'])) {
|
||||
echo '<strong>Gekaufte Items:</strong><ul style="margin:.3em 0 .7em 1.2em">';
|
||||
foreach ($decoded['items'] as $item) {
|
||||
$item_id = isset($item['id']) ? $item['id'] : '';
|
||||
$item_id = preg_replace('/^minecraft:/', '', $item_id);
|
||||
$amount = isset($item['amount']) ? (int)$item['amount'] : 1;
|
||||
echo '<li><span style="color:var(--c-primary)">' . esc_html($item_id) . '</span> <span style="color:var(--c-muted)">x' . $amount . '</span></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
if (isset($decoded['coupon']['code'])) {
|
||||
$c = $decoded['coupon'];
|
||||
echo '<div style="margin:.2em 0 .5em 0"><strong>Coupon:</strong> <span style="color:var(--c-success)">' . esc_html($c['code']) . '</span>';
|
||||
if (isset($c['discount'])) echo ' <span style="color:var(--c-muted)">(' . intval($c['discount']) . '% Rabatt)</span>';
|
||||
echo '</div>';
|
||||
}
|
||||
} elseif (!empty($response)) {
|
||||
echo '<strong>Antwort:</strong> ' . esc_html($response) . '<br>';
|
||||
}
|
||||
?>
|
||||
<span style="font-size:.85em;color:var(--c-muted)">Bestell-ID: <?php echo (int)$order->id; ?></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.wbf-shop-order-toggle').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var idx = btn.getAttribute('data-idx');
|
||||
var details = document.getElementById('wbf-shop-order-details-' + idx);
|
||||
if (details.style.display === 'none') {
|
||||
details.style.display = '';
|
||||
btn.querySelector('i').classList.remove('fa-chevron-down');
|
||||
btn.querySelector('i').classList.add('fa-chevron-up');
|
||||
} else {
|
||||
details.style.display = 'none';
|
||||
btn.querySelector('i').classList.remove('fa-chevron-up');
|
||||
btn.querySelector('i').classList.add('fa-chevron-down');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
TAB 1 — Profil bearbeiten + Weitere Profilangaben
|
||||
@@ -1537,6 +1675,122 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
2FA — Zwei-Faktor-Authentifizierung (TOTP)
|
||||
══════════════════════════════════════════════════ -->
|
||||
<?php if ( class_exists('WBF_TOTP') ) :
|
||||
$wbf_2fa_active = WBF_TOTP::is_enabled_for($current->id);
|
||||
?>
|
||||
<div class="wbf-profile-card wbf-2fa-card" id="wbf2faCard">
|
||||
<div class="wbf-profile-card__header" style="background:rgba(234,179,8,.07);border-bottom-color:rgba(234,179,8,.2)">
|
||||
<i class="fas fa-shield-halved" style="color:#eab308"></i>
|
||||
Zwei-Faktor-Authentifizierung (2FA)
|
||||
<?php if ( $wbf_2fa_active ): ?>
|
||||
<span class="wbf-2fa-badge wbf-2fa-badge--on"><i class="fas fa-check-circle"></i> Aktiv</span>
|
||||
<?php else: ?>
|
||||
<span class="wbf-2fa-badge wbf-2fa-badge--off"><i class="fas fa-circle-xmark"></i> Inaktiv</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wbf-profile-card__body">
|
||||
|
||||
<!-- ── 2FA bereits aktiv: Deaktivierungs-Formular ── -->
|
||||
<?php if ( $wbf_2fa_active ): ?>
|
||||
<div id="wbf2faActive">
|
||||
<p style="color:var(--c-muted);font-size:.85rem;margin-bottom:1rem">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Dein Account ist mit einem Authenticator gesichert.
|
||||
Zum Deaktivieren Passwort und aktuellen Code eingeben.
|
||||
</p>
|
||||
<div class="wbf-profile-edit-grid">
|
||||
<div class="wbf-form-row">
|
||||
<label>Aktuelles Passwort</label>
|
||||
<input type="password" id="wbf2faDisablePw" placeholder="••••••" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="wbf-form-row">
|
||||
<label>Authenticator-Code</label>
|
||||
<input type="text" id="wbf2faDisableCode" placeholder="123456"
|
||||
maxlength="6" inputmode="numeric" autocomplete="one-time-code"
|
||||
style="letter-spacing:.2em;font-size:1.15rem;font-family:monospace">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn" id="wbf2faDisableBtn"
|
||||
style="background:rgba(220,38,38,.1);color:#dc2626;border-color:rgba(220,38,38,.3)">
|
||||
<i class="fas fa-shield-xmark"></i> 2FA deaktivieren
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbf2faDisableMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<!-- ── 2FA noch nicht aktiv: Setup-Wizard ── -->
|
||||
<div id="wbf2faInactive">
|
||||
<p style="color:var(--c-muted);font-size:.85rem;margin-bottom:1rem">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Schütze deinen Account zusätzlich mit einer Authenticator-App
|
||||
(Google Authenticator, Aegis, Bitwarden, Authy, 2FAS…).
|
||||
</p>
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf2faStartBtn">
|
||||
<i class="fas fa-shield-halved"></i> 2FA einrichten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 1: QR-Code scannen -->
|
||||
<div id="wbf2faStep1" style="display:none">
|
||||
<p style="font-size:.85rem;color:var(--c-muted);margin-bottom:.75rem">
|
||||
<strong>Schritt 1:</strong> Scanne diesen QR-Code mit deiner Authenticator-App.
|
||||
</p>
|
||||
<div id="wbf2faQr" style="display:inline-block;padding:10px;background:#fff;border-radius:8px;margin-bottom:.75rem"></div>
|
||||
<p style="font-size:.8rem;color:var(--c-muted);margin-bottom:.5rem">
|
||||
Kein QR-Scanner? Gib diesen Code manuell ein:
|
||||
</p>
|
||||
<code id="wbf2faSecret" style="font-size:.9rem;letter-spacing:.1em;background:var(--c-bg-2);padding:4px 10px;border-radius:4px;user-select:all"></code>
|
||||
<div class="wbf-profile-card__footer" style="margin-top:1rem">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf2faToStep2">
|
||||
Weiter <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2: Code bestätigen -->
|
||||
<div id="wbf2faStep2" style="display:none">
|
||||
<p style="font-size:.85rem;color:var(--c-muted);margin-bottom:.75rem">
|
||||
<strong>Schritt 2:</strong> Gib den 6-stelligen Code aus deiner App ein.
|
||||
</p>
|
||||
<div class="wbf-form-row" style="max-width:220px">
|
||||
<label>Bestätigungs-Code</label>
|
||||
<input type="text" id="wbf2faVerifyCode" placeholder="123456"
|
||||
maxlength="6" inputmode="numeric" autocomplete="one-time-code"
|
||||
style="letter-spacing:.25em;font-size:1.3rem;text-align:center;font-family:monospace">
|
||||
</div>
|
||||
<div class="wbf-profile-card__footer">
|
||||
<button class="wbf-btn" id="wbf2faBackBtn" style="opacity:.7">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</button>
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf2faVerifyBtn">
|
||||
<i class="fas fa-check"></i> Bestätigen & aktivieren
|
||||
</button>
|
||||
<span class="wbf-msg" id="wbf2faVerifyMsg"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 3: Erfolg -->
|
||||
<div id="wbf2faStep3" style="display:none;text-align:center;padding:1.5rem 0">
|
||||
<div style="font-size:2.5rem;margin-bottom:.5rem">🔒</div>
|
||||
<strong style="font-size:1rem;color:var(--c-text)">2FA erfolgreich aktiviert!</strong>
|
||||
<p style="font-size:.85rem;color:var(--c-muted);margin:.5rem 0 0">
|
||||
Ab jetzt wird beim Login ein Code aus deiner App abgefragt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php endif; // 2fa_active ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; // class_exists WBF_TOTP ?>
|
||||
|
||||
<?php endif; /* end Tab 4 */ ?>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
@@ -1551,7 +1805,7 @@ class WBF_Shortcodes {
|
||||
<div class="wbf-profile-card__header">
|
||||
<i class="fas fa-plug"></i> Verbundene Dienste
|
||||
</div>
|
||||
<div class="wbf-profile-card__body" style="padding:0">
|
||||
<div class="wbf-profile-card__body wbf-connections-body">
|
||||
|
||||
<?php if ( class_exists('MC_Gallery_Forum_Bridge') ) :
|
||||
$mc_content = apply_filters('wbf_profile_tab_content', '', 'minecraft', $profile);
|
||||
@@ -1561,7 +1815,7 @@ class WBF_Shortcodes {
|
||||
<i class="fas fa-cubes" style="color:#65a30d"></i>
|
||||
</div>
|
||||
<div class="wbf-connection-card__head">
|
||||
<span class="wbf-connection-card__title">Minecraft</span>
|
||||
<span class="wbf-connection-card__title">Gallerie Verbindung</span>
|
||||
</div>
|
||||
<div class="wbf-connection-card__content">
|
||||
<?php echo $mc_content; ?>
|
||||
@@ -1569,6 +1823,201 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// ── StatusAPI Bridge: Account-Verknüpfung & Ingame-Benachrichtigungen ──
|
||||
$mc_enabled = class_exists( 'WBF_MC_Bridge' ) && WBF_MC_Bridge::is_enabled();
|
||||
$mc_uuid = $mc_enabled ? WBF_MC_Bridge::get_mc_uuid( $profile->id ) : '';
|
||||
$mc_name = $mc_enabled ? WBF_MC_Bridge::get_mc_name( $profile->id ) : '';
|
||||
$mc_linked = ! empty( $mc_uuid );
|
||||
?>
|
||||
<div class="wbf-connection-card">
|
||||
<div class="wbf-connection-card__icon" style="background:rgba(101,163,13,.15);border-color:rgba(101,163,13,.3)">
|
||||
<i class="fas fa-cubes" style="color:#65a30d"></i>
|
||||
</div>
|
||||
<div class="wbf-connection-card__head">
|
||||
<span class="wbf-connection-card__title">Minecraft InGame Verbindung</span>
|
||||
<?php if ( ! $mc_enabled ) : ?>
|
||||
<span class="wbf-connection-badge" style="color:#9ca3af;background:rgba(156,163,175,.1);border-color:rgba(156,163,175,.3)">
|
||||
<i class="fas fa-circle-xmark"></i> Nicht konfiguriert
|
||||
</span>
|
||||
<?php elseif ( $mc_linked ) : ?>
|
||||
<span class="wbf-connection-badge wbf-connection-badge--connected">
|
||||
<i class="fas fa-check-circle"></i> Verbunden
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wbf-connection-badge wbf-connection-badge--disconnected">
|
||||
<i class="fas fa-circle-xmark"></i> Nicht verbunden
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wbf-connection-card__content">
|
||||
<?php if ( ! $mc_enabled ) : ?>
|
||||
<p class="wbf-connection-card__desc" style="color:var(--c-muted)">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Die Minecraft Bridge ist noch nicht eingerichtet.
|
||||
Ein Admin muss sie zuerst in den Forum-Einstellungen aktivieren.
|
||||
</p>
|
||||
<?php elseif ( $mc_linked ) : ?>
|
||||
<div class="wbf-mc-linked-info" style="display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem">
|
||||
<img src="https://mc-heads.net/avatar/<?php echo urlencode( $mc_name ?: $mc_uuid ); ?>/40"
|
||||
alt="" width="40" height="40"
|
||||
style="border-radius:4px;image-rendering:pixelated">
|
||||
<div>
|
||||
<strong style="color:var(--c-text)"><?php echo esc_html( $mc_name ?: $mc_uuid ); ?></strong><br>
|
||||
<small style="color:var(--c-muted);font-size:.75rem"><?php echo esc_html( $mc_uuid ); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:.82rem;color:var(--c-muted);margin:.25rem 0 .75rem">
|
||||
<i class="fas fa-bell" style="color:#65a30d"></i>
|
||||
Du erhältst Ingame-Benachrichtigungen bei Antworten, Erwähnungen und PNs.
|
||||
</p>
|
||||
<div id="wbf-mc-msg" style="font-size:.82rem;margin-bottom:.5rem"></div>
|
||||
<button type="button" class="wbf-btn wbf-btn--ghost" id="wbf-mc-unlink-btn"
|
||||
onclick="wbfMcUnlink()">
|
||||
<i class="fas fa-unlink"></i> Verknüpfung aufheben
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<p class="wbf-connection-card__desc">
|
||||
Verknüpfe deinen Minecraft-Account für Ingame-Benachrichtigungen
|
||||
bei neuen Antworten, Erwähnungen und Privatnachrichten.
|
||||
</p>
|
||||
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:.75rem">
|
||||
<i class="fas fa-terminal"></i>
|
||||
Schritt 1: Token generieren →
|
||||
Schritt 2: <code>/forumlink <token></code> ingame eingeben
|
||||
</p>
|
||||
<div id="wbf-mc-token-box" style="display:none;background:var(--c-surface);border:1px solid var(--c-border);border-radius:8px;padding:.85rem 1rem;margin-bottom:.75rem">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin-bottom:.4rem">
|
||||
<span style="font-size:.75rem;color:var(--c-muted);text-transform:uppercase;letter-spacing:.05em">Dein Token</span>
|
||||
<span id="wbf-mc-token-timer" style="font-size:.75rem;color:#f97316;font-weight:600"></span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.5rem">
|
||||
<code id="wbf-mc-token-value"
|
||||
style="font-size:1.4rem;letter-spacing:.25em;font-weight:700;color:var(--c-accent);flex:1"></code>
|
||||
<button type="button" class="wbf-btn wbf-btn--sm" id="wbf-mc-copy-btn"
|
||||
onclick="wbfMcCopyToken()" title="Befehl kopieren">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top:.5rem;font-size:.8rem;color:var(--c-muted)">
|
||||
Ingame eingeben: <code id="wbf-mc-cmd-value">/forumlink </code>
|
||||
</div>
|
||||
<div style="margin-top:.5rem">
|
||||
<div style="height:4px;border-radius:2px;background:var(--c-border);overflow:hidden">
|
||||
<div id="wbf-mc-token-progress"
|
||||
style="height:100%;background:#65a30d;transition:width 1s linear;width:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wbf-mc-msg" style="font-size:.82rem;margin-bottom:.5rem"></div>
|
||||
<button type="button" class="wbf-btn wbf-btn--primary" id="wbf-mc-gen-btn"
|
||||
onclick="wbfMcGenerateToken()">
|
||||
<i class="fas fa-key"></i> Token generieren
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var _pollInterval = null;
|
||||
var _timerInterval = null;
|
||||
var _expiry = 0;
|
||||
|
||||
window.wbfMcGenerateToken = function() {
|
||||
var btn = document.getElementById('wbf-mc-gen-btn');
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generiere...';
|
||||
msg.textContent = '';
|
||||
jQuery.post(WBF.ajax_url, { action: 'wbf_mc_generate_token', nonce: WBF.nonce }, function(r) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-rotate"></i> Neuen Token generieren';
|
||||
if (r.success) {
|
||||
var token = r.data.token;
|
||||
_expiry = Math.floor(Date.now() / 1000) + ((r.data.expires_in || 15) * 60);
|
||||
document.getElementById('wbf-mc-token-value').textContent = token;
|
||||
document.getElementById('wbf-mc-cmd-value').textContent = '/forumlink ' + token;
|
||||
document.getElementById('wbf-mc-token-box').style.display = 'block';
|
||||
wbfMcStartTimer((r.data.expires_in || 15) * 60);
|
||||
wbfMcStartPolling();
|
||||
} else {
|
||||
if (r.data && r.data.linked) {
|
||||
msg.innerHTML = '<span style="color:#16a34a"><i class="fas fa-check-circle"></i> ' + (r.data.message || 'Bereits verknüpft.') + '</span>';
|
||||
setTimeout(function(){ location.reload(); }, 1500);
|
||||
} else {
|
||||
msg.innerHTML = '<span style="color:#dc2626"><i class="fas fa-circle-xmark"></i> ' + (r.data && r.data.message ? r.data.message : 'Fehler') + '</span>';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function wbfMcStartTimer(seconds) {
|
||||
clearInterval(_timerInterval);
|
||||
var timerEl = document.getElementById('wbf-mc-token-timer');
|
||||
var progressEl = document.getElementById('wbf-mc-token-progress');
|
||||
var total = seconds;
|
||||
_timerInterval = setInterval(function() {
|
||||
var remaining = _expiry - Math.floor(Date.now() / 1000);
|
||||
if (remaining <= 0) {
|
||||
clearInterval(_timerInterval); clearInterval(_pollInterval);
|
||||
if (timerEl) timerEl.textContent = 'Abgelaufen';
|
||||
if (progressEl) progressEl.style.width = '0%';
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#f97316"><i class="fas fa-clock"></i> Token abgelaufen — bitte neuen generieren.</span>';
|
||||
return;
|
||||
}
|
||||
var m = Math.floor(remaining / 60), s = remaining % 60;
|
||||
if (timerEl) timerEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;
|
||||
if (progressEl) {
|
||||
progressEl.style.width = Math.max(0, (remaining / total) * 100) + '%';
|
||||
progressEl.style.background = remaining < 60 ? '#dc2626' : remaining < 180 ? '#f97316' : '#65a30d';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function wbfMcStartPolling() {
|
||||
clearInterval(_pollInterval);
|
||||
_pollInterval = setInterval(function() {
|
||||
jQuery.post(WBF.ajax_url, { action: 'wbf_mc_link_status', nonce: WBF.nonce }, function(r) {
|
||||
if (r.success && r.data && r.data.linked) {
|
||||
clearInterval(_pollInterval); clearInterval(_timerInterval);
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#16a34a"><i class="fas fa-check-circle"></i> ✓ Verknüpft mit <strong>' + (r.data.mc_name || r.data.mc_uuid) + '</strong>! Seite lädt neu...</span>';
|
||||
setTimeout(function(){ location.reload(); }, 1800);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
window.wbfMcCopyToken = function() {
|
||||
var cmd = document.getElementById('wbf-mc-cmd-value').textContent;
|
||||
var btn = document.getElementById('wbf-mc-copy-btn');
|
||||
var done = function() { btn.innerHTML = '<i class="fas fa-check"></i>'; btn.style.color = '#16a34a'; setTimeout(function(){ btn.innerHTML = '<i class="fas fa-copy"></i>'; btn.style.color = ''; }, 2000); };
|
||||
if (navigator.clipboard) { navigator.clipboard.writeText(cmd).then(done); }
|
||||
else { var ta = document.createElement('textarea'); ta.value = cmd; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done(); }
|
||||
};
|
||||
|
||||
window.wbfMcUnlink = function() {
|
||||
if (!confirm('Minecraft-Verknüpfung wirklich aufheben?')) return;
|
||||
var btn = document.getElementById('wbf-mc-unlink-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Trenne...';
|
||||
jQuery.post(WBF.ajax_url, { action: 'wbf_mc_unlink', nonce: WBF.nonce }, function(r) {
|
||||
if (r.success) {
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#16a34a"><i class="fas fa-check"></i> ' + (r.data.message || 'Verknüpfung aufgehoben.') + '</span>';
|
||||
setTimeout(function(){ location.reload(); }, 1200);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-unlink"></i> Verknüpfung aufheben';
|
||||
var msg = document.getElementById('wbf-mc-msg');
|
||||
if (msg) msg.innerHTML = '<span style="color:#dc2626">Fehler: ' + (r.data && r.data.message ? r.data.message : 'Unbekannt') + '</span>';
|
||||
}
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// ── Discord-Card (eingebaut, kein extra Plugin nötig) ──────────────
|
||||
$discord_meta = WBF_DB::get_user_meta( $profile->id );
|
||||
@@ -1657,6 +2106,7 @@ class WBF_Shortcodes {
|
||||
</div><!-- /.wbf-profile-layout -->
|
||||
</div>
|
||||
</div>
|
||||
<?php self::render_auth_modal(); ?>
|
||||
<?php return ob_get_clean();
|
||||
}
|
||||
|
||||
@@ -2579,16 +3029,7 @@ class WBF_Shortcodes {
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (WBF_Roles::level($current->role) >= 80): // Nur Admins/Mods ?>
|
||||
<div style="margin-top:2.5rem;text-align:center">
|
||||
<button class="wbf-btn wbf-btn--primary" id="wbf-discord-sync-btn" type="button">
|
||||
<i class="fab fa-discord"></i> Discord-Rollen-Sync manuell anstoßen
|
||||
</button>
|
||||
<span id="wbf-discord-sync-msg" style="margin-left:1rem;font-size:.95em;display:none"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($members)): ?>
|
||||
<div class="wbf-empty" style="grid-column:1/-1">
|
||||
<i class="fas fa-users-slash"></i>
|
||||
|
||||
179
includes/class-forum-totp.php
Normal file
179
includes/class-forum-totp.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
/**
|
||||
* WBF_TOTP — RFC 6238 Time-based One-Time Password (TOTP)
|
||||
*
|
||||
* Keine externe Bibliothek nötig — reines PHP 7.0+.
|
||||
* Kompatibel mit: Google Authenticator, Aegis, Authy, Bitwarden, 2FAS, etc.
|
||||
*
|
||||
* Secrets werden in forum_user_meta gespeichert (meta_key = 'totp_secret').
|
||||
* Kein Schema-Change an der Haupt-Usertabelle nötig.
|
||||
*/
|
||||
class WBF_TOTP {
|
||||
|
||||
const DIGITS = 6;
|
||||
const PERIOD = 30; // Sekunden pro Schritt
|
||||
const WINDOW = 1; // ±1 Step Toleranz (= ±30 s Uhrabweichung OK)
|
||||
const SECRET_LEN = 20; // Bytes → 32 Base32-Zeichen
|
||||
|
||||
// Meta-Keys
|
||||
const META_SECRET = 'totp_secret';
|
||||
const META_PENDING = 'totp_secret_pending';
|
||||
|
||||
// Session-Key für ausstehenden Login
|
||||
const SESSION_PENDING = 'wbf_2fa_pending';
|
||||
|
||||
// ── Secret ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Erzeugt einen neuen, kryptografisch sicheren Base32-Secret.
|
||||
* @return string z.B. "JBSWY3DPEBLW64TMMQ======"
|
||||
*/
|
||||
public static function generate_secret() {
|
||||
return self::base32_encode( random_bytes( self::SECRET_LEN ) );
|
||||
}
|
||||
|
||||
// ── Verifikation ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Prüft ob $code für $secret zum aktuellen Zeitfenster passt.
|
||||
*
|
||||
* @param string $secret Base32-Secret des Users
|
||||
* @param string $code 6-stelliger Code aus der Authenticator-App
|
||||
* @param int $window Anzahl Steps Toleranz (default = 1 = ±30 s)
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify( $secret, $code, $window = self::WINDOW ) {
|
||||
// Leerzeichen tolerieren (z.B. "123 456")
|
||||
$code = preg_replace( '/\s+/', '', (string) $code );
|
||||
if ( strlen($code) !== self::DIGITS ) return false;
|
||||
if ( ! ctype_digit($code) ) return false;
|
||||
|
||||
$key = self::base32_decode( $secret );
|
||||
if ( empty($key) ) return false;
|
||||
|
||||
$ts = (int) floor( time() / self::PERIOD );
|
||||
|
||||
for ( $i = -$window; $i <= $window; $i++ ) {
|
||||
$expected = self::hotp( $key, $ts + $i );
|
||||
// Timing-safe Vergleich
|
||||
if ( hash_equals( $expected, $code ) ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── HOTP-Kern (RFC 4226) ─────────────────────────────────────────────────
|
||||
|
||||
private static function hotp( $key, $counter ) {
|
||||
// 64-bit Big-Endian Counter
|
||||
$msg = pack( 'N', 0 ) . pack( 'N', $counter );
|
||||
|
||||
$hash = hash_hmac( 'sha1', $msg, $key, true );
|
||||
$offset = ord( $hash[19] ) & 0x0f;
|
||||
|
||||
$code = (
|
||||
( ord($hash[$offset ]) & 0x7f ) << 24 |
|
||||
( ord($hash[$offset + 1]) & 0xff ) << 16 |
|
||||
( ord($hash[$offset + 2]) & 0xff ) << 8 |
|
||||
( ord($hash[$offset + 3]) & 0xff )
|
||||
) % ( 10 ** self::DIGITS );
|
||||
|
||||
return str_pad( (string) $code, self::DIGITS, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
// ── otpauth:// URI ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt die otpauth:// URI zurück — wird vom QR-Code-Generator verwendet.
|
||||
*
|
||||
* @param string $username Forum-Benutzername
|
||||
* @param string $secret Base32-Secret
|
||||
* @param string|null $issuer Anzeigename in der App (default: Blogname)
|
||||
* @return string
|
||||
*/
|
||||
public static function get_otpauth_uri( $username, $secret, $issuer = null ) {
|
||||
if ( ! $issuer ) {
|
||||
$issuer = html_entity_decode( get_bloginfo('name'), ENT_QUOTES ) ?: 'WP Business Forum';
|
||||
}
|
||||
$label = rawurlencode( $issuer . ':' . $username );
|
||||
return 'otpauth://totp/' . $label . '?'
|
||||
. 'secret=' . rawurlencode( $secret )
|
||||
. '&issuer=' . rawurlencode( $issuer )
|
||||
. '&algorithm=SHA1'
|
||||
. '&digits=' . self::DIGITS
|
||||
. '&period=' . self::PERIOD;
|
||||
}
|
||||
|
||||
// ── User-Helfer ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Ist 2FA für diesen User aktiv? */
|
||||
public static function is_enabled_for( $user_id ) {
|
||||
$s = WBF_DB::get_user_meta_single( (int) $user_id, self::META_SECRET );
|
||||
return ! empty( $s );
|
||||
}
|
||||
|
||||
/**
|
||||
* 2FA für einen User deaktivieren (löscht Secret + ggf. pending Secret).
|
||||
* Kann von Admin und User selbst (nach Verifikation) aufgerufen werden.
|
||||
*/
|
||||
public static function disable_for( $user_id ) {
|
||||
global $wpdb;
|
||||
$uid = (int) $user_id;
|
||||
$wpdb->delete(
|
||||
"{$wpdb->prefix}forum_user_meta",
|
||||
[ 'user_id' => $uid, 'meta_key' => self::META_SECRET ],
|
||||
[ '%d', '%s' ]
|
||||
);
|
||||
$wpdb->delete(
|
||||
"{$wpdb->prefix}forum_user_meta",
|
||||
[ 'user_id' => $uid, 'meta_key' => self::META_PENDING ],
|
||||
[ '%d', '%s' ]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Base32 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static $b32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
public static function base32_encode( $input ) {
|
||||
$output = '';
|
||||
$buf = 0;
|
||||
$buf_bits = 0;
|
||||
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
|
||||
$buf = ( $buf << 8 ) | ord( $input[$i] );
|
||||
$buf_bits += 8;
|
||||
while ( $buf_bits >= 5 ) {
|
||||
$buf_bits -= 5;
|
||||
$output .= self::$b32[ ( $buf >> $buf_bits ) & 0x1f ];
|
||||
}
|
||||
}
|
||||
if ( $buf_bits > 0 ) {
|
||||
$output .= self::$b32[ ( $buf << ( 5 - $buf_bits ) ) & 0x1f ];
|
||||
}
|
||||
// Padding to multiple of 8
|
||||
while ( strlen($output) % 8 !== 0 ) $output .= '=';
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function base32_decode( $input ) {
|
||||
// Leerzeichen & Padding entfernen, Uppercase
|
||||
$input = strtoupper( preg_replace( '/\s+/', '', $input ) );
|
||||
$input = rtrim( $input, '=' );
|
||||
$map = array_flip( str_split( self::$b32 ) );
|
||||
|
||||
$output = '';
|
||||
$buf = 0;
|
||||
$bits = 0;
|
||||
for ( $i = 0, $len = strlen($input); $i < $len; $i++ ) {
|
||||
if ( ! isset( $map[ $input[$i] ] ) ) continue; // ungültiges Zeichen ignorieren
|
||||
$buf = ( $buf << 5 ) | $map[ $input[$i] ];
|
||||
$bits += 5;
|
||||
if ( $bits >= 8 ) {
|
||||
$bits -= 8;
|
||||
$output .= chr( ( $buf >> $bits ) & 0xff );
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
32
includes/forum-statusapi.php
Normal file
32
includes/forum-statusapi.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// Ingame-Benachrichtigung via StatusAPI
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
|
||||
|
||||
function wbf_notify_ingame($player, $message) {
|
||||
// Einstellungen laden
|
||||
$settings = function_exists('wbf_get_settings') ? wbf_get_settings() : [];
|
||||
$enabled = !empty($settings['mc_bridge_enabled']);
|
||||
$api_url = trim($settings['mc_bridge_api_url'] ?? '');
|
||||
$api_secret = trim($settings['mc_bridge_api_secret'] ?? '');
|
||||
if (!$enabled || !$api_url || !$api_secret) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = rtrim($api_url, '/') . '/notify-pn';
|
||||
$data = [
|
||||
'player' => $player,
|
||||
'message' => $message
|
||||
];
|
||||
$args = [
|
||||
'body' => json_encode($data),
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-API-Key' => $api_secret,
|
||||
],
|
||||
'timeout' => 2,
|
||||
'data_format' => 'body',
|
||||
];
|
||||
wp_remote_post($url, $args);
|
||||
}
|
||||
Reference in New Issue
Block a user