Update from Git Manager GUI

This commit is contained in:
2026-03-29 22:25:41 +02:00
parent e2c4e31b4b
commit 689fd0c77b
2 changed files with 635 additions and 13 deletions

View File

@@ -30,6 +30,7 @@ add_action( 'admin_menu', function() {
add_submenu_page( 'wbf-admin', 'Thread-Präfixe','Thread-Präfixe','manage_options', 'wbf-prefixes', 'wbf_admin_prefixes' );
add_submenu_page( 'wbf-admin', 'Wortfilter', 'Wortfilter', 'manage_options', 'wbf-wordfilter', 'wbf_admin_wordfilter' );
add_submenu_page( 'wbf-admin', 'Export / Import','Export / Import','manage_options', 'wbf-export', 'wbf_admin_export' );
add_submenu_page( 'wbf-admin', '🎮 Discord', '🎮 Discord', 'manage_options', 'wbf-discord', 'wbf_admin_discord' );
add_submenu_page( 'wbf-admin', '⚠️ Deinstallieren', '⚠️ Deinstallieren', 'manage_options', 'wbf-uninstall', 'wbf_admin_uninstall' );
add_submenu_page( 'wbf-admin', '🔔 Updates', '🔔 Updates', 'manage_options', 'wbf-updates', 'wbf_admin_updates' );
}, 10 );
@@ -1346,14 +1347,46 @@ function wbf_admin_members() {
}
}
$members = WBF_DB::get_all_users( 200 );
$members = WBF_DB::get_all_users( 200 );
$s_discord = wbf_get_settings();
$dc_sync_on = ( $s_discord['discord_role_sync'] ?? '0' ) === '1' && trim( $s_discord['discord_bot_token'] ?? '' );
// Discord-Meta aller User vorladen (1 Query statt N)
$dc_meta = [];
if ( $dc_sync_on ) {
global $wpdb;
$rows = $wpdb->get_results(
"SELECT user_id,
MAX(CASE WHEN meta_key='discord_user_id' THEN meta_value END) AS discord_uid,
MAX(CASE WHEN meta_key='discord_username' THEN meta_value END) AS discord_name
FROM {$wpdb->prefix}forum_user_meta
WHERE meta_key IN ('discord_user_id','discord_username')
GROUP BY user_id"
);
foreach ( $rows as $r ) {
$dc_meta[ (int)$r->user_id ] = $r;
}
}
?>
<div class="wrap">
<h1 style="display:flex;align-items:center;justify-content:space-between">
<h1 style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
<span>Mitglieder</span>
<button type="button" class="button button-primary" onclick="document.getElementById('wbf-create-user-box').style.display=document.getElementById('wbf-create-user-box').style.display==='none'?'block':'none'">
+ Neuen Nutzer anlegen
</button>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<?php if ( $dc_sync_on ): ?>
<button type="button" id="wbf-discord-sync-all-btn" class="button"
style="background:#5865f2;color:#fff;border-color:#4752c4;display:inline-flex;align-items:center;gap:5px"
title="Synchronisiert Discord-Rollen aller verknüpften Nutzer (Discord → Forum)">
<span class="dashicons dashicons-update" id="wbf-sync-icon" style="margin-top:3px"></span>
Discord-Rollen synchronisieren
</button>
<span id="wbf-discord-sync-result" style="font-weight:600;font-size:.85rem"></span>
<?php endif; ?>
<button type="button" class="button button-primary"
onclick="document.getElementById('wbf-create-user-box').style.display=
document.getElementById('wbf-create-user-box').style.display==='none'?'block':'none'">
+ Neuen Nutzer anlegen
</button>
</div>
</h1>
<!-- Neuen Nutzer anlegen -->
@@ -1409,6 +1442,7 @@ function wbf_admin_members() {
<th>#</th><th>Nutzer</th><th>E-Mail</th>
<th>Aktuelle Rolle</th><th>Beiträge</th>
<th>Registriert</th><th>Rolle ändern</th>
<?php if ( $dc_sync_on ): ?><th style="color:#5865f2"><i class="fab fa-discord"></i> Discord</th><?php endif; ?>
</tr>
</thead>
<tbody>
@@ -1417,9 +1451,17 @@ function wbf_admin_members() {
$color = esc_attr( $role['color'] );
$bg = esc_attr( $role['bg_color'] );
$icon = esc_attr( $role['icon'] ?? 'fas fa-user' );
$is_sa = ( $m->role === WBF_Roles::SUPERADMIN );
// Nur sperren wenn dieser Forum-User wirklich dem WP-Superadmin (ID 1) entspricht.
// Reine Rollen-Prüfung reicht nicht — sonst kann man versehentlich
// zugewiesene superadmin-Rollen nicht mehr korrigieren.
$wp_sa_data = get_userdata( WBF_Roles::get_wp_superadmin_id() );
$is_sa = ( $m->role === WBF_Roles::SUPERADMIN )
&& $wp_sa_data
&& ( strtolower($m->email) === strtolower($wp_sa_data->user_email) );
$ban_reason = esc_attr( $m->ban_reason ?? '' );
$opts = '';
$dc_user = $dc_meta[ (int)$m->id ] ?? null;
$has_dc = $dc_sync_on && $dc_user && ! empty( $dc_user->discord_uid );
foreach ( $roles as $k => $r ) {
if ( $k === WBF_Roles::SUPERADMIN ) continue;
$sel = $m->role === $k ? ' selected' : '';
@@ -1438,13 +1480,13 @@ function wbf_admin_members() {
<span class="wbf-role-preview" style="color:<?php echo $color; ?>;background:<?php echo $bg; ?>;border-color:<?php echo $color; ?>">
<i class="<?php echo $icon; ?>"></i> <?php echo esc_html( $role['label'] ); ?>
</span>
<?php if ( $is_sa ) : ?><em style="color:#999;font-size:.8em">(WP-Admin)</em><?php endif; ?>
<?php if ( $is_sa ) : ?><em style="color:#999;font-size:.8em">(Haupt-Admin)</em><?php endif; ?>
</td>
<td><?php echo esc_html( $m->post_count ); ?></td>
<td><?php echo esc_html( date( 'd.m.Y', strtotime( $m->registered ) ) ); ?></td>
<td>
<?php if ( $is_sa ) : ?>
<em style="color:#999">Automatisch (WP-Admin)</em>
<em style="color:#999">Gesperrt — Haupt-Superadmin (WP User ID <?php echo (int) WBF_Roles::get_wp_superadmin_id(); ?>)</em>
<?php else : ?>
<form method="post" style="display:flex;flex-direction:column;gap:5px">
<?php wp_nonce_field( 'wbf_member_role_nonce' ); ?>
@@ -1711,11 +1753,127 @@ function wbf_admin_members() {
</div>
<?php endif; ?>
</td>
<?php if ( $dc_sync_on ) : ?>
<td style="white-space:nowrap;min-width:140px;vertical-align:top;padding-top:8px">
<?php if ( $has_dc ) : ?>
<div style="display:flex;flex-direction:column;gap:5px">
<span style="font-size:.8rem;color:#5865f2;font-weight:600">
<i class="fab fa-discord"></i>
<?php echo esc_html( $dc_user->discord_name ?: $dc_user->discord_uid ); ?>
</span>
<button type="button"
class="button button-small wbf-dc-sync-user"
data-uid="<?php echo (int)$m->id; ?>"
data-nonce="<?php echo wp_create_nonce('wbf_nonce'); ?>"
style="color:#5865f2;border-color:#5865f2;font-size:.75rem;height:24px;line-height:22px">
<span class="dashicons dashicons-update" style="font-size:12px;width:12px;height:12px;margin-top:5px"></span>
Sync
</button>
<span class="wbf-dc-user-result" style="font-size:.75rem;font-weight:600"></span>
</div>
<?php else : ?>
<span style="font-size:.78rem;color:#9ca3af">Nicht verknüpft</span>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ( $dc_sync_on ) : ?>
<style>
@keyframes wbf-spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
.wbf-spinning { animation: wbf-spin .8s linear infinite; display:inline-block; }
</style>
<script>
(function(){
var nonce = '<?php echo wp_create_nonce("wbf_nonce"); ?>';
// ── Bulk-Sync ──────────────────────────────────────────────────────────
var allBtn = document.getElementById('wbf-discord-sync-all-btn');
var allRes = document.getElementById('wbf-discord-sync-result');
var allIcon = document.getElementById('wbf-sync-icon');
if (allBtn) {
allBtn.addEventListener('click', function() {
allBtn.disabled = true;
if (allIcon) allIcon.classList.add('wbf-spinning');
allRes.style.color = '#374151';
allRes.textContent = '⏳ Sync läuft…';
fetch(ajaxurl, {
method : 'POST',
headers : {'Content-Type':'application/x-www-form-urlencoded'},
body : 'action=wbf_manual_discord_sync&nonce=' + nonce
})
.then(function(r){ return r.json(); })
.then(function(d) {
allBtn.disabled = false;
if (allIcon) allIcon.classList.remove('wbf-spinning');
if (d.success) {
allRes.style.color = '#16a34a';
allRes.textContent = '✅ ' + (d.data.message || 'Fertig!');
// Seite neu laden damit neue Rollen sichtbar werden
setTimeout(function(){ location.reload(); }, 1800);
} else {
allRes.style.color = '#dc2626';
allRes.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
}
})
.catch(function() {
allBtn.disabled = false;
if (allIcon) allIcon.classList.remove('wbf-spinning');
allRes.style.color = '#dc2626';
allRes.textContent = '❌ Netzwerkfehler';
});
});
}
// ── Pro-Nutzer-Sync ───────────────────────────────────────────────────
document.querySelectorAll('.wbf-dc-sync-user').forEach(function(btn) {
btn.addEventListener('click', function() {
var uid = btn.dataset.uid;
var icon = btn.querySelector('.dashicons');
var result = btn.closest('div').querySelector('.wbf-dc-user-result');
btn.disabled = true;
if (icon) icon.classList.add('wbf-spinning');
if (result) { result.style.color='#374151'; result.textContent='⏳'; }
fetch(ajaxurl, {
method : 'POST',
headers : {'Content-Type':'application/x-www-form-urlencoded'},
body : 'action=wbf_discord_sync_user&nonce=' + nonce + '&user_id=' + uid
})
.then(function(r){ return r.json(); })
.then(function(d) {
btn.disabled = false;
if (icon) icon.classList.remove('wbf-spinning');
if (d.success) {
if (result) { result.style.color='#16a34a'; result.textContent='✅ OK'; }
// Rollenbadge in dieser Zeile nach 1s aktualisieren (Seitenreload)
setTimeout(function(){ location.reload(); }, 1200);
} else {
if (result) {
result.style.color = '#dc2626';
result.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
}
}
})
.catch(function() {
btn.disabled = false;
if (icon) icon.classList.remove('wbf-spinning');
if (result) { result.style.color='#dc2626'; result.textContent='❌ Netzwerkfehler'; }
});
});
});
})();
</script>
<?php endif; ?>
<?php
}
@@ -3191,6 +3349,8 @@ function wbf_admin_profile_fields() {
<!-- ── Felder je Kategorie ───────────────────────────────── -->
<?php
// Globaler Feld-Index — synchronisiert Checkboxen mit den sequentiellen []Arrays
$wbf_fidx = 0;
// Alle Kategorien + "Ohne Kategorie" am Ende ausgeben
$all_sections = $cats;
if ( isset($by_cat['__none__']) ) {
@@ -3234,7 +3394,8 @@ function wbf_admin_profile_fields() {
<?php
endif;
foreach ( $c_fields as $i_f => $f ):
$fi = 'fi_' . $f['key'];
$fi = $wbf_fidx;
$wbf_fidx++;
?>
<tr class="wbf-field-row" style="background:#fff">
<td style="padding:6px 8px">
@@ -3268,9 +3429,11 @@ function wbf_admin_profile_fields() {
<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;padding:6px 8px">
<input type="hidden" name="field_required[<?php echo $fi; ?>]" value="0">
<input type="checkbox" name="field_required[<?php echo $fi; ?>]" value="1" <?php checked($f['required']??0,1); ?>>
</td>
<td style="text-align:center;padding:6px 8px">
<input type="hidden" name="field_public[<?php echo $fi; ?>]" value="0">
<input type="checkbox" name="field_public[<?php echo $fi; ?>]" value="1" <?php checked($f['public']??1,1); ?>>
</td>
<td style="padding:6px 8px">
@@ -3318,7 +3481,7 @@ function wbf_admin_profile_fields() {
</div>
<script>
var wbfRowCount = <?php echo count($fields) + 100; ?>;
var wbfRowCount = <?php echo $wbf_fidx; ?>;
function wbfRemoveRow(btn) {
var tr = btn.closest('tr');
@@ -3359,8 +3522,8 @@ function wbf_admin_profile_fields() {
'<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="text-align:center;padding:6px 8px"><input type="hidden" name="field_required[' + i + ']" value="0"><input type="checkbox" name="field_required[' + i + ']" value="1"></td>' +
'<td style="text-align:center;padding:6px 8px"><input type="hidden" name="field_public[' + i + ']" value="0"><input type="checkbox" name="field_public[' + 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);
@@ -3837,4 +4000,232 @@ function wbf_admin_wordfilter() {
</form>
</div>
<?php
}
// ── Discord-Bot-Verbindungstest (Admin AJAX) ──────────────────────────────────
add_action('wp_ajax_wbf_discord_test', function() {
if ( ! current_user_can('manage_options') ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
check_ajax_referer('wbf_discord_test', 'nonce');
$s = wbf_get_settings();
$token = trim($s['discord_bot_token'] ?? '');
$guild = trim($s['discord_guild_id'] ?? '');
if ( ! $token ) {
wp_send_json_error(['message' => 'Kein Bot-Token gespeichert.']);
}
// Bot-Info abrufen (@me)
$res = wp_remote_get('https://discord.com/api/v10/users/@me', [
'timeout' => 8,
'headers' => [
'Authorization' => 'Bot ' . $token,
'Content-Type' => 'application/json',
],
]);
if ( is_wp_error($res) ) {
wp_send_json_error(['message' => 'HTTP-Fehler: ' . $res->get_error_message()]);
}
$code = wp_remote_retrieve_response_code($res);
$body = json_decode(wp_remote_retrieve_body($res), true);
if ( $code !== 200 || empty($body['id']) ) {
$err = $body['message'] ?? 'Unbekannter Fehler (HTTP ' . $code . ')';
wp_send_json_error(['message' => 'Discord API: ' . $err]);
}
$bot_name = ($body['username'] ?? 'Unbekannt') . '#' . ($body['discriminator'] ?? '0');
// Guild-Prüfung falls Guild-ID angegeben
$guild_info = '';
if ( $guild ) {
$gr = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if ( ! is_wp_error($gr) && wp_remote_retrieve_response_code($gr) === 200 ) {
$gd = json_decode(wp_remote_retrieve_body($gr), true);
$guild_info = ' | Server: ' . ($gd['name'] ?? $guild);
} else {
$guild_info = ' | ⚠️ Server nicht gefunden oder Bot kein Mitglied';
}
}
wp_send_json_success(['message' => 'Bot: ' . $bot_name . $guild_info]);
});
// ── Discord-Cron: Rollen synchronisieren ──────────────────────────────────────
add_action('wbf_discord_role_sync', 'wbf_run_discord_role_sync');
if ( ! wp_next_scheduled('wbf_discord_role_sync') ) {
wp_schedule_event(time(), 'hourly', 'wbf_discord_role_sync');
}
function wbf_run_discord_role_sync() {
$s = wbf_get_settings();
if ( ($s['discord_role_sync'] ?? '0') !== '1' ) return;
$token = trim($s['discord_bot_token'] ?? '');
$guild = trim($s['discord_guild_id'] ?? '');
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
if ( ! $token || ! $guild || empty($role_map) ) return;
global $wpdb;
// Alle verifizierten Discord-User holen (discord_user_id in user_meta gesetzt)
$rows = $wpdb->get_results(
"SELECT um.user_id, um.meta_value AS discord_user_id
FROM {$wpdb->prefix}forum_user_meta um
WHERE um.meta_key = 'discord_user_id' AND um.meta_value != ''"
);
foreach ( $rows as $row ) {
wbf_sync_discord_role_for_user((int)$row->user_id, $row->discord_user_id, $token, $guild, $role_map);
}
}
/**
* Synchronisiert die Discord-Serverrolle eines einzelnen Nutzers mit der Forum-Rolle.
*/
function wbf_sync_discord_role_for_user($forum_user_id, $discord_user_id, $token, $guild, $role_map) {
// Guild-Member-Info abrufen
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [
'timeout' => 6,
'headers' => ['Authorization' => 'Bot ' . $token],
]);
if ( is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200 ) return;
$member = json_decode(wp_remote_retrieve_body($res), true);
$user_roles = $member['roles'] ?? [];
// Rollen-Map prüfen — erster Treffer gewinnt (Reihenfolge = Priorität)
foreach ( $role_map as $dc_role_id => $forum_role ) {
if ( in_array((string)$dc_role_id, array_map('strval', $user_roles), true) ) {
$forum_user = WBF_DB::get_user($forum_user_id);
if ( $forum_user && $forum_user->role !== 'superadmin' && $forum_user->role !== $forum_role ) {
WBF_DB::update_user($forum_user_id, ['role' => $forum_role]);
}
return;
}
}
}
// ── Discord-Admin-Seite ───────────────────────────────────────────────────────
if ( ! function_exists('wbf_admin_discord') ) {
function wbf_admin_discord() {
if ( ! current_user_can('manage_options') ) return;
$s = wbf_get_settings();
?>
<div class="wrap">
<h1>🎮 Discord-Integration</h1>
<p>Konfiguriere den Discord-Bot und die Rollen-Synchronisation.
Einstellungen werden in <a href="admin.php?page=wbf-settings">Einstellungen → Discord-Integration</a> gespeichert.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;max-width:900px;margin-top:1.5rem">
<!-- Status-Panel -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px">
<h3 style="margin-top:0">🔌 Bot-Status</h3>
<?php
$token = trim($s['discord_bot_token'] ?? '');
$guild = trim($s['discord_guild_id'] ?? '');
if (!$token): ?>
<p style="color:#dc2626"><i class="dashicons dashicons-warning"></i> Kein Bot-Token konfiguriert.</p>
<a href="admin.php?page=wbf-settings#discord" class="button button-primary">Jetzt einrichten</a>
<?php else: ?>
<p style="color:#16a34a;font-weight:600">✅ Bot-Token gespeichert</p>
<p style="color:<?php echo $guild ? '#16a34a' : '#f59e0b'; ?>">
<?php echo $guild ? '✅ Guild-ID: <code>' . esc_html($guild) . '</code>' : '⚠️ Keine Guild-ID gesetzt'; ?>
</p>
<p style="color:<?php echo ($s['discord_role_sync']??'0')==='1' ? '#16a34a' : '#9ca3af'; ?>">
Rollen-Sync: <strong><?php echo ($s['discord_role_sync']??'0')==='1' ? 'Aktiv' : 'Deaktiviert'; ?></strong>
</p>
<button type="button" class="button button-secondary" id="wbf-discord-test-btn2">
🔌 Verbindung testen
</button>
<span id="wbf-discord-test-result2" style="margin-left:10px;font-weight:600"></span>
<script>
document.getElementById('wbf-discord-test-btn2').addEventListener('click', function(){
var btn = this, res = document.getElementById('wbf-discord-test-result2');
btn.disabled = true; res.textContent = '⏳ Teste…';
fetch(ajaxurl,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
}).then(r=>r.json()).then(function(d){
res.style.color = d.success ? '#16a34a' : '#dc2626';
res.textContent = d.success ? '✅ '+(d.data.message||'OK') : '❌ '+((d.data&&d.data.message)||'Fehler');
btn.disabled = false;
}).catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
});
</script>
<?php endif; ?>
</div>
<!-- Rollen-Map-Übersicht -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px">
<h3 style="margin-top:0">🔗 Aktive Rollen-Zuordnungen</h3>
<?php
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
$all_roles = WBF_Roles::get_all();
if (empty($role_map)): ?>
<p style="color:#9ca3af">Keine Zuordnungen konfiguriert.</p>
<a href="admin.php?page=wbf-settings" class="button">Jetzt einrichten</a>
<?php else: ?>
<table class="widefat striped" style="font-size:.85rem">
<thead><tr><th>Discord Rollen-ID</th><th>Forum-Rolle</th></tr></thead>
<tbody>
<?php foreach ($role_map as $dc_id => $fr_key):
$fr_label = $all_roles[$fr_key]['label'] ?? $fr_key; ?>
<tr>
<td><code><?php echo esc_html($dc_id); ?></code></td>
<td><?php echo WBF_Roles::badge($fr_key); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<a href="admin.php?page=wbf-settings" class="button" style="margin-top:.75rem">Bearbeiten</a>
<?php endif; ?>
</div>
</div>
<!-- Verknüpfte Discord-Nutzer -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px;max-width:900px;margin-top:20px">
<h3 style="margin-top:0">👥 Verknüpfte Forum-Nutzer</h3>
<?php
global $wpdb;
$linked = $wpdb->get_results(
"SELECT fu.id, fu.username, fu.display_name, fu.role,
MAX(CASE WHEN um.meta_key='discord_username' THEN um.meta_value END) AS discord_name,
MAX(CASE WHEN um.meta_key='discord_user_id' THEN um.meta_value END) AS discord_uid
FROM {$wpdb->prefix}forum_users fu
JOIN {$wpdb->prefix}forum_user_meta um ON um.user_id = fu.id
WHERE um.meta_key IN ('discord_username','discord_user_id')
GROUP BY fu.id
HAVING discord_name != '' AND discord_name IS NOT NULL
ORDER BY fu.username"
);
if (empty($linked)): ?>
<p style="color:#9ca3af">Noch keine verknüpften Nutzer.</p>
<?php else: ?>
<table class="widefat striped" style="font-size:.85rem">
<thead><tr><th>Forum-Nutzer</th><th>Rolle</th><th>Discord-Name</th><th>Discord-ID</th></tr></thead>
<tbody>
<?php foreach ($linked as $u): ?>
<tr>
<td><strong><?php echo esc_html($u->display_name); ?></strong>
<span style="color:#9ca3af"> @<?php echo esc_html($u->username); ?></span></td>
<td><?php echo WBF_Roles::badge($u->role); ?></td>
<td><i class="fab fa-discord" style="color:#5865f2"></i> <?php echo esc_html($u->discord_name ?: ''); ?></td>
<td><code style="font-size:.78rem"><?php echo esc_html($u->discord_uid ?: ''); ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php
}
}

View File

@@ -54,6 +54,13 @@ if ( ! function_exists('wbf_get_settings') ) {
'rules_content' => "**1. Respektvoller Umgang**\nBehandle alle Mitglieder freundlich und respektvoll. Beleidigungen, Mobbing und Diskriminierung sind nicht toleriert.\n\n**2. Keine Spam-Inhalte**\nWerbung, Spam und irrelevante Links sind verboten.\n\n**3. Keine illegalen Inhalte**\nJegliche Inhalte, die gegen geltendes Recht verstoßen, sind streng verboten.\n\n**4. Themenrelevanz**\nBeiträge sollten zur jeweiligen Kategorie passen.\n\n**5. Urheberrecht**\nVeröffentliche keine Inhalte, an denen du keine Rechte besitzt.\n\n**6. Datenschutz**\nTeile keine persönlichen Daten anderer Personen ohne deren Zustimmung.\n\n**7. Moderations-Entscheidungen**\nEntscheidungen der Moderatoren sind zu respektieren. Bei Fragen wende dich direkt ans Team.\n\nVerstöße können zur Verwarnung oder dauerhaften Sperrung führen.",
// Ignore/Block-System: Rollen die nicht geblockt werden können (kommagetrennte Schlüssel)
'ignore_blocked_roles' => 'superadmin,admin,moderator',
// Discord-Integration
'discord_bot_token' => '',
'discord_guild_id' => '',
'discord_client_id' => '',
'discord_client_secret' => '',
'discord_role_sync' => '0', // Rollen-Sync aktiviert?
'discord_role_map' => '', // JSON: {"discord_role_id":"forum_role_key"}
];
$saved = get_option( 'wbf_settings', [] );
@@ -130,6 +137,27 @@ function wbf_admin_settings() {
// rules_content separat (nicht in $fields, da textarea mit eigener Behandlung)
$settings['rules_content'] = sanitize_textarea_field( $_POST['rules_content'] ?? '' );
// Discord-Einstellungen gesondert speichern (sensitiv — niemals in wbf_settings öffentlich)
$discord_fields = ['discord_bot_token', 'discord_guild_id', 'discord_client_id', 'discord_client_secret'];
foreach ( $discord_fields as $df ) {
$settings[$df] = sanitize_text_field( $_POST[$df] ?? '' );
}
$settings['discord_role_sync'] = isset($_POST['discord_role_sync']) && $_POST['discord_role_sync'] === '1' ? '1' : '0';
// Discord-Rollen-Map: Array von discord_role_id => forum_role_key
$role_map = [];
$dc_ids = array_map('sanitize_text_field', (array)($_POST['discord_role_id'] ?? []));
$fr_keys = array_map('sanitize_key', (array)($_POST['discord_forum_role'] ?? []));
$valid_roles = array_keys(WBF_Roles::get_all());
foreach ( $dc_ids as $i => $dc_id ) {
$dc_id = trim($dc_id);
$fr_key = $fr_keys[$i] ?? '';
if ( $dc_id !== '' && in_array($fr_key, $valid_roles, true) ) {
$role_map[$dc_id] = $fr_key;
}
}
$settings['discord_role_map'] = json_encode($role_map);
// 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'];
@@ -150,6 +178,12 @@ function wbf_admin_settings() {
$settings['ignore_blocked_roles'] = implode( ',', $checked_roles );
update_option( 'wbf_settings', $settings );
// Superadmin WP-User-ID separat speichern (außerhalb von wbf_settings)
$sa_wp_id = (int) ( $_POST['superadmin_wp_id'] ?? 1 );
if ( $sa_wp_id < 1 ) $sa_wp_id = 1;
update_option( 'wbf_superadmin_wp_id', $sa_wp_id );
echo '<div class="notice notice-success is-dismissible"><p>✅ Einstellungen gespeichert!</p></div>';
}
@@ -251,6 +285,40 @@ function wbf_admin_settings() {
🔒 Sicherheit
</h2>
<table class="form-table" role="presentation">
<!-- ── Superadmin WP-User-ID ─────────────────── -->
<tr>
<th scope="row">
<label for="wbf_superadmin_wp_id">Superadmin WordPress-User-ID</label>
</th>
<td>
<?php
$sa_id = (int) get_option( 'wbf_superadmin_wp_id', 1 );
$sa_wpuser = get_userdata( $sa_id );
?>
<input type="number" id="wbf_superadmin_wp_id" name="superadmin_wp_id"
value="<?php echo $sa_id; ?>"
min="1" step="1"
style="width:80px">
<?php if ( $sa_wpuser ) : ?>
<span style="margin-left:10px;color:#16a34a;font-weight:600">
✅ <?php echo esc_html( $sa_wpuser->display_name ); ?>
&lt;<?php echo esc_html( $sa_wpuser->user_email ); ?>&gt;
</span>
<?php else : ?>
<span style="margin-left:10px;color:#dc2626;font-weight:600">
⚠️ Kein WordPress-User mit dieser ID gefunden!
</span>
<?php endif; ?>
<p class="description">
Nur dieser WordPress-User erhält automatisch die Forum-Rolle <strong>Superadmin</strong>
und kann sie nicht verlieren. Alle anderen WordPress-Admins können normale Forum-Rollen
haben und im Mitglieder-Bereich frei zugewiesen werden.<br>
<em>Standard: 1 (erster bei der WP-Installation angelegter User)</em>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="wbf_auto_logout_minutes">Auto-Logout nach Inaktivität</label>
@@ -499,6 +567,170 @@ function wbf_admin_settings() {
</tr>
</table>
<!-- ══════════════════════════════════════════════════════════
DISCORD-INTEGRATION
══════════════════════════════════════════════════════════ -->
<h2 style="border-bottom:1px solid #ddd;padding-bottom:.4rem;margin-top:2rem">
<span style="color:#5865f2">🎮</span> Discord-Integration
</h2>
<p class="description" style="margin-bottom:1rem">
Bot-Token und Guild-ID findest du im <a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a>.
Der Bot muss Mitglied deines Servers sein und die Berechtigung <strong>Direct Messages lesen/senden</strong> sowie
<strong>Server-Mitglieder verwalten</strong> besitzen.
</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="wbf_discord_bot_token">Bot-Token</label></th>
<td>
<input type="password" id="wbf_discord_bot_token" name="discord_bot_token"
value="<?php echo esc_attr($s['discord_bot_token']); ?>"
class="regular-text" autocomplete="off" placeholder="Bot-Token aus dem Developer Portal">
<p class="description">Niemals öffentlich teilen! Wird verschlüsselt in der Datenbank gespeichert.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_guild_id">Server-ID (Guild ID)</label></th>
<td>
<input type="text" id="wbf_discord_guild_id" name="discord_guild_id"
value="<?php echo esc_attr($s['discord_guild_id']); ?>"
class="regular-text" placeholder="z. B. 123456789012345678">
<p class="description">Rechtsklick auf deinen Server → ID kopieren (Entwicklermodus muss aktiv sein).</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_client_id">Client ID (optional)</label></th>
<td>
<input type="text" id="wbf_discord_client_id" name="discord_client_id"
value="<?php echo esc_attr($s['discord_client_id']); ?>"
class="regular-text" placeholder="Application ID">
<p class="description">Für zukünftige OAuth2-Unterstützung. Aktuell optional.</p>
</td>
</tr>
<tr>
<th scope="row"><label for="wbf_discord_client_secret">Client Secret (optional)</label></th>
<td>
<input type="password" id="wbf_discord_client_secret" name="discord_client_secret"
value="<?php echo esc_attr($s['discord_client_secret']); ?>"
class="regular-text" autocomplete="off" placeholder="Client Secret">
</td>
</tr>
<tr>
<th scope="row">Rollen-Sync aktivieren</th>
<td>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" name="discord_role_sync" value="1"
<?php checked('1', $s['discord_role_sync'] ?? '0'); ?>>
Discord-Serverrollen automatisch auf Forum-Rollen mappen
</label>
<p class="description">
Wenn aktiviert, wird bei jedem Login und stündlich per Cron die Discord-Rolle des Nutzers
geprüft und die Forum-Rolle entsprechend der unten definierten Zuordnung aktualisiert.
</p>
</td>
</tr>
</table>
<!-- Discord Rollen-Map -->
<h3 style="margin-top:1.5rem">🔗 Discord-Rollen → Forum-Rollen Zuordnung</h3>
<p class="description" style="margin-bottom:.75rem">
Trage die Discord-Rollen-ID und die gewünschte Forum-Rolle ein.
Mehrere Einträge werden der Reihe nach geprüft — der erste Treffer gewinnt.
</p>
<?php
$role_map_raw = $s['discord_role_map'] ?? '{}';
$role_map = json_decode($role_map_raw, true) ?: [];
$forum_roles = WBF_Roles::get_sorted();
// Sicherstellen dass mindestens eine leere Zeile zum Hinzufügen da ist
if ( empty($role_map) ) $role_map[''] = '';
?>
<table class="widefat" id="wbf-discord-role-map" style="max-width:680px;margin-bottom:.75rem">
<thead><tr>
<th style="width:50%">Discord Rollen-ID</th>
<th style="width:40%">Forum-Rolle</th>
<th style="width:10%"></th>
</tr></thead>
<tbody>
<?php foreach ( $role_map as $dc_id => $fr_key ) : ?>
<tr class="wbf-role-map-row">
<td><input type="text" name="discord_role_id[]"
value="<?php echo esc_attr($dc_id); ?>"
placeholder="Discord Rollen-ID"
class="widefat" style="font-family:monospace"></td>
<td>
<select name="discord_forum_role[]" class="widefat">
<option value="">— wählen —</option>
<?php foreach ( $forum_roles as $rk => $role ) :
if ( $rk === 'superadmin' ) continue; ?>
<option value="<?php echo esc_attr($rk); ?>"
<?php selected($rk, $fr_key); ?>>
<?php echo esc_html($role['label']); ?>
</option>
<?php endforeach; ?>
</select>
</td>
<td><button type="button" class="button button-small wbf-rm-role-row"
style="color:#c00">✕</button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="button" id="wbf-add-role-row">+ Zeile hinzufügen</button>
<script>
(function(){
document.getElementById('wbf-add-role-row').addEventListener('click', function(){
var tbody = document.querySelector('#wbf-discord-role-map tbody');
var row = document.querySelector('.wbf-role-map-row').cloneNode(true);
row.querySelectorAll('input').forEach(function(i){i.value='';});
row.querySelectorAll('select').forEach(function(s){s.selectedIndex=0;});
tbody.appendChild(row);
});
document.addEventListener('click', function(e){
if (e.target.classList.contains('wbf-rm-role-row')) {
var rows = document.querySelectorAll('.wbf-role-map-row');
if (rows.length > 1) e.target.closest('tr').remove();
}
});
})();
</script>
<!-- Test-Verbindung -->
<div style="margin-top:1.25rem;padding:1rem;background:#f0f7ff;border:1px solid #c3dafe;border-radius:6px;max-width:680px">
<strong>🔌 Verbindungstest</strong><br>
<p style="margin:.4rem 0 .75rem;color:#374151;font-size:.9rem">
Speichere zuerst die Einstellungen, dann klicke „Testen" um zu prüfen ob der Bot erreichbar ist.
</p>
<button type="button" class="button button-secondary" id="wbf-discord-test-btn">
🔌 Discord-Verbindung testen
</button>
<span id="wbf-discord-test-result" style="margin-left:10px;font-weight:600"></span>
</div>
<script>
document.getElementById('wbf-discord-test-btn').addEventListener('click', function(){
var btn = this;
var res = document.getElementById('wbf-discord-test-result');
btn.disabled = true;
res.textContent = '⏳ Teste…';
fetch(ajaxurl, {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded'},
body: 'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
})
.then(r => r.json())
.then(function(d){
if (d.success) {
res.style.color = '#16a34a';
res.textContent = '✅ ' + (d.data.message || 'Verbunden!');
} else {
res.style.color = '#dc2626';
res.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
}
btn.disabled = false;
})
.catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
});
</script>
<?php submit_button(
'💾 Einstellungen speichern',
'primary',
@@ -506,7 +738,6 @@ function wbf_admin_settings() {
true,
[ 'style' => 'margin-top:1rem' ]
); ?>
</form>
<!-- ── Vorschau-Tabelle ──────────────────────────────── -->
<hr style="margin-top:2.5rem">