$v) { if (strpos($k, 'HTTP_') === 0) { $name = strtolower(str_replace('_', '-', substr($k, 5))); $headers[$name] = $v; } } return $headers; } private static function requireCsrf(): void { self::ensureSession(); $headers = self::getHeadersLower(); $received = trim($headers['x-csrf-token'] ?? ($_POST['csrfToken'] ?? '')); if (!isset($_SESSION['csrf_token']) || $received !== $_SESSION['csrf_token']) { http_response_code(403); header('Content-Type: application/json'); echo json_encode(['error' => 'Invalid CSRF token']); exit; } } private static function requireAuth(): void { self::ensureSession(); if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { http_response_code(401); header('Content-Type: application/json'); echo json_encode(['error' => 'Unauthorized']); exit; } } /* -------------------- Permissions helpers -------------------- */ private static function loadPerms(string $username): array { try { if (function_exists('loadUserPermissions')) { $p = loadUserPermissions($username); return is_array($p) ? $p : []; } if (class_exists('userModel') && method_exists('userModel', 'getUserPermissions')) { $all = userModel::getUserPermissions(); if (is_array($all)) { if (isset($all[$username])) return (array)$all[$username]; $lk = strtolower($username); if (isset($all[$lk])) return (array)$all[$lk]; } } } catch (\Throwable $e) { /* ignore */ } return []; } private static function getPerms(): array { self::ensureSession(); $u = $_SESSION['username'] ?? ''; return $u ? self::loadPerms($u) : []; } private static function isAdmin(array $perms = []): bool { self::ensureSession(); if (!empty($_SESSION['isAdmin'])) return true; if (!empty($perms['admin']) || !empty($perms['isAdmin'])) return true; // Fallback: role from users.txt (role "1" means admin) $u = $_SESSION['username'] ?? ''; if ($u && class_exists('userModel') && method_exists('userModel', 'getUserRole')) { $roleStr = userModel::getUserRole($u); if ($roleStr === '1') return true; } return false; } private static function isFolderOnly(array $perms): bool { return !empty($perms['folderOnly']) || !empty($perms['userFolderOnly']) || !empty($perms['UserFolderOnly']); } private static function requireNotReadOnly(): void { $perms = self::getPerms(); if (!empty($perms['readOnly'])) { http_response_code(403); header('Content-Type: application/json'); echo json_encode(['error' => 'Read-only users are not allowed to perform this action.']); exit; } } private static function requireAdmin(): void { $perms = self::getPerms(); if (!self::isAdmin($perms)) { http_response_code(403); header('Content-Type: application/json'); echo json_encode(['error' => 'Admin privileges required.']); exit; } } private static function formatBytes(int $bytes): string { if ($bytes < 1024) return $bytes . " B"; if ($bytes < 1048576) return round($bytes / 1024, 2) . " KB"; if ($bytes < 1073741824) return round($bytes / 1048576, 2) . " MB"; return round($bytes / 1073741824, 2) . " GB"; } /** Return true if user is explicit owner of the folder or any of its ancestors (admins also true). */ private static function ownsFolderOrAncestor(string $folder, string $username, array $perms): bool { if (self::isAdmin($perms)) return true; $folder = ACL::normalizeFolder($folder); $f = $folder; while ($f !== '' && strtolower($f) !== 'root') { if (ACL::isOwner($username, $perms, $f)) return true; $pos = strrpos($f, '/'); $f = ($pos === false) ? '' : substr($f, 0, $pos); } return false; } /** * Enforce per-folder scope for folder-only accounts. * $need: 'read' | 'write' | 'manage' | 'share' | 'read_own' (default 'read') * Returns null if allowed, or an error string if forbidden. */ // In FolderController.php private static function enforceFolderScope( string $folder, string $username, array $perms, string $need = 'read' ): ?string { // Admins bypass scope if (self::isAdmin($perms)) return null; // If this account isn't folder-scoped, don't gate here if (!self::isFolderOnly($perms)) return null; $folder = ACL::normalizeFolder($folder); // If user owns folder or an ancestor, allow $f = $folder; while ($f !== '' && strtolower($f) !== 'root') { if (ACL::isOwner($username, $perms, $f)) return null; $pos = strrpos($f, '/'); $f = ($pos === false) ? '' : substr($f, 0, $pos); } // Normalize aliases so callers can pass either camelCase or snake_case switch ($need) { case 'manage': $ok = ACL::canManage($username, $perms, $folder); break; // legacy: case 'write': $ok = ACL::canWrite($username, $perms, $folder); break; case 'share': $ok = ACL::canShare($username, $perms, $folder); break; // read flavors: case 'read_own': $ok = ACL::canReadOwn($username, $perms, $folder); break; case 'read': $ok = ACL::canRead($username, $perms, $folder); break; // granular write-ish: case 'create': $ok = ACL::canCreate($username, $perms, $folder); break; case 'upload': $ok = ACL::canUpload($username, $perms, $folder); break; case 'edit': $ok = ACL::canEdit($username, $perms, $folder); break; case 'rename': $ok = ACL::canRename($username, $perms, $folder); break; case 'copy': $ok = ACL::canCopy($username, $perms, $folder); break; case 'move': $ok = ACL::canMove($username, $perms, $folder); break; case 'delete': $ok = ACL::canDelete($username, $perms, $folder); break; case 'extract': $ok = ACL::canExtract($username, $perms, $folder); break; // granular share (support both key styles) case 'shareFile': case 'share_file': $ok = ACL::canShareFile($username, $perms, $folder); break; case 'shareFolder': case 'share_folder':$ok = ACL::canShareFolder($username, $perms, $folder); break; default: // Default to full read if unknown need was passed $ok = ACL::canRead($username, $perms, $folder); } return $ok ? null : "Forbidden: folder scope violation."; } /** Returns true if caller can ignore ownership (admin or bypassOwnership/default). */ private static function canBypassOwnership(array $perms): bool { if (self::isAdmin($perms)) return true; return (bool)($perms['bypassOwnership'] ?? (defined('DEFAULT_BYPASS_OWNERSHIP') ? DEFAULT_BYPASS_OWNERSHIP : false)); } /** ACL-aware folder owner check (explicit). */ private static function isFolderOwner(string $folder, string $username, array $perms): bool { return ACL::isOwner($username, $perms, $folder); } /* -------------------- API: Create Folder -------------------- */ public function createFolder(): void { header('Content-Type: application/json'); self::requireAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed.']); return; } self::requireCsrf(); self::requireNotReadOnly(); try { $input = json_decode(file_get_contents('php://input'), true) ?? []; if (!isset($input['folderName'])) { http_response_code(400); echo json_encode(['error' => 'Folder name not provided.']); return; } $folderName = trim((string)$input['folderName']); $parentIn = isset($input['parent']) ? trim((string)$input['parent']) : 'root'; if (!preg_match(REGEX_FOLDER_NAME, $folderName)) { http_response_code(400); echo json_encode(['error' => 'Invalid folder name.']); return; } if ($parentIn !== '' && strcasecmp($parentIn, 'root') !== 0 && !preg_match(REGEX_FOLDER_NAME, $parentIn)) { http_response_code(400); echo json_encode(['error' => 'Invalid parent folder name.']); return; } $parent = ($parentIn === '' ? 'root' : $parentIn); $username = $_SESSION['username'] ?? ''; $perms = self::getPerms(); // Need create on parent OR ownership on parent/ancestor if (!(ACL::canCreateFolder($username, $perms, $parent) || self::ownsFolderOrAncestor($parent, $username, $perms))) { http_response_code(403); echo json_encode(['error' => 'Forbidden: manager/owner required on parent.']); exit; } // Folder-scope gate for folder-only accounts (need create on parent) if ($msg = self::enforceFolderScope($parent, $username, $perms, 'manage')) { http_response_code(403); echo json_encode(['error' => $msg]); return; } $result = FolderModel::createFolder($folderName, $parent, $username); if (empty($result['success'])) { http_response_code(400); echo json_encode($result); return; } echo json_encode($result); } catch (Throwable $e) { error_log('createFolder fatal: '.$e->getMessage().' @ '.$e->getFile().':'.$e->getLine()); http_response_code(500); echo json_encode(['error' => 'Internal error creating folder.']); } } /* -------------------- API: Delete Folder -------------------- */ public function deleteFolder(): void { header('Content-Type: application/json'); self::requireAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(["error" => "Method not allowed."]); exit; } self::requireCsrf(); self::requireNotReadOnly(); $input = json_decode(file_get_contents('php://input'), true); if (!isset($input['folder'])) { http_response_code(400); echo json_encode(["error" => "Folder name not provided."]); exit; } $folder = trim((string)$input['folder']); if (strcasecmp($folder, 'root') === 0) { http_response_code(400); echo json_encode(["error" => "Cannot delete root folder."]); exit; } if (!preg_match(REGEX_FOLDER_NAME, $folder)) { http_response_code(400); echo json_encode(["error" => "Invalid folder name."]); exit; } $username = $_SESSION['username'] ?? ''; $perms = self::getPerms(); // Folder-scope: need manage (owner) OR explicit manage grant if ($msg = self::enforceFolderScope($folder, $username, $perms, 'manage')) { http_response_code(403); echo json_encode(["error" => $msg]); exit; } // Require either manage permission or ancestor ownership (strong gate) $canManage = ACL::canManage($username, $perms, $folder) || self::ownsFolderOrAncestor($folder, $username, $perms); if (!$canManage) { http_response_code(403); echo json_encode(["error" => "Forbidden: you lack manage rights for this folder."]); exit; } // If not bypassing ownership, require ownership (direct or ancestor) as an extra safeguard if (!self::canBypassOwnership($perms) && !self::ownsFolderOrAncestor($folder, $username, $perms)) { http_response_code(403); echo json_encode(["error" => "Forbidden: you are not the folder owner."]); exit; } $result = FolderModel::deleteFolder($folder); echo json_encode($result); exit; } /* -------------------- API: Rename Folder -------------------- */ public function renameFolder(): void { header('Content-Type: application/json'); self::requireAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed.']); exit; } self::requireCsrf(); self::requireNotReadOnly(); $input = json_decode(file_get_contents('php://input'), true); if (!isset($input['oldFolder']) || !isset($input['newFolder'])) { http_response_code(400); echo json_encode(['error' => 'Required folder names not provided.']); exit; } $oldFolder = trim((string)$input['oldFolder']); $newFolder = trim((string)$input['newFolder']); if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) { http_response_code(400); echo json_encode(['error' => 'Invalid folder name(s).']); exit; } $username = $_SESSION['username'] ?? ''; $perms = self::getPerms(); // Must be allowed to manage the old folder if ($msg = self::enforceFolderScope($oldFolder, $username, $perms, 'manage')) { http_response_code(403); echo json_encode(["error" => $msg]); exit; } // For the new folder path, require write scope (we're "creating" a path) if ($msg = self::enforceFolderScope($newFolder, $username, $perms, 'manage')) { http_response_code(403); echo json_encode(["error" => "New path not allowed: " . $msg]); exit; } // Strong gates: need manage on old OR ancestor owner; need manage on new parent OR ancestor owner $canManageOld = ACL::canManage($username, $perms, $oldFolder) || self::ownsFolderOrAncestor($oldFolder, $username, $perms); if (!$canManageOld) { http_response_code(403); echo json_encode(['error' => 'Forbidden: you lack manage rights on the source folder.']); exit; } // If not bypassing ownership, require ownership (direct or ancestor) on the old folder if (!self::canBypassOwnership($perms) && !self::ownsFolderOrAncestor($oldFolder, $username, $perms)) { http_response_code(403); echo json_encode(['error' => 'Forbidden: you are not the folder owner.']); exit; } $result = FolderModel::renameFolder($oldFolder, $newFolder); echo json_encode($result); exit; } /* -------------------- API: Get Folder List -------------------- */ public function getFolderList(): void { header('Content-Type: application/json'); self::requireAuth(); // Optional "folder" filter (supports nested like "team/reports") $parent = $_GET['folder'] ?? null; if ($parent !== null && $parent !== '' && strcasecmp($parent, 'root') !== 0) { $parts = array_filter(explode('/', trim($parent, "/\\ ")), fn($p) => $p !== ''); if (empty($parts)) { http_response_code(400); echo json_encode(["error" => "Invalid folder name."]); exit; } foreach ($parts as $seg) { if (!preg_match(REGEX_FOLDER_NAME, $seg)) { http_response_code(400); echo json_encode(["error" => "Invalid folder name."]); exit; } } $parent = implode('/', $parts); } $username = $_SESSION['username'] ?? ''; $perms = self::getPerms(); $isAdmin = self::isAdmin($perms); // 1) Full list from model $all = FolderModel::getFolderList(); // each row: ["folder","fileCount","metadataFile"] if (!is_array($all)) { echo json_encode([]); exit; } // 2) Filter by view rights if (!$isAdmin) { $all = array_values(array_filter($all, function ($row) use ($username, $perms) { $f = $row['folder'] ?? ''; if ($f === '') return false; // Full view if canRead OR owns ancestor; otherwise allow if read_own granted $fullView = ACL::canRead($username, $perms, $f) || FolderController::ownsFolderOrAncestor($f, $username, $perms); $ownOnly = ACL::hasGrant($username, $f, 'read_own'); return $fullView || $ownOnly; })); } // 3) Optional parent filter (applies to both admin and non-admin) if ($parent && strcasecmp($parent, 'root') !== 0) { $pref = $parent . '/'; $all = array_values(array_filter($all, function ($row) use ($parent, $pref) { $f = $row['folder'] ?? ''; return ($f === $parent) || (strpos($f, $pref) === 0); })); } echo json_encode($all); exit; } /* -------------------- Public Shared Folder HTML -------------------- */ public function shareFolder(): void { $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING); $providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING); $page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT); if ($page === false || $page < 1) $page = 1; if (empty($token)) { http_response_code(400); header('Content-Type: application/json'); echo json_encode(["error" => "Missing token."]); exit; } $data = FolderModel::getSharedFolderData($token, $providedPass, $page); if (isset($data['needs_password']) && $data['needs_password'] === true) { header("Content-Type: text/html; charset=utf-8"); ?>
This folder is protected by a password. Please enter the password to view its contents.