This commit is contained in:
@@ -153,6 +153,10 @@ final class AccountPages
|
||||
$info = '';
|
||||
$crypto = null;
|
||||
try { $crypto = new Crypto($app->config()); } catch (\Throwable) {}
|
||||
$communityCfg = dirname(__DIR__, 2) . '/config/community.php';
|
||||
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
|
||||
$community = $pdo ? new Community($pdo, $communityConfig) : null;
|
||||
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
@@ -282,6 +286,16 @@ final class AccountPages
|
||||
'id' => $eventId,
|
||||
]);
|
||||
$info = 'Event wurde abgesagt.';
|
||||
} elseif ($action === 'community_admin_apply') {
|
||||
if (!$community || !$communityAccess) {
|
||||
throw new \RuntimeException('Community-Funktionen sind aktuell nicht verfügbar.');
|
||||
}
|
||||
$points = $community->computePoints($userId);
|
||||
if (!$communityAccess->canApplyForForumAdmin($userId, $points)) {
|
||||
throw new \RuntimeException('Du kannst dich aktuell nicht als Forum-Admin bewerben.');
|
||||
}
|
||||
$communityAccess->submitApplication($userId, (string)($_POST['motivation'] ?? ''));
|
||||
$info = 'Deine Bewerbung wurde eingereicht.';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
@@ -353,7 +367,33 @@ final class AccountPages
|
||||
$editEvent = $stmt?->fetch(\PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
return compact('flash','info','error','profile','children','eventsUpcoming','eventsPast','editEvent');
|
||||
$communityPoints = $community ? $community->computePoints($userId) : 0.0;
|
||||
$communityLevel = $community ? $community->membershipLevel($communityPoints) : ['label' => '', 'icon' => ''];
|
||||
$communityRoles = $communityAccess ? $communityAccess->getUserRoles($userId) : [];
|
||||
$communityApplication = $communityAccess ? $communityAccess->getLatestApplication($userId) : null;
|
||||
$communityCanApply = $communityAccess ? $communityAccess->canApplyForForumAdmin($userId, $communityPoints) : false;
|
||||
$communityRestrictions = $communityAccess ? $communityAccess->getRestrictionState($userId) : [
|
||||
'thread_create_blocked' => false,
|
||||
'reply_blocked' => false,
|
||||
'reason' => null,
|
||||
];
|
||||
|
||||
return compact(
|
||||
'flash',
|
||||
'info',
|
||||
'error',
|
||||
'profile',
|
||||
'children',
|
||||
'eventsUpcoming',
|
||||
'eventsPast',
|
||||
'editEvent',
|
||||
'communityPoints',
|
||||
'communityLevel',
|
||||
'communityRoles',
|
||||
'communityApplication',
|
||||
'communityCanApply',
|
||||
'communityRestrictions'
|
||||
);
|
||||
}
|
||||
|
||||
private static function geocodeAddress(?string $street, ?string $zip, ?string $city, ?string $region): array
|
||||
|
||||
@@ -5,6 +5,10 @@ namespace App;
|
||||
|
||||
final class Community
|
||||
{
|
||||
private ?array $forumStructure = null;
|
||||
private array $tableCache = [];
|
||||
private array $columnCache = [];
|
||||
|
||||
public function __construct(private \PDO $pdo, private array $config)
|
||||
{
|
||||
}
|
||||
@@ -24,11 +28,41 @@ final class Community
|
||||
|
||||
public function createThread(int $userId, string $title, string $body): void
|
||||
{
|
||||
$this->createThreadInBoard($userId, null, $title, $body);
|
||||
}
|
||||
|
||||
public function createThreadInBoard(int $userId, ?string $boardSlug, string $title, string $body): void
|
||||
{
|
||||
$title = trim($title);
|
||||
$body = trim($body);
|
||||
if ($title === '' || $body === '') {
|
||||
throw new \RuntimeException('Titel und Text sind erforderlich.');
|
||||
}
|
||||
|
||||
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards')) {
|
||||
$boardId = null;
|
||||
if ($boardSlug !== null && $boardSlug !== '') {
|
||||
$board = $this->getBoardBySlug($boardSlug);
|
||||
if (!$board) {
|
||||
throw new \RuntimeException('Forum-Bereich nicht gefunden.');
|
||||
}
|
||||
$boardId = (int)$board['id'];
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT INTO forum_threads (board_id, user_id, title, body) VALUES (:boardId, :uid, :title, :body)');
|
||||
$stmt->bindValue(':boardId', $boardId, $boardId === null ? \PDO::PARAM_NULL : \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':uid', $userId, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':title', $title, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':body', $body, \PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT INTO forum_threads (user_id, title, body) VALUES (:uid, :title, :body)');
|
||||
$stmt->execute([
|
||||
':uid' => $userId,
|
||||
':title' => trim($title),
|
||||
':body' => trim($body),
|
||||
':title' => $title,
|
||||
':body' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -42,7 +76,57 @@ final class Community
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchThreads(string $query, int $limit = 50): array
|
||||
public function updateThread(int $threadId, string $title, string $body): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare('UPDATE forum_threads SET title = :title, body = :body, updated_at = NOW() WHERE id = :id');
|
||||
$stmt->execute([
|
||||
'id' => $threadId,
|
||||
'title' => trim($title),
|
||||
'body' => trim($body),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteThread(int $threadId): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare('DELETE FROM forum_threads WHERE id = :id');
|
||||
$stmt->execute(['id' => $threadId]);
|
||||
}
|
||||
|
||||
public function updatePost(int $postId, string $body): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare('UPDATE forum_posts SET body = :body, updated_at = NOW() WHERE id = :id');
|
||||
$stmt->execute([
|
||||
'id' => $postId,
|
||||
'body' => trim($body),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deletePost(int $postId): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare('DELETE FROM forum_posts WHERE id = :id');
|
||||
$stmt->execute(['id' => $postId]);
|
||||
}
|
||||
|
||||
public function toggleHelpfulHighlight(int $postId, ?int $userId): void
|
||||
{
|
||||
if (!$this->hasColumn('forum_posts', 'highlighted_at')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($userId === null) {
|
||||
$stmt = $this->pdo->prepare('UPDATE forum_posts SET highlighted_by = NULL, highlighted_at = NULL WHERE id = :id');
|
||||
$stmt->execute(['id' => $postId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('UPDATE forum_posts SET highlighted_by = :uid, highlighted_at = NOW() WHERE id = :id');
|
||||
$stmt->execute([
|
||||
'uid' => $userId,
|
||||
'id' => $postId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchThreads(string $query, int $limit = 50, ?string $boardSlug = null): array
|
||||
{
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
@@ -56,11 +140,24 @@ final class Community
|
||||
$params[$ph2] = '%' . $tok . '%';
|
||||
$i++;
|
||||
}
|
||||
$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)) : '';
|
||||
|
||||
$sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at,
|
||||
u.id as uid, u.created_at as user_created,
|
||||
p.display_name,
|
||||
$boardSelect,
|
||||
(SELECT COUNT(*) FROM forum_posts fp WHERE fp.thread_id = ft.id) AS answers,
|
||||
COALESCE(
|
||||
(SELECT MAX(fp.created_at) FROM forum_posts fp WHERE fp.thread_id = ft.id),
|
||||
@@ -84,7 +181,8 @@ final class Community
|
||||
FROM forum_threads ft
|
||||
JOIN users u ON u.id = ft.user_id
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
WHERE 1=1 $where
|
||||
$boardJoin
|
||||
WHERE 1=1 $where $boardWhere
|
||||
ORDER BY ft.created_at DESC
|
||||
LIMIT :lim";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
@@ -96,14 +194,20 @@ final class Community
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function listThreads(int $limit = 50): array
|
||||
public function listThreads(int $limit = 50, ?string $boardSlug = null): array
|
||||
{
|
||||
return $this->searchThreads('', $limit);
|
||||
return $this->searchThreads('', $limit, $boardSlug);
|
||||
}
|
||||
|
||||
public function getThread(int $id): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT ft.*, p.display_name FROM forum_threads ft LEFT JOIN user_profiles p ON p.user_id = ft.user_id WHERE ft.id = :id');
|
||||
$select = 'ft.*, p.display_name';
|
||||
$join = '';
|
||||
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
|
||||
$select .= ', fb.slug AS board_slug, fb.title AS board_title, fc.slug AS category_slug, fc.title AS category_title';
|
||||
$join = ' LEFT JOIN forum_boards fb ON fb.id = ft.board_id LEFT JOIN forum_categories fc ON fc.id = fb.category_id ';
|
||||
}
|
||||
$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;
|
||||
@@ -111,11 +215,97 @@ final class Community
|
||||
|
||||
public function listPosts(int $threadId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT fp.*, p.display_name FROM forum_posts fp LEFT JOIN user_profiles p ON p.user_id = fp.user_id WHERE fp.thread_id = :id ORDER BY fp.created_at ASC');
|
||||
$select = 'fp.*, p.display_name';
|
||||
if ($this->hasColumn('forum_posts', 'highlighted_at')) {
|
||||
$select .= ', hp.display_name AS highlighted_by_name';
|
||||
} else {
|
||||
$select .= ', NULL AS highlighted_by_name';
|
||||
}
|
||||
$join = ' LEFT JOIN user_profiles p ON p.user_id = fp.user_id ';
|
||||
if ($this->hasColumn('forum_posts', 'highlighted_at')) {
|
||||
$join .= ' LEFT JOIN user_profiles hp ON hp.user_id = fp.highlighted_by ';
|
||||
}
|
||||
$stmt = $this->pdo->prepare("SELECT $select FROM forum_posts fp $join WHERE fp.thread_id = :id ORDER BY fp.created_at ASC");
|
||||
$stmt->execute([':id' => $threadId]);
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function getPost(int $postId): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM forum_posts WHERE id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $postId]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function listForumCategories(): array
|
||||
{
|
||||
$structure = $this->forumStructure();
|
||||
$boards = [];
|
||||
foreach ($structure as $category) {
|
||||
foreach ($category['boards'] as $board) {
|
||||
$boards[$board['slug']] = $board;
|
||||
}
|
||||
}
|
||||
|
||||
$threadsByBoard = [];
|
||||
foreach ($this->listThreads(200) as $thread) {
|
||||
$slug = (string)($thread['board_slug'] ?? '');
|
||||
if ($slug === '' || !isset($boards[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$threadsByBoard[$slug][] = $thread;
|
||||
}
|
||||
|
||||
foreach ($structure as &$category) {
|
||||
foreach ($category['boards'] as &$board) {
|
||||
$items = $threadsByBoard[$board['slug']] ?? [];
|
||||
$board['thread_count'] = count($items);
|
||||
$replyCount = 0;
|
||||
foreach ($items as $item) {
|
||||
$replyCount += (int)($item['answers'] ?? 0);
|
||||
}
|
||||
$board['post_count'] = $replyCount;
|
||||
$board['latest_thread'] = $items[0] ?? null;
|
||||
}
|
||||
unset($board);
|
||||
}
|
||||
unset($category);
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
public function getBoardBySlug(string $slug): ?array
|
||||
{
|
||||
foreach ($this->forumStructure() as $category) {
|
||||
foreach ($category['boards'] as $board) {
|
||||
if ($board['slug'] === $slug) {
|
||||
if ($this->hasTable('forum_boards')) {
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT fb.id, fb.slug, fb.title, fb.description, fb.icon, fc.slug AS category_slug, fc.title AS category_title
|
||||
FROM forum_boards fb
|
||||
JOIN forum_categories fc ON fc.id = fb.category_id
|
||||
WHERE fb.slug = :slug
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute(['slug' => $slug]);
|
||||
$dbBoard = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($dbBoard) {
|
||||
return array_merge($board, $dbBoard);
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($board, [
|
||||
'category_slug' => $category['slug'],
|
||||
'category_title' => $category['title'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function computePoints(int $userId): float
|
||||
{
|
||||
// Primär: aggregierte Werte aus user_points_totals, Fallback: Summe aus user_points
|
||||
@@ -223,4 +413,92 @@ final class Community
|
||||
$fallback = $levels ? $levels[count($levels)-1] : ['label' => 'New Daddy','icon' => ''];
|
||||
return ['label' => $fallback['label'], 'icon' => $fallback['icon'] ?? ''];
|
||||
}
|
||||
|
||||
private function forumStructure(): array
|
||||
{
|
||||
if ($this->forumStructure !== null) {
|
||||
return $this->forumStructure;
|
||||
}
|
||||
|
||||
$file = dirname(__DIR__, 2) . '/config/community_forums.php';
|
||||
$this->forumStructure = file_exists($file) ? (array) require $file : [];
|
||||
$this->syncForumStructure();
|
||||
|
||||
return $this->forumStructure;
|
||||
}
|
||||
|
||||
private function syncForumStructure(): void
|
||||
{
|
||||
if (!$this->hasTable('forum_categories') || !$this->hasTable('forum_boards')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->forumStructure as $categoryIndex => $category) {
|
||||
$stmt = $this->pdo->prepare('INSERT INTO forum_categories (slug, title, sort_order) VALUES (:slug, :title, :sortOrder) ON DUPLICATE KEY UPDATE title = VALUES(title), sort_order = VALUES(sort_order)');
|
||||
$stmt->execute([
|
||||
'slug' => $category['slug'],
|
||||
'title' => $category['title'],
|
||||
'sortOrder' => $categoryIndex,
|
||||
]);
|
||||
|
||||
$catIdStmt = $this->pdo->prepare('SELECT id FROM forum_categories WHERE slug = :slug LIMIT 1');
|
||||
$catIdStmt->execute(['slug' => $category['slug']]);
|
||||
$categoryId = (int)$catIdStmt->fetchColumn();
|
||||
|
||||
foreach ($category['boards'] as $boardIndex => $board) {
|
||||
$boardStmt = $this->pdo->prepare('
|
||||
INSERT INTO forum_boards (category_id, slug, title, description, icon, sort_order)
|
||||
VALUES (:categoryId, :slug, :title, :description, :icon, :sortOrder)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
category_id = VALUES(category_id),
|
||||
title = VALUES(title),
|
||||
description = VALUES(description),
|
||||
icon = VALUES(icon),
|
||||
sort_order = VALUES(sort_order)
|
||||
');
|
||||
$boardStmt->execute([
|
||||
'categoryId' => $categoryId,
|
||||
'slug' => $board['slug'],
|
||||
'title' => $board['title'],
|
||||
'description' => $board['description'],
|
||||
'icon' => $board['icon'] ?? null,
|
||||
'sortOrder' => $boardIndex,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (array_key_exists($table, $this->tableCache)) {
|
||||
return $this->tableCache[$table];
|
||||
}
|
||||
|
||||
try {
|
||||
$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 $this->tableCache[$table] = (bool)$stmt->fetchColumn();
|
||||
} catch (\Throwable) {
|
||||
return $this->tableCache[$table] = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
{
|
||||
$key = $table . '.' . $column;
|
||||
if (array_key_exists($key, $this->columnCache)) {
|
||||
return $this->columnCache[$key];
|
||||
}
|
||||
|
||||
try {
|
||||
$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 $this->columnCache[$key] = (bool)$stmt->fetchColumn();
|
||||
} catch (\Throwable) {
|
||||
return $this->columnCache[$key] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
484
src/App/CommunityAccess.php
Normal file
484
src/App/CommunityAccess.php
Normal file
@@ -0,0 +1,484 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class CommunityAccess
|
||||
{
|
||||
private array $tableCache = [];
|
||||
private array $columnCache = [];
|
||||
|
||||
public function __construct(private \PDO $pdo, private array $communityConfig)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUserRoles(int $userId): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
if ($this->hasTable('user_roles')) {
|
||||
$stmt = $this->pdo->prepare('SELECT role FROM user_roles WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $userId]);
|
||||
$roles = array_map('strval', $stmt->fetchAll(\PDO::FETCH_COLUMN) ?: []);
|
||||
}
|
||||
|
||||
if ($userId === 1 && !in_array('owner', $roles, true)) {
|
||||
$roles[] = 'owner';
|
||||
}
|
||||
|
||||
if (in_array('owner', $roles, true)) {
|
||||
foreach (['site_admin', 'forum_admin'] as $derived) {
|
||||
if (!in_array($derived, $roles, true)) {
|
||||
$roles[] = $derived;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('site_admin', $roles, true) && !in_array('forum_admin', $roles, true)) {
|
||||
$roles[] = 'forum_admin';
|
||||
}
|
||||
|
||||
return array_values(array_unique($roles));
|
||||
}
|
||||
|
||||
public function hasRole(int $userId, string $role): bool
|
||||
{
|
||||
return in_array($role, $this->getUserRoles($userId), true);
|
||||
}
|
||||
|
||||
public function canModerateForum(int $userId): bool
|
||||
{
|
||||
return $this->hasRole($userId, 'forum_admin');
|
||||
}
|
||||
|
||||
public function canManageApplications(int $userId): bool
|
||||
{
|
||||
return $this->hasRole($userId, 'site_admin');
|
||||
}
|
||||
|
||||
public function canManageRoles(int $userId): bool
|
||||
{
|
||||
return $this->hasRole($userId, 'owner');
|
||||
}
|
||||
|
||||
public function getRestrictionState(int $userId): array
|
||||
{
|
||||
$state = [
|
||||
'thread_create_blocked' => false,
|
||||
'reply_blocked' => false,
|
||||
'reason' => null,
|
||||
];
|
||||
|
||||
if ($userId <= 0 || !$this->hasTable('community_user_restrictions')) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT restriction_type, reason
|
||||
FROM community_user_restrictions
|
||||
WHERE user_id = :uid
|
||||
AND active = 1
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
');
|
||||
$stmt->execute(['uid' => $userId]);
|
||||
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||
if (isset($state[$row['restriction_type']])) {
|
||||
$state[$row['restriction_type']] = true;
|
||||
if ($state['reason'] === null && !empty($row['reason'])) {
|
||||
$state['reason'] = (string)$row['reason'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
public function canCreateThread(int $userId): bool
|
||||
{
|
||||
return !$this->getRestrictionState($userId)['thread_create_blocked'];
|
||||
}
|
||||
|
||||
public function canReply(int $userId): bool
|
||||
{
|
||||
return !$this->getRestrictionState($userId)['reply_blocked'];
|
||||
}
|
||||
|
||||
public function canApplyForForumAdmin(int $userId, float $points): bool
|
||||
{
|
||||
if ($userId <= 0 || $points < 750.0 || !$this->hasTable('community_admin_applications')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['forum_admin', 'site_admin', 'owner'] as $role) {
|
||||
if ($this->hasRole($userId, $role)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$latest = $this->getLatestApplication($userId);
|
||||
return !$latest || $latest['status'] !== 'open';
|
||||
}
|
||||
|
||||
public function getLatestApplication(int $userId): ?array
|
||||
{
|
||||
if ($userId <= 0 || !$this->hasTable('community_admin_applications')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT caa.*, p.display_name AS decided_by_name
|
||||
FROM community_admin_applications caa
|
||||
LEFT JOIN user_profiles p ON p.user_id = caa.decided_by
|
||||
WHERE caa.user_id = :uid
|
||||
ORDER BY caa.created_at DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute(['uid' => $userId]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function submitApplication(int $userId, string $motivation): void
|
||||
{
|
||||
if (!$this->hasTable('community_admin_applications')) {
|
||||
throw new \RuntimeException('Admin-Bewerbungen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$motivation = trim($motivation);
|
||||
if ($motivation === '') {
|
||||
throw new \RuntimeException('Bitte begründe deine Bewerbung.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT INTO community_admin_applications (user_id, motivation, status) VALUES (:uid, :motivation, :status)');
|
||||
$stmt->execute([
|
||||
'uid' => $userId,
|
||||
'motivation' => $motivation,
|
||||
'status' => 'open',
|
||||
]);
|
||||
}
|
||||
|
||||
public function listApplications(string $status = 'open'): array
|
||||
{
|
||||
if (!$this->hasTable('community_admin_applications')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = '
|
||||
SELECT caa.*, up.display_name, u.email, dp.display_name AS decided_by_name
|
||||
FROM community_admin_applications caa
|
||||
JOIN users u ON u.id = caa.user_id
|
||||
LEFT JOIN user_profiles up ON up.user_id = caa.user_id
|
||||
LEFT JOIN user_profiles dp ON dp.user_id = caa.decided_by
|
||||
';
|
||||
$params = [];
|
||||
if ($status !== '') {
|
||||
$sql .= ' WHERE caa.status = :status';
|
||||
$params['status'] = $status;
|
||||
}
|
||||
$sql .= ' ORDER BY caa.created_at DESC';
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function decideApplication(int $adminUserId, int $applicationId, string $decision, ?string $reason = null): void
|
||||
{
|
||||
if (!$this->canManageApplications($adminUserId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung.');
|
||||
}
|
||||
if (!$this->hasTable('community_admin_applications')) {
|
||||
throw new \RuntimeException('Admin-Bewerbungen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$decision = $decision === 'approved' ? 'approved' : 'rejected';
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM community_admin_applications WHERE id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $applicationId]);
|
||||
$application = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!$application) {
|
||||
throw new \RuntimeException('Bewerbung nicht gefunden.');
|
||||
}
|
||||
|
||||
$update = $this->pdo->prepare('
|
||||
UPDATE community_admin_applications
|
||||
SET status = :status, decision_reason = :reason, decided_by = :decidedBy, decided_at = NOW()
|
||||
WHERE id = :id
|
||||
');
|
||||
$update->execute([
|
||||
'status' => $decision,
|
||||
'reason' => trim((string)$reason) ?: null,
|
||||
'decidedBy' => $adminUserId,
|
||||
'id' => $applicationId,
|
||||
]);
|
||||
|
||||
if ($decision === 'approved') {
|
||||
$this->assignRole($adminUserId, (int)$application['user_id'], 'forum_admin');
|
||||
}
|
||||
}
|
||||
|
||||
public function assignRole(int $actingUserId, int $targetUserId, string $role): void
|
||||
{
|
||||
if (!$this->canManageRoles($actingUserId) && !$this->canManageApplications($actingUserId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung.');
|
||||
}
|
||||
if (!$this->hasTable('user_roles')) {
|
||||
throw new \RuntimeException('Rollen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
if (!in_array($role, ['forum_admin', 'site_admin', 'owner'], true)) {
|
||||
throw new \RuntimeException('Ungültige Rolle.');
|
||||
}
|
||||
|
||||
if ($role !== 'forum_admin' && !$this->canManageRoles($actingUserId)) {
|
||||
throw new \RuntimeException('Nur der Inhaber darf diese Rolle vergeben.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT IGNORE INTO user_roles (user_id, role, assigned_by) VALUES (:uid, :role, :by)');
|
||||
$stmt->execute([
|
||||
'uid' => $targetUserId,
|
||||
'role' => $role,
|
||||
'by' => $actingUserId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function revokeRole(int $actingUserId, int $targetUserId, string $role): void
|
||||
{
|
||||
if (!$this->canManageRoles($actingUserId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung.');
|
||||
}
|
||||
if (!$this->hasTable('user_roles')) {
|
||||
throw new \RuntimeException('Rollen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('DELETE FROM user_roles WHERE user_id = :uid AND role = :role');
|
||||
$stmt->execute([
|
||||
'uid' => $targetUserId,
|
||||
'role' => $role,
|
||||
]);
|
||||
}
|
||||
|
||||
public function listRoleAssignments(): array
|
||||
{
|
||||
if (!$this->hasTable('user_roles')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->query('
|
||||
SELECT ur.user_id, ur.role, ur.assigned_at, up.display_name, u.email
|
||||
FROM user_roles ur
|
||||
JOIN users u ON u.id = ur.user_id
|
||||
LEFT JOIN user_profiles up ON up.user_id = ur.user_id
|
||||
ORDER BY FIELD(ur.role, "owner", "site_admin", "forum_admin"), ur.assigned_at ASC
|
||||
');
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function setRestriction(int $actingUserId, int $targetUserId, string $type, string $reason): void
|
||||
{
|
||||
if (!$this->canModerateForum($actingUserId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung.');
|
||||
}
|
||||
if (!$this->hasTable('community_user_restrictions')) {
|
||||
throw new \RuntimeException('Community-Sperren stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
if (!in_array($type, ['thread_create_blocked', 'reply_blocked'], true)) {
|
||||
throw new \RuntimeException('Ungültige Sperre.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('
|
||||
INSERT INTO community_user_restrictions (user_id, restriction_type, reason, active, created_by)
|
||||
VALUES (:uid, :type, :reason, 1, :by)
|
||||
');
|
||||
$stmt->execute([
|
||||
'uid' => $targetUserId,
|
||||
'type' => $type,
|
||||
'reason' => trim($reason) ?: null,
|
||||
'by' => $actingUserId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function clearRestriction(int $actingUserId, int $targetUserId, string $type): void
|
||||
{
|
||||
if (!$this->canModerateForum($actingUserId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung.');
|
||||
}
|
||||
if (!$this->hasTable('community_user_restrictions')) {
|
||||
throw new \RuntimeException('Community-Sperren stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('UPDATE community_user_restrictions SET active = 0 WHERE user_id = :uid AND restriction_type = :type');
|
||||
$stmt->execute([
|
||||
'uid' => $targetUserId,
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
|
||||
public function submitReport(int $userId, string $targetType, int $targetId, string $reason): void
|
||||
{
|
||||
if (!$this->hasTable('forum_reports')) {
|
||||
throw new \RuntimeException('Meldungen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('INSERT INTO forum_reports (reporter_user_id, target_type, target_id, reason, status) VALUES (:uid, :type, :targetId, :reason, :status)');
|
||||
$stmt->execute([
|
||||
'uid' => $userId,
|
||||
'type' => $targetType,
|
||||
'targetId' => $targetId,
|
||||
'reason' => trim($reason),
|
||||
'status' => 'open',
|
||||
]);
|
||||
}
|
||||
|
||||
public function listOpenReports(): array
|
||||
{
|
||||
if (!$this->hasTable('forum_reports')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->query('
|
||||
SELECT fr.*, up.display_name AS reporter_name
|
||||
FROM forum_reports fr
|
||||
LEFT JOIN user_profiles up ON up.user_id = fr.reporter_user_id
|
||||
WHERE fr.status = "open"
|
||||
ORDER BY fr.created_at DESC
|
||||
');
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function resolveReport(int $actingUserId, int $reportId, string $note = ''): void
|
||||
{
|
||||
if (!$this->canModerateForum($actingUserId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung.');
|
||||
}
|
||||
if (!$this->hasTable('forum_reports')) {
|
||||
throw new \RuntimeException('Meldungen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('
|
||||
UPDATE forum_reports
|
||||
SET status = "resolved", moderator_user_id = :uid, moderator_note = :note, resolved_at = NOW()
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->execute([
|
||||
'uid' => $actingUserId,
|
||||
'note' => trim($note) ?: null,
|
||||
'id' => $reportId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function votePost(int $userId, int $postId, int $value): void
|
||||
{
|
||||
if (!$this->hasTable('forum_post_feedback')) {
|
||||
throw new \RuntimeException('Bewertungen stehen erst nach dem Datenbank-Update zur Verfügung.');
|
||||
}
|
||||
|
||||
$value = $value >= 1 ? 1 : -1;
|
||||
$stmt = $this->pdo->prepare('
|
||||
INSERT INTO forum_post_feedback (post_id, user_id, value)
|
||||
VALUES (:postId, :uid, :value)
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = CURRENT_TIMESTAMP
|
||||
');
|
||||
$stmt->execute([
|
||||
'postId' => $postId,
|
||||
'uid' => $userId,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPostFeedbackSummary(array $postIds): array
|
||||
{
|
||||
if (!$postIds || !$this->hasTable('forum_post_feedback')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($postIds), '?'));
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT post_id,
|
||||
SUM(CASE WHEN value = 1 THEN 1 ELSE 0 END) AS helpful_count,
|
||||
SUM(CASE WHEN value = -1 THEN 1 ELSE 0 END) AS unhelpful_count
|
||||
FROM forum_post_feedback
|
||||
WHERE post_id IN ($placeholders)
|
||||
GROUP BY post_id
|
||||
");
|
||||
$stmt->execute(array_values($postIds));
|
||||
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$out[(int)$row['post_id']] = [
|
||||
'helpful' => (int)$row['helpful_count'],
|
||||
'unhelpful' => (int)$row['unhelpful_count'],
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function getUserPostVotes(int $userId, array $postIds): array
|
||||
{
|
||||
if ($userId <= 0 || !$postIds || !$this->hasTable('forum_post_feedback')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($postIds), '?'));
|
||||
$params = array_merge([$userId], array_values($postIds));
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT post_id, value
|
||||
FROM forum_post_feedback
|
||||
WHERE user_id = ?
|
||||
AND post_id IN ($placeholders)
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$out = [];
|
||||
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||
$out[(int)$row['post_id']] = (int)$row['value'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function canHighlightHelpful(float $points): bool
|
||||
{
|
||||
return $points >= 500.0;
|
||||
}
|
||||
|
||||
public function supportsApplications(): bool
|
||||
{
|
||||
return $this->hasTable('community_admin_applications');
|
||||
}
|
||||
|
||||
public function supportsReports(): bool
|
||||
{
|
||||
return $this->hasTable('forum_reports');
|
||||
}
|
||||
|
||||
public function supportsFeedback(): bool
|
||||
{
|
||||
return $this->hasTable('forum_post_feedback');
|
||||
}
|
||||
|
||||
public function supportsRestrictions(): bool
|
||||
{
|
||||
return $this->hasTable('community_user_restrictions');
|
||||
}
|
||||
|
||||
public function hasModerationTables(): bool
|
||||
{
|
||||
return $this->hasTable('user_roles');
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (array_key_exists($table, $this->tableCache)) {
|
||||
return $this->tableCache[$table];
|
||||
}
|
||||
|
||||
try {
|
||||
$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 $this->tableCache[$table] = (bool)$stmt->fetchColumn();
|
||||
} catch (\Throwable) {
|
||||
return $this->tableCache[$table] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user