level == "Admin") { // Validate path for security (fast check) if (strpos($path, '..') !== false) { return self::$accessCache[$cacheKey] = false; } // Special handling for operations that need false (NOT locked/hidden) if ($attr === 'hidden' || $attr === 'locked') { return self::$accessCache[$cacheKey] = false; // false means NOT hidden/locked (writable/deletable) } return self::$accessCache[$cacheKey] = true; // true means ALLOW operation (read/write/rm/etc) } // Validate path for security (Normal users) if (!self::validatePath($path)) { Log::warning("Path validation failed", ['path' => $path, 'user' => $user->level]); return self::$accessCache[$cacheKey] = false; } // folderName already retrieved above for global exclusion check // $folderName = self::getFolderNameFromPath($path); // --------------------------------------------------------- // GLOBAL EXCLUSION LIST (Moved to top) // --------------------------------------------------------- // For non-admin users, handle system folders based on operation if (self::isSystemFolder($folderName)) { // For these operations, explicitly hide/lock system folders if (in_array($attr, ['read', 'write', 'upload', 'mkdir', 'mkfile', 'rename', 'copy', 'move', 'rm', 'edit'])) { return self::$accessCache[$cacheKey] = false; // Block these operations } // For hidden check, return true to hide if ($attr === 'hidden') { return self::$accessCache[$cacheKey] = true; // Hide system folders from non-admin } // For locked check, return true to lock if ($attr === 'locked') { return self::$accessCache[$cacheKey] = true; // Lock system folders for non-admin } // For other operations, return null (use default behavior) return self::$accessCache[$cacheKey] = null; } // For other users, implement basic permission checks $module = "document-manager"; // Check basic module permission first // Cache basic permissions static $modulePermissions = []; if (!isset($modulePermissions[$user->level])) { $modulePermissions[$user->level] = [ 'read' => isAuth($module, "read"), 'write' => isAuth($module, "write"), 'modify' => isAuth($module, "modify") ]; } $hasModuleRead = $modulePermissions[$user->level]['read']; $hasModuleWrite = $modulePermissions[$user->level]['write']; $hasModuleModify = $modulePermissions[$user->level]['modify']; /* // DEBUG: Check if PTO user has basic permissions if ($user->level === 'Manager (PTO)') { error_log("PTO User Basic Permissions - Read: " . ($hasModuleRead ? 'YES' : 'NO') . ", Write: " . ($hasModuleWrite ? 'YES' : 'NO') . ", Modify: " . ($hasModuleModify ? 'YES' : 'NO')); } */ // Debug logging - enabled for testing /* Log::info("Elfinder Access Check", [ 'user_level' => $user->level, 'user_id' => $user->id ?? 'unknown', 'attr' => $attr, 'path' => $path, 'folder_name' => $folderName, 'module' => $module, 'permission_separation' => 'write_edit_separated', 'is_dir' => is_dir($path), 'is_file' => is_file($path), 'path_exists' => file_exists($path), 'has_module_read' => $hasModuleRead, 'has_module_write' => $hasModuleWrite, 'has_module_modify' => $hasModuleModify ]); */ switch($attr) { case 'read': // For folders, check if user should see this folder at all if (is_dir($path)) { // First check if this is a system folder for non-admin users if ($user->level !== "Admin" && self::isSystemFolder($folderName)) { return self::$accessCache[$cacheKey] = false; // Block system folders from non-admin } return self::$accessCache[$cacheKey] = self::checkFolderVisibility($user, $folderName, $module); } else { // For files, check if parent folder is system folder for non-admin if ($user->level !== "Admin" && self::isSystemFolder($folderName)) { return self::$accessCache[$cacheKey] = false; // Block files in system folders from non-admin } // For files, check read permission return self::checkFolderReadPermission($user, $folderName, $module); } case 'write': case 'upload': case 'mkdir': case 'mkfile': case 'rename': case 'copy': case 'move': // Check if user has write permission for this specific folder $writeAuth = self::checkFolderWritePermission($user, $folderName, $module); // Log::info("Write permission check", ['result' => $writeAuth, 'folder' => $folderName, 'operation' => $attr]); return self::$accessCache[$cacheKey] = $writeAuth; case 'edit': // Special handling for edit operations - check file type and edit permission $editAuth = self::checkFileEditPermission($user, $path, $module); // Log::info("Edit permission check", ['result' => $editAuth, 'path' => $path, 'operation' => $attr]); return self::$accessCache[$cacheKey] = $editAuth; case 'rm': // Check if user has delete permission for this specific folder $rmAuth = self::checkFolderDeletePermission($user, $folderName, $module); // Log::info("Remove permission check", ['result' => $rmAuth, 'folder' => $folderName]); // Additional protection for critical files if (self::isCriticalFile($path)) { // Log::info("Critical file protection - delete blocked", ['path' => $path]); return self::$accessCache[$cacheKey] = false; } return self::$accessCache[$cacheKey] = $rmAuth; case 'hidden': // Check if folder should be hidden from user // For non-admin users, hide system folders if ($user->level !== "Admin") { if (self::isSystemFolder($folderName)) { return self::$accessCache[$cacheKey] = true; // Hide system folders from non-admin } } // Check general visibility return self::$accessCache[$cacheKey] = !self::checkFolderVisibility($user, $folderName, $module); default: // For other operations, return null for default behavior /* Log::info("Default permission check", [ 'attr' => $attr, 'folder' => $folderName, 'user_level' => $user->level ]); */ return self::$accessCache[$cacheKey] = null; // Use Elfinder's default behavior } } /** * Cache for folder names to improve performance */ private static $folderNameCache = []; /** * Get folder name from path with improved path normalization (supports subfolders) */ private static function getFolderNameFromPath($path) { // Check cache first if (isset(self::$folderNameCache[$path])) { return self::$folderNameCache[$path]; } $originalPath = $path; // Normalize path separators $path = str_replace('\\', '/', $path); // Remove common path prefixes (case-insensitive) $path = preg_replace('#^.*?/storage/documents/?#i', '', $path); $path = preg_replace('#^.*?/storage/office-server/?#i', '', $path); $path = preg_replace('#^.*?/public/storage/documents/?#i', '', $path); $path = preg_replace('#^.*?/public/storage/office-server/?#i', '', $path); $path = preg_replace('#^.*?/Applications/MAMP/htdocs/stellar/public/storage/documents/?#i', '', $path); $path = preg_replace('#^.*?/Applications/MAMP/htdocs/stellar/public/storage/office-server/?#i', '', $path); // Additional path patterns for Elfinder $path = preg_replace('#^.*?/storage/documents$#i', '', $path); $path = preg_replace('#^.*?/storage/documents/$#i', '', $path); // Clean up any remaining leading/trailing slashes $path = trim($path, '/'); // For subfolder support, return the full relative path instead of just first folder // This allows us to check permissions for specific subfolders $folderName = $path; // Log path processing for debugging /* Log::info("Path processing", [ 'original_path' => $originalPath, 'normalized_path' => $path, 'folder_name' => $folderName, 'has_slashes' => strpos($folderName, '/') !== false, 'is_empty' => empty($folderName) ]); */ self::$folderNameCache[$originalPath] = $folderName; return $folderName; } /** * Check if folder should be visible to user (for folder listing) */ private static function checkFolderVisibility($user, $folderName, $module) { // Root path - always visible if user has basic read permission if (empty($folderName)) { return isAuth($module, "read"); } // Office Server files - only Admin can see if (self::isOfficeServerPath($folderName)) { return $user->level == "Admin"; } // Check if this is a system folder - only Admin can see if (self::isSystemFolder($folderName)) { return $user->level == "Admin"; } // Check if user has any permission for this folder (read, write, or edit) // This includes both explicit permissions and inherited permissions $hasReadPermission = self::checkFolderReadPermission($user, $folderName, $module); $hasWritePermission = self::checkFolderWritePermission($user, $folderName, $module); $hasEditPermission = self::checkFolderEditPermission($user, $folderName, $module); return $hasReadPermission || $hasWritePermission || $hasEditPermission; } /** * Check if user has read permission for specific folder */ private static function checkFolderReadPermission($user, $folderName, $module) { // Root path - allow access if user has basic read permission if (empty($folderName)) { return isAuth($module, "read"); } // Office Server files - only Admin can read if (self::isOfficeServerPath($folderName)) { return $user->level == "Admin"; } // Check dynamic permission using cached/recursive check return self::checkDynamicPermission($user->level, $folderName, 'read'); } /** * Check if user has write permission for specific folder * Write permission covers: upload, create, rename, copy, move, delete */ private static function checkFolderWritePermission($user, $folderName, $module) { // Office Server files - only Admin can write if (self::isOfficeServerPath($folderName)) { return $user->level == "Admin"; } // Check dynamic permission using cached/recursive check return self::checkDynamicPermission($user->level, $folderName, 'write'); } /** * Check if user has edit permission for specific folder * Edit permission covers: edit file content only (not file operations) */ private static function checkFolderEditPermission($user, $folderName, $module) { // Office Server files - only Admin can edit if (self::isOfficeServerPath($folderName)) { return $user->level == "Admin"; } // Check dynamic permission using cached/recursive check return self::checkDynamicPermission($user->level, $folderName, 'edit'); } /** * Check if user has delete permission for specific folder */ private static function checkFolderDeletePermission($user, $folderName, $module) { // Office Server files - only Admin can delete if (self::isOfficeServerPath($folderName)) { return $user->level == "Admin"; } // Delete permission uses write permission (write includes delete operations) return self::checkFolderWritePermission($user, $folderName, $module); } /** * Check if user can edit specific file types based on edit permission * Edit permission covers: edit file content only (not file operations) */ private static function checkFileEditPermission($user, $path, $module) { // Get file extension $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); // Define editable file types $editableFileTypes = ['pdf', 'xlsx', 'xlsb', 'csv', 'doc', 'docx', 'txt']; // Check if file type is editable if (!in_array($extension, $editableFileTypes)) { Log::info("File type not editable", ['extension' => $extension, 'path' => $path]); return false; } // Check folder-based permissions for edit $folderName = self::getFolderNameFromPath($path); // Check dynamic permission using cached/recursive check return self::checkDynamicPermission($user->level, $folderName, 'edit'); } /** * Static cache for permissions during a single request * Structure: ['UserLevel' => ['folder/path' => ['read' => bool, 'write' => bool]]] */ private static $permissionsCache = []; /** * Load all permissions for a user level into static cache */ private static function loadPermissionsIntoCache($userLevel) { if (isset(self::$permissionsCache[$userLevel])) { return; } // Get all permissions from Model (which uses Redis/File Cache) // This returns a collection grouped by folder_path $rawPermissions = \App\Models\DocumentManagerPermission::getUserLevelPermissions($userLevel); $formatted = []; foreach ($rawPermissions as $path => $permissions) { $formatted[$path] = []; foreach ($permissions as $perm) { // Store the boolean value directly $formatted[$path][$perm->permission_type] = $perm->is_allowed; } } self::$permissionsCache[$userLevel] = $formatted; Log::info("Permissions loaded into static cache", [ 'user_level' => $userLevel, 'folder_count' => count($formatted) ]); } /** * Check dynamic permission using cached data with inheritance */ private static function checkDynamicPermission($userLevel, $folderName, $permissionType) { // Ensure cache is loaded self::loadPermissionsIntoCache($userLevel); // 1. Check for explicit permission on this folder if (isset(self::$permissionsCache[$userLevel][$folderName])) { $folderPerms = self::$permissionsCache[$userLevel][$folderName]; if (isset($folderPerms[$permissionType])) { // Explicit permission found (either true or false) // Return exactly what is set, do not inherit if explicitly set return $folderPerms[$permissionType]; } } // 2. If no explicit permission, check parent folder inheritance // BUT ONLY for subfolders, not for main folders $parentFolder = self::getParentFolder($folderName); if ($parentFolder && $parentFolder !== $folderName && strpos($folderName, '/') !== false) { $inherited = self::checkDynamicPermission($userLevel, $parentFolder, $permissionType); if ($inherited) { // Log only if allowed via inheritance (to reduce noise) // Log::info("Permission granted via inheritance", ['folder' => $folderName, 'parent' => $parentFolder, 'type' => $permissionType]); } return $inherited; } // 3. No explicit permission and no parent - default deny return false; } /** * Get parent folder from a folder path */ private static function getParentFolder($folderPath) { // Remove trailing slash if exists $folderPath = rtrim($folderPath, '/'); // Split by slash $parts = explode('/', $folderPath); // If only one part, no parent if (count($parts) <= 1) { return null; } // Remove last part to get parent array_pop($parts); return implode('/', $parts); } /** * Check if file is a critical system file that should be protected */ private static function isCriticalFile($path) { $fileName = basename($path); $criticalFiles = [ 'index.php', 'config.php', '.htaccess', 'web.config', 'robots.txt', 'sitemap.xml', '.env', 'composer.json', 'composer.lock', 'package.json', 'yarn.lock' ]; return in_array(strtolower($fileName), array_map('strtolower', $criticalFiles)); } /** * Validate file path for security */ private static function validatePath($path) { // Check for path traversal attempts if (strpos($path, '..') !== false || strpos($path, '//') !== false) { Log::warning("Path traversal attempt detected", ['path' => $path]); return false; } // Check for suspicious file extensions $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $dangerousExtensions = ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'bat', 'cmd', 'sh', 'js']; if (in_array($extension, $dangerousExtensions)) { Log::warning("Dangerous file extension detected", ['path' => $path, 'extension' => $extension]); return false; } return true; } /** * Get file size in human readable format */ private static function formatFileSize($bytes) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, 2) . ' ' . $units[$i]; } /** * Check if path is in Office Server directory */ private static function isOfficeServerPath($folderName) { // Check if the path contains office-server return strpos($folderName, 'office-server') !== false || strpos($folderName, 'Office_Server') !== false || strpos($folderName, 'office_server') !== false; } /** * Check if path is a network drive path */ private static function isNetworkDrivePath($path) { // Check for network drive patterns $networkPatterns = [ '/^\\\\/', // Windows UNC path '/^\/\/[^\/]+/', // Unix network path '/^smb:\/\//', // SMB protocol '/^cifs:\/\//', // CIFS protocol ]; foreach ($networkPatterns as $pattern) { if (preg_match($pattern, $path)) { return true; } } return false; } /** * Check if folder is a system folder (only visible to Admin) * System folders are defined in config/document-folders.php * Uses static cache for performance optimization */ private static function isSystemFolder($folderName) { // Static cache to avoid repeated config reads static $exactMatches = null; static $regexPatterns = null; static $cache = []; // Return cached result if available if (isset($cache[$folderName])) { return $cache[$folderName]; } // Load patterns once if ($exactMatches === null) { $exactMatches = config('document-folders.system_patterns.exact', []); $regexPatterns = config('document-folders.system_patterns.regex', []); } // Check exact matches first (faster) if (in_array($folderName, $exactMatches)) { $cache[$folderName] = true; return true; } // Check regex patterns foreach ($regexPatterns as $pattern) { if (preg_match($pattern, $folderName)) { $cache[$folderName] = true; return true; } } $cache[$folderName] = false; return false; } }