Update from Git Manager GUI

This commit is contained in:
2026-03-30 20:41:51 +02:00
parent 56f8c01b52
commit f4d0ec73c0
7 changed files with 1384 additions and 112 deletions

View File

@@ -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 );
@@ -603,11 +552,11 @@ class WBF_Ajax {
IMAGETYPE_GIF => 'image/gif',
IMAGETYPE_WEBP => 'image/webp',
];
$et = @exif_imagetype( $tmp );
$real_mime = $et_map[$et] ?? '';
$et = @exif_imagetype( $tmp );
$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' ] );