485 lines
16 KiB
PHP
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;
|
|
}
|
|
}
|
|
}
|