db = VDatabase::getInstance(); $this->logger = VLogger::getInstance(); $this->cache = VRedis::getInstance(); $this->initializeDefaultRoles(); } /** * Check if user has permission * @param int|string $userId User ID or current session user * @param string $permission Permission name * @param array $context Additional context for permission check * @return bool True if user has permission */ /** * Flexible permission checker supporting both legacy and new call signatures. * * Supported usages: * - hasPermission($userId, $permission, $context = []) * - hasPermission($permission, $userId = 'current', $context = []) * - hasPermission($permission, $contextArray) // uses current session user * * @param mixed $arg1 User ID or permission name depending on signature * @param mixed $arg2 Permission name, user ID, or context array * @param mixed $arg3 Context array when using legacy signature * @return bool */ public function hasPermission($arg1 = null, $arg2 = null, $arg3 = []) { try { $permission = null; $userId = null; $context = []; if (is_string($arg1) && ($arg2 === null || is_scalar($arg2) || is_array($arg2))) { // New-style signature: ($permission, $userId?, $context?) $permission = $arg1; if (is_array($arg2) && ($arg3 === [] || $arg3 === null)) { $context = $arg2; $userId = 'current'; } else { $userId = $arg2; $context = is_array($arg3) ? $arg3 : []; } } else { // Legacy signature: ($userId, $permission, $context) $userId = $arg1; $permission = $arg2; $context = is_array($arg3) ? $arg3 : []; } if (is_array($userId)) { // Handle accidental signature: hasPermission($permission, $context) $context = $userId; $userId = 'current'; } if (!is_array($context)) { $context = []; } if (!$permission || !is_string($permission)) { $this->logger->warning('Permission check called without a valid permission name', [ 'arg1' => $arg1, 'arg2' => $arg2, 'arg3' => $arg3 ]); return false; } // Handle current user if ($userId === 'current' || $userId === null) { $userId = $_SESSION['USER_ID'] ?? null; } if (!$userId) { // Guest user - check guest permissions return $this->checkGuestPermission($permission, $context); } // Get user roles $userRoles = $this->getUserRoles($userId); if (empty($userRoles)) { // No roles assigned - treat as guest return $this->checkGuestPermission($permission, $context); } // Check if any role has the permission foreach ($userRoles as $role) { if ($this->roleHasPermission($role, $permission, $context)) { // Log permission grant for audit $this->logPermissionCheck($userId, $permission, true, $role, $context); return true; } } // Check for resource-specific permissions if (isset($context['resource_type']) && isset($context['resource_id'])) { if ($this->hasResourcePermission($userId, $permission, $context['resource_type'], $context['resource_id'])) { $this->logPermissionCheck($userId, $permission, true, 'resource_specific', $context); return true; } } // Log permission denial $this->logPermissionCheck($userId, $permission, false, implode(',', $userRoles), $context); return false; } catch (Exception $e) { $this->logger->error('Permission check failed', [ 'user_id' => $userId, 'permission' => $permission, 'context' => $context, 'error' => $e->getMessage() ]); // Fail secure - deny permission on error return false; } } /** * Check if a user (or current session) has a specific role. * Falls back to guest semantics when no authenticated user is present. * * @param string $role Target role name * @param int|string|null $userId User identifier, `'current'` (default) uses session * @return bool */ public function hasRole($role, $userId = 'current') { try { if ($userId === 'current' || $userId === null) { $userId = $_SESSION['USER_ID'] ?? null; } if (!$userId) { return $role === self::ROLE_GUEST; } $roles = $this->getUserRoles($userId); return in_array($role, $roles, true); } catch (Exception $e) { $this->logger->error('Role check failed', [ 'user_id' => $userId, 'role' => $role, 'error' => $e->getMessage() ]); return false; } } /** * Assign role to user * @param int $userId User ID * @param string $role Role name * @param int $assignedBy User ID who assigned the role * @param string $reason Reason for assignment * @return bool Success status */ public function assignRole($userId, $role, $assignedBy = null, $reason = '') { try { // Validate role exists if (!$this->roleExists($role)) { throw new Exception("Role does not exist: {$role}"); } // Check if user already has this role if ($this->userHasRole($userId, $role)) { return true; // Already has role } // Insert role assignment $assignmentData = [ 'user_id' => $userId, 'role_name' => $role, 'assigned_by' => $assignedBy, 'assigned_at' => date('Y-m-d H:i:s'), 'reason' => $reason, 'status' => 'active' ]; $assignmentId = $this->db->doInsert('db_user_roles', $assignmentData); if ($assignmentId) { // Clear user role cache $this->clearUserRoleCache($userId); // Log role assignment $this->logger->info('Role assigned to user', [ 'user_id' => $userId, 'role' => $role, 'assigned_by' => $assignedBy, 'reason' => $reason ]); // Send notification to user $this->sendRoleNotification($userId, 'role_assigned', $role); $this->handleRoleAssignmentSideEffects($userId, $role); return true; } return false; } catch (Exception $e) { $this->logger->error('Role assignment failed', [ 'user_id' => $userId, 'role' => $role, 'assigned_by' => $assignedBy, 'error' => $e->getMessage() ]); return false; } } /** * Remove role from user * @param int $userId User ID * @param string $role Role name * @param int $removedBy User ID who removed the role * @param string $reason Reason for removal * @return bool Success status */ public function removeRole($userId, $role, $removedBy = null, $reason = '') { try { // Update role assignment status $updateData = [ 'status' => 'revoked', 'revoked_by' => $removedBy, 'revoked_at' => date('Y-m-d H:i:s'), 'revoke_reason' => $reason ]; $result = $this->db->doUpdate( 'db_user_roles', ['user_id', 'role_name', 'status'], $updateData, [$userId, $role, 'active'] ); if ($result) { // Clear user role cache $this->clearUserRoleCache($userId); // Log role removal $this->logger->info('Role removed from user', [ 'user_id' => $userId, 'role' => $role, 'removed_by' => $removedBy, 'reason' => $reason ]); // Send notification to user $this->sendRoleNotification($userId, 'role_removed', $role); $this->handleRoleRemovalSideEffects($userId, $role); return true; } return false; } catch (Exception $e) { $this->logger->error('Role removal failed', [ 'user_id' => $userId, 'role' => $role, 'removed_by' => $removedBy, 'error' => $e->getMessage() ]); return false; } } /** * Get user roles * @param int $userId User ID * @return array User roles */ public function getUserRoles($userId) { if (isset($this->userRoles[$userId])) { return $this->userRoles[$userId]; } // Try cache first $cacheKey = "user_roles_{$userId}"; if ($this->cache->isConnected()) { $cachedRoles = $this->cache->get($cacheKey); if ($cachedRoles !== false) { $this->userRoles[$userId] = $cachedRoles; return $cachedRoles; } } // Get from database $query = "SELECT role_name FROM db_user_roles WHERE user_id = ? AND status = 'active'"; $result = $this->db->doQuery($query, [$userId]); $roles = []; while ($row = $this->db->doFetch($result)) { $roles[] = $row['role_name']; } // Add default member role if user has no roles if (empty($roles) && $this->userExists($userId)) { $roles[] = self::ROLE_MEMBER; } // Cache roles $this->userRoles[$userId] = $roles; if ($this->cache->isConnected()) { $this->cache->set($cacheKey, $roles, 3600); // Cache for 1 hour } return $roles; } /** * Check if role has permission * @param string $role Role name * @param string $permission Permission name * @param array $context Permission context * @return bool True if role has permission */ public function roleHasPermission($role, $permission, $context = []) { // Get role permissions $rolePermissions = $this->getRolePermissions($role); // Check direct permission if (in_array($permission, $rolePermissions)) { return true; } // Check wildcard permissions $permissionParts = explode('.', $permission); for ($i = count($permissionParts) - 1; $i > 0; $i--) { $wildcardPerm = implode('.', array_slice($permissionParts, 0, $i)) . '.*'; if (in_array($wildcardPerm, $rolePermissions)) { return true; } } // Check context-specific permissions if (!empty($context)) { return $this->checkContextualPermission($role, $permission, $context); } return false; } /** * Get role permissions * @param string $role Role name * @return array Role permissions */ public function getRolePermissions($role) { if (isset($this->rolePermissions[$role])) { return $this->rolePermissions[$role]; } // Try cache first $cacheKey = "role_permissions_{$role}"; if ($this->cache->isConnected()) { $cachedPermissions = $this->cache->get($cacheKey); if ($cachedPermissions !== false) { $this->rolePermissions[$role] = $cachedPermissions; return $cachedPermissions; } } // Get from database $query = "SELECT permission_name FROM db_role_permissions WHERE role_name = ? AND status = 'active'"; $result = $this->db->doQuery($query, [$role]); $permissions = []; while ($row = $this->db->doFetch($result)) { $permissions[] = $row['permission_name']; } // Add default permissions for built-in roles $permissions = array_merge($permissions, $this->getDefaultRolePermissions($role)); // Cache permissions $this->rolePermissions[$role] = $permissions; if ($this->cache->isConnected()) { $this->cache->set($cacheKey, $permissions, 3600); // Cache for 1 hour } return $permissions; } /** * Create custom role * @param string $roleName Role name * @param string $displayName Display name * @param string $description Role description * @param array $permissions Initial permissions * @param int $createdBy User ID who created the role * @return bool Success status */ public function createRole($roleName, $displayName, $description = '', $permissions = [], $createdBy = null) { try { // Check if role already exists if ($this->roleExists($roleName)) { throw new Exception("Role already exists: {$roleName}"); } // Insert role $roleData = [ 'role_name' => $roleName, 'display_name' => $displayName, 'description' => $description, 'created_by' => $createdBy, 'created_at' => date('Y-m-d H:i:s'), 'status' => 'active' ]; $roleId = $this->db->doInsert('db_roles', $roleData); if ($roleId) { // Add permissions foreach ($permissions as $permission) { $this->addRolePermission($roleName, $permission); } $this->logger->info('Custom role created', [ 'role_name' => $roleName, 'display_name' => $displayName, 'permissions' => $permissions, 'created_by' => $createdBy ]); return true; } return false; } catch (Exception $e) { $this->logger->error('Role creation failed', [ 'role_name' => $roleName, 'error' => $e->getMessage() ]); return false; } } /** * Add permission to role * @param string $role Role name * @param string $permission Permission name * @return bool Success status */ public function addRolePermission($role, $permission) { try { $permissionData = [ 'role_name' => $role, 'permission_name' => $permission, 'granted_at' => date('Y-m-d H:i:s'), 'status' => 'active' ]; $result = $this->db->doInsert('db_role_permissions', $permissionData); if ($result) { // Clear role permission cache $this->clearRolePermissionCache($role); $this->logger->info('Permission added to role', [ 'role' => $role, 'permission' => $permission ]); return true; } return false; } catch (Exception $e) { $this->logger->error('Failed to add role permission', [ 'role' => $role, 'permission' => $permission, 'error' => $e->getMessage() ]); return false; } } /** * Initialize default system roles and permissions */ private function initializeDefaultRoles() { $defaultRoles = [ self::ROLE_GUEST => [ 'display_name' => 'Guest', 'description' => 'Non-registered users', 'permissions' => [ 'content.view', 'content.search', 'user.register' ] ], self::ROLE_MEMBER => [ 'display_name' => 'Member', 'description' => 'Registered users', 'permissions' => [ 'content.*', 'user.profile.edit', 'user.upload.basic', 'user.comment', 'user.like', 'user.subscribe' ] ], self::ROLE_VERIFIED => [ 'display_name' => 'Verified User', 'description' => 'Email verified users', 'permissions' => [ 'content.*', 'user.*', 'upload.advanced' ] ], self::ROLE_STREAMER => [ 'display_name' => 'Streamer', 'description' => 'Users allowed to broadcast live streams', 'permissions' => [ 'live.stream.basic', 'live.stream.advanced', 'live.stream.record', 'live.stream.quality' ] ], self::ROLE_CREATOR => [ 'display_name' => 'Content Creator', 'description' => 'Content creators with enhanced publishing features', 'permissions' => [ 'content.*', 'user.*', 'upload.*', 'analytics.view', 'monetization.basic' ] ], self::ROLE_MODERATOR => [ 'display_name' => 'Moderator', 'description' => 'Content moderators', 'permissions' => [ 'content.*', 'user.*', 'moderation.*', 'admin.content.moderate', 'admin.users.moderate' ] ], self::ROLE_ADMIN => [ 'display_name' => 'Administrator', 'description' => 'Site administrators', 'permissions' => [ 'content.*', 'user.*', 'admin.*', 'system.settings', 'system.logs.view' ] ], self::ROLE_SUPER_ADMIN => [ 'display_name' => 'Super Administrator', 'description' => 'Full system access', 'permissions' => [ '*' // All permissions ] ] ]; // This would typically be run during installation/setup // For now, we'll store the default permissions in memory foreach ($defaultRoles as $roleName => $roleData) { $this->rolePermissions[$roleName] = $roleData['permissions']; } } /** * Get default permissions for built-in roles * @param string $role Role name * @return array Default permissions */ private function getDefaultRolePermissions($role) { $defaultPermissions = [ self::ROLE_GUEST => [ 'content.view', 'content.search', 'user.register' ], self::ROLE_MEMBER => [ 'content.view', 'content.search', 'user.profile.edit', 'user.upload.basic', 'user.comment', 'user.like', 'user.subscribe' ], self::ROLE_VERIFIED => [ 'content.view', 'content.search', 'user.profile.edit', 'user.upload.basic', 'user.upload.advanced', 'user.comment', 'user.like', 'user.subscribe' ], self::ROLE_STREAMER => [ 'live.stream.basic', 'live.stream.advanced', 'live.stream.record', 'live.stream.quality' ], self::ROLE_CREATOR => [ 'content.view', 'content.search', 'user.profile.edit', 'user.upload.basic', 'user.upload.advanced', 'user.comment', 'user.like', 'user.subscribe', 'analytics.view', 'monetization.basic' ], self::ROLE_MODERATOR => [ 'content.view', 'content.search', 'content.moderate', 'user.profile.edit', 'user.moderate', 'admin.content.moderate', 'admin.users.moderate' ], self::ROLE_ADMIN => [ 'content.*', 'user.*', 'admin.*', 'system.settings', 'system.logs.view' ], self::ROLE_SUPER_ADMIN => [ '*' ] ]; return $defaultPermissions[$role] ?? []; } /** * Helper methods */ private function checkGuestPermission($permission, $context) { $guestPermissions = $this->getDefaultRolePermissions(self::ROLE_GUEST); return in_array($permission, $guestPermissions) || in_array('*', $guestPermissions); } private function roleGrantsStreaming($role) { $streamingRoles = [ self::ROLE_STREAMER, self::ROLE_ADMIN, self::ROLE_SUPER_ADMIN ]; return in_array($role, $streamingRoles, true); } private function handleRoleAssignmentSideEffects($userId, $role) { if (!$this->roleGrantsStreaming($role)) { return; } try { VStreamKeyManager::getInstance()->activateStreaming($userId); } catch (Exception $e) { $this->logger->error('Failed to activate streaming during role assignment', [ 'user_id' => $userId, 'role' => $role, 'error' => $e->getMessage() ]); } } private function handleRoleRemovalSideEffects($userId, $role) { if (!$this->roleGrantsStreaming($role)) { return; } try { // If the user still has streaming permission via another role, do not disable. if ($this->hasPermission($userId, 'live.stream.basic')) { return; } VStreamKeyManager::getInstance()->deactivateStreaming($userId); } catch (Exception $e) { $this->logger->error('Failed to deactivate streaming during role removal', [ 'user_id' => $userId, 'role' => $role, 'error' => $e->getMessage() ]); } } private function roleExists($role) { $query = "SELECT COUNT(*) as count FROM db_roles WHERE role_name = ? AND status = 'active'"; $result = $this->db->doQuery($query, [$role]); $row = $this->db->doFetch($result); return ($row['count'] > 0) || in_array($role, [ self::ROLE_GUEST, self::ROLE_MEMBER, self::ROLE_VERIFIED, self::ROLE_CREATOR, self::ROLE_MODERATOR, self::ROLE_ADMIN, self::ROLE_SUPER_ADMIN ]); } private function userHasRole($userId, $role) { $userRoles = $this->getUserRoles($userId); return in_array($role, $userRoles); } private function userExists($userId) { $query = "SELECT COUNT(*) as count FROM db_accountuser WHERE usr_id = ?"; $result = $this->db->doQuery($query, [$userId]); $row = $this->db->doFetch($result); return $row['count'] > 0; } private function hasResourcePermission($userId, $permission, $resourceType, $resourceId) { // Check if user owns the resource if ($this->userOwnsResource($userId, $resourceType, $resourceId)) { return true; } // Check resource-specific permissions $query = "SELECT COUNT(*) as count FROM db_resource_permissions WHERE user_id = ? AND resource_type = ? AND resource_id = ? AND permission_name = ? AND status = 'active'"; $result = $this->db->doQuery($query, [$userId, $resourceType, $resourceId, $permission]); $row = $this->db->doFetch($result); return $row['count'] > 0; } private function userOwnsResource($userId, $resourceType, $resourceId) { $ownershipQueries = [ 'video' => "SELECT COUNT(*) as count FROM db_videofiles WHERE file_key = ? AND usr_id = ?", 'channel' => "SELECT COUNT(*) as count FROM db_channels WHERE ch_id = ? AND usr_id = ?", 'playlist' => "SELECT COUNT(*) as count FROM db_playlists WHERE pl_id = ? AND usr_id = ?" ]; if (!isset($ownershipQueries[$resourceType])) { return false; } $result = $this->db->doQuery($ownershipQueries[$resourceType], [$resourceId, $userId]); $row = $this->db->doFetch($result); return $row['count'] > 0; } private function checkContextualPermission($role, $permission, $context) { // Implement context-specific permission logic // For example, moderators can only moderate content in their assigned categories if ($role === self::ROLE_MODERATOR && isset($context['category'])) { // Check if moderator is assigned to this category $query = "SELECT COUNT(*) as count FROM db_moderator_categories WHERE role_name = ? AND category = ? AND status = 'active'"; $result = $this->db->doQuery($query, [$role, $context['category']]); $row = $this->db->doFetch($result); return $row['count'] > 0; } return false; } private function clearUserRoleCache($userId) { unset($this->userRoles[$userId]); if ($this->cache->isConnected()) { $this->cache->delete("user_roles_{$userId}"); } } private function clearRolePermissionCache($role) { unset($this->rolePermissions[$role]); if ($this->cache->isConnected()) { $this->cache->delete("role_permissions_{$role}"); } } private function logPermissionCheck($userId, $permission, $granted, $role, $context) { $this->logger->info('Permission check', [ 'user_id' => $userId, 'permission' => $permission, 'granted' => $granted, 'role' => $role, 'context' => $context, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown' ]); } private function sendRoleNotification($userId, $type, $role) { try { $queue = new VQueue(); $notificationData = [ 'type' => 'in_app', 'recipient' => $userId, 'subject' => $type === 'role_assigned' ? 'Role Assigned' : 'Role Removed', 'message' => $type === 'role_assigned' ? "You have been assigned the role: {$role}" : "Your role has been removed: {$role}", 'template_data' => [ 'notification_type' => $type, 'data' => ['role' => $role] ] ]; $queue->enqueue('NotificationJob', $notificationData, 'notifications'); } catch (Exception $e) { $this->logger->error('Failed to send role notification', [ 'user_id' => $userId, 'type' => $type, 'role' => $role, 'error' => $e->getMessage() ]); } } }