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; } } }