Update from Git Manager GUI

This commit is contained in:
2026-03-22 00:40:18 +01:00
parent ead2f3a62a
commit 65d2371239
5 changed files with 1912 additions and 173 deletions

View File

@@ -18,6 +18,9 @@ class WBF_Ajax {
'wbf_create_poll',
'wbf_toggle_bookmark',
'wbf_set_thread_prefix',
'wbf_toggle_ignore',
'wbf_change_email',
'wbf_save_notification_prefs',
];
foreach ($actions as $action) {
add_action('wp_ajax_nopriv_' . $action, [__CLASS__, str_replace('wbf_','handle_',$action)]);
@@ -411,9 +414,37 @@ class WBF_Ajax {
if (empty($_FILES['avatar'])) wp_send_json_error(['message'=>'Keine Datei.']);
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
$mime = $_FILES['avatar']['type'] ?? '';
if (!in_array($mime, $allowed_types)) wp_send_json_error(['message'=>'Nur JPG, PNG, GIF und WebP erlaubt.']);
if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
// Dateigröße vor dem MIME-Check prüfen
if ( $_FILES['avatar']['size'] > 2 * 1024 * 1024 ) {
wp_send_json_error(['message'=>'Maximale Dateigröße: 2 MB.']);
}
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] kommt vom Client
// und ist beliebig fälschbar (z.B. PHP-Datei als image/jpeg getarnt).
// finfo_file() liest den echten Magic-Byte der temporären Datei.
$tmp = $_FILES['avatar']['tmp_name'] ?? '';
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
wp_send_json_error(['message'=>'Ungültiger Datei-Upload.']);
}
if ( function_exists('finfo_open') ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$real_mime = finfo_file( $finfo, $tmp );
finfo_close( $finfo );
} else {
// Fallback: exif_imagetype() wenn finfo nicht verfügbar
$et_map = [
IMAGETYPE_JPEG => 'image/jpeg',
IMAGETYPE_PNG => 'image/png',
IMAGETYPE_GIF => 'image/gif',
IMAGETYPE_WEBP => 'image/webp',
];
$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.']);
}
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -468,16 +499,36 @@ class WBF_Ajax {
// Nur Bilder erlauben
$allowed_types = ['image/jpeg','image/png','image/gif','image/webp'];
$mime = $_FILES['image']['type'] ?? '';
if ( ! in_array($mime, $allowed_types) ) {
wp_send_json_error(['message' => 'Nur JPG, PNG, GIF und WebP erlaubt.']);
}
// Max 5 MB
// Max 5 MB — Größe zuerst prüfen bevor teure MIME-Erkennung läuft
if ( $_FILES['image']['size'] > 5 * 1024 * 1024 ) {
wp_send_json_error(['message' => 'Maximale Dateigröße: 5 MB.']);
}
// Server-seitige MIME-Typ-Prüfung — $_FILES['type'] ist client-kontrolliert
// und kann beliebig auf 'image/jpeg' gesetzt werden, auch für .php-Dateien.
$tmp = $_FILES['image']['tmp_name'] ?? '';
if ( ! $tmp || ! is_uploaded_file( $tmp ) ) {
wp_send_json_error(['message' => 'Ungültiger Datei-Upload.']);
}
if ( function_exists('finfo_open') ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$real_mime = finfo_file( $finfo, $tmp );
finfo_close( $finfo );
} else {
$et_map = [
IMAGETYPE_JPEG => 'image/jpeg',
IMAGETYPE_PNG => 'image/png',
IMAGETYPE_GIF => 'image/gif',
IMAGETYPE_WEBP => 'image/webp',
];
$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.']);
}
require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
@@ -588,6 +639,18 @@ class WBF_Ajax {
$is_mod = WBF_DB::can($user, 'delete_post');
if ( ! $is_own && ! $is_mod ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
// Post-Bearbeitungslimit prüfen — gilt auch für Thread-Erstbeiträge
// (spiegelt identisches Verhalten zu handle_edit_post() wider)
if ( $is_own && ! $is_mod ) {
$limit_min = (int)( wbf_get_settings()['post_edit_limit'] ?? 30 );
if ( $limit_min > 0 ) {
$age_min = ( time() - strtotime( $thread->created_at ) ) / 60;
if ( $age_min > $limit_min ) {
wp_send_json_error(['message' => "Bearbeitung nur innerhalb von {$limit_min} Minuten nach dem Erstellen möglich."]);
}
}
}
global $wpdb;
$wpdb->update(
"{$wpdb->prefix}forum_threads",
@@ -682,6 +745,11 @@ class WBF_Ajax {
if ($to_id === (int)$user->id) wp_send_json_error(['message'=>'Du kannst dir nicht selbst schreiben.']);
if (!WBF_DB::get_user($to_id)) wp_send_json_error(['message'=>'Empfänger nicht gefunden.']);
// DM-Blockierung: Empfänger hat Sender ignoriert
if ( WBF_DB::is_ignored( $to_id, $user->id ) ) {
wp_send_json_error(['message' => 'Diese Person akzeptiert keine Nachrichten von dir.']);
}
$id = WBF_DB::send_message($user->id, $to_id, $content);
// Notify recipient
WBF_DB::create_notification($to_id, 'message', $id, $user->id);
@@ -739,14 +807,22 @@ class WBF_Ajax {
// ── User-Autocomplete (für @Erwähnungen + DM) ─────────────────────────────
public static function handle_user_suggest() {
// Nur eingeloggte Nutzer dürfen die User-Suche nutzen
// (verhindert Enumeration aller Usernamen + Rollendaten durch Gäste)
if ( ! WBF_Auth::is_forum_logged_in() ) {
wp_send_json_success(['users'=>[]]);
}
$q = sanitize_text_field($_POST['q'] ?? $_GET['q'] ?? '');
if (mb_strlen($q) < 1) wp_send_json_success(['users'=>[]]);
global $wpdb;
$like = $wpdb->esc_like($q) . '%';
// Rolle wird bewusst NICHT zurückgegeben — nicht für Autocomplete nötig
// und verhindert Informationsleck über Rollen-Verteilung im Forum.
$users = $wpdb->get_results($wpdb->prepare(
"SELECT id, username, display_name, avatar_url, role
"SELECT id, username, display_name, avatar_url
FROM {$wpdb->prefix}forum_users
WHERE username LIKE %s OR display_name LIKE %s
WHERE (username LIKE %s OR display_name LIKE %s)
AND role != 'banned'
ORDER BY display_name ASC LIMIT 8",
$like, $like
));
@@ -817,6 +893,13 @@ class WBF_Ajax {
private static function send_notification_email( $to_user, $type, $actor_name, $extra = [] ) {
if ( ! $to_user || empty($to_user->email) ) return;
// Prüfen ob der User diesen Benachrichtigungstyp aktiviert hat
// Standard: alle aktiviert (1). User kann im Profil deaktivieren (0).
$pref_key = 'notify_' . $type; // notify_reply, notify_mention, notify_message
$meta = WBF_DB::get_user_meta( $to_user->id );
// Nur deaktivieren wenn explizit auf '0' gesetzt — Standard ist aktiviert
if ( isset($meta[$pref_key]) && $meta[$pref_key] === '0' ) return;
$blog_name = get_bloginfo('name');
$forum_url = wbf_get_forum_url();
$from_email = get_option('admin_email');
@@ -897,6 +980,17 @@ class WBF_Ajax {
$email = sanitize_email( $_POST['email'] ?? '' );
if ( ! is_email($email) ) wp_send_json_error(['message'=>'Ungültige E-Mail-Adresse.']);
// ── Rate-Limiting: max. 1 Reset-Mail pro E-Mail-Adresse alle 15 Minuten ──
// Verhindert, dass ein Angreifer tausende Reset-Mails pro Sekunde
// für beliebige Adressen triggert und den Mail-Server überlastet.
$rate_key = 'wbf_pwreset_' . md5( strtolower( $email ) );
if ( get_transient( $rate_key ) !== false ) {
// Immer Erfolg melden — kein Leak ob Rate-Limit oder kein Account
wp_send_json_success(['message'=>'Falls diese E-Mail registriert ist, wurde eine E-Mail gesendet.']);
}
// Cooldown setzen — 15 Minuten
set_transient( $rate_key, 1, 15 * MINUTE_IN_SECONDS );
$user = WBF_DB::get_user_by('email', $email);
// Immer Erfolg melden (kein User-Enumeration)
if ( ! $user ) {
@@ -922,7 +1016,13 @@ class WBF_Ajax {
}
public static function handle_reset_password() {
self::verify();
// Kein self::verify() hier — Gäste haben keine Forum-Session.
// Das Reset-Token selbst authentifiziert die Anfrage.
// Wir prüfen trotzdem den WP-Nonce als CSRF-Schutz; dieser wird
// von wp_localize_script für alle Besucher (auch Gäste) generiert.
if ( ! check_ajax_referer( 'wbf_nonce', 'nonce', false ) ) {
wp_send_json_error(['message' => 'Sicherheitsfehler.']);
}
$token = sanitize_text_field( $_POST['token'] ?? '' );
$password = $_POST['password'] ?? '';
$password2= $_POST['password2'] ?? '';
@@ -1207,6 +1307,90 @@ class WBF_Ajax {
wp_send_json_success(['prefix' => $prefix]);
}
// ── E-Mail-Adresse ändern ─────────────────────────────────────────────────
public static function handle_change_email() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$new_email = sanitize_email( $_POST['new_email'] ?? '' );
$password = $_POST['password'] ?? '';
if ( ! is_email($new_email) ) {
wp_send_json_error(['message' => 'Ungültige E-Mail-Adresse.']);
}
if ( empty($password) ) {
wp_send_json_error(['message' => 'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
}
if ( ! password_verify($password, $user->password) ) {
wp_send_json_error(['message' => 'Falsches Passwort.']);
}
if ( strtolower($new_email) === strtolower($user->email) ) {
wp_send_json_error(['message' => 'Das ist bereits deine aktuelle E-Mail-Adresse.']);
}
// Prüfen ob E-Mail bereits vergeben
$existing = WBF_DB::get_user_by('email', $new_email);
if ( $existing && (int)$existing->id !== (int)$user->id ) {
wp_send_json_error(['message' => 'Diese E-Mail-Adresse ist bereits registriert.']);
}
WBF_DB::update_user($user->id, ['email' => $new_email]);
wp_send_json_success(['message' => 'E-Mail-Adresse erfolgreich geändert.']);
}
// ── Benachrichtigungs-Einstellungen speichern ─────────────────────────────
public static function handle_save_notification_prefs() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error(['message' => 'Nicht eingeloggt.']);
$allowed = ['notify_reply', 'notify_mention', 'notify_message'];
foreach ( $allowed as $key ) {
// 1 wenn Checkbox aktiviert, 0 wenn deaktiviert
$val = isset($_POST[$key]) && $_POST[$key] === '1' ? '1' : '0';
WBF_DB::set_user_meta($user->id, $key, $val);
}
wp_send_json_success(['message' => 'Benachrichtigungs-Einstellungen gespeichert.']);
}
// ── User ignorieren / Ignorierung aufheben ────────────────────────────────
public static function handle_toggle_ignore() {
self::verify();
$user = WBF_Auth::get_current_user();
if ( ! $user ) wp_send_json_error( ['message' => 'Nicht eingeloggt.'] );
$ignored_id = (int)( $_POST['ignored_id'] ?? 0 );
if ( ! $ignored_id ) wp_send_json_error( ['message' => 'Ungültiger Nutzer.'] );
if ( $ignored_id === (int)$user->id ) {
wp_send_json_error( ['message' => 'Du kannst dich nicht selbst ignorieren.'] );
}
$target = WBF_DB::get_user( $ignored_id );
if ( ! $target ) wp_send_json_error( ['message' => 'Nutzer nicht gefunden.'] );
// Prüfen ob diese Rolle geblockt werden darf (konfigurierbar in den Einstellungen)
if ( ! wbf_can_be_ignored( $target ) ) {
$role_label = WBF_Roles::get($target->role)['label'] ?? $target->role;
wp_send_json_error( ['message' => 'Nutzer mit der Rolle "' . $role_label . '" können nicht ignoriert werden.'] );
}
$now_ignored = WBF_DB::toggle_ignore( $user->id, $ignored_id );
wp_send_json_success( [
'ignored' => $now_ignored,
'ignored_id' => $ignored_id,
'display_name' => $target->display_name,
'message' => $now_ignored
? esc_html( $target->display_name ) . ' wird jetzt ignoriert.'
: 'Ignorierung von ' . esc_html( $target->display_name ) . ' aufgehoben.',
] );
}
}
add_action( 'init', [ 'WBF_Ajax', 'init' ] );