Files
papa-kind-treff.info/src/App/CommunityAccess.php
Lars Gebhardt-Kusche 3ff5767900
All checks were successful
Deploy / deploy (push) Successful in 47s
community
2026-07-18 01:53:44 +02:00

485 lines
16 KiB
PHP

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