get_col( "DESCRIBE {$wpdb->prefix}forum_users" ); if ( ! in_array( 'profile_public', $cols ) ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}forum_users ADD COLUMN profile_public TINYINT(1) NOT NULL DEFAULT 1" ); // Alle bestehenden User explizit auf öffentlich setzen $wpdb->query( "UPDATE {$wpdb->prefix}forum_users SET profile_public = 1 WHERE profile_public IS NULL" ); } update_option( 'wbf_db_version', 2 ); } }, 10 ); // ── Session frühzeitig starten (PHP 8.3 Fix) ──────────────────────────────── // session_start() MUSS vor jedem HTML-Output laufen. // plugins_loaded (Prio 1) ist der früheste sichere Zeitpunkt in WordPress. // Der 'init'-Hook (in class-forum-auth.php) läuft als Fallback weiterhin, // aber dieser frühe Aufruf verhindert den PHP 8.3 E_WARNING "headers already sent". add_action( 'plugins_loaded', function() { WBF_Auth::init(); }, 1 ); // ── Superadmin-Sync ─────────────────────────────────────────────────────────── add_action( 'wp_login', function() { WBF_Roles::sync_superadmin(); } ); add_action( 'init', function() { WBF_Roles::sync_superadmin(); } ); // ── Body-Klasse ─────────────────────────────────────────────────────────────── add_filter( 'body_class', function( $classes ) { global $post; if ( $post && has_shortcode( $post->post_content, 'business_forum' ) ) { $classes[] = 'wbf-forum-page'; } return $classes; }); // ── Cron: Abgelaufene Sperren aufheben ──────────────────────────────────────── add_action( 'wbf_check_expired_bans', function() { WBF_DB::check_expired_bans(); } ); if ( ! wp_next_scheduled( 'wbf_check_expired_bans' ) ) { wp_schedule_event( time(), 'hourly', 'wbf_check_expired_bans' ); } register_deactivation_hook( __FILE__, function() { wp_clear_scheduled_hook( 'wbf_check_expired_bans' ); wp_clear_scheduled_hook( 'wbf_check_for_updates' ); } ); // ── Forum-URL Hilfsfunktion ─────────────────────────────────────────────────── function wbf_get_forum_url() { // 1. Gespeicherte Seite aus dem Setup-Wizard $page_id = get_option('wbf_forum_page_id'); if ( $page_id ) { $url = get_permalink( $page_id ); if ( $url ) return $url; } // 2. Fallback: Seite mit [business_forum] Shortcode suchen (direkt im post_content) global $wpdb; $page_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND post_content LIKE %s LIMIT 1", '%[business_forum]%' ) ); if ( $page_id ) return get_permalink( $page_id ); // 3. Letzter Fallback: aktuelle Seite return home_url('/'); } // ── Assets ──────────────────────────────────────────────────────────────────── add_action( 'wp_enqueue_scripts', function() { wp_enqueue_style( 'wbf-style', WBF_URL . 'assets/css/forum-style.css', [], WBF_VERSION ); wp_enqueue_script( 'wbf-script', WBF_URL . 'assets/js/forum-script.js', ['jquery'], WBF_VERSION, true ); // 2FA: QR-Code-Bibliothek (nur laden wenn User eingeloggt oder auf Loginseite) wp_enqueue_script( 'qrcodejs', 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js', [], '1.0.0', true ); wp_add_inline_script( 'wbf-script', wbf_get_2fa_inline_js(), 'after' ); $wbf_user = WBF_Auth::get_current_user(); if ( $wbf_user ) { WBF_DB::touch_last_active( $wbf_user->id ); } wp_localize_script( 'wbf-script', 'WBF', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('wbf_nonce'), 'logged_in' => WBF_Auth::is_forum_logged_in() ? 'yes' : 'no', 'auto_logout_minutes' => (int)( wbf_get_settings()['auto_logout_minutes'] ?? 30 ), 'my_id' => $wbf_user ? (int)$wbf_user->id : 0, 'unread_dm' => $wbf_user ? WBF_DB::count_unread_messages($wbf_user->id) : 0, 'forum_url' => wbf_get_forum_url(), 'reactions' => WBF_DB::get_allowed_reactions(), ]); }); // ══════════════════════════════════════════════════════════════════════════════ // ── Update-Checker ──────────────────────────────────────────────────────────── // Prüft täglich gegen die Gitea-Releases-API ob eine neue Version verfügbar ist. // Releases-URL: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases // ══════════════════════════════════════════════════════════════════════════════ define( 'WBF_UPDATE_API', 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Business-Forum/releases?limit=1&page=1' ); define( 'WBF_RELEASES_PAGE', 'https://git.viper.ipv64.net/M_Viper/WP-Business-Forum/releases' ); define( 'WBF_UPDATE_TRANSIENT','wbf_update_check' ); /** * Holt die neueste Release-Info von Gitea (gecacht per Transient, 12h). * Gibt null zurück wenn kein Update verfügbar oder API nicht erreichbar. * * @return array|null ['version'=>string, 'url'=>string, 'name'=>string, 'published'=>string, 'body'=>string] */ function wbf_get_latest_release() { $cached = get_transient( WBF_UPDATE_TRANSIENT ); if ( $cached !== false ) { return $cached ?: null; // false = noch nie gecacht, '' = kein Update } $response = wp_remote_get( WBF_UPDATE_API, [ 'timeout' => 8, 'user-agent' => 'WP-Business-Forum/' . WBF_VERSION . '; ' . get_bloginfo('url'), 'sslverify' => true, ] ); if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200 ) { // Bei Fehler 1h warten bevor erneut versucht set_transient( WBF_UPDATE_TRANSIENT, '', HOUR_IN_SECONDS ); return null; } $body = wp_remote_retrieve_body( $response ); $releases = json_decode( $body, true ); if ( empty($releases) || ! is_array($releases) || empty($releases[0]) ) { set_transient( WBF_UPDATE_TRANSIENT, '', 12 * HOUR_IN_SECONDS ); return null; } $latest = $releases[0]; $version = ltrim( $latest['tag_name'] ?? '', 'v' ); // "v1.2.0" → "1.2.0" $info = [ 'version' => $version, 'url' => $latest['html_url'] ?? WBF_RELEASES_PAGE, 'name' => $latest['name'] ?? $latest['tag_name'] ?? $version, 'published' => $latest['published_at'] ?? '', 'body' => wp_strip_all_tags( $latest['body'] ?? '' ), ]; // 12 Stunden cachen set_transient( WBF_UPDATE_TRANSIENT, $info, 12 * HOUR_IN_SECONDS ); return $info; } /** * Prüft ob ein Update verfügbar ist. * Gibt die Release-Info zurück wenn Gitea-Version > installierte Version. */ function wbf_update_available() { $latest = wbf_get_latest_release(); if ( ! $latest || empty($latest['version']) ) return null; if ( version_compare( $latest['version'], WBF_VERSION, '>' ) ) { return $latest; } return null; } // ── Cron: täglich Update prüfen (Cache warm halten) ────────────────────────── add_action( 'wbf_check_for_updates', function() { delete_transient( WBF_UPDATE_TRANSIENT ); wbf_get_latest_release(); } ); if ( ! wp_next_scheduled( 'wbf_check_for_updates' ) ) { wp_schedule_event( time(), 'twicedaily', 'wbf_check_for_updates' ); } // ── Admin-Notice wenn Update verfügbar ─────────────────────────────────────── add_action( 'admin_notices', function() { if ( ! current_user_can('manage_options') ) return; $update = wbf_update_available(); if ( ! $update ) return; // Notice ausblenden wenn der User sie weggeklickt hat (per GET-Parameter) if ( isset($_GET['wbf_dismiss_update']) && check_admin_referer('wbf_dismiss_update') ) { set_transient( 'wbf_update_dismissed_' . WBF_VERSION, $update['version'], 7 * DAY_IN_SECONDS ); wp_safe_redirect( remove_query_arg(['wbf_dismiss_update','_wpnonce']) ); exit; } $dismissed = get_transient( 'wbf_update_dismissed_' . WBF_VERSION ); if ( $dismissed === $update['version'] ) return; $dismiss_url = wp_nonce_url( add_query_arg('wbf_dismiss_update', '1'), 'wbf_dismiss_update' ); $changelog_url = esc_url( $update['url'] ); $new_ver = esc_html( $update['version'] ); $cur_ver = esc_html( WBF_VERSION ); echo "
🔔
WP Business Forum — Update verfügbar!

Version {$new_ver} ist verfügbar. Du verwendest {$cur_ver}.

📋 Changelog & Download Später erinnern
"; } ); // ── Update-Badge im WP-Admin-Menü ───────────────────────────────────────────── add_action( 'admin_menu', function() { $update = wbf_update_available(); if ( ! $update ) return; global $menu; if ( ! is_array($menu) ) return; foreach ( $menu as &$item ) { if ( isset($item[2]) && $item[2] === 'wbf-admin' ) { $item[0] .= ' 1'; break; } } }, 999 ); // ── Manuellen Cache-Reset erlauben (für die Admin-UI) ───────────────────────── add_action( 'admin_init', function() { if ( ! isset($_GET['wbf_refresh_update']) ) return; if ( ! current_user_can('manage_options') ) return; if ( ! check_admin_referer('wbf_refresh_update') ) return; delete_transient( WBF_UPDATE_TRANSIENT ); wp_safe_redirect( remove_query_arg(['wbf_refresh_update','_wpnonce']) ); exit; } ); // ══════════════════════════════════════════════════════════════════════════════ // ── 2FA Inline-JavaScript ───────────────────────────────────────────────────── // Liefert das JS für: Login-2FA-Step, Profil-Setup-Wizard, Deaktivierung // ══════════════════════════════════════════════════════════════════════════════ function wbf_get_2fa_inline_js() { return <<<'JS' (function ($) { 'use strict'; /* ══════════════════════════════════════════════════════════════ 2FA — Login-Flow Wenn der Server 2fa_required:true zurückgibt, zeigt das Login-Formular eine Code-Eingabe anstatt die Fehlermeldung. ══════════════════════════════════════════════════════════════ */ // Original-Login-Handler überschreiben um 2FA abzufangen $(document).off('click', '.wbf-login-submit-btn'); $(document).on('click', '.wbf-login-submit-btn', function () { var $btn = $(this).prop('disabled', true).html(''); var $box = $(this).closest('.wbf-auth-box'); // 2FA-Panel verstecken falls sichtbar $box.find('.wbf-2fa-login-step').remove(); $.post(WBF.ajax_url, { action: 'wbf_login', nonce: WBF.nonce, username: $box.find('.wbf-field-username').val(), password: $box.find('.wbf-field-password').val(), remember_me: $box.find('.wbf-field-remember').is(':checked') ? '1' : '' }, function (res) { if (res && res.success) { location.reload(); } else if (res && res.data && res.data['2fa_required']) { // 2FA erforderlich — Code-Eingabe einblenden $btn.prop('disabled', false).html(' Einloggen'); wbfShow2faLoginStep($box); } else { var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler'; $box.find('.wbf-login-msg').text(msg).css('color', '#f05252').show(); setTimeout(function () { $box.find('.wbf-login-msg').fadeOut(); }, 4000); $btn.prop('disabled', false).html(' Einloggen'); } }, 'json').fail(function (xhr) { $box.find('.wbf-login-msg').text('Verbindungsfehler (' + xhr.status + ')').css('color', '#f05252').show(); $btn.prop('disabled', false).html(' Einloggen'); }); }); function wbfShow2faLoginStep($box) { // Altes Modal entfernen falls vorhanden $('#wbf2faLoginModal').remove(); var modal = '
' + '
' + '
' + '🛡️' + '
' + 'Zwei-Faktor-Authentifizierung' + '

Gib den Code aus deiner Authenticator-App ein.

' + '
' + '
' + '' + '
' + '' + '' + '
' + '' + '
' + '
'; $('body').append(modal); // Kurze Verzögerung für CSS-Transition setTimeout(function () { $('#wbf2faLoginModal').addClass('wbf-2fa-modal--visible'); $('#wbf2faLoginModal .wbf-2fa-code-input').focus(); }, 20); } // 2FA-Code absenden $(document).on('click', '.wbf-2fa-submit-btn', function () { var $step = $(this).closest('.wbf-2fa-modal-box'); var $btn = $(this).prop('disabled', true).html(''); var code = $step.find('.wbf-2fa-code-input').val().replace(/\s+/g, ''); if (code.length !== 6) { $step.find('.wbf-2fa-msg').text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show(); $btn.prop('disabled', false).html(' Code bestätigen'); return; } $.post(WBF.ajax_url, { action: 'wbf_2fa_verify_login', code: code }, function (res) { if (res && res.success) { location.reload(); } else { var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.'; $step.find('.wbf-2fa-msg').text(msg).css('color', '#f05252').show(); $step.find('.wbf-2fa-code-input').val('').focus(); $btn.prop('disabled', false).html(' Code bestätigen'); } }, 'json').fail(function (xhr) { $step.find('.wbf-2fa-msg').text('Verbindungsfehler.').css('color', '#f05252').show(); $btn.prop('disabled', false).html(' Code bestätigen'); }); }); // Enter-Taste im Code-Feld $(document).on('keydown', '.wbf-2fa-code-input', function (e) { if (e.key === 'Enter') $(this).closest('.wbf-2fa-login-step').find('.wbf-2fa-submit-btn').click(); }); // Abbrechen: 2FA-Modal schließen $(document).on('click', '.wbf-2fa-cancel-btn', function () { var $modal = $('#wbf2faLoginModal'); $modal.removeClass('wbf-2fa-modal--visible'); setTimeout(function () { $modal.remove(); }, 250); }); // Klick außerhalb des Modals schließt es $(document).on('click', '#wbf2faLoginModal', function (e) { if ($(e.target).is('#wbf2faLoginModal')) { var $modal = $(this); $modal.removeClass('wbf-2fa-modal--visible'); setTimeout(function () { $modal.remove(); }, 250); } }); /* ══════════════════════════════════════════════════════════════ 2FA — Profil-Setup-Wizard ══════════════════════════════════════════════════════════════ */ // Schritt 1 starten: Secret + QR generieren $(document).on('click', '#wbf2faStartBtn', function () { var $btn = $(this).prop('disabled', true).html(' Lädt…'); $.post(WBF.ajax_url, { action: 'wbf_2fa_setup_begin', nonce: WBF.nonce }, function (res) { if (!res || !res.success) { $btn.prop('disabled', false).html(' 2FA einrichten'); alert((res && res.data && res.data.message) ? res.data.message : 'Fehler'); return; } var secret = res.data.secret; var uri = res.data.uri; // QR-Code rendern (qrcodejs) $('#wbf2faQr').empty(); if (typeof QRCode !== 'undefined') { // QR-Code in isolierten Wrapper einbetten (kein Flex-Kontext) var qrEl = document.getElementById('wbf2faQr'); qrEl.innerHTML = ''; new QRCode(qrEl, { text: uri, width: 200, height: 200, correctLevel: QRCode.CorrectLevel.M }); // Kein JS-Eingriff nötig — CSS übernimmt Größe + img-Verstecken } else { // Fallback: Link anzeigen $('#wbf2faQr').html( 'otpauth Link' ); } // Secret für manuelle Eingabe formatiert anzeigen (Leerzeichen alle 4 Zeichen) var fmt = secret.replace(/=/g, '').replace(/(.{4})/g, '$1 ').trim(); $('#wbf2faSecret').text(fmt); // Panels tauschen $('#wbf2faInactive').hide(); $('#wbf2faStep1').fadeIn(200); }, 'json').fail(function () { $btn.prop('disabled', false).html(' 2FA einrichten'); alert('Verbindungsfehler. Bitte Seite neu laden.'); }); }); // Weiter zu Schritt 2 $(document).on('click', '#wbf2faToStep2', function () { $('#wbf2faStep1').hide(); $('#wbf2faStep2').fadeIn(200); $('#wbf2faVerifyCode').focus(); }); // Zurück zu Schritt 1 $(document).on('click', '#wbf2faBackBtn', function () { $('#wbf2faStep2').hide(); $('#wbf2faStep1').fadeIn(200); }); // Schritt 2: Code bestätigen und 2FA aktivieren $(document).on('click', '#wbf2faVerifyBtn', function () { var $btn = $(this).prop('disabled', true).html(''); var $msg = $('#wbf2faVerifyMsg'); var code = $('#wbf2faVerifyCode').val().replace(/\s+/g, ''); if (code.length !== 6) { $msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show(); $btn.prop('disabled', false).html(' Bestätigen & aktivieren'); return; } $.post(WBF.ajax_url, { action: 'wbf_2fa_setup_verify', nonce: WBF.nonce, code: code }, function (res) { if (res && res.success) { $('#wbf2faStep2').hide(); $('#wbf2faStep3').fadeIn(300); // Badge im Header aktualisieren $('#wbf2faCard .wbf-2fa-badge') .removeClass('wbf-2fa-badge--off') .addClass('wbf-2fa-badge--on') .html(' Aktiv'); // Nach 2 Sek. Seite neu laden damit der Header-Status stimmt setTimeout(function () { location.reload(); }, 2500); } else { var msg = (res && res.data && res.data.message) ? res.data.message : 'Ungültiger Code.'; $msg.text(msg).css('color', '#f05252').show(); $('#wbf2faVerifyCode').val('').focus(); $btn.prop('disabled', false).html(' Bestätigen & aktivieren'); } }, 'json').fail(function () { $msg.text('Verbindungsfehler.').css('color', '#f05252').show(); $btn.prop('disabled', false).html(' Bestätigen & aktivieren'); }); }); // Enter-Taste im Verifikationsfeld $(document).on('keydown', '#wbf2faVerifyCode', function (e) { if (e.key === 'Enter') $('#wbf2faVerifyBtn').click(); }); /* ══════════════════════════════════════════════════════════════ 2FA — Deaktivierung (Profil) ══════════════════════════════════════════════════════════════ */ $(document).on('click', '#wbf2faDisableBtn', function () { var $btn = $(this).prop('disabled', true).html(''); var $msg = $('#wbf2faDisableMsg'); var pw = $('#wbf2faDisablePw').val(); var code = $('#wbf2faDisableCode').val().replace(/\s+/g, ''); if (!pw) { $msg.text('Bitte Passwort eingeben.').css('color', '#f05252').show(); $btn.prop('disabled', false) .html(' 2FA deaktivieren'); return; } if (code.length !== 6) { $msg.text('Bitte 6-stelligen Code eingeben.').css('color', '#f05252').show(); $btn.prop('disabled', false) .html(' 2FA deaktivieren'); return; } $.post(WBF.ajax_url, { action: 'wbf_2fa_disable', nonce: WBF.nonce, password: pw, code: code }, function (res) { if (res && res.success) { $msg.text('✔ ' + (res.data.message || '2FA deaktiviert.')).css('color', '#56cf7e').show(); setTimeout(function () { location.reload(); }, 1500); } else { var msg = (res && res.data && res.data.message) ? res.data.message : 'Fehler.'; $msg.text(msg).css('color', '#f05252').show(); $('#wbf2faDisableCode').val('').focus(); $btn.prop('disabled', false) .html(' 2FA deaktivieren'); } }, 'json').fail(function () { $msg.text('Verbindungsfehler.').css('color', '#f05252').show(); $btn.prop('disabled', false) .html(' 2FA deaktivieren'); }); }); }(jQuery)); JS; }