Community
Dein Community-Status
diff --git a/public/page/staging-users.php b/public/page/staging-users.php
new file mode 100644
index 0000000..cf3dfef
--- /dev/null
+++ b/public/page/staging-users.php
@@ -0,0 +1,102 @@
+pdo();
+$users = [];
+$error = '';
+
+try {
+ if (!$pdo) {
+ throw new RuntimeException('Keine Datenbankverbindung verfügbar.');
+ }
+
+ $hasRolesTable = false;
+ try {
+ $stmt = $pdo->query("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'user_roles' LIMIT 1");
+ $hasRolesTable = (bool)$stmt->fetchColumn();
+ } catch (Throwable) {
+ $hasRolesTable = false;
+ }
+
+ $sql = '
+ SELECT u.id,
+ u.email,
+ u.status,
+ u.created_at,
+ COALESCE(p.display_name, "") AS display_name
+ FROM users u
+ LEFT JOIN user_profiles p ON p.user_id = u.id
+ ORDER BY u.id ASC
+ ';
+ $stmt = $pdo->query($sql);
+ $users = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
+
+ if ($hasRolesTable && $users) {
+ $roleStmt = $pdo->query('SELECT user_id, role FROM user_roles ORDER BY user_id ASC, role ASC');
+ $rolesByUser = [];
+ foreach ($roleStmt->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
+ $rolesByUser[(int)$row['user_id']][] = (string)$row['role'];
+ }
+ foreach ($users as &$user) {
+ $user['roles'] = $rolesByUser[(int)$user['id']] ?? [];
+ }
+ unset($user);
+ }
+} catch (Throwable $e) {
+ $error = $e->getMessage();
+}
+?>
+
+
+
+
Staging: Registrierte Benutzer
+
+
+
+
+
+ Fehler: = htmlspecialchars($error, ENT_QUOTES) ?>
+
+
+
+
Diese Seite ist nur im Staging verfügbar.
+
+
+
Keine Benutzer gefunden.
+
+
+
+
+
+ | ID |
+ Anzeigename |
+ E-Mail |
+ Status |
+ Registriert |
+ Rollen |
+
+
+
+
+
+ | = (int)$user['id'] ?> |
+ = htmlspecialchars((string)$user['display_name'], ENT_QUOTES) ?> |
+ = htmlspecialchars((string)$user['email'], ENT_QUOTES) ?> |
+ = htmlspecialchars((string)$user['status'], ENT_QUOTES) ?> |
+ = htmlspecialchars((string)$user['created_at'], ENT_QUOTES) ?> |
+ = htmlspecialchars(implode(', ', $user['roles'] ?? []), ENT_QUOTES) ?> |
+
+
+
+
+
+
+
+
+
diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php
index 589c588..b623613 100755
--- a/src/App/AccountPages.php
+++ b/src/App/AccountPages.php
@@ -157,6 +157,7 @@ final class AccountPages
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
$community = $pdo ? new Community($pdo, $communityConfig) : null;
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
+ $communityMigration = $pdo ? new CommunityMigration($pdo) : null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
@@ -296,6 +297,12 @@ final class AccountPages
}
$communityAccess->submitApplication($userId, (string)($_POST['motivation'] ?? ''));
$info = 'Deine Bewerbung wurde eingereicht.';
+ } elseif ($action === 'community_migrate') {
+ if (!$communityAccess || !$communityMigration || !$communityAccess->canManageApplications($userId)) {
+ throw new \RuntimeException('Keine Berechtigung für die Community-Migration.');
+ }
+ $communityMigration->apply();
+ $info = 'Die Community-Migration wurde ausgeführt.';
}
} catch (\Throwable $e) {
$error = $e->getMessage();
@@ -377,6 +384,8 @@ final class AccountPages
'reply_blocked' => false,
'reason' => null,
];
+ $communityIsSiteAdmin = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
+ $communityMigrationStatus = ($communityMigration && $communityIsSiteAdmin) ? $communityMigration->status() : null;
return compact(
'flash',
@@ -392,7 +401,9 @@ final class AccountPages
'communityRoles',
'communityApplication',
'communityCanApply',
- 'communityRestrictions'
+ 'communityRestrictions',
+ 'communityIsSiteAdmin',
+ 'communityMigrationStatus'
);
}
diff --git a/src/App/Community.php b/src/App/Community.php
index 0b27a4f..1327755 100755
--- a/src/App/Community.php
+++ b/src/App/Community.php
@@ -140,16 +140,12 @@ final class Community
$params[$ph2] = '%' . $tok . '%';
$i++;
}
+ $defaultBoard = $this->getBoardBySlug('wochenendideen');
$boardJoin = '';
$boardSelect = "NULL AS board_slug, NULL AS board_title, NULL AS category_slug, NULL AS category_title";
- $boardWhere = '';
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
$boardJoin = ' LEFT JOIN forum_boards fb ON fb.id = ft.board_id LEFT JOIN forum_categories fc ON fc.id = fb.category_id ';
$boardSelect = 'fb.slug AS board_slug, fb.title AS board_title, fc.slug AS category_slug, fc.title AS category_title';
- if ($boardSlug !== null && $boardSlug !== '') {
- $boardWhere = ' AND fb.slug = :boardSlug';
- $params[':boardSlug'] = $boardSlug;
- }
}
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
@@ -182,7 +178,7 @@ final class Community
JOIN users u ON u.id = ft.user_id
LEFT JOIN user_profiles p ON p.user_id = u.id
$boardJoin
- WHERE 1=1 $where $boardWhere
+ WHERE 1=1 $where
ORDER BY ft.created_at DESC
LIMIT :lim";
$stmt = $this->pdo->prepare($sql);
@@ -191,7 +187,12 @@ final class Community
}
$stmt->bindValue(':lim', $limit, \PDO::PARAM_INT);
$stmt->execute();
- return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
+ $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
+ $rows = array_map(fn(array $row) => $this->applyFallbackBoard($row, $defaultBoard), $rows);
+ if ($boardSlug !== null && $boardSlug !== '') {
+ $rows = array_values(array_filter($rows, fn(array $row): bool => (string)($row['board_slug'] ?? '') === $boardSlug));
+ }
+ return $rows;
}
public function listThreads(int $limit = 50, ?string $boardSlug = null): array
@@ -210,7 +211,10 @@ final class Community
$stmt = $this->pdo->prepare("SELECT $select FROM forum_threads ft LEFT JOIN user_profiles p ON p.user_id = ft.user_id $join WHERE ft.id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
- return $row ?: null;
+ if (!$row) {
+ return null;
+ }
+ return $this->applyFallbackBoard($row, $this->getBoardBySlug('wochenendideen'));
}
public function listPosts(int $threadId): array
@@ -477,6 +481,19 @@ final class Community
}
}
+ private function applyFallbackBoard(array $row, ?array $defaultBoard): array
+ {
+ if (!empty($row['board_slug']) || !$defaultBoard) {
+ return $row;
+ }
+
+ $row['board_slug'] = $defaultBoard['slug'] ?? null;
+ $row['board_title'] = $defaultBoard['title'] ?? null;
+ $row['category_slug'] = $defaultBoard['category_slug'] ?? null;
+ $row['category_title'] = $defaultBoard['category_title'] ?? null;
+ return $row;
+ }
+
private function hasTable(string $table): bool
{
if (array_key_exists($table, $this->tableCache)) {
diff --git a/src/App/CommunityMigration.php b/src/App/CommunityMigration.php
new file mode 100644
index 0000000..0b7a53d
--- /dev/null
+++ b/src/App/CommunityMigration.php
@@ -0,0 +1,154 @@
+ $this->hasTable('forum_categories'),
+ 'forum_boards' => $this->hasTable('forum_boards'),
+ 'forum_post_feedback' => $this->hasTable('forum_post_feedback'),
+ 'forum_reports' => $this->hasTable('forum_reports'),
+ 'user_roles' => $this->hasTable('user_roles'),
+ 'community_admin_applications' => $this->hasTable('community_admin_applications'),
+ 'community_user_restrictions' => $this->hasTable('community_user_restrictions'),
+ 'forum_threads.board_id' => $this->hasColumn('forum_threads', 'board_id'),
+ 'forum_posts.highlighted_by' => $this->hasColumn('forum_posts', 'highlighted_by'),
+ 'forum_posts.highlighted_at' => $this->hasColumn('forum_posts', 'highlighted_at'),
+ ];
+
+ $missing = array_keys(array_filter($checks, static fn(bool $ok): bool => !$ok));
+
+ return [
+ 'complete' => $missing === [],
+ 'checks' => $checks,
+ 'missing' => $missing,
+ ];
+ }
+
+ public function apply(): array
+ {
+ $statements = [
+ 'CREATE TABLE IF NOT EXISTS forum_categories (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ slug VARCHAR(120) NOT NULL UNIQUE,
+ title VARCHAR(180) NOT NULL,
+ sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+
+ 'CREATE TABLE IF NOT EXISTS forum_boards (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ category_id BIGINT UNSIGNED NOT NULL,
+ slug VARCHAR(120) NOT NULL UNIQUE,
+ title VARCHAR(180) NOT NULL,
+ description TEXT NULL,
+ icon VARCHAR(32) NULL,
+ sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ INDEX idx_fb_category (category_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+
+ 'ALTER TABLE forum_threads
+ ADD COLUMN IF NOT EXISTS board_id BIGINT UNSIGNED NULL,
+ ADD INDEX IF NOT EXISTS idx_ft_board (board_id)',
+
+ 'ALTER TABLE forum_posts
+ ADD COLUMN IF NOT EXISTS highlighted_by BIGINT UNSIGNED NULL,
+ ADD COLUMN IF NOT EXISTS highlighted_at DATETIME NULL',
+
+ 'CREATE TABLE IF NOT EXISTS forum_post_feedback (
+ post_id BIGINT UNSIGNED NOT NULL,
+ user_id BIGINT UNSIGNED NOT NULL,
+ value TINYINT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (post_id, user_id),
+ INDEX idx_fpf_value (value)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+
+ 'CREATE TABLE IF NOT EXISTS forum_reports (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ reporter_user_id BIGINT UNSIGNED NOT NULL,
+ target_type ENUM("thread","post") NOT NULL,
+ target_id BIGINT UNSIGNED NOT NULL,
+ reason TEXT NOT NULL,
+ status ENUM("open","resolved","dismissed") NOT NULL DEFAULT "open",
+ moderator_user_id BIGINT UNSIGNED NULL,
+ moderator_note TEXT NULL,
+ resolved_at DATETIME NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_fr_status (status),
+ INDEX idx_fr_target (target_type, target_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+
+ 'CREATE TABLE IF NOT EXISTS user_roles (
+ user_id BIGINT UNSIGNED NOT NULL,
+ role ENUM("forum_admin","site_admin","owner") NOT NULL,
+ assigned_by BIGINT UNSIGNED NULL,
+ assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (user_id, role)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+
+ 'CREATE TABLE IF NOT EXISTS community_admin_applications (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT UNSIGNED NOT NULL,
+ motivation TEXT NOT NULL,
+ status ENUM("open","approved","rejected") NOT NULL DEFAULT "open",
+ decision_reason TEXT NULL,
+ decided_by BIGINT UNSIGNED NULL,
+ decided_at DATETIME NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_caa_status (status)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+
+ 'CREATE TABLE IF NOT EXISTS community_user_restrictions (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT UNSIGNED NOT NULL,
+ restriction_type ENUM("thread_create_blocked","reply_blocked") NOT NULL,
+ reason TEXT NULL,
+ active TINYINT(1) NOT NULL DEFAULT 1,
+ created_by BIGINT UNSIGNED NOT NULL,
+ expires_at DATETIME NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_cur_user_active (user_id, active)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+ ];
+
+ foreach ($statements as $sql) {
+ $this->pdo->exec($sql);
+ }
+
+ // Initial owner/site-admin bootstrap for user 1 if roles table exists.
+ if ($this->hasTable('user_roles')) {
+ $stmt = $this->pdo->prepare('INSERT IGNORE INTO user_roles (user_id, role, assigned_by) VALUES (1, "owner", 1)');
+ $stmt->execute();
+ $stmt = $this->pdo->prepare('INSERT IGNORE INTO user_roles (user_id, role, assigned_by) VALUES (1, "site_admin", 1)');
+ $stmt->execute();
+ }
+
+ return $this->status();
+ }
+
+ private function hasTable(string $table): bool
+ {
+ $stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table LIMIT 1');
+ $stmt->execute(['table' => $table]);
+ return (bool)$stmt->fetchColumn();
+ }
+
+ private function hasColumn(string $table, string $column): bool
+ {
+ $stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = :table AND column_name = :column LIMIT 1');
+ $stmt->execute([
+ 'table' => $table,
+ 'column' => $column,
+ ]);
+ return (bool)$stmt->fetchColumn();
+ }
+}