5 Commits
1.0.2 ... 1.0.3

Author SHA1 Message Date
43fcc6cb95 Update from Git Manager GUI 2026-03-29 13:41:29 +02:00
1c229ab72b Upload via Git Manager GUI - wp-business-forum.php 2026-03-29 11:41:28 +00:00
781dbf9f41 Upload via Git Manager GUI - uninstall.php 2026-03-29 11:41:28 +00:00
3b7fd16301 Update from Git Manager GUI 2026-03-29 13:41:26 +02:00
3ea89e9841 Update from Git Manager GUI 2026-03-29 13:41:24 +02:00
13 changed files with 1467 additions and 328 deletions

View File

@@ -553,7 +553,6 @@ function wbf_admin_page() {
['wbf-trash', 'fas fa-trash-can', 'Papierkorb', $deleted_count>0?$deleted_count:0, true],
['wbf-export', 'fas fa-database', 'Export / Import'],
['wbf-settings', 'fas fa-gear', 'Einstellungen'],
['wbf-updates', 'fas fa-arrow-up-from-bracket', '🔔 Updates', $update_info ? 1 : 0],
];
foreach ($nav as $n):
if ($n===null) { echo '<div class="wbf-nav__sep"></div>'; continue; }
@@ -1079,6 +1078,10 @@ function wbf_admin_categories() {
foreach ( $p->children as $c ) {
$cd = wp_nonce_url( "?page=wbf-categories&delete_cat={$c->id}", "delete_cat_{$c->id}" );
$crole = WBF_Roles::get( $c->min_role ?? 'member' );
$clbadge = ( $c->min_role ?? 'member' ) !== 'member'
? " <span style='color:" . esc_attr( $crole['color'] ) . ";font-size:.8em'>🔒 " . esc_html( $crole['label'] ) . "</span>" : '';
$cguestbadge = ( (int)($c->guest_visible ?? 1) === 0 )
? " <span style='color:#f59e0b;font-size:.8em'>👁 Nur eingeloggt</span>" : '';
$reorder_c =
'<form method="post" style="display:inline-flex;gap:2px">'
. wp_nonce_field( 'wbf_reorder_nonce', '_wpnonce', true, false )
@@ -1097,7 +1100,7 @@ function wbf_admin_categories() {
echo "<tr>
<td style='padding-left:2.5rem'>↳ " . esc_html( $c->name ) . "</td>
<td>" . esc_html( $c->thread_count ) . "</td>
<td>" . esc_html( $crole['label'] ) . "$clockbadge$cguestbadge</td>
<td>" . esc_html( $crole['label'] ) . "$clbadge$cguestbadge</td>
<td style='white-space:nowrap'>{$reorder_c} <a href='?page=wbf-categories&edit={$c->id}'>Bearbeiten</a> | <a href='" . esc_url( $cd ) . "' onclick='return confirm(\"Löschen?\")'>Löschen</a></td>
</tr>";
}
@@ -1428,6 +1431,7 @@ function wbf_admin_members() {
<td>
<strong><?php echo esc_html( $m->display_name ); ?></strong><br>
<small style="color:#999">@<?php echo esc_html( $m->username ); ?></small>
<?php do_action( 'wbf_admin_user_row_extra', $m ); ?>
</td>
<td><?php echo esc_html( $m->email ); ?></td>
<td>
@@ -3039,38 +3043,93 @@ function wbf_admin_export() {
function wbf_admin_profile_fields() {
// ── Speichern ────────────────────────────────────────────────────────────
// ── Kategorien speichern ─────────────────────────────────────────────────
if ( isset($_POST['wbf_save_profile_fields']) && check_admin_referer('wbf_profile_fields_nonce') ) {
// Kategorien
$cat_ids = $_POST['cat_id'] ?? [];
$cat_names = $_POST['cat_name'] ?? [];
$cat_icons = $_POST['cat_icon'] ?? [];
$cats = [];
foreach ( $cat_ids as $ci => $cid ) {
$cid = sanitize_key( $cid );
if ( ! $cid ) continue;
$cats[] = [
'id' => $cid,
'name' => sanitize_text_field( $cat_names[$ci] ?? $cid ),
'icon' => sanitize_text_field( $cat_icons[$ci] ?? '📋' ),
];
}
// Neue Kategorie
if ( ! empty( trim( $_POST['new_cat_name'] ?? '' ) ) ) {
$new_name = sanitize_text_field( $_POST['new_cat_name'] );
$new_icon = sanitize_text_field( $_POST['new_cat_icon'] ?? '📋' );
$new_id = 'cat_' . sanitize_key( $new_name ) . '_' . time();
$cats[] = [ 'id' => $new_id, 'name' => $new_name, 'icon' => $new_icon ];
}
WBF_DB::save_profile_field_categories( $cats );
// Felder
$keys = $_POST['field_key'] ?? [];
$labels = $_POST['field_label'] ?? [];
$types = $_POST['field_type'] ?? [];
$placeholders = $_POST['field_placeholder'] ?? [];
$required = $_POST['field_required'] ?? [];
$public = $_POST['field_public'] ?? [];
$options = $_POST['field_options'] ?? [];
$phs = $_POST['field_placeholder'] ?? [];
$req = $_POST['field_required'] ?? [];
$pub = $_POST['field_public'] ?? [];
$opts = $_POST['field_options'] ?? [];
$cats_f = $_POST['field_category'] ?? [];
$valid_cat_ids = array_column( $cats, 'id' );
$fields = [];
foreach ( $keys as $i => $raw_key ) {
$key = sanitize_key( $raw_key );
if ( ! $key ) continue;
// Reservierte Keys sperren
if ( in_array($key, ['username','email','password','display_name','bio','signature','avatar_url','role']) ) continue;
$cat_id = sanitize_key( $cats_f[$i] ?? '' );
if ( ! in_array( $cat_id, $valid_cat_ids ) ) $cat_id = '';
$fields[] = [
'key' => $key,
'label' => sanitize_text_field( $labels[$i] ?? $key ),
'type' => in_array($types[$i] ?? '', ['text','url','textarea','select','number']) ? $types[$i] : 'text',
'placeholder' => sanitize_text_field( $placeholders[$i] ?? '' ),
'required' => ! empty( $required[$i] ) ? 1 : 0,
'public' => ! empty( $public[$i] ) ? 1 : 0,
'options' => sanitize_textarea_field( $options[$i] ?? '' ),
'type' => in_array($types[$i] ?? '', ['text','url','textarea','select','number','date']) ? $types[$i] : 'text',
'placeholder' => sanitize_text_field( $phs[$i] ?? '' ),
'required' => ! empty( $req[$i] ) ? 1 : 0,
'public' => ! empty( $pub[$i] ) ? 1 : 0,
'options' => sanitize_textarea_field( $opts[$i] ?? '' ),
'category_id' => $cat_id,
];
}
WBF_DB::save_profile_field_defs( $fields );
echo '<div class="notice notice-success is-dismissible"><p>✅ Profilfelder gespeichert!</p></div>';
echo '<div class="notice notice-success is-dismissible"><p>✅ Profilfelder & Kategorien gespeichert!</p></div>';
}
// Kategorie löschen
if ( isset($_GET['wbf_del_cat']) && check_admin_referer('wbf_del_cat') ) {
$del_id = sanitize_key( $_GET['wbf_del_cat'] );
$cats = WBF_DB::get_profile_field_categories();
$cats = array_values( array_filter( $cats, fn($c) => $c['id'] !== $del_id ) );
WBF_DB::save_profile_field_categories( $cats );
// Felder dieser Kategorie auf "keine Kategorie" setzen
$fields = WBF_DB::get_profile_field_defs();
foreach ( $fields as &$f ) {
if ( ($f['category_id'] ?? '') === $del_id ) $f['category_id'] = '';
}
WBF_DB::save_profile_field_defs( $fields );
wp_safe_redirect( remove_query_arg(['wbf_del_cat','_wpnonce']) );
exit;
}
$fields = WBF_DB::get_profile_field_defs();
$types = ['text'=>'Text','url'=>'URL/Link','textarea'=>'Mehrzeiliger Text','select'=>'Auswahlliste','number'=>'Zahl'];
$cats = WBF_DB::get_profile_field_categories();
$cat_map = array_column( $cats, null, 'id' );
$type_opts = ['text'=>'Text','url'=>'URL/Link','textarea'=>'Mehrzeiliger Text','select'=>'Auswahlliste','number'=>'Zahl','date'=>'Datum (Alter)'];
// Felder nach Kategorie gruppieren
$by_cat = [];
foreach ( $fields as $f ) {
$cid = $f['category_id'] ?? '';
if ( ! $cid || ! isset($cat_map[$cid]) ) $cid = '__none__';
$by_cat[$cid][] = $f;
}
?>
<div class="wrap">
<h1>👤 Benutzerdefinierte Profilfelder</h1>
@@ -3079,111 +3138,233 @@ function wbf_admin_profile_fields() {
<form method="post" id="wbfFieldsForm">
<?php wp_nonce_field('wbf_profile_fields_nonce'); ?>
<table class="widefat" style="margin-bottom:1rem">
<!-- ── Kategorien-Verwaltung ──────────────────────────────── -->
<div style="background:#f0f4ff;border:1px solid #c7d3f8;border-radius:10px;padding:18px 20px;margin-bottom:22px">
<h3 style="margin:0 0 14px;font-size:1rem;display:flex;align-items:center;gap:8px">
🗂️ Kategorien verwalten
</h3>
<table class="widefat" style="margin-bottom:12px;background:#fff;border-radius:7px;overflow:hidden">
<thead>
<tr>
<th style="width:140px">Schlüssel</th>
<th style="width:160px">Bezeichnung</th>
<th style="width:110px">Typ</th>
<th>Platzhalter</th>
<th style="width:110px">Optionen <small>(bei Auswahl)</small></th>
<th style="width:70px;text-align:center">Pflicht</th>
<th style="width:70px;text-align:center">Öffentlich</th>
<tr style="background:#eef2ff">
<th style="width:56px;padding:8px 10px">Icon</th>
<th style="padding:8px 10px">Kategorie-Name</th>
<th style="width:50px"></th>
</tr>
</thead>
<tbody id="wbfFieldRows">
<tbody>
<?php foreach ( $cats as $ci => $cat ): ?>
<tr>
<td style="padding:6px 10px">
<input type="hidden" name="cat_id[]" value="<?php echo esc_attr($cat['id']); ?>">
<input type="text" name="cat_icon[]" value="<?php echo esc_attr($cat['icon']); ?>"
style="width:46px;text-align:center;font-size:1.1rem;padding:4px 2px;border:1px solid #d0d9f7;border-radius:5px">
</td>
<td style="padding:6px 10px">
<input type="text" name="cat_name[]" value="<?php echo esc_attr($cat['name']); ?>"
style="width:100%;max-width:340px;border:1px solid #d0d9f7;border-radius:5px;padding:5px 8px">
</td>
<td style="padding:6px 10px;text-align:center">
<?php
$rows = $fields;
// Mindestens eine leere Zeile wenn noch keine Felder
if ( empty($rows) ) $rows[] = ['key'=>'','label'=>'','type'=>'text','placeholder'=>'','required'=>0,'public'=>1,'options'=>''];
foreach ( $rows as $i => $f ):
$del_url = wp_nonce_url(
add_query_arg(['page'=>'wbf-profile-fields','wbf_del_cat'=>$cat['id']], admin_url('admin.php')),
'wbf_del_cat'
); ?>
<a href="<?php echo esc_url($del_url); ?>"
onclick="return confirm('Kategorie löschen? Felder dieser Kategorie bleiben erhalten (ohne Kategorie).')"
style="color:#dc2626;text-decoration:none;font-size:1.1rem;font-weight:700" title="Kategorie löschen">×</a>
</td>
</tr>
<?php endforeach; ?>
<tr style="background:#f8faff">
<td style="padding:6px 10px">
<input type="text" name="new_cat_icon" value="" placeholder="📋"
style="width:46px;text-align:center;font-size:1.1rem;padding:4px 2px;border:1px dashed #a5b4f8;border-radius:5px">
</td>
<td style="padding:6px 10px" colspan="2">
<input type="text" name="new_cat_name" value="" placeholder="+ Neue Kategorie (Namen eingeben und speichern)"
style="width:100%;max-width:380px;border:1px dashed #a5b4f8;border-radius:5px;padding:5px 8px;color:#555">
</td>
</tr>
</tbody>
</table>
</div>
<!-- ── Felder je Kategorie ───────────────────────────────── -->
<?php
// Alle Kategorien + "Ohne Kategorie" am Ende ausgeben
$all_sections = $cats;
if ( isset($by_cat['__none__']) ) {
$all_sections[] = ['id'=>'__none__','name'=>'Ohne Kategorie','icon'=>'📋'];
}
foreach ( $all_sections as $cat ):
$cid = $cat['id'];
$c_fields = $by_cat[$cid] ?? [];
?>
<div class="wbf-cat-section" style="border:1px solid #e0e6f8;border-radius:10px;margin-bottom:16px;overflow:hidden">
<div style="background:#eef2ff;padding:10px 16px;display:flex;align-items:center;gap:8px;cursor:pointer"
onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'':'none'">
<span style="font-size:1.1rem"><?php echo esc_html($cat['icon']); ?></span>
<strong style="font-size:.95rem"><?php echo esc_html($cat['name']); ?></strong>
<span style="color:#94a3b8;font-size:.8rem;margin-left:4px"><?php echo count($c_fields); ?> Feld<?php echo count($c_fields)!==1?'er':''; ?></span>
</div>
<div class="wbf-cat-body">
<table class="widefat" style="border:none">
<thead>
<tr style="background:#f8faff">
<th style="width:130px">Schlüssel</th>
<th style="width:150px">Bezeichnung</th>
<th style="width:105px">Typ</th>
<th>Platzhalter</th>
<th style="width:105px">Optionen</th>
<th style="width:65px;text-align:center">Pflicht</th>
<th style="width:70px;text-align:center">Öffentlich</th>
<th style="width:130px">Kategorie</th>
<th style="width:44px"></th>
</tr>
</thead>
<tbody class="wbf-field-rows">
<?php
if ( empty($c_fields) ):
?>
<tr class="wbf-no-fields-hint" style="background:#fafafa">
<td colspan="9" style="color:#aaa;font-style:italic;font-size:.85rem;padding:10px 14px">
Noch keine Felder in dieser Kategorie.
</td>
</tr>
<?php
endif;
foreach ( $c_fields as $i_f => $f ):
$fi = 'fi_' . $f['key'];
?>
<tr class="wbf-field-row" style="background:#fff">
<td>
<td style="padding:6px 8px">
<input type="text" name="field_key[]"
value="<?php echo esc_attr($f['key']); ?>"
placeholder="z.b. website"
style="width:100%;font-family:monospace;font-size:.82rem"
<?php echo $f['key'] ? 'readonly' : ''; ?>>
<?php if ($f['key']): ?>
<p style="font-size:.7rem;color:#999;margin:2px 0 0">Schlüssel kann nach Erstellung nicht geändert werden.</p>
<p style="font-size:.68rem;color:#999;margin:2px 0 0">Nicht änderbar</p>
<?php endif; ?>
</td>
<td><input type="text" name="field_label[]" value="<?php echo esc_attr($f['label']); ?>" placeholder="Website" style="width:100%"></td>
<td>
<td style="padding:6px 8px">
<input type="text" name="field_label[]" value="<?php echo esc_attr($f['label']); ?>" placeholder="Website" style="width:100%">
</td>
<td style="padding:6px 8px">
<select name="field_type[]" style="width:100%" onchange="wbfToggleOptions(this)">
<?php foreach ($types as $val=>$lbl): ?>
<?php foreach ($type_opts as $val=>$lbl): ?>
<option value="<?php echo $val; ?>" <?php selected($f['type']??'text',$val); ?>><?php echo $lbl; ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input type="text" name="field_placeholder[]" value="<?php echo esc_attr($f['placeholder']??''); ?>" placeholder="https://..." style="width:100%"></td>
<td>
<td style="padding:6px 8px">
<input type="text" name="field_placeholder[]" value="<?php echo esc_attr($f['placeholder']??''); ?>" placeholder="https://..." style="width:100%">
</td>
<td style="padding:6px 8px">
<textarea name="field_options[]" rows="2"
placeholder="Option 1&#10;Option 2&#10;Option 3"
placeholder="Option 1&#10;Option 2"
style="width:100%;font-size:.78rem;<?php echo ($f['type']??'text')==='select'?'':'display:none'; ?>"
class="wbf-options-field"><?php echo esc_textarea($f['options']??''); ?></textarea>
<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem;<?php echo ($f['type']??'text')==='select'?'display:none':''; ?>">—</span>
</td>
<td style="text-align:center">
<input type="checkbox" name="field_required[<?php echo $i; ?>]" value="1" <?php checked($f['required']??0,1); ?>>
<td style="text-align:center;padding:6px 8px">
<input type="checkbox" name="field_required[<?php echo $fi; ?>]" value="1" <?php checked($f['required']??0,1); ?>>
</td>
<td style="text-align:center">
<input type="checkbox" name="field_public[<?php echo $i; ?>]" value="1" <?php checked($f['public']??1,1); ?>>
<td style="text-align:center;padding:6px 8px">
<input type="checkbox" name="field_public[<?php echo $fi; ?>]" value="1" <?php checked($f['public']??1,1); ?>>
</td>
<td>
<button type="button" class="button" onclick="this.closest('tr').remove()" style="color:#dc2626;border-color:#fca5a5">✕</button>
<td style="padding:6px 8px">
<select name="field_category[]" style="width:100%;font-size:.82rem">
<option value="">— Ohne —</option>
<?php foreach ($cats as $co): ?>
<option value="<?php echo esc_attr($co['id']); ?>" <?php selected($f['category_id']??'',$co['id']); ?>>
<?php echo esc_html($co['icon'].' '.$co['name']); ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td style="padding:6px 8px">
<button type="button" class="button" onclick="wbfRemoveRow(this)" style="color:#dc2626;border-color:#fca5a5;padding:2px 7px">✕</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div style="padding:10px 14px">
<button type="button" class="button" onclick="wbfAddField(this,'<?php echo esc_attr($cid); ?>')" style="font-size:.82rem">
+ Feld hinzufügen
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<div style="display:flex;gap:.75rem;align-items:center;margin-bottom:1.5rem">
<button type="button" class="button" id="wbfAddField">+ Feld hinzufügen</button>
<?php submit_button('💾 Felder speichern','primary','wbf_save_profile_fields',false); ?>
<div style="display:flex;gap:.75rem;align-items:center;margin:1rem 0 1.5rem">
<?php submit_button('💾 Felder & Kategorien speichern','primary','wbf_save_profile_fields',false); ?>
</div>
</form>
<hr>
<h3> Hinweise</h3>
<ul style="color:#555;line-height:1.8;font-size:.9rem">
<ul style="color:#555;line-height:1.9;font-size:.9rem">
<li><strong>Schlüssel</strong>: Einmalig, nur Kleinbuchstaben/Zahlen/Unterstrich. Kann nach dem ersten Speichern nicht mehr geändert werden.</li>
<li><strong>Typ URL</strong>: Wird automatisch als klickbarer Link dargestellt.</li>
<li><strong>Typ Datum (Alter)</strong>: Speichert das Geburtsdatum und zeigt nur das Alter an.</li>
<li><strong>Typ Auswahlliste</strong>: Trage die Optionen zeilenweise ins Optionen-Feld ein.</li>
<li><strong>Öffentlich</strong>: Wenn deaktiviert, sieht nur der Nutzer selbst das Feld in seinem Profil.</li>
<li><strong>Pflicht</strong>: Verhindert das Speichern wenn das Feld leer ist.</li>
<li><strong>Kategorie</strong>: Felder werden im Profil nach Kategorie gruppiert dargestellt.</li>
</ul>
</div>
<script>
var wbfRowCount = <?php echo count($fields) ?: 1; ?>;
document.getElementById('wbfAddField').addEventListener('click', function() {
var wbfRowCount = <?php echo count($fields) + 100; ?>;
function wbfRemoveRow(btn) {
var tr = btn.closest('tr');
tr.remove();
}
function wbfAddField(btn, catId) {
var i = wbfRowCount++;
var tbody = btn.closest('.wbf-cat-body').querySelector('.wbf-field-rows');
// Remove "no fields" hint row if present
var hint = tbody.querySelector('.wbf-no-fields-hint');
if (hint) hint.remove();
// Build category <options>
var cats = <?php echo json_encode( array_values($cats) ); ?>;
var catOpts = '<option value="">— Ohne —</option>';
cats.forEach(function(c) {
var sel = (c.id === catId) ? ' selected' : '';
catOpts += '<option value="' + c.id + '"' + sel + '>' + c.icon + ' ' + c.name + '</option>';
});
var tr = document.createElement('tr');
tr.className = 'wbf-field-row';
tr.style.background = '#fff';
tr.innerHTML = `
<td><input type="text" name="field_key[]" placeholder="mein_feld" style="width:100%;font-family:monospace;font-size:.82rem"></td>
<td><input type="text" name="field_label[]" placeholder="Mein Feld" style="width:100%"></td>
<td><select name="field_type[]" style="width:100%" onchange="wbfToggleOptions(this)">
<option value="text">Text</option>
<option value="url">URL/Link</option>
<option value="textarea">Mehrzeiliger Text</option>
<option value="select">Auswahlliste</option>
<option value="number">Zahl</option>
</select></td>
<td><input type="text" name="field_placeholder[]" placeholder="..." style="width:100%"></td>
<td>
<textarea name="field_options[]" rows="2" placeholder="Option 1&#10;Option 2" style="width:100%;font-size:.78rem;display:none" class="wbf-options-field"></textarea>
<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem"></span>
</td>
<td style="text-align:center"><input type="checkbox" name="field_required[${i}]" value="1"></td>
<td style="text-align:center"><input type="checkbox" name="field_public[${i}]" value="1" checked></td>
<td><button type="button" class="button" onclick="this.closest('tr').remove()" style="color:#dc2626;border-color:#fca5a5">✕</button></td>`;
document.getElementById('wbfFieldRows').appendChild(tr);
});
tr.innerHTML =
'<td style="padding:6px 8px"><input type="text" name="field_key[]" placeholder="mein_feld" style="width:100%;font-family:monospace;font-size:.82rem"></td>' +
'<td style="padding:6px 8px"><input type="text" name="field_label[]" placeholder="Mein Feld" style="width:100%"></td>' +
'<td style="padding:6px 8px"><select name="field_type[]" style="width:100%" onchange="wbfToggleOptions(this)">' +
'<option value="text">Text</option>' +
'<option value="url">URL/Link</option>' +
'<option value="textarea">Mehrzeiliger Text</option>' +
'<option value="select">Auswahlliste</option>' +
'<option value="number">Zahl</option>' +
'<option value="date">Datum (Alter)</option>' +
'</select></td>' +
'<td style="padding:6px 8px"><input type="text" name="field_placeholder[]" placeholder="..." style="width:100%"></td>' +
'<td style="padding:6px 8px">' +
'<textarea name="field_options[]" rows="2" placeholder="Option 1\nOption 2" style="width:100%;font-size:.78rem;display:none" class="wbf-options-field"></textarea>' +
'<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem">—</span>' +
'</td>' +
'<td style="text-align:center;padding:6px 8px"><input type="checkbox" name="field_required[new_' + i + ']" value="1"></td>' +
'<td style="text-align:center;padding:6px 8px"><input type="checkbox" name="field_public[new_' + i + ']" value="1" checked></td>' +
'<td style="padding:6px 8px"><select name="field_category[]" style="width:100%;font-size:.82rem">' + catOpts + '</select></td>' +
'<td style="padding:6px 8px"><button type="button" class="button" onclick="wbfRemoveRow(this)" style="color:#dc2626;border-color:#fca5a5;padding:2px 7px">✕</button></td>';
tbody.appendChild(tr);
}
function wbfToggleOptions(sel) {
var tr = sel.closest('tr');
@@ -3550,26 +3731,34 @@ function wbf_admin_prefixes() {
<!-- Formular -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:18px">
<h3 style="margin:0 0 14px;font-size:.95rem"><?php echo $edit ? 'Bearbeiten: '.esc_html($edit->label) : 'Neuer Präfix'; ?></h3>
<?php
// PHP 8 safe: extract values once — avoids null->property warnings
$px_label = $edit ? $edit->label : '';
$px_color = $edit ? $edit->color : '#ffffff';
$px_bg = $edit ? $edit->bg_color : '#475569';
$px_order = $edit ? (int) $edit->sort_order : 0;
$px_id = $edit ? (int) $edit->id : 0;
?>
<h3 style="margin:0 0 14px;font-size:.95rem"><?php echo $edit ? 'Bearbeiten: '.esc_html($px_label) : 'Neuer Präfix'; ?></h3>
<form method="post">
<?php wp_nonce_field('wbf_prefix_nonce'); ?>
<?php if ($edit): ?><input type="hidden" name="prefix_id" value="<?php echo (int)$edit->id; ?>"><?php endif; ?>
<?php if ($edit): ?><input type="hidden" name="prefix_id" value="<?php echo $px_id; ?>"><?php endif; ?>
<table class="form-table" style="margin:0">
<tr>
<th style="padding:6px 10px 6px 0;width:110px">Label *</th>
<td><input type="text" name="label" value="<?php echo esc_attr($edit->label ?? ''); ?>" class="regular-text" required placeholder="z.B. GELÖST"></td>
<td><input type="text" name="label" value="<?php echo esc_attr($px_label); ?>" style="width:100%;box-sizing:border-box" required placeholder="z.B. GELÖST"></td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0">Textfarbe</th>
<td><input type="color" name="color" value="<?php echo esc_attr($edit->color ?? '#ffffff'); ?>"></td>
<td><input type="color" name="color" value="<?php echo esc_attr($px_color); ?>"></td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0">Hintergrund</th>
<td><input type="color" name="bg_color" value="<?php echo esc_attr($edit->bg_color ?? '#475569'); ?>"></td>
<td><input type="color" name="bg_color" value="<?php echo esc_attr($px_bg); ?>"></td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0">Reihenfolge</th>
<td><input type="number" name="sort_order" value="<?php echo (int)($edit->sort_order ?? 0); ?>" class="small-text"></td>
<td><input type="number" name="sort_order" value="<?php echo $px_order; ?>" class="small-text"></td>
</tr>
</table>
<div style="margin-top:12px;display:flex;gap:8px">

View File

@@ -130,6 +130,13 @@ function wbf_admin_settings() {
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
// Checkbox-Felder explizit als '0' speichern wenn nicht angehakt,
// damit array_filter(...,'strlen') sie nicht wegwirft und der Default '1' greift.
$checkbox_fields = ['maintenance_mode', 'rules_enabled', 'rules_accept_required'];
foreach ( $checkbox_fields as $cb ) {
$settings[$cb] = isset($_POST[$cb]) && $_POST[$cb] === '1' ? '1' : '0';
}
// ignore_blocked_roles: kommagetrennte Liste der gewählten Rollen-Keys
$all_role_keys = array_keys( WBF_Roles::get_all() );
$checked_roles = array_intersect(

View File

@@ -3116,3 +3116,605 @@ select.wbf-cf-input option { background: var(--c-surface2); color: var(--c-text)
font-size: .68rem;
opacity: .7;
}
/* ════════════════════════════════════════════════════════════════════
PROFIL v3 — Sidebar-Links Layout (wbf-prof)
════════════════════════════════════════════════════════════════════ */
.wbf-prof {
display: grid;
grid-template-columns: 260px 1fr;
gap: 1.5rem;
align-items: start;
margin-bottom: 2rem;
}
/* ── SIDEBAR ─────────────────────────────────────────────────────── */
.wbf-prof__sidebar {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: 1.75rem 1.25rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: .75rem;
position: sticky;
top: 80px;
}
/* Avatar */
.wbf-prof__av-wrap {
position: relative;
width: 110px;
height: 110px;
margin-bottom: .25rem;
}
.wbf-prof__av-ring {
position: absolute;
inset: -4px;
border-radius: 50%;
background: conic-gradient(from 0deg, var(--c-primary), #6366f1, var(--c-primary));
opacity: .6;
animation: wbf-spin 8s linear infinite;
}
@keyframes wbf-spin { to { transform: rotate(360deg); } }
.wbf-prof__av {
position: relative;
width: 110px;
height: 110px;
border-radius: 50%;
border: 3px solid var(--c-surface);
object-fit: cover;
display: block;
background: var(--c-bg2);
box-shadow: 0 0 28px rgba(0,180,216,.25);
}
.wbf-prof__av-online {
position: absolute;
bottom: 5px; right: 5px;
width: 16px; height: 16px;
border-radius: 50%;
background: #22c55e;
border: 3px solid var(--c-surface);
box-shadow: 0 0 8px rgba(34,197,94,.7);
}
.wbf-prof__av-camera {
position: absolute;
inset: 0;
border-radius: 50%;
background: rgba(0,0,0,.6);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.1rem;
cursor: pointer;
opacity: 0;
transition: opacity .18s;
}
.wbf-prof__av-wrap:hover .wbf-prof__av-camera { opacity: 1; }
/* Identity */
.wbf-prof__sb-name {
font-size: 1.1rem;
font-weight: 800;
color: var(--c-text);
letter-spacing: -.01em;
}
.wbf-prof__sb-role { margin-top: -.15rem; }
.wbf-prof__sb-status {
font-size: .75rem;
color: var(--c-muted);
display: flex;
align-items: center;
gap: .35rem;
justify-content: center;
}
.wbf-prof__sb-status--online { color: #22c55e; font-weight: 600; }
.wbf-prof__dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 6px rgba(34,197,94,.8);
animation: wbf-pulse 2s infinite;
}
/* Stats grid */
.wbf-prof__sb-stats {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: .5rem;
width: 100%;
margin: .25rem 0;
border-top: 1px solid var(--c-border);
border-bottom: 1px solid var(--c-border);
padding: .9rem 0;
}
.wbf-prof__sb-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: .2rem;
}
.wbf-prof__sb-stat span {
font-size: 1.15rem;
font-weight: 700;
color: var(--c-text);
line-height: 1;
}
.wbf-prof__sb-stat em {
font-style: normal;
font-size: .65rem;
color: var(--c-muted);
text-transform: uppercase;
letter-spacing: .06em;
}
/* Level progress */
.wbf-prof__sb-level {
width: 100%;
display: flex;
flex-direction: column;
gap: .35rem;
}
.wbf-prof__sb-level-labels {
display: flex;
justify-content: space-between;
font-size: .75rem;
font-weight: 600;
}
.wbf-prof__sb-level-xp {
color: var(--c-muted);
font-weight: 400;
}
.wbf-prof__sb-level-bar {
height: 6px;
background: rgba(255,255,255,.07);
border-radius: 6px;
overflow: hidden;
}
.wbf-prof__sb-level-bar > div {
height: 100%;
border-radius: 6px;
transition: width .6s ease;
}
.wbf-prof__sb-level-next {
font-size: .72rem;
color: var(--c-muted);
text-align: center;
}
/* Meta */
.wbf-prof__sb-meta {
display: flex;
align-items: center;
gap: .45rem;
font-size: .78rem;
color: var(--c-muted);
}
.wbf-prof__sb-meta i { color: var(--c-primary); }
/* Sidebar custom field blocks */
.wbf-prof__sb-block {
width: 100%;
border-top: 1px solid var(--c-border);
padding-top: .85rem;
display: flex;
flex-direction: column;
gap: .5rem;
text-align: left;
}
.wbf-prof__sb-block-title {
font-size: .65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .09em;
color: var(--c-primary);
margin-bottom: .15rem;
}
.wbf-prof__sb-field { display: flex; flex-direction: column; gap: .1rem; }
.wbf-prof__sb-field-lbl {
font-size: .72rem;
color: var(--c-muted);
font-weight: 600;
}
.wbf-prof__sb-field-lbl i { margin-right: .25rem; }
.wbf-prof__sb-field-val {
font-size: .82rem;
color: var(--c-text-dim);
word-break: break-all;
}
.wbf-prof__sb-field-val--link {
color: var(--c-primary);
text-decoration: none;
}
.wbf-prof__sb-field-val--link:hover { text-decoration: underline; }
/* ── MAIN ────────────────────────────────────────────────────────── */
.wbf-prof__main {
display: flex;
flex-direction: column;
gap: 0;
min-width: 0;
}
/* Header card */
.wbf-prof__header-card {
position: relative;
border-radius: var(--radius) var(--radius) 0 0;
overflow: hidden;
border: 1px solid var(--c-border);
border-bottom: none;
}
.wbf-prof__header-card-bg {
position: absolute; inset: 0;
background: linear-gradient(135deg, #0a1628 0%, #0d1f3c 50%, #061218 100%);
}
.wbf-prof__header-card-bg::after {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse 70% 120% at 85% 50%, rgba(0,180,216,.2) 0%, transparent 65%);
}
.wbf-prof__header-card-inner {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.5rem 1.75rem;
flex-wrap: wrap;
}
.wbf-prof__header-name {
font-size: 1.5rem;
font-weight: 800;
color: #fff;
margin: 0 0 .35rem;
letter-spacing: -.02em;
}
.wbf-prof__header-sub {
display: flex;
align-items: center;
gap: .6rem;
margin-bottom: .5rem;
flex-wrap: wrap;
}
.wbf-prof__header-online {
display: inline-flex;
align-items: center;
gap: .35rem;
font-size: .72rem;
font-weight: 700;
color: #22c55e;
background: rgba(34,197,94,.12);
border: 1px solid rgba(34,197,94,.25);
border-radius: 20px;
padding: .15rem .65rem;
}
.wbf-prof__header-online span {
width: 6px; height: 6px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 5px rgba(34,197,94,.8);
animation: wbf-pulse 2s infinite;
}
.wbf-prof__header-bio {
font-size: .85rem;
color: rgba(255,255,255,.6);
margin: 0;
max-width: 420px;
}
.wbf-prof__header-btns {
display: flex;
gap: .6rem;
flex-wrap: wrap;
flex-shrink: 0;
}
/* Tabs */
.wbf-prof__tabs {
display: flex;
background: var(--c-surface);
border: 1px solid var(--c-border);
border-top: none;
padding: 0 .75rem;
overflow-x: auto;
scrollbar-width: none;
}
.wbf-prof__tabs::-webkit-scrollbar { display: none; }
.wbf-prof__tab {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .85rem 1rem;
font-size: .82rem;
font-weight: 600;
color: var(--c-muted);
text-decoration: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
white-space: nowrap;
transition: color .15s, border-color .15s;
}
.wbf-prof__tab:hover { color: var(--c-text); }
.wbf-prof__tab.active { color: var(--c-primary); border-bottom-color: var(--c-primary); }
.wbf-prof__tab i { font-size: .8rem; }
/* Tab body */
.wbf-prof__tab-body {
background: var(--c-bg);
border: 1px solid var(--c-border);
border-top: none;
border-radius: 0 0 var(--radius) var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Section headers */
.wbf-prof__section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: .75rem;
}
.wbf-prof__section-header > span {
font-size: .82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--c-text-dim);
display: flex;
align-items: center;
gap: .45rem;
}
.wbf-prof__section-header > span i { color: var(--c-primary); }
.wbf-prof__section-more {
font-size: .75rem;
color: var(--c-primary);
text-decoration: none;
display: flex;
align-items: center;
gap: .3rem;
opacity: .8;
transition: opacity .15s;
}
.wbf-prof__section-more:hover { opacity: 1; }
/* Post cards */
.wbf-prof__posts {
display: flex;
flex-direction: column;
gap: .6rem;
}
.wbf-prof__post-card {
display: block;
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
text-decoration: none;
transition: border-color .15s, transform .12s;
overflow: hidden;
}
.wbf-prof__post-card:hover {
border-color: rgba(0,180,216,.4);
transform: translateX(3px);
}
.wbf-prof__post-card-inner {
display: flex;
align-items: flex-start;
gap: .9rem;
padding: .9rem 1.1rem;
}
.wbf-prof__post-card-icon { flex-shrink: 0; }
.wbf-prof__post-card-type-icon {
width: 36px; height: 36px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: .85rem;
}
.wbf-prof__post-card-type-icon--thread { background: rgba(0,180,216,.15); color: var(--c-primary); }
.wbf-prof__post-card-type-icon--reply { background: rgba(99,102,241,.15); color: #818cf8; }
.wbf-prof__post-card-body { flex: 1; min-width: 0; }
.wbf-prof__post-card-title {
font-size: .88rem;
font-weight: 600;
color: var(--c-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: .2rem;
}
.wbf-prof__post-card-preview {
font-size: .78rem;
color: var(--c-muted);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: .35rem;
}
.wbf-prof__post-card-meta {
display: flex;
gap: .75rem;
font-size: .72rem;
color: var(--c-muted);
flex-wrap: wrap;
align-items: center;
}
.wbf-prof__post-card-meta i { margin-right: .2rem; }
.wbf-prof__post-card-type { font-weight: 600; }
.wbf-prof__post-card-type--thread { color: var(--c-primary); }
.wbf-prof__post-card-type--reply { color: #818cf8; }
.wbf-prof__post-card-time {
font-size: .72rem;
color: var(--c-muted);
white-space: nowrap;
flex-shrink: 0;
display: flex;
align-items: center;
gap: .4rem;
margin-top: .2rem;
}
/* Overview 2-col grid */
.wbf-prof__overview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
align-items: start;
}
/* Stat cards */
.wbf-prof__stat-cards {
display: flex;
flex-direction: column;
gap: .65rem;
}
.wbf-prof__stat-card {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
padding: .9rem 1rem;
position: relative;
overflow: hidden;
}
.wbf-prof__stat-card::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
}
.wbf-prof__stat-card--blue::before { background: var(--c-primary); }
.wbf-prof__stat-card--pink::before { background: #ec4899; }
.wbf-prof__stat-card--gold::before { background: #fbbf24; }
.wbf-prof__stat-card-icon {
font-size: 1rem;
margin-bottom: .3rem;
}
.wbf-prof__stat-card--blue .wbf-prof__stat-card-icon { color: var(--c-primary); }
.wbf-prof__stat-card--pink .wbf-prof__stat-card-icon { color: #ec4899; }
.wbf-prof__stat-card--gold .wbf-prof__stat-card-icon { color: #fbbf24; }
.wbf-prof__stat-card-val {
font-size: 1.4rem;
font-weight: 800;
color: var(--c-text);
line-height: 1.1;
}
.wbf-prof__stat-card-lbl {
font-size: .72rem;
color: var(--c-muted);
margin-top: .15rem;
}
.wbf-prof__stat-card-sub {
font-size: .7rem;
color: var(--c-muted);
margin-top: .3rem;
}
.wbf-prof__stat-card-bar {
height: 3px;
background: rgba(255,255,255,.07);
border-radius: 3px;
margin-top: .6rem;
overflow: hidden;
}
.wbf-prof__stat-card--blue .wbf-prof__stat-card-bar > div { background: var(--c-primary); height: 100%; border-radius: 3px; }
.wbf-prof__stat-card--pink .wbf-prof__stat-card-bar > div { background: #ec4899; height: 100%; border-radius: 3px; }
/* Badges */
.wbf-prof__badges {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: .65rem;
}
.wbf-prof__badge {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-sm);
padding: .75rem .5rem;
text-align: center;
opacity: .45;
transition: opacity .15s;
}
.wbf-prof__badge--active { opacity: 1; }
.wbf-prof__badge-icon {
width: 40px; height: 40px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
margin: 0 auto .5rem;
}
.wbf-prof__badge-name {
font-size: .72rem;
font-weight: 700;
color: var(--c-text);
}
.wbf-prof__badge-sub {
font-size: .62rem;
color: var(--c-muted);
margin-top: .1rem;
}
/* Activity list */
.wbf-prof__activity-list {
display: flex;
flex-direction: column;
gap: 0;
}
.wbf-prof__activity-item {
display: flex;
gap: .75rem;
align-items: flex-start;
padding: .75rem 0;
border-bottom: 1px solid rgba(255,255,255,.04);
}
.wbf-prof__activity-item:last-child { border-bottom: none; }
.wbf-prof__activity-av { flex-shrink: 0; }
.wbf-prof__activity-body { flex: 1; min-width: 0; }
.wbf-prof__activity-title {
display: block;
font-size: .82rem;
font-weight: 600;
color: var(--c-text);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: .2rem;
}
.wbf-prof__activity-title:hover { color: var(--c-primary); }
.wbf-prof__activity-meta {
font-size: .7rem;
color: var(--c-muted);
}
.wbf-prof__activity-meta i { margin-right: .2rem; }
.wbf-prof__activity-time {
font-size: .7rem;
color: var(--c-muted);
white-space: nowrap;
flex-shrink: 0;
}
/* Settings grid */
.wbf-prof__settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
align-items: start;
}
/* Mobile */
@media (max-width: 900px) {
.wbf-prof { grid-template-columns: 1fr; }
.wbf-prof__sidebar { position: static; }
.wbf-prof__overview-grid,
.wbf-prof__settings-grid { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
.wbf-prof__tab-body { padding: 1rem; }
.wbf-prof__header-card-inner { padding: 1.1rem; }
.wbf-prof__stat-cards { flex-direction: column; }
.wbf-prof__badges { grid-template-columns: repeat(3,1fr); }
}

View File

@@ -510,17 +510,16 @@
});
});
/* ── Profil speichern ───────────────────────────────────────── */
$(document).on('click', '#wbfSaveProfile, #wbfSaveProfileCf', function () {
/* ── Profil speichern (alles auf einmal) ───────────────────── */
$(document).on('click', '#wbfSaveProfile', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $(this).siblings('.wbf-msg').length ? $(this).siblings('.wbf-msg') : $('#wbfProfileMsg');
var $msg = $('#wbfProfileMsg');
var data = {
display_name: $('#wbfEditName').val(),
bio: $('#wbfEditBio').val(),
signature: $('#wbfEditSignature').val(),
new_password: $('#wbfNewPassword').val()
signature: $('#wbfEditSignature').val()
};
// Benutzerdefinierte Profilfelder einsammeln
// Alle benutzerdefinierten Felder (alle Kategorien) einsammeln
$('.wbf-cf-input').each(function () {
data[$(this).data('field')] = $(this).val();
});
@@ -533,6 +532,40 @@
});
});
/* ── Passwort ändern ────────────────────────────────────────── */
$(document).on('click', '#wbfSavePassword', function () {
var $btn = $(this).prop('disabled', true);
var $msg = $('#wbfPasswordMsg');
var cur = $('#wbfCurrentPassword').val();
var pw1 = $('#wbfNewPassword').val();
var pw2 = $('#wbfNewPassword2').val();
if (!cur) {
showMsg($msg, 'Bitte aktuelles Passwort eingeben.', false);
return $btn.prop('disabled', false);
}
if (pw1.length < 6) {
showMsg($msg, 'Neues Passwort mindestens 6 Zeichen.', false);
return $btn.prop('disabled', false);
}
if (pw1 !== pw2) {
showMsg($msg, 'Die Passwörter stimmen nicht überein.', false);
return $btn.prop('disabled', false);
}
wbfPost('wbf_update_profile', {
current_password: cur,
new_password: pw1
}, function (d) {
showMsg($msg, d.message, true);
$('#wbfCurrentPassword, #wbfNewPassword, #wbfNewPassword2').val('');
$btn.prop('disabled', false);
}, function (d) {
showMsg($msg, d.message || 'Fehler', false);
$btn.prop('disabled', false);
});
});
/* ── Signatur Zeichenzähler ─────────────────────────────────── */
$(document).on('input', '#wbfEditSignature', function () {
$('#wbfSigCount').text($(this).val().length);
@@ -542,10 +575,16 @@
$(document).on('change', '#wbfAvatarFile', function () {
var file = this.files[0];
if (!file) return;
// Sofort-Vorschau — synchron, kein Callback, kein Warten
var objectUrl = URL.createObjectURL(file);
$('#wbfProfileAvatar').attr('src', objectUrl).css('opacity', '.6');
var fd = new FormData();
fd.append('action', 'wbf_upload_avatar');
fd.append('nonce', WBF.nonce);
fd.append('avatar', file);
$.ajax({
url: WBF.ajax_url,
type: 'POST',
@@ -553,9 +592,19 @@
processData: false,
contentType: false,
success: function (res) {
$('#wbfProfileAvatar').css('opacity', '1');
if (res.success) {
$('.wbf-profile-page__avatar').attr('src', res.data.avatar_url);
// Object-URL freigeben, endgültige Server-URL setzen
URL.revokeObjectURL(objectUrl);
var finalUrl = res.data.avatar_url + '?v=' + Date.now();
$('#wbfProfileAvatar').attr('src', finalUrl);
// Topbar-Avatar ebenfalls aktualisieren
$('.wbf-topbar__user img').attr('src', finalUrl);
$('.wbf-profile-widget__avatar img').attr('src', finalUrl);
}
},
error: function () {
$('#wbfProfileAvatar').css('opacity', '1');
}
});
});
@@ -1091,7 +1140,7 @@
var html = '';
d.notifications.forEach(function (n) {
var isUnread = n.is_read == 0;
var avatar = n.actor_avatar || '';
var avatar = $('<img>').attr('src', n.actor_avatar || '').attr('alt', '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
var base = WBF.forum_url || window.location.href.split('?')[0];
var sep = base.indexOf('?') !== -1 ? '&' : '?';
var actor = '<strong>' + $('<span>').text(n.actor_name).html() + '</strong>';
@@ -1118,7 +1167,7 @@
}
html += '<a class="wbf-notif-item' + (isUnread ? ' wbf-notif-item--unread' : '') + '" href="' + url + '">' +
'<div class="wbf-notif-item__avatar"><img src="' + avatar + '" alt=""></div>' +
'<div class="wbf-notif-item__avatar">' + avatar + '</div>' +
'<div class="wbf-notif-item__body">' +
'<div class="wbf-notif-item__text">' + text +
(sub ? '<br><span style="color:var(--c-muted);font-size:.78rem">' + $('<span>').text(sub).html() + '</span>' : '') +
@@ -1218,7 +1267,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Loeschen"><i class="fas fa-trash-can"></i></button>';
var html = '<div class="' + cls + '" data-msg-id="' + m.id + '">';
if (!isMine) {
html += '<img src="' + (m.sender_avatar||'') + '" class="wbf-dm-inbox-item__avatar">';
html += $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML;
}
html += '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
@@ -1452,7 +1501,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-mention-item" data-username="' + $('<span>').text(u.username).html() + '">'
+ '<img src="' + (u.avatar_url || '') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ '<span>' + $('<span>').text(u.display_name).html() + '</span>'
+ '<small>@' + $('<span>').text(u.username).html() + '</small>'
+ '</div>';
@@ -1500,7 +1549,7 @@
var delBtn = '<button class="wbf-dm-msg__del" data-id="' + m.id + '" title="Löschen"><i class="fas fa-trash-can"></i></button>';
if (!isMine) {
html += '<div class="' + cls + '" data-msg-id="' + m.id + '">'
+ '<img src="' + (m.sender_avatar || '') + '" class="wbf-dm-msg__avatar">'
+ $('<img>').attr('src', m.sender_avatar || '').attr('class', 'wbf-dm-msg__avatar')[0].outerHTML
+ '<div class="wbf-dm-msg__bubble"><div class="wbf-dm-msg__text">'
+ $('<span>').text(m.content).html().replace(/\n/g,'<br>')
+ '</div><div class="wbf-dm-msg__time">' + time + delBtn + '</div></div></div>';
@@ -1524,7 +1573,7 @@
var href = window.location.pathname + '?forum_dm=inbox&with=' + conv.partner_id;
var unread = parseInt(conv.unread_cnt) > 0;
html += '<a class="wbf-dm-inbox-item' + (unread ? ' wbf-dm-inbox-item--unread' : '') + '" href="' + href + '">'
+ '<img src="' + (conv.partner_avatar || '') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', conv.partner_avatar || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<div class="wbf-dm-inbox-item__body">'
+ '<span class="wbf-dm-inbox-item__name">' + $('<span>').text(conv.partner_name).html() + '</span>'
+ (unread ? '<span class="wbf-dm-inbox-item__badge">' + conv.unread_cnt + '</span>' : '')
@@ -1550,7 +1599,7 @@
var backUrl = window.location.pathname + '?forum_dm=inbox';
$('#wbfDmHeader').html(
'<a href="' + backUrl + '" class="wbf-dm-back-btn" title="Zurück zur Inbox"><i class="fas fa-arrow-left"></i></a>'
+ '<img src="' + (p.avatar_url||'') + '" class="wbf-dm-inbox-item__avatar">'
+ $('<img>').attr('src', p.avatar_url || '').attr('class', 'wbf-dm-inbox-item__avatar')[0].outerHTML
+ '<strong>' + $('<span>').text(p.display_name).html() + '</strong>'
+ '<a href="?forum_profile=' + p.id + '" style="font-size:.78rem;color:var(--c-muted);text-decoration:none">@' + $('<span>').text(p.username).html() + '</a>'
);
@@ -1634,7 +1683,7 @@
var html = '';
d.users.forEach(function(u) {
html += '<div class="wbf-tag-suggest-item" data-id="' + u.id + '" data-name="' + $('<span>').text(u.display_name).html() + '">'
+ '<img src="' + (u.avatar_url||'') + '" width="22" height="22" style="border-radius:50%;flex-shrink:0">'
+ $('<img>').attr('src', u.avatar_url || '').attr('width', '22').attr('height', '22').css({'border-radius':'50%','flex-shrink':'0'})[0].outerHTML
+ $('<span>').text(u.display_name).html()
+ '<span style="color:var(--c-muted);font-size:.75rem">@' + $('<span>').text(u.username).html() + '</span>'
+ '</div>';

View File

@@ -43,23 +43,46 @@ class WBF_Ajax {
// ── Auth ──────────────────────────────────────────────────────────────────
public static function handle_login() {
// Brute-Force-Schutz: max. 10 Versuche pro IP in 15 Minuten
$ip_key = 'wbf_login_fail_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
$fails = (int) get_transient( $ip_key );
if ( $fails >= 10 ) {
wp_send_json_error([
'message' => 'Zu viele fehlgeschlagene Loginversuche. Bitte warte 15 Minuten.',
'locked' => true,
]);
}
// Login braucht keinen Nonce — Credentials sind die Authentifizierung
$result = WBF_Auth::login(
sanitize_text_field($_POST['username'] ?? ''),
$_POST['password'] ?? ''
);
if ($result['success']) {
// Erfolgreicher Login: Fehlzähler löschen
delete_transient( $ip_key );
$u = $result['user'];
if ( ! empty($_POST['remember_me']) ) {
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]);
} else {
// Fehlversuch zählen — außer bei gesperrten Konten (kein Passwortfehler)
if ( empty($result['banned']) ) {
set_transient( $ip_key, $fails + 1, 15 * MINUTE_IN_SECONDS );
}
wp_send_json_error($result);
}
}
public static function handle_register() {
// Brute-Force/Spam-Schutz: max. 5 Registrierungen pro IP pro Stunde
$reg_ip_key = 'wbf_reg_ip_' . md5( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
$reg_fails = (int) get_transient( $reg_ip_key );
if ( $reg_fails >= 5 ) {
wp_send_json_error(['message' => 'Zu viele Registrierungsversuche von dieser IP. Bitte warte eine Stunde.']);
}
// Spam-Schutz: Honeypot + Zeitlimit
if ( ! empty($_POST['wbf_website']) ) {
wp_send_json_error(['message' => 'Spam erkannt.']);
@@ -98,6 +121,8 @@ class WBF_Ajax {
sanitize_text_field($_POST['display_name'] ?? '')
);
if ($result['success']) {
// Registrierungs-Zähler für IP erhöhen
set_transient( $reg_ip_key, $reg_fails + 1, HOUR_IN_SECONDS );
$u = $result['user'];
// Einladungscode einlösen
$reg_mode2 = wbf_get_settings()['registration_mode'] ?? 'open';
@@ -226,9 +251,11 @@ class WBF_Ajax {
}
// Thread-Abonnenten benachrichtigen
$subscribers = WBF_DB::get_thread_subscribers($thread_id);
// $notif_users is a flat array of IDs (from get_col) — cast to int for comparison
$notif_ids = array_map('intval', $notif_users);
foreach ($subscribers as $sub) {
if ((int)$sub->id === (int)$user->id) continue; // nicht sich selbst
if (in_array($sub->id, array_column($notif_users, 'id') ?: [])) continue; // schon benachrichtigt
if (in_array((int)$sub->id, $notif_ids, true)) continue; // schon benachrichtigt
self::send_notification_email($sub, 'reply', $user->display_name, [
'thread_id' => $thread_id,
'thread_title' => $thread->title,
@@ -372,6 +399,19 @@ class WBF_Ajax {
if (!empty($_POST['new_password'])) {
if (strlen($_POST['new_password']) < 6) wp_send_json_error(['message'=>'Passwort mindestens 6 Zeichen.']);
// Sicherheit: aktuelles Passwort muss zur Bestätigung angegeben werden
$current_pw = $_POST['current_password'] ?? '';
if ( empty($current_pw) ) {
wp_send_json_error(['message'=>'Bitte aktuelles Passwort zur Bestätigung eingeben.']);
}
if ( ! password_verify($current_pw, $user->password) ) {
wp_send_json_error(['message'=>'Aktuelles Passwort ist falsch.']);
}
// Bestätigungsfeld server-seitig prüfen
$new_pw2 = $_POST['new_password2'] ?? '';
if ( ! empty($new_pw2) && $new_pw2 !== $_POST['new_password'] ) {
wp_send_json_error(['message'=>'Die Passwörter stimmen nicht überein.']);
}
$update['password'] = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
}
@@ -397,6 +437,15 @@ class WBF_Ajax {
$value = sanitize_textarea_field( $raw );
} elseif ( $def['type'] === 'number' ) {
$value = is_numeric($raw) ? (string)(float)$raw : '';
} elseif ( $def['type'] === 'date' ) {
// Datum validieren — nur YYYY-MM-DD, nicht in der Zukunft
$raw_date = sanitize_text_field( trim($raw) );
if ( preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw_date) ) {
$ts = strtotime($raw_date);
$value = ($ts && $ts <= time()) ? $raw_date : '';
} else {
$value = '';
}
} else {
$value = sanitize_text_field( $raw );
}
@@ -594,7 +643,8 @@ class WBF_Ajax {
self::verify();
$query = sanitize_text_field( $_POST['query'] ?? '' );
if ( mb_strlen( $query ) < 2 ) wp_send_json_error(['message' => 'Suchbegriff zu kurz.']);
$results = WBF_DB::search( $query, 40 );
$current_search = WBF_Auth::get_current_user();
$results = WBF_DB::search( $query, 40, $current_search );
wp_send_json_success(['results' => $results, 'query' => $query]);
}
@@ -1141,6 +1191,12 @@ class WBF_Ajax {
self::verify();
$user = WBF_Auth::get_current_user();
if (!$user) wp_send_json_error(['message'=>'Nicht eingeloggt.']);
// Sicherstellen dass Spalte existiert (Schutz für bestehende Installs)
global $wpdb;
$cols = $wpdb->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" );
}
$current = (int)($user->profile_public ?? 1);
$new = $current ? 0 : 1;
WBF_DB::update_user($user->id, ['profile_public'=>$new]);

View File

@@ -6,8 +6,25 @@ class WBF_Auth {
const SESSION_KEY = 'wbf_forum_user';
public static function init() {
// PHP 8.3: session_start() nach gesendeten Headers erzeugt E_WARNING,
// der direkt in den HTML-Output fließt und das Layout zerstört.
// Lösung: headers_sent() prüfen + session_start() mit Cookie-Optionen aufrufen.
if ( ! session_id() ) {
session_start();
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 = [
'cookie_httponly' => true,
'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;
}
session_start( $session_opts );
}
// Auto-login via Remember-Me cookie if not already logged in
if ( empty( $_SESSION[ self::SESSION_KEY ] ) && isset( $_COOKIE['wbf_remember'] ) ) {
@@ -55,6 +72,7 @@ class WBF_Auth {
]);
// Frisch laden und einloggen
$user = WBF_DB::get_user( $user->id );
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
$_SESSION[ self::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
return array( 'success' => true, 'user' => $user );
@@ -67,6 +85,7 @@ class WBF_Auth {
}
return array( 'success' => false, 'banned' => true, 'message' => $reason );
}
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
$_SESSION[ self::SESSION_KEY ] = $user->id;
WBF_DB::touch_last_active( $user->id );
return array( 'success' => true, 'user' => $user );
@@ -96,6 +115,7 @@ class WBF_Auth {
'avatar_url' => $avatar,
));
if ( session_id() ) session_regenerate_id( true ); // Session Fixation verhindern
$_SESSION[ self::SESSION_KEY ] = $id;
return array('success'=>true,'user'=>WBF_DB::get_user($id));
}

View File

@@ -490,7 +490,7 @@ class WBF_DB {
}
// Move post_count contribution too
$post_count = (int)$wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id
"SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id
));
if ( $post_count > 0 ) {
$wpdb->query($wpdb->prepare(
@@ -512,7 +512,7 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
LEFT JOIN {$wpdb->prefix}forum_prefixes p ON p.id = t.prefix_id
WHERE t.id = %d", $id
WHERE t.id = %d AND t.deleted_at IS NULL", $id
));
}
@@ -572,7 +572,7 @@ class WBF_DB {
public static function count_posts( $thread_id ) {
global $wpdb;
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d", $thread_id));
return (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE thread_id=%d AND deleted_at IS NULL", $thread_id));
}
public static function create_post( $data ) {
@@ -643,8 +643,8 @@ class WBF_DB {
public static function get_stats() {
global $wpdb;
return [
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived'"),
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts"),
'threads' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE status != 'archived' AND deleted_at IS NULL"),
'posts' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL"),
'members' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users"),
'newest' => $wpdb->get_var("SELECT display_name FROM {$wpdb->prefix}forum_users ORDER BY registered DESC LIMIT 1"),
];
@@ -731,9 +731,23 @@ class WBF_DB {
// ── Suche ─────────────────────────────────────────────────────────────────
public static function search( $query, $limit = 30 ) {
public static function search( $query, $limit = 30, $user = null ) {
global $wpdb;
$like = '%' . $wpdb->esc_like( $query ) . '%';
// Kategorie-Sichtbarkeit: Gäste und Member dürfen keine privaten Kategorien sehen
$user_level = $user ? WBF_Roles::level( $user->role ) : -99;
if ( $user_level >= 50 ) {
// Moderatoren+ sehen alles (inkl. soft-deleted ist extra)
$cat_filter = '';
} elseif ( $user ) {
// Eingeloggte Member/VIP: nur guest_visible oder eigene Rolle reicht
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role IN ('member','vip'))";
} else {
// Gäste: nur komplett öffentliche Kategorien
$cat_filter = "AND c.guest_visible = 1 AND (c.min_role IS NULL OR c.min_role = 'member')";
}
return $wpdb->get_results( $wpdb->prepare(
"SELECT 'thread' AS result_type,
t.id, t.title, t.content, t.created_at, t.reply_count,
@@ -742,7 +756,9 @@ class WBF_DB {
FROM {$wpdb->prefix}forum_threads t
JOIN {$wpdb->prefix}forum_users u ON u.id = t.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
WHERE (t.title LIKE %s OR t.content LIKE %s) AND t.status != 'archived'
WHERE (t.title LIKE %s OR t.content LIKE %s)
AND t.status != 'archived' AND t.deleted_at IS NULL
$cat_filter
UNION ALL
SELECT 'post' AS result_type,
p.id, t.title, p.content, p.created_at, 0 AS reply_count,
@@ -752,7 +768,9 @@ class WBF_DB {
JOIN {$wpdb->prefix}forum_threads t ON t.id = p.thread_id
JOIN {$wpdb->prefix}forum_users u ON u.id = p.user_id
JOIN {$wpdb->prefix}forum_categories c ON c.id = t.category_id
WHERE p.content LIKE %s AND t.status != 'archived'
WHERE p.content LIKE %s
AND p.deleted_at IS NULL AND t.status != 'archived' AND t.deleted_at IS NULL
$cat_filter
ORDER BY created_at DESC
LIMIT %d",
$like, $like, $like, $limit
@@ -1476,6 +1494,25 @@ class WBF_DB {
update_option( 'wbf_profile_fields', $fields );
}
public static function get_profile_field_categories() {
$cats = get_option( 'wbf_profile_field_cats', null );
if ( $cats === null ) {
// Default-Kategorien beim ersten Aufruf
$defaults = [
[ 'id' => 'cat_allgemein', 'name' => 'Allgemein', 'icon' => '👤' ],
[ 'id' => 'cat_kontakt', 'name' => 'Kontakt', 'icon' => '✉️' ],
[ 'id' => 'cat_social', 'name' => 'Social Media', 'icon' => '🌐' ],
];
update_option( 'wbf_profile_field_cats', $defaults );
return $defaults;
}
return is_array( $cats ) ? $cats : [];
}
public static function save_profile_field_categories( $cats ) {
update_option( 'wbf_profile_field_cats', $cats );
}
public static function get_user_meta( $user_id ) {
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare(

View File

@@ -103,6 +103,7 @@ class WBF_Export {
case 'settings':
$data['settings'] = get_option( 'wbf_settings', [] );
$data['profile_fields'] = get_option( 'wbf_profile_fields', [] );
$data['profile_field_cats'] = get_option( 'wbf_profile_field_cats', [] );
$data['reactions_cfg'] = get_option( 'wbf_reactions', [] );
$data['word_filter'] = get_option( 'wbf_word_filter', '' );
break;
@@ -275,6 +276,7 @@ class WBF_Export {
}
if ( isset( $data['profile_fields'] ) ) {
update_option( 'wbf_profile_fields', $data['profile_fields'] );
if ( isset($data['profile_field_cats']) ) update_option( 'wbf_profile_field_cats', $data['profile_field_cats'] );
$log[] = '✅ Profilfeld-Definitionen (' . count( $data['profile_fields'] ) . ') importiert.';
}
if ( isset( $data['reactions_cfg'] ) && is_array( $data['reactions_cfg'] ) ) {
@@ -1172,7 +1174,7 @@ class WBF_Export {
/** Prüft ob eine Tabelle existiert */
private static function table_exists( string $table ): bool {
global $wpdb;
return $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table;
return $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table ) ) === $table;
}
/** Erstellt ein standardisiertes Ergebnis-Array */

View File

@@ -43,12 +43,12 @@ class WBF_Levels {
return $defaults;
}
$levels = (array) $saved;
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
return $levels;
}
public static function save( $levels ) {
usort( $levels, fn($a,$b) => (int)$a['min'] <=> (int)$b['min'] );
usort( $levels, function($a, $b) { return (int)$a['min'] <=> (int)$b['min']; } );
update_option( self::OPTION_KEY, $levels );
}

View File

@@ -108,7 +108,7 @@ class WBF_Roles {
/** Nach Level sortiert (höchstes zuerst) */
public static function get_sorted() {
$all = self::get_all();
uasort($all, fn($a,$b) => $b['level'] <=> $a['level']);
uasort($all, function($a, $b) { return $b['level'] <=> $a['level']; });
return $all;
}

View File

@@ -9,6 +9,15 @@ class WBF_Shortcodes {
// ── Helpers ───────────────────────────────────────────────────────────────
/** Alter aus Geburtsdatum berechnen */
public static function calc_age( $date_str ) {
if ( ! $date_str || ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_str) ) return null;
$birth = new DateTime( $date_str );
$today = new DateTime();
if ( $birth > $today ) return null;
return (int) $birth->diff($today)->y;
}
public static function time_ago( $datetime ) {
$diff = time() - strtotime($datetime);
if ($diff < 60) return 'Gerade eben';
@@ -149,8 +158,12 @@ class WBF_Shortcodes {
// ── Router ────────────────────────────────────────────────────────────────
public static function forum_main( $atts ) {
// Server-seitiger Logout-Fallback
// Server-seitiger Logout-Fallback — Nonce-Schutz gegen CSRF
if (isset($_GET['wbf_do_logout'])) {
if ( ! isset($_GET['_wpnonce']) || ! wp_verify_nonce( sanitize_text_field($_GET['_wpnonce']), 'wbf_logout' ) ) {
wp_redirect( wbf_get_forum_url() );
exit;
}
WBF_Auth::logout();
wp_redirect( wbf_get_forum_url() );
exit;
@@ -310,7 +323,7 @@ class WBF_Shortcodes {
</div>
<div class="wbf-profile-widget__actions">
<a href="?forum_profile=<?php echo (int)$current->id; ?>" class="wbf-btn wbf-btn--sm">Profil</a>
<a href="<?php echo esc_url(wbf_get_forum_url() . '?wbf_do_logout=1'); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
<a href="<?php echo esc_url(wp_nonce_url(wbf_get_forum_url() . '?wbf_do_logout=1', 'wbf_logout')); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
</div>
</div>
<?php endif; ?>
@@ -893,7 +906,9 @@ class WBF_Shortcodes {
$is_own = $current && $current->id == $profile->id;
$is_staff = $current && WBF_Roles::level($current->role) >= 50;
// Profil-Sichtbarkeit prüfen
if (!$is_own && !$is_staff && (int)($profile->profile_public ?? 1) === 0) {
// profile_public NULL = Spalte fehlt noch = als öffentlich (1) behandeln
$profile_public = isset($profile->profile_public) ? (int)$profile->profile_public : 1;
if (!$is_own && !$is_staff && $profile_public === 0) {
ob_start(); ?>
<div class="wbf-wrap"><?php self::render_topbar($current); ?>
<div class="wbf-container wbf-mt">
@@ -907,11 +922,17 @@ class WBF_Shortcodes {
$bookmarks = $is_own ? WBF_DB::get_user_bookmarks($current->id, 50) : [];
$ignore_list = $is_own ? WBF_DB::get_ignore_list($current->id) : [];
$cf_defs = WBF_DB::get_profile_field_defs();
$cf_cats = WBF_DB::get_profile_field_categories();
$cf_cat_map = array_column( $cf_cats, null, 'id' );
$cf_vals = WBF_DB::get_user_meta( $profile->id );
// Aktiven Tab aus URL lesen (tab=1|2|3), Standard: 1 für eigenes, 2 für fremdes
$active_tab = (int)($_GET['ptab'] ?? ($is_own ? 1 : 2));
$active_tab = in_array($active_tab, [1,2,3]) ? $active_tab : ($is_own ? 1 : 2);
// Tab 1 + 3 nur für eigenes Profil
// Tab-ID: numerisch (14) oder String-Slug (z.B. 'mc' von der Forum-Bridge)
$ptab_raw = $_GET['ptab'] ?? ($is_own ? 1 : 2);
$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]) ) {
$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;
ob_start(); ?>
@@ -930,6 +951,7 @@ class WBF_Shortcodes {
<div class="wbf-profile-sidebar__avatar-wrap">
<img src="<?php echo esc_url($profile->avatar_url); ?>"
alt="<?php echo esc_attr($profile->display_name); ?>"
id="wbfProfileAvatar"
class="wbf-profile-sidebar__avatar">
<?php if ($is_own): ?>
<label class="wbf-avatar-upload-btn" title="Avatar ändern">
@@ -981,26 +1003,70 @@ class WBF_Shortcodes {
<p class="wbf-profile-sidebar__sig"><?php echo nl2br(esc_html($profile->signature)); ?></p>
</div>
<?php endif; ?>
<!-- Öffentliche Custom Fields -->
<?php foreach ($cf_defs as $def):
if (!$is_own && empty($def['public'])) continue;
$val = trim($cf_vals[$def['key']] ?? '');
if ($val === '') continue; ?>
<!-- Öffentliche Custom Fields — nach Kategorie gruppiert -->
<?php
$cf_by_cat_sb = [];
foreach ( $cf_defs as $def_sb ) {
if (!$is_own && empty($def_sb['public'])) continue;
$val_sb = trim($cf_vals[$def_sb['key']] ?? '');
if ($val_sb === '') continue;
$cid_sb = $def_sb['category_id'] ?? '';
if (!$cid_sb || !isset($cf_cat_map[$cid_sb])) $cid_sb = '__none__';
$cf_by_cat_sb[$cid_sb][] = ['def'=>$def_sb,'val'=>$val_sb];
}
$sb_sections = $cf_cats;
if (isset($cf_by_cat_sb['__none__'])) {
$sb_sections[] = ['id'=>'__none__','name'=>'Weitere Infos','icon'=>''];
}
foreach ($sb_sections as $scat_sb):
$scid_sb = $scat_sb['id'];
if (empty($cf_by_cat_sb[$scid_sb])) continue;
?>
<div class="wbf-profile-sidebar__section">
<span class="wbf-profile-sidebar__section-label">
<i class="fas fa-<?php echo $def['type']==='url'?'link':($def['type']==='number'?'hashtag':'tag'); ?>"></i>
<?php echo esc_html($def['label']); ?>
</span>
<?php if ($def['type'] === 'url'): ?>
<a href="<?php echo esc_url($val); ?>" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);font-size:.85rem;word-break:break-all">
<?php echo esc_html(mb_strtolower(preg_replace('#^https?://#i','',$val))); ?>
</a>
<?php elseif ($def['type'] === 'textarea'): ?>
<p style="font-size:.85rem"><?php echo nl2br(esc_html($val)); ?></p>
<?php else: ?>
<p style="font-size:.85rem"><?php echo esc_html($val); ?></p>
<span class="wbf-profile-sidebar__section-label" style="display:flex;align-items:center;gap:5px;margin-bottom:4px">
<?php if(!empty($scat_sb['icon'])): ?>
<span style="font-size:.9rem"><?php echo esc_html($scat_sb['icon']); ?></span>
<?php endif; ?>
<?php echo esc_html($scat_sb['name']); ?>
</span>
<?php foreach ($cf_by_cat_sb[$scid_sb] as $cf_entry_sb):
$def_sb = $cf_entry_sb['def'];
$val_sb = $cf_entry_sb['val'];
// Auto-Link für Telegram und Discord anhand des Feld-Keys erkennen
$key_sb = strtolower($def_sb['key']);
$is_telegram = strpos($key_sb, 'telegram') !== false;
$is_discord = strpos($key_sb, 'discord') !== false;
?>
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:4px;font-size:.85rem">
<span style="color:var(--c-muted,#94a3b8);flex-shrink:0"><?php echo esc_html($def_sb['label']); ?></span>
<?php if ($def_sb['type'] === 'url'): ?>
<a href="<?php echo esc_url($val_sb); ?>" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);word-break:break-all;text-align:right">
<?php echo esc_html(mb_strtolower(preg_replace('#^https?://#i','',$val_sb))); ?>
</a>
<?php elseif ($is_telegram):
// Username bereinigen: @ und Leerzeichen entfernen
$tg_user = ltrim(trim($val_sb), '@');
$tg_url = 'https://t.me/' . rawurlencode($tg_user);
?>
<a href="<?php echo esc_url($tg_url); ?>" target="_blank" rel="noopener noreferrer"
style="color:#29b6f6;text-align:right;font-size:inherit">
@<?php echo esc_html($tg_user); ?>
</a>
<?php elseif ($is_discord): ?>
<span style="color:#7289da;text-align:right;font-size:inherit">
<?php echo esc_html($val_sb); ?>
</span>
<?php elseif ($def_sb['type'] === 'textarea'): ?>
<span style="text-align:right"><?php echo nl2br(esc_html($val_sb)); ?></span>
<?php elseif ($def_sb['type'] === 'date'):
$age_sb = self::calc_age($val_sb); ?>
<span><?php echo $age_sb !== null ? esc_html((string)$age_sb) . ' Jahre' : '—'; ?></span>
<?php else: ?>
<span style="text-align:right"><?php echo esc_html($val_sb); ?></span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</aside>
@@ -1043,6 +1109,16 @@ class WBF_Shortcodes {
class="wbf-profile-tab<?php echo $active_tab===3?' active':''; ?>">
<i class="fas fa-shield-halved"></i> Privatsphäre
</a>
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=4"
class="wbf-profile-tab<?php echo $active_tab===4?' active':''; ?>">
<i class="fas fa-lock"></i> Sicherheit
</a>
<?php if ( class_exists('MC_Gallery_Forum_Bridge') ) : ?>
<a href="?forum_profile=<?php echo (int)$profile->id; ?>&ptab=mc"
class="wbf-profile-tab<?php echo $active_tab==='mc'?' active':''; ?>">
<i class="fas fa-cubes"></i> Minecraft
</a>
<?php endif; ?>
</div>
<?php endif; ?>
@@ -1057,16 +1133,10 @@ class WBF_Shortcodes {
<i class="fas fa-sliders"></i> Profil bearbeiten
</div>
<div class="wbf-profile-card__body">
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Anzeigename</label>
<input type="text" id="wbfEditName" value="<?php echo esc_attr($profile->display_name); ?>">
</div>
<div class="wbf-form-row">
<label>Neues Passwort <small>(leer = nicht ändern)</small></label>
<input type="password" id="wbfNewPassword" placeholder="••••••">
</div>
</div>
<div class="wbf-form-row">
<label>Bio</label>
<textarea id="wbfEditBio" rows="2"><?php echo esc_textarea($profile->bio); ?></textarea>
@@ -1076,62 +1146,36 @@ class WBF_Shortcodes {
<textarea id="wbfEditSignature" rows="2" maxlength="300" placeholder="Deine Signatur…"><?php echo esc_textarea($profile->signature ?? ''); ?></textarea>
<div class="wbf-sig-counter"><span id="wbfSigCount"><?php echo mb_strlen($profile->signature??''); ?></span>/300</div>
</div>
<div class="wbf-form-row" style="display:flex;align-items:center;gap:.75rem">
<label style="font-size:.82rem;color:var(--c-muted)">Profil öffentlich sichtbar</label>
<?php $pub = (int)($profile->profile_public ?? 1); ?>
<button type="button" id="wbfToggleProfileVis"
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
data-state="<?php echo $pub; ?>">
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
<?php echo $pub?'Öffentlich':'Privat'; ?>
</button>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfile">
<i class="fas fa-save"></i> Speichern
</button>
<span class="wbf-msg" id="wbfProfileMsg"></span>
</div>
</div>
</div>
<!-- E-Mail-Adresse ändern -->
<!-- Weitere Profilangaben — nach Kategorie gruppiert (ohne eigene Speichern-Buttons) -->
<?php
$cf_edit_by_cat = [];
foreach ( $cf_defs as $def_e ) {
$cid_e = $def_e['category_id'] ?? '';
if (!$cid_e || !isset($cf_cat_map[$cid_e])) $cid_e = '__none__';
$cf_edit_by_cat[$cid_e][] = $def_e;
}
$edit_sections = $cf_cats;
if (isset($cf_edit_by_cat['__none__'])) {
$edit_sections[] = ['id'=>'__none__','name'=>'Weitere Angaben','icon'=>'📋'];
}
if (!empty($cf_defs)):
foreach ($edit_sections as $ecat):
$ecid = $ecat['id'];
if (empty($cf_edit_by_cat[$ecid])) continue;
?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-envelope"></i> E-Mail-Adresse
</div>
<div class="wbf-profile-card__body">
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:1rem">
Aktuelle Adresse: <strong style="color:var(--c-text)"><?php echo esc_html($profile->email); ?></strong>
</p>
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Neue E-Mail-Adresse</label>
<input type="email" id="wbfNewEmail" placeholder="neue@email.de" autocomplete="off">
</div>
<div class="wbf-form-row">
<label>Aktuelles Passwort <small>(zur Bestätigung)</small></label>
<input type="password" id="wbfEmailPassword" placeholder="••••••" autocomplete="current-password">
</div>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveEmail">
<i class="fas fa-envelope"></i> E-Mail ändern
</button>
<span class="wbf-msg" id="wbfEmailMsg"></span>
</div>
</div>
</div>
<!-- Weitere Profilangaben -->
<?php if (!empty($cf_defs)): ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-sliders"></i> Weitere Profilangaben
<?php if(!empty($ecat['icon'])): ?>
<span style="margin-right:5px"><?php echo esc_html($ecat['icon']); ?></span>
<?php else: ?><i class="fas fa-sliders"></i><?php endif; ?>
<?php echo esc_html($ecat['name']); ?>
</div>
<div class="wbf-profile-card__body">
<div class="wbf-profile-edit-grid">
<?php foreach ($cf_defs as $def):
<?php foreach ($cf_edit_by_cat[$ecid] as $def):
$k = esc_attr($def['key']);
$lbl = esc_html($def['label']);
$ph = esc_attr($def['placeholder'] ?? '');
@@ -1154,6 +1198,13 @@ class WBF_Shortcodes {
<?php selected($cf_vals[$def['key']] ?? '', $opt); ?>><?php echo esc_html($opt); ?></option>
<?php endforeach; ?>
</select>
<?php elseif ($def['type'] === 'date'): ?>
<input type="date"
class="wbf-cf-input"
data-field="cf_<?php echo $k; ?>"
value="<?php echo esc_attr($cf_vals[$def['key']] ?? ''); ?>"
max="<?php echo date('Y-m-d'); ?>"
<?php echo $req; ?>>
<?php else: ?>
<input type="<?php echo $def['type']==='url'?'url':($def['type']==='number'?'number':'text'); ?>"
class="wbf-cf-input"
@@ -1165,15 +1216,17 @@ class WBF_Shortcodes {
</div>
<?php endforeach; ?>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfileCf">
<i class="fas fa-save"></i> Speichern
</div>
</div>
<?php endforeach; endif; ?>
<!-- Globaler Speichern-Button für Tab 1 -->
<div style="display:flex;align-items:center;gap:1rem;padding:.25rem 0 .5rem">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveProfile" style="min-width:160px">
<i class="fas fa-save"></i> Alles speichern
</button>
<span class="wbf-msg" id="wbfProfileCfMsg"></span>
<span class="wbf-msg" id="wbfProfileMsg"></span>
</div>
</div>
</div>
<?php endif; ?>
<?php endif; /* end Tab 1 */ ?>
@@ -1260,6 +1313,29 @@ class WBF_Shortcodes {
══════════════════════════════════════════════════ -->
<?php if ($is_own && $active_tab === 3): ?>
<!-- Profil-Sichtbarkeit -->
<?php $pub = (int)($profile->profile_public ?? 1); ?>
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-eye"></i> Profil-Sichtbarkeit
</div>
<div class="wbf-profile-card__body">
<div class="wbf-form-row" style="display:flex;align-items:center;gap:1rem">
<div>
<div style="font-size:.9rem;font-weight:600;margin-bottom:3px">Profil öffentlich sichtbar</div>
<div style="font-size:.8rem;color:var(--c-muted)">Wenn deaktiviert, können nur du selbst und Moderatoren dein Profil sehen.</div>
</div>
<button type="button" id="wbfToggleProfileVis"
class="wbf-btn wbf-btn--sm<?php echo $pub?' wbf-btn--primary':''; ?>"
data-state="<?php echo $pub; ?>"
style="margin-left:auto;white-space:nowrap">
<i class="fas fa-<?php echo $pub?'eye':'eye-slash'; ?>"></i>
<?php echo $pub?'Öffentlich':'Privat'; ?>
</button>
</div>
</div>
</div>
<!-- Ignorierte Nutzer -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
@@ -1394,6 +1470,78 @@ class WBF_Shortcodes {
<?php endif; /* end Tab 3 */ ?>
<!-- ══════════════════════════════════════════════════
TAB 4 — Sicherheit (Passwort & E-Mail)
══════════════════════════════════════════════════ -->
<?php if ($is_own && $active_tab === 4): ?>
<!-- Passwort ändern -->
<div class="wbf-profile-card" style="border-color:rgba(0,180,216,.25)">
<div class="wbf-profile-card__header" style="background:rgba(0,180,216,.07);border-bottom-color:rgba(0,180,216,.18)">
<i class="fas fa-lock" style="color:var(--c-primary)"></i> Passwort ändern
</div>
<div class="wbf-profile-card__body">
<div class="wbf-form-row">
<label>Aktuelles Passwort</label>
<input type="password" id="wbfCurrentPassword" placeholder="Dein aktuelles Passwort" autocomplete="current-password">
</div>
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Neues Passwort <small>(min. 6 Zeichen)</small></label>
<input type="password" id="wbfNewPassword" placeholder="Neues Passwort" autocomplete="new-password">
</div>
<div class="wbf-form-row">
<label>Neues Passwort wiederholen</label>
<input type="password" id="wbfNewPassword2" placeholder="Passwort bestätigen" autocomplete="new-password">
</div>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSavePassword">
<i class="fas fa-key"></i> Passwort ändern
</button>
<span class="wbf-msg" id="wbfPasswordMsg"></span>
</div>
</div>
</div>
<!-- E-Mail-Adresse ändern -->
<div class="wbf-profile-card">
<div class="wbf-profile-card__header">
<i class="fas fa-envelope"></i> E-Mail-Adresse
</div>
<div class="wbf-profile-card__body">
<p style="font-size:.82rem;color:var(--c-muted);margin-bottom:1rem">
Aktuelle Adresse: <strong style="color:var(--c-text)"><?php echo esc_html($profile->email); ?></strong>
</p>
<div class="wbf-profile-edit-grid">
<div class="wbf-form-row">
<label>Neue E-Mail-Adresse</label>
<input type="email" id="wbfNewEmail" placeholder="neue@email.de" autocomplete="off">
</div>
<div class="wbf-form-row">
<label>Aktuelles Passwort <small>(zur Bestätigung)</small></label>
<input type="password" id="wbfEmailPassword" placeholder="••••••" autocomplete="current-password">
</div>
</div>
<div class="wbf-profile-card__footer">
<button class="wbf-btn wbf-btn--primary" id="wbfSaveEmail">
<i class="fas fa-envelope"></i> E-Mail ändern
</button>
<span class="wbf-msg" id="wbfEmailMsg"></span>
</div>
</div>
</div>
<?php endif; /* end Tab 4 */ ?>
<!-- ══════════════════════════════════════════════════
TAB MC — Minecraft-Konto verknüpfen (Bridge)
Wird nur gerendert wenn MC Gallery Forum Bridge aktiv ist.
══════════════════════════════════════════════════ -->
<?php if ( $is_own && $active_tab === 'mc' && class_exists('MC_Gallery_Forum_Bridge') ) :
echo apply_filters('wbf_profile_tab_content', '', 'minecraft', $profile);
endif; /* end Tab MC */ ?>
</div><!-- /.wbf-profile-main -->
</div><!-- /.wbf-profile-layout -->
</div>
@@ -1608,7 +1756,7 @@ class WBF_Shortcodes {
if ($maint_s === '1' && (!$cur_s || WBF_Roles::level($cur_s->role) < 50)) return self::view_maintenance();
$query = sanitize_text_field($_GET['q'] ?? '');
$current = WBF_Auth::get_current_user();
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40) : [];
$results = mb_strlen($query) >= 2 ? WBF_DB::search($query, 40, $current) : [];
ob_start(); ?>
<div class="wbf-wrap">
<?php self::render_topbar($current); ?>
@@ -1711,7 +1859,7 @@ class WBF_Shortcodes {
<?php echo esc_html($current->display_name); ?>
<?php echo self::role_badge($current->role); ?>
</a>
<a href="<?php echo esc_url(wbf_get_forum_url() . '?wbf_do_logout=1'); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
<a href="<?php echo esc_url(wp_nonce_url(wbf_get_forum_url() . '?wbf_do_logout=1', 'wbf_logout')); ?>" class="wbf-btn wbf-btn--sm wbf-btn--outline"><?php echo esc_html(wbf_get_settings()['btn_logout']); ?></a>
<?php else: ?>
<button class="wbf-btn wbf-btn--sm" id="wbfOpenLogin"><?php echo esc_html(wbf_get_settings()['btn_login']); ?></button>
<button class="wbf-btn wbf-btn--sm wbf-btn--primary" id="wbfOpenRegister"><?php echo esc_html(wbf_get_settings()['btn_register']); ?></button>

View File

@@ -48,6 +48,7 @@ $options = [
'wbf_level_config',
'wbf_levels_enabled',
'wbf_profile_fields',
'wbf_profile_field_cats',
'wbf_reactions',
'wbf_forum_page_id',
'wbf_superadmin_email',

View File

@@ -3,10 +3,11 @@
* Plugin Name: WP Business Forum
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Business-Forum
* Description: Professionelles Forum mit eigenem Login, Rollen, Signaturen, Hierarchie und Moderations-Tools.
* Version: 1.0.2
* Version: 1.0.3
* Author: M_Viper
* Author URI: https://m-viper.de
* Text Domain: wp-business-forum
* Requires PHP: 7.0
*/
if ( ! defined( 'ABSPATH' ) ) exit;
@@ -39,6 +40,33 @@ add_action( 'plugins_loaded', function() {
WBF_Export::hooks();
}, 5 );
// ── DB-Schema sicherstellen (läuft bei jedem Seitenaufruf, sehr günstig) ─────
// Stellt sicher dass neue Spalten auch auf bestehenden Installs vorhanden sind,
// ohne dass das Plugin erneut deaktiviert/aktiviert werden muss.
add_action( 'plugins_loaded', function() {
$db_ver = (int) get_option( 'wbf_db_version', 0 );
if ( $db_ver < 2 ) {
global $wpdb;
// profile_public: Sicherheits-kritisch — muss immer existieren
$cols = $wpdb->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(); } );