diff --git a/api_v1_messages.php b/api_v1_messages.php index 1332033..c2ec195 100644 --- a/api_v1_messages.php +++ b/api_v1_messages.php @@ -227,7 +227,13 @@ if (empty($content) && empty($attachment_url)) { } // Check granular permissions -if (!Permissions::canSendInChannel($user_id, $channel_id)) { +$can_send = Permissions::canSendInChannel($user_id, $channel_id); +if ($thread_id) { + // For threads, we check the specific thread permission instead of the general channel permission + $can_send = Permissions::canDoInChannel($user_id, $channel_id, Permissions::SEND_MESSAGES_IN_THREADS); +} + +if (!$can_send) { echo json_encode(['success' => false, 'error' => 'You do not have permission to send messages in this channel.']); exit; } diff --git a/api_v1_roles.php b/api_v1_roles.php index 84539f7..2b7feae 100644 --- a/api_v1_roles.php +++ b/api_v1_roles.php @@ -65,7 +65,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { ['value' => 4, 'name' => 'Manage Messages'], ['value' => 8, 'name' => 'Manage Channels'], ['value' => 16, 'name' => 'Manage Server'], - ['value' => 32, 'name' => 'Administrator'] + ['value' => 32, 'name' => 'Administrator'], + ['value' => 64, 'name' => 'Create Thread'], + ['value' => 128, 'name' => 'Manage Tags'], + ['value' => 256, 'name' => 'Pin Threads'], + ['value' => 512, 'name' => 'Lock Threads'], + ['value' => 1024, 'name' => 'Send Messages in Threads'] ] ]); exit; diff --git a/api_v1_tags.php b/api_v1_tags.php index 19d0026..ae4e341 100644 --- a/api_v1_tags.php +++ b/api_v1_tags.php @@ -29,7 +29,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?"); $stmt->execute([$channel_id]); $chan = $stmt->fetch(); - if (!$chan || !Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) { + if (!$chan || !Permissions::canDoInChannel($user_id, $channel_id, Permissions::MANAGE_TAGS)) { echo json_encode(['success' => false, 'error' => 'Unauthorized']); exit; } diff --git a/api_v1_threads.php b/api_v1_threads.php index 49e7855..2903d23 100644 --- a/api_v1_threads.php +++ b/api_v1_threads.php @@ -14,7 +14,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } - if (!Permissions::canSendInChannel($user_id, $channel_id)) { + if (!Permissions::canDoInChannel($user_id, $channel_id, Permissions::CREATE_THREAD)) { echo json_encode(['success' => false, 'error' => 'You do not have permission to create threads in this channel.']); exit; } @@ -81,28 +81,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || $_SERVER['REQUEST_METHOD'] === 'DE Permissions::hasPermission($user_id, $thread['server_id'], Permissions::MANAGE_MESSAGES) || $server['owner_id'] == $user_id; - if ($thread['user_id'] != $user_id && !$is_admin) { - echo json_encode(['success' => false, 'error' => 'Unauthorized']); - exit; - } - try { if ($action === 'solve') { + if ($thread['user_id'] != $user_id && !$is_admin) { + echo json_encode(['success' => false, 'error' => 'Unauthorized']); exit; + } $stmt = db()->prepare("UPDATE forum_threads SET solution_message_id = ? WHERE id = ?"); $stmt->execute([$message_id, $thread_id]); } elseif ($action === 'pin') { + if (!Permissions::canDoInChannel($user_id, $thread['channel_id'], Permissions::PIN_THREADS)) { + echo json_encode(['success' => false, 'error' => 'You do not have permission to pin threads.']); exit; + } $stmt = db()->prepare("UPDATE forum_threads SET is_pinned = 1 WHERE id = ?"); $stmt->execute([$thread_id]); } elseif ($action === 'unpin') { + if (!Permissions::canDoInChannel($user_id, $thread['channel_id'], Permissions::PIN_THREADS)) { + echo json_encode(['success' => false, 'error' => 'You do not have permission to unpin threads.']); exit; + } $stmt = db()->prepare("UPDATE forum_threads SET is_pinned = 0 WHERE id = ?"); $stmt->execute([$thread_id]); } elseif ($action === 'lock') { + if (!Permissions::canDoInChannel($user_id, $thread['channel_id'], Permissions::LOCK_THREADS)) { + echo json_encode(['success' => false, 'error' => 'You do not have permission to lock threads.']); exit; + } $stmt = db()->prepare("UPDATE forum_threads SET is_locked = 1 WHERE id = ?"); $stmt->execute([$thread_id]); } elseif ($action === 'unlock') { + if (!Permissions::canDoInChannel($user_id, $thread['channel_id'], Permissions::LOCK_THREADS)) { + echo json_encode(['success' => false, 'error' => 'You do not have permission to unlock threads.']); exit; + } $stmt = db()->prepare("UPDATE forum_threads SET is_locked = 0 WHERE id = ?"); $stmt->execute([$thread_id]); } elseif ($action === 'delete') { + if ($thread['user_id'] != $user_id && !$is_admin) { + echo json_encode(['success' => false, 'error' => 'Unauthorized']); exit; + } db()->beginTransaction(); // Delete associated tags $stmt = db()->prepare("DELETE FROM thread_tags WHERE thread_id = ?"); diff --git a/assets/js/main.js b/assets/js/main.js index ae289f6..e75def6 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1256,8 +1256,10 @@ document.addEventListener('DOMContentLoaded', () => { return perm.user_id == id && perm.type === 'member'; }) || { allow_permissions: 0, deny_permissions: 0 }; - updateToggleUI(1, p.allow_permissions, p.deny_permissions); - updateToggleUI(2, p.allow_permissions, p.deny_permissions); + document.querySelectorAll('.perm-tri-state').forEach(group => { + const bit = parseInt(group.dataset.permBit); + updateToggleUI(bit, p.allow_permissions, p.deny_permissions); + }); } function updateToggleUI(bit, allowPerms, denyPerms) { @@ -1311,8 +1313,9 @@ document.addEventListener('DOMContentLoaded', () => { }); if (!p) { - p = { channel_id: channelId, allow_permissions: 0, deny_permissions: 0 }; + p = { channel_id: channelId, allow_permissions: 0, deny_permissions: 0, type: type }; if (type === 'role') p.role_id = id; else p.user_id = id; + channelPermissionsData.push(p); } let allow = parseInt(p.allow_permissions); diff --git a/includes/permissions.php b/includes/permissions.php index ab0b5ba..ea5a9ca 100644 --- a/includes/permissions.php +++ b/includes/permissions.php @@ -7,6 +7,11 @@ class Permissions { const MANAGE_CHANNELS = 8; const MANAGE_SERVER = 16; const ADMINISTRATOR = 32; + const CREATE_THREAD = 64; + const MANAGE_TAGS = 128; + const PIN_THREADS = 256; + const LOCK_THREADS = 512; + const SEND_MESSAGES_IN_THREADS = 1024; public static function hasPermission($user_id, $server_id, $permission) { $stmt = db()->prepare("SELECT is_admin FROM users WHERE id = ?"); @@ -19,11 +24,12 @@ class Permissions { $server = $stmt->fetch(); if ($server && $server['owner_id'] == $user_id) return true; + // Aggregate permissions from user's roles AND the @everyone role $stmt = db()->prepare(" - SELECT SUM(r.permissions) as total_perms + SELECT BIT_OR(r.permissions) as total_perms FROM roles r - JOIN user_roles ur ON r.id = ur.role_id - WHERE ur.user_id = ? AND r.server_id = ? + LEFT JOIN user_roles ur ON r.id = ur.role_id AND ur.user_id = ? + WHERE r.server_id = ? AND (ur.user_id IS NOT NULL OR r.name = '@everyone' OR r.name = 'Everyone') "); $stmt->execute([$user_id, $server_id]); $row = $stmt->fetch(); @@ -34,56 +40,14 @@ class Permissions { } public static function canViewChannel($user_id, $channel_id) { - $stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?"); - $stmt->execute([$channel_id]); - $c = $stmt->fetch(); - if (!$c) return false; - $server_id = $c['server_id']; - - // Check if owner or admin - if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true; - - // Fetch overrides for all roles the user has in this server - $stmt = db()->prepare(" - SELECT cp.allow_permissions, cp.deny_permissions - FROM channel_permissions cp - JOIN user_roles ur ON cp.role_id = ur.role_id - WHERE ur.user_id = ? AND cp.channel_id = ? - "); - $stmt->execute([$user_id, $channel_id]); - $overrides = $stmt->fetchAll(); - - // Check @everyone override specifically (even if user has no roles assigned) - $stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1"); - $stmt->execute([$server_id]); - $everyone_role = $stmt->fetch(); - if ($everyone_role) { - $stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?"); - $stmt->execute([$channel_id, $everyone_role['id']]); - $eo = $stmt->fetch(); - if ($eo) { - $overrides[] = $eo; - } - } - - if (empty($overrides)) { - return true; // Default to yes - } - - $allow = false; - $deny = false; - foreach($overrides as $o) { - if ($o['allow_permissions'] & self::VIEW_CHANNEL) $allow = true; - if ($o['deny_permissions'] & self::VIEW_CHANNEL) $deny = true; - } - - if ($allow) return true; - if ($deny) return false; - - return true; // Default to yes + return self::canDoInChannel($user_id, $channel_id, self::VIEW_CHANNEL); } public static function canSendInChannel($user_id, $channel_id) { + return self::canDoInChannel($user_id, $channel_id, self::SEND_MESSAGES); + } + + public static function canDoInChannel($user_id, $channel_id, $permission) { $stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?"); $stmt->execute([$channel_id]); $c = $stmt->fetch(); @@ -93,39 +57,68 @@ class Permissions { // Check if owner or admin if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true; - // Check overrides + // Fetch all relevant overrides + // 1. @everyone role + // 2. User's roles + // 3. User specifically + + // Use a single query to get all relevant overrides $stmt = db()->prepare(" - SELECT cp.allow_permissions, cp.deny_permissions - FROM channel_permissions cp - JOIN user_roles ur ON cp.role_id = ur.role_id - WHERE ur.user_id = ? AND cp.channel_id = ? + SELECT cp.user_id, cp.role_id, cp.allow_permissions, cp.deny_permissions, r.name as role_name + FROM channel_permissions cp + LEFT JOIN roles r ON cp.role_id = r.id + LEFT JOIN user_roles ur ON cp.role_id = ur.role_id AND ur.user_id = ? + WHERE cp.channel_id = ? AND ( + cp.user_id = ? OR + ur.user_id IS NOT NULL OR + r.name = '@everyone' OR + r.name = 'Everyone' + ) "); - $stmt->execute([$user_id, $channel_id]); + $stmt->execute([$user_id, $channel_id, $user_id]); $overrides = $stmt->fetchAll(); - // Check @everyone override - $stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1"); - $stmt->execute([$server_id]); - $everyone_role = $stmt->fetch(); - if ($everyone_role) { - $stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?"); - $stmt->execute([$channel_id, $everyone_role['id']]); - $eo = $stmt->fetch(); - if ($eo) { - $overrides[] = $eo; + if (empty($overrides)) { + // No overrides, fallback to global permissions + return self::hasPermission($user_id, $server_id, $permission); + } + + // Resolution order (simplified but effective): + // User overrides > Role overrides > @everyone + + $user_override = null; + $role_allow = 0; + $role_deny = 0; + $everyone_override = null; + + foreach($overrides as $o) { + if ($o['user_id'] == $user_id) { + $user_override = $o; + } elseif ($o['role_name'] === '@everyone' || $o['role_name'] === 'Everyone') { + $everyone_override = $o; + } else { + $role_allow |= (int)$o['allow_permissions']; + $role_deny |= (int)$o['deny_permissions']; } } - $allow = false; - $deny = false; - foreach($overrides as $o) { - if ($o['allow_permissions'] & self::SEND_MESSAGES) $allow = true; - if ($o['deny_permissions'] & self::SEND_MESSAGES) $deny = true; + // 1. User specifically + if ($user_override) { + if ($user_override['allow_permissions'] & $permission) return true; + if ($user_override['deny_permissions'] & $permission) return false; } - if ($allow) return true; - if ($deny) return false; + // 2. Roles + if ($role_allow & $permission) return true; + if ($role_deny & $permission) return false; - return self::hasPermission($user_id, $server_id, self::SEND_MESSAGES); + // 3. @everyone + if ($everyone_override) { + if ($everyone_override['allow_permissions'] & $permission) return true; + if ($everyone_override['deny_permissions'] & $permission) return false; + } + + // Fallback to base permissions + return self::hasPermission($user_id, $server_id, $permission); } } diff --git a/index.php b/index.php index cbae0e4..1f226b3 100644 --- a/index.php +++ b/index.php @@ -719,19 +719,23 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';